diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index dfe2d2d..1bfc67e 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -561,6 +561,7 @@ export const enum InvoiceMessage { DANAK_SUBSCRIPTION_NOT_FOUND = "خرید داناک با این شناسه یافت نشد", BUSINESS_ID_REQUIRED = "شناسه کسب و کار مورد نیاز است", BUSINESS_ID_SHOULD_BE_A_STRING = "شناسه کسب و کار باید یک رشته باشد", + DELETED = "صورت حساب با موفقیت حذف شد", } export const enum LearningMessage { diff --git a/src/modules/invoices/invoices.controller.ts b/src/modules/invoices/invoices.controller.ts index 79dc598..c9298e8 100755 --- a/src/modules/invoices/invoices.controller.ts +++ b/src/modules/invoices/invoices.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; import { ApiOperation } from "@nestjs/swagger"; import { ApplyDiscountDto } from "./DTO/apply-discount.dto"; @@ -34,6 +34,13 @@ export class InvoicesController { return this.invoiceService.updateInvoiceAdmin(paramDto.id, updateDto); } + @ApiOperation({ summary: "delete an invoice ==> admin route" }) + @PermissionsDec(PermissionEnum.INVOICES) + @Delete(":id") + deleteInvoice(@Param() paramDto: ParamDto) { + return this.invoiceService.deleteInvoiceAdmin(paramDto.id); + } + @ApiOperation({ summary: "get all invoices ==> admin route" }) @PermissionsDec(PermissionEnum.INVOICES) @Get() diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index f219794..06abb53 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -202,6 +202,105 @@ export class InvoicesService { await queryRunner.release(); } } + + //********************************** */ + + async deleteInvoiceAdmin(invoiceId: string) { + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const invoice = await queryRunner.manager.findOne(this.invoiceRepository.target, { + where: { id: invoiceId }, + }); + + if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); + + this.logger.log(`Deleting invoice: ${invoiceId} (numeric ID: ${invoice.numericId}, status: ${invoice.status})`); + + this.logger.log(`Starting cleanup of queue jobs for invoice: ${invoiceId}`); + await this.cleanupInvoiceQueueJobs(invoiceId); + this.logger.log(`Completed cleanup of queue jobs for invoice: ${invoiceId}`); + + await queryRunner.manager.remove(this.invoiceRepository.target, invoice); + + await queryRunner.commitTransaction(); + + return { + message: InvoiceMessage.DELETED, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + /** + * Clean up all queue jobs related to a specific invoice + */ + private async cleanupInvoiceQueueJobs(invoiceId: string): Promise { + try { + this.logger.log(`Starting cleanup of queue jobs for invoice: ${invoiceId}`); + + this.logger.log(`Removing recurring invoice jobs for invoice: ${invoiceId}`); + await this.removeRecurringInvoiceJobs(invoiceId); + + this.logger.log(`Removing reminder invoice jobs for invoice: ${invoiceId}`); + await this.removeReminderInvoiceJobs(invoiceId); + + this.logger.log(`Removing admin notification jobs for invoice: ${invoiceId}`); + await this.removeAdminNotificationJobs(invoiceId); + + this.logger.log(`Checking for additional main queue jobs for invoice: ${invoiceId}`); + const mainQueueJobs = await this.invoiceQueue.getJobs(["active", "waiting", "delayed", "failed", "completed"]); + + let mainJobsRemoved = 0; + for (const job of mainQueueJobs) { + const jobData = job.data; + if (jobData && (jobData.invoiceId === invoiceId || (jobData.invoice && jobData.invoice.id === invoiceId))) { + if ( + job.name !== INVOICE.RECURRING_JOB_NAME && + job.name !== INVOICE.REMINDER_JOB_NAME && + job.name !== INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_NAME + ) { + await job.remove(); + mainJobsRemoved++; + this.logger.log(`Removed additional invoice queue job: ${job.name} (ID: ${job.id}) for invoice: ${invoiceId}`); + } + } + } + + this.logger.log(`Checking for external invoice queue jobs for invoice: ${invoiceId}`); + const externalQueueJobs = await this.externalInvoiceQueue.getJobs(["active", "waiting", "delayed", "failed", "completed"]); + + let externalJobsRemoved = 0; + for (const job of externalQueueJobs) { + const jobData = job.data; + if ( + jobData && + (jobData.invoiceId === invoiceId || + (jobData.invoice && jobData.invoice.id === invoiceId) || + (jobData.invoice && jobData.invoice.numericId === invoiceId)) + ) { + await job.remove(); + externalJobsRemoved++; + this.logger.log(`Removed external invoice queue job: ${job.name} (ID: ${job.id}) for invoice: ${invoiceId}`); + } + } + + this.logger.log( + `Successfully cleaned up queue jobs for invoice: ${invoiceId}. Main jobs removed: ${mainJobsRemoved}, External jobs removed: ${externalJobsRemoved}`, + ); + } catch (error) { + this.logger.error(`Error cleaning up queue jobs for invoice ${invoiceId}:`, error); + // don't throw error here to avoid blocking invoice deletion + } + } + //********************************** */ async approveInvoiceRequest(invoiceId: string, userId: string) { @@ -922,6 +1021,7 @@ export class InvoicesService { // await this.removeRecurringInvoiceJobs(invoice.id); await this.removeReminderInvoiceJobs(invoice.id); + await this.removeAdminNotificationJobs(invoice.id); if (updateDto.isRecurring) { if (!updateDto.recurringPeriod) throw new BadRequestException(InvoiceMessage.RECURRING_PERIOD_REQUIRED); @@ -964,21 +1064,52 @@ export class InvoicesService { private async removeRecurringInvoiceJobs(invoiceId: string) { const existingJobs = await this.invoiceQueue.getJobs(["delayed", "waiting", "active"]); + let removedCount = 0; + for (const job of existingJobs) { - if (job.name === INVOICE.RECURRING_JOB_NAME && job.data.invoiceId === invoiceId) { + if (job.name === INVOICE.RECURRING_JOB_NAME && job.data?.invoiceId === invoiceId) { await job.remove(); + removedCount++; } } + + if (removedCount > 0) { + this.logger.log(`Removed ${removedCount} recurring invoice jobs for invoice: ${invoiceId}`); + } } //*********************************** */ private async removeReminderInvoiceJobs(invoiceId: string) { const existingJobs = await this.invoiceQueue.getJobs(["delayed", "waiting", "active"]); + let removedCount = 0; + for (const job of existingJobs) { - if (job.name === INVOICE.REMINDER_JOB_NAME && job.data.invoiceId === invoiceId) { + if (job.name === INVOICE.REMINDER_JOB_NAME && job.data?.invoiceId === invoiceId) { await job.remove(); + removedCount++; } } + + if (removedCount > 0) { + this.logger.log(`Removed ${removedCount} reminder invoice jobs for invoice: ${invoiceId}`); + } + } + + //*********************************** */ + private async removeAdminNotificationJobs(invoiceId: string) { + const existingJobs = await this.invoiceQueue.getJobs(["delayed", "waiting", "active"]); + let removedCount = 0; + + for (const job of existingJobs) { + if (job.name === INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_NAME && job.data?.invoiceId === invoiceId) { + await job.remove(); + removedCount++; + } + } + + if (removedCount > 0) { + this.logger.log(`Removed ${removedCount} admin notification jobs for invoice: ${invoiceId}`); + } } ///********************************** */ diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index 89a88c3..2402064 100755 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -367,6 +367,18 @@ export class SubscriptionsService { return { workspaces: userSubscriptions }; } + //************************************ */ + async checkUserAccessToService(userId: string, serviceId: string) { + const userSubscriptions = await this.userSubscriptionsRepository.findOne({ + where: { user: { id: userId }, plan: { service: { id: serviceId } }, status: SubscriptionStatus.ACTIVE }, + relations: { + plan: { service: true }, + staff: true, + }, + }); + return userSubscriptions ? true : false; + } + //************************************ */ async addProvisioningJob(userSubscription: UserSubscription, plan: SubscriptionPlan, user: User) { this.logger.debug(`Adding provisioning job for user ${user.id} and subscription ${userSubscription.id}`); diff --git a/src/modules/subscriptions/subscriptions.controller.ts b/src/modules/subscriptions/subscriptions.controller.ts index b1fbc31..2d8a70b 100755 --- a/src/modules/subscriptions/subscriptions.controller.ts +++ b/src/modules/subscriptions/subscriptions.controller.ts @@ -94,9 +94,15 @@ export class SubscriptionsController { return this.userQuickAccessService.removeQuickAccess(userId, paramDto.id); } + @ApiOperation({ summary: "check user access to a service" }) + @Get("check-access/:serviceId") + checkUserAccessToService(@Param() serviceIdParamDto: ServiceIdParamDto, @UserDec("id") userId: string) { + return this.subscriptionService.checkUserAccessToService(userId, serviceIdParamDto.serviceId); + } + @ApiOperation({ summary: "Check if user has access to a service and return all their workspaces for that service (internal use)" }) @Get("workspaces/:serviceId") - async getUserServiceAccess(@Param() serviceIdParamDto: ServiceIdParamDto, @UserDec("id") userId: string) { + getUserServiceAccess(@Param() serviceIdParamDto: ServiceIdParamDto, @UserDec("id") userId: string) { return this.subscriptionService.getUserSubscriptionsForService(userId, serviceIdParamDto.serviceId); } }