From 170bb867d008523347542d3739c905c9d6254cc4 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sun, 4 May 2025 14:48:39 +0330 Subject: [PATCH] chore: add notif service for new criticisms ticket and subscribtion --- src/app.module.ts | 4 +- src/modules/auth/providers/auth.service.ts | 22 ++++- src/modules/blogs/providers/blogs.service.ts | 2 +- src/modules/criticisms/criticisms.module.ts | 3 +- .../providers/criticisms.service.ts | 63 +++++++++--- .../providers/danak-services.service.ts | 2 +- src/modules/invoices/constants/index.ts | 33 ++++--- src/modules/invoices/invoices.module.ts | 2 +- .../invoices/providers/invoices.service.ts | 99 +++++++++++-------- .../invoices/queue/invoice.processor.ts | 59 +++++++++-- .../providers/notifications.service.ts | 2 +- .../providers/subscriptions.service.ts | 6 +- .../tickets/providers/tickets.service.ts | 26 ++++- src/modules/users/providers/admins.service.ts | 6 +- 14 files changed, 236 insertions(+), 93 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index bb745ff..443884e 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,11 +7,13 @@ import { ConfigModule } from "@nestjs/config"; import { ThrottlerModule } from "@nestjs/throttler"; import { TypeOrmModule } from "@nestjs/typeorm"; import { MailerModule } from "@nestjs-modules/mailer"; +import { TelegrafModule } from "nestjs-telegraf"; import { bullMqConfig } from "./configs/bullmq.config"; import { cacheConfig } from "./configs/cache.config"; import { mailerConfig } from "./configs/mailer.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; +import { telegrafConfig } from "./configs/telegraf.config"; import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AddressModule } from "./modules/address/address.module"; @@ -41,7 +43,7 @@ import { MonitoringModule } from "./monitoring/monitoring.module"; @Module({ imports: [ - // TelegrafModule.forRootAsync(telegrafConfig()), + TelegrafModule.forRootAsync(telegrafConfig()), MailerModule.forRootAsync(mailerConfig()), BullModule.forRootAsync(bullMqConfig()), ThrottlerModule.forRootAsync(rateLimitConfig()), diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 846e169..d8f6aad 100755 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -6,6 +6,7 @@ import { AuthMessage, UserMessage } from "../../../common/enums/message.enum"; import { NotificationsService } from "../../notifications/providers/notifications.service"; import { ReferralsService } from "../../referrals/providers/referrals.service"; import { Role } from "../../users/entities/role.entity"; +import { AdminsService } from "../../users/providers/admins.service"; import { UsersService } from "../../users/providers/users.service"; import { OTPService } from "../../utils/providers/otp.service"; import { PasswordService } from "../../utils/providers/password.service"; @@ -15,11 +16,11 @@ import { CompleteRegistrationDto } from "../DTO/complete-register.dto"; import { CheckUserExistDto, LoginPasswordDTO } from "../DTO/loginPassword.dto"; import { RequestOtpDto } from "../DTO/request-otp.dto"; import { VerifyOtpDto } from "../DTO/verify-otp.dto"; - @Injectable() export class AuthService { constructor( private readonly usersService: UsersService, + private readonly adminsService: AdminsService, private readonly passwordService: PasswordService, private readonly otpService: OTPService, private readonly tokensService: TokensService, @@ -56,9 +57,10 @@ export class AuthService { const { phone, code, referralCode } = completeRegistrationDto; const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + // const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER"); if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); @@ -71,6 +73,20 @@ export class AuthService { const tokens = await this.tokensService.generateTokens(user, queryRunner); + const superAdmins = await this.adminsService.getSuperAdmins(queryRunner); + + for (const admin of superAdmins) { + await this.notificationService.createNewCustomerNotification( + admin.id, + { + userPhone: user.phone, + fullName: `${user.firstName} ${user.lastName}`, + mobile: user.phone, + }, + queryRunner, + ); + } + await queryRunner.commitTransaction(); return { diff --git a/src/modules/blogs/providers/blogs.service.ts b/src/modules/blogs/providers/blogs.service.ts index 720de55..b5957a6 100755 --- a/src/modules/blogs/providers/blogs.service.ts +++ b/src/modules/blogs/providers/blogs.service.ts @@ -258,7 +258,7 @@ export class BlogsService { await queryRunner.manager.save(this.blogCommentsRepository.target, comment); - const superAdmin = await this.adminsService.getSuperAdmins(); + const superAdmin = await this.adminsService.getSuperAdmins(queryRunner); for (const admin of superAdmin) { await this.notificationsService.createNewBlogCommentNotification( diff --git a/src/modules/criticisms/criticisms.module.ts b/src/modules/criticisms/criticisms.module.ts index c89eb38..1b0c202 100755 --- a/src/modules/criticisms/criticisms.module.ts +++ b/src/modules/criticisms/criticisms.module.ts @@ -7,10 +7,11 @@ import { Criticism } from "./entities/criticism.entity"; import { CriticismsService } from "./providers/criticisms.service"; import { CriticismFileRepository } from "./repositories/criticisms-file.repository"; import { CriticismRepository } from "./repositories/criticisms.repository"; +import { NotificationModule } from "../notifications/notifications.module"; import { UsersModule } from "../users/users.module"; @Module({ - imports: [TypeOrmModule.forFeature([Criticism, CriticismFile]), UsersModule], + imports: [TypeOrmModule.forFeature([Criticism, CriticismFile]), UsersModule, NotificationModule], providers: [CriticismsService, CriticismRepository, CriticismFileRepository], controllers: [CriticismController], exports: [CriticismsService], diff --git a/src/modules/criticisms/providers/criticisms.service.ts b/src/modules/criticisms/providers/criticisms.service.ts index d5e8f52..1b01554 100755 --- a/src/modules/criticisms/providers/criticisms.service.ts +++ b/src/modules/criticisms/providers/criticisms.service.ts @@ -1,35 +1,49 @@ import { BadRequestException, Injectable } from "@nestjs/common"; +import { DataSource, QueryRunner } from "typeorm"; import { CommonMessage, CriticismMessage } from "../../../common/enums/message.enum"; +import { NotificationsService } from "../../notifications/providers/notifications.service"; +import { AdminsService } from "../../users/providers/admins.service"; import { UsersService } from "../../users/providers/users.service"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { CreateCriticismDto } from "../DTO/create-criticism.dto"; import { SearchCriticismQueryDto } from "../DTO/search-criticism-query.dto"; +import { Criticism } from "../entities/criticism.entity"; import { CriticismRepository } from "../repositories/criticisms.repository"; - @Injectable() export class CriticismsService { constructor( private readonly criticismRepository: CriticismRepository, private readonly usersService: UsersService, + private readonly dataSource: DataSource, + private readonly adminsService: AdminsService, + private readonly notificationsService: NotificationsService, ) {} async create(createDto: CreateCriticismDto, userId: string) { - const { user } = await this.usersService.findOneById(userId); - const files = - createDto.files?.map((file) => { - return { fileUrl: file }; - }) || []; - const criticism = this.criticismRepository.create({ - ...createDto, - user, - files, - }); - await this.criticismRepository.save(criticism); - return { - message: CommonMessage.CREATED, - criticism, - }; + const queryRunner = this.dataSource.createQueryRunner(); + try { + const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); + const files = + createDto.files?.map((file) => { + return { fileUrl: file }; + }) || []; + const criticism = queryRunner.manager.create(this.criticismRepository.target, { ...createDto, user, files }); + await queryRunner.manager.save(this.criticismRepository.target, criticism); + + await this.notifySuperAdmins(criticism, queryRunner); + + await queryRunner.commitTransaction(); + return { + message: CommonMessage.CREATED, + criticism, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } //##################################### ## @@ -79,4 +93,21 @@ export class CriticismsService { message: CommonMessage.DELETED, }; } + + private async notifySuperAdmins(criticism: Criticism, queryRunner: QueryRunner) { + const superAdmins = await this.adminsService.getSuperAdmins(queryRunner); + + for (const admin of superAdmins) { + await this.notificationsService.createNewCriticismNotification( + admin.id, + { + fullName: `${criticism.user.firstName} ${criticism.user.lastName}`, + userPhone: criticism.user.phone, + userEmail: criticism.user.email, + title: criticism.title, + }, + queryRunner, + ); + } + } } diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index fd6fa90..94d1caa 100755 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -522,7 +522,7 @@ export class DanakServicesService { await queryRunner.manager.save(this.danakServiceReviewRepository.target, review); - const superAdmins = await this.adminsService.getSuperAdmins(); + const superAdmins = await this.adminsService.getSuperAdmins(queryRunner); for (const admin of superAdmins) { await this.notificationsService.createNewServiceReviewNotification( diff --git a/src/modules/invoices/constants/index.ts b/src/modules/invoices/constants/index.ts index 4c4c4ba..0888d3b 100755 --- a/src/modules/invoices/constants/index.ts +++ b/src/modules/invoices/constants/index.ts @@ -1,20 +1,29 @@ export const INVOICE = Object.freeze({ - INVOICE_QUEUE_NAME: "invoice", - INVOICE_REMINDER_JOB_NAME: "reminderInvoice", - INVOICE_RECURRING_JOB_NAME: "recurringInvoice", + QUEUE_NAME: "invoice", + REMINDER_JOB_NAME: "reminderInvoice", + RECURRING_JOB_NAME: "recurringInvoice", + SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_NAME: "subscriptionAdminNotification", // - INVOICE_REMINDER_JOB_PRIORITY: 1, // high priority - INVOICE_REMINDER_JOB_ATTEMPTS: 3, // 3 times - INVOICE_REMINDER_JOB_BACKOFF: 5 * 1000, // ms - INVOICE_REMINDER_JOB_TIMEOUT: 10000, // timeout after 10 seconds - INVOICE_REMINDER_REPEAT_DELAY: 1 * 24 * 60 * 60 * 1000, // 1 day delay in milliseconds + REMINDER_JOB_PRIORITY: 1, // high priority + REMINDER_JOB_ATTEMPTS: 3, // 3 times + REMINDER_JOB_BACKOFF: 5 * 1000, // ms + REMINDER_JOB_TIMEOUT: 10000, // timeout after 10 seconds + REMINDER_REPEAT_DELAY: 1 * 24 * 60 * 60 * 1000, // 1 day delay in milliseconds // - INVOICE_RECURRING_JOB_PRIORITY: 1, // high priority - INVOICE_RECURRING_JOB_ATTEMPTS: 3, // retry 3 times - INVOICE_RECURRING_JOB_BACKOFF: 5 * 1000, // retry after 1 second - INVOICE_RECURRING_JOB_TIMEOUT: 10000, // timeout after 10 seconds + RECURRING_JOB_PRIORITY: 1, // high priority + RECURRING_JOB_ATTEMPTS: 3, // retry 3 times + RECURRING_JOB_BACKOFF: 5 * 1000, // retry after 5 second + RECURRING_JOB_TIMEOUT: 10000, // timeout after 10 seconds // + + SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_DELAY: 1000, // delay after 1 second + SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_PRIORITY: 1, // high priority + SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_ATTEMPTS: 3, // retry 3 times + SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_BACKOFF: 5 * 1000, // retry after 5 second + SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_TIMEOUT: 10000, // timeout after 10 seconds + // + FINE_PERCENTAGE: 0.01, // 1% of the total price MAX_DAYS_AFTER_OVERDUE: 10, DUEDATE: 7, diff --git a/src/modules/invoices/invoices.module.ts b/src/modules/invoices/invoices.module.ts index 198f4cc..018bb1b 100755 --- a/src/modules/invoices/invoices.module.ts +++ b/src/modules/invoices/invoices.module.ts @@ -23,7 +23,7 @@ import { WalletsModule } from "../wallets/wallets.module"; @Module({ imports: [ LoggerModule, - BullModule.registerQueue({ name: INVOICE.INVOICE_QUEUE_NAME }), + BullModule.registerQueue({ name: INVOICE.QUEUE_NAME }), TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]), UsersModule, WalletsModule, diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 53331ed..c2b21cf 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -35,7 +35,7 @@ export class InvoicesService { private readonly logger = new Logger(InvoicesService.name); constructor( - @InjectQueue(INVOICE.INVOICE_QUEUE_NAME) private readonly invoiceQueue: Queue, + @InjectQueue(INVOICE.QUEUE_NAME) private readonly invoiceQueue: Queue, private readonly notificationsService: NotificationsService, private readonly invoiceRepository: InvoicesRepository, private readonly invoiceItemsRepository: InvoiceItemsRepository, @@ -380,12 +380,13 @@ export class InvoicesService { ); await this.invoiceQueue.add( - INVOICE.INVOICE_REMINDER_JOB_NAME, + INVOICE.REMINDER_JOB_NAME, { invoiceId: invoice.id }, { delay: dayjs(invoice.dueDate).subtract(INVOICE.DAYS_BEFORE_OVERDUE, "day").diff(dayjs()), - attempts: INVOICE.INVOICE_REMINDER_JOB_ATTEMPTS, - backoff: { type: "exponential", delay: INVOICE.INVOICE_REMINDER_JOB_BACKOFF }, + attempts: INVOICE.REMINDER_JOB_ATTEMPTS, + backoff: { type: "exponential", delay: INVOICE.REMINDER_JOB_BACKOFF }, + priority: INVOICE.REMINDER_JOB_PRIORITY, }, ); @@ -470,9 +471,11 @@ export class InvoicesService { async payInvoice(invoiceId: string, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); const invoice = await this.getPendingInvoiceByIdWithQueryRunner(invoiceId, user.id, queryRunner); @@ -494,6 +497,7 @@ export class InvoicesService { // await queryRunner.manager.save(UserSubscription, userSubscription); await this.walletsService.createSubscriptionTransaction(invoice.totalPrice, userWallet.id, queryRunner); + await this.addNotifyAdminForSubscriptionPaymentToQueue(invoice); // } else { await this.walletsService.createInvoiceTransaction(invoice.totalPrice, userWallet.id, queryRunner); @@ -603,40 +607,6 @@ export class InvoicesService { return delayMs; } - //*********************************** */ - private async scheduleInvoiceJobs(invoice: Invoice, createDto?: CreateInvoiceDto) { - // Add to queue for billing reminder - await this.invoiceQueue.add( - INVOICE.INVOICE_REMINDER_JOB_NAME, - { invoiceId: invoice.id }, - { - delay: dayjs(invoice.dueDate).subtract(INVOICE.DAYS_BEFORE_OVERDUE, "day").diff(dayjs()), - attempts: INVOICE.INVOICE_REMINDER_JOB_ATTEMPTS, - backoff: { type: "exponential", delay: INVOICE.INVOICE_REMINDER_JOB_BACKOFF }, - }, - ); - - if (createDto?.isRecurring && !createDto?.recurringPeriod) throw new BadRequestException(InvoiceMessage.RECURRING_PERIOD_REQUIRED); - - // Add to queue for recurring invoice if applicable - if (createDto?.isRecurring && createDto?.recurringPeriod) { - await this.invoiceQueue.add( - INVOICE.INVOICE_RECURRING_JOB_NAME, - { - invoiceId: invoice.id, - adminCreated: true, - }, - { - delay: await this.calculateRecurringDelay(createDto.recurringPeriod), - attempts: INVOICE.INVOICE_RECURRING_JOB_ATTEMPTS, - backoff: { type: "exponential", delay: INVOICE.INVOICE_REMINDER_JOB_BACKOFF }, - }, - ); - - this.logger.log(`Scheduled recurring invoice for user ${createDto.userId} with interval ${createDto.recurringPeriod}`); - } - } - //*********************************** */ async applyDiscount(invoiceId: string, discountCode: string, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); @@ -721,4 +691,53 @@ export class InvoicesService { if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID_OR_NOT_BELONG_TO_USER); return invoice; } + + //*********************************** */ + private async scheduleInvoiceJobs(invoice: Invoice, createDto?: CreateInvoiceDto) { + // Add to queue for billing reminder + await this.invoiceQueue.add( + INVOICE.REMINDER_JOB_NAME, + { invoiceId: invoice.id }, + { + delay: dayjs(invoice.dueDate).subtract(INVOICE.DAYS_BEFORE_OVERDUE, "day").diff(dayjs()), + attempts: INVOICE.REMINDER_JOB_ATTEMPTS, + backoff: { type: "exponential", delay: INVOICE.REMINDER_JOB_BACKOFF }, + priority: INVOICE.REMINDER_JOB_PRIORITY, + }, + ); + + if (createDto?.isRecurring && !createDto?.recurringPeriod) throw new BadRequestException(InvoiceMessage.RECURRING_PERIOD_REQUIRED); + + // Add to queue for recurring invoice if applicable + if (createDto?.isRecurring && createDto?.recurringPeriod) { + await this.invoiceQueue.add( + INVOICE.RECURRING_JOB_NAME, + { + invoiceId: invoice.id, + adminCreated: true, + }, + { + delay: await this.calculateRecurringDelay(createDto.recurringPeriod), + attempts: INVOICE.RECURRING_JOB_ATTEMPTS, + backoff: { type: "exponential", delay: INVOICE.RECURRING_JOB_BACKOFF }, + priority: INVOICE.RECURRING_JOB_PRIORITY, + }, + ); + + this.logger.log(`Scheduled recurring invoice for user ${createDto.userId} with interval ${createDto.recurringPeriod}`); + } + } + //*********************************** */ + private async addNotifyAdminForSubscriptionPaymentToQueue(invoice: Invoice) { + await this.invoiceQueue.add( + INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_NAME, + { invoiceId: invoice.id }, + { + delay: INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_DELAY, + attempts: INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_ATTEMPTS, + backoff: { type: "exponential", delay: INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_BACKOFF }, + priority: INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_PRIORITY, + }, + ); + } } diff --git a/src/modules/invoices/queue/invoice.processor.ts b/src/modules/invoices/queue/invoice.processor.ts index 553bbfc..c516def 100755 --- a/src/modules/invoices/queue/invoice.processor.ts +++ b/src/modules/invoices/queue/invoice.processor.ts @@ -15,10 +15,10 @@ import { Invoice } from "../entities/invoice.entity"; import { InvoiceStatus } from "../enums/invoice-status.enum"; import { InvoicesService } from "../providers/invoices.service"; -@Processor(INVOICE.INVOICE_QUEUE_NAME, { concurrency: 5 }) +@Processor(INVOICE.QUEUE_NAME, { concurrency: 5 }) export class InvoiceProcessor extends WorkerProcessor { constructor( - @InjectQueue(INVOICE.INVOICE_QUEUE_NAME) private readonly invoiceQueue: Queue, + @InjectQueue(INVOICE.QUEUE_NAME) private readonly invoiceQueue: Queue, private readonly invoicesService: InvoicesService, private readonly dataSource: DataSource, private readonly notificationService: NotificationsService, @@ -30,10 +30,12 @@ export class InvoiceProcessor extends WorkerProcessor { async process(job: Job, token?: string) { this.logger.log(job); switch (job.name) { - case INVOICE.INVOICE_REMINDER_JOB_NAME: + case INVOICE.REMINDER_JOB_NAME: return this.sendBillInvoiceReminder(job, token); - case INVOICE.INVOICE_RECURRING_JOB_NAME: + case INVOICE.RECURRING_JOB_NAME: return this.createRecurringInvoice(job, token); + case INVOICE.SUBSCRIPTION_ADMIN_NOTIFICATION_JOB_NAME: + return this.notifyAdminForSubscriptionInvoice(job); default: this.logger.error(`Unknown job name: ${job.name}`); return; @@ -176,7 +178,7 @@ export class InvoiceProcessor extends WorkerProcessor { private async fetchInvoice(invoiceId: string, queryRunner: QueryRunner) { const invoice = await queryRunner.manager.findOne(Invoice, { where: { id: invoiceId }, - relations: { user: true, items: { subscriptionPlan: { plan: true } } }, + relations: { user: true, items: { subscriptionPlan: { plan: { service: true } } } }, }); if (!invoice) throw new Error(`Invoice not found: ${invoiceId}`); @@ -291,12 +293,12 @@ export class InvoiceProcessor extends WorkerProcessor { private async scheduleNextReminder(invoiceId: string) { this.logger.log(`Invoice ${invoiceId} is still unpaid. Scheduling a new reminder.`); await this.invoiceQueue.add( - INVOICE.INVOICE_REMINDER_JOB_NAME, + INVOICE.REMINDER_JOB_NAME, { invoiceId }, { - delay: INVOICE.INVOICE_REMINDER_REPEAT_DELAY, // 1 day delay in milliseconds - attempts: INVOICE.INVOICE_REMINDER_JOB_ATTEMPTS, - backoff: { type: "exponential", delay: INVOICE.INVOICE_REMINDER_JOB_BACKOFF }, + delay: INVOICE.REMINDER_REPEAT_DELAY, // 1 day delay in milliseconds + attempts: INVOICE.REMINDER_JOB_ATTEMPTS, + backoff: { type: "exponential", delay: INVOICE.REMINDER_JOB_BACKOFF }, }, ); } @@ -326,4 +328,43 @@ export class InvoiceProcessor extends WorkerProcessor { admins.map((admin) => this.notificationService.createRecurringInvoiceNotification(admin.id, notificationPayload, queryRunner)), ); } + + //********************************** */ + + private async notifyAdminForSubscriptionInvoice(job: Job<{ invoiceId: string }>) { + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const { invoiceId } = job.data; + const invoice = await this.fetchInvoice(invoiceId, queryRunner); + + const superAdmins = await this.fetchAdmin(queryRunner); + + for (const admin of superAdmins) { + await this.notificationService.createNewSubscriptionNotification( + admin.id, + { + date: invoice.dueDate, + userPhone: invoice.user.phone, + userEmail: invoice.user.email, + serviceName: invoice.items[0]?.subscriptionPlan?.plan.service.name ?? "", + fullName: invoice.user.firstName + " " + invoice.user.lastName, + }, + queryRunner, + ); + } + + await queryRunner.commitTransaction(); + return true; + } catch (error) { + this.logger.error(`Failed to notify admins for subscription invoice:`, error); + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } } diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index a56e2f9..1bec1eb 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -525,7 +525,7 @@ export class NotificationsService { } //************************************************* */ - async createNewTicketNotification(recipientId: string, data: INewTicketNotificationData, queryRunner: QueryRunner) { + async createNewTicketGlobalNotification(recipientId: string, data: INewTicketNotificationData, queryRunner: QueryRunner) { const message = NotificationMessage.NEW_TICKET_MESSAGE.replace("[ticketSubject]", data.ticketSubject); await this.smsService.sendGlobalNewTicketSms(data.userPhone, data.ticketId, data.ticketSubject, data.fullName); diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index 235f5b3..df886eb 100755 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -196,9 +196,11 @@ export class SubscriptionsService { async subscribeToPlan(serviceId: string, subscribeDto: SubscribeServiceDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); const plan = await queryRunner.manager.findOne(SubscriptionPlan, { diff --git a/src/modules/tickets/providers/tickets.service.ts b/src/modules/tickets/providers/tickets.service.ts index e7d1ecb..db3459c 100755 --- a/src/modules/tickets/providers/tickets.service.ts +++ b/src/modules/tickets/providers/tickets.service.ts @@ -7,6 +7,7 @@ import { CommonMessage, TicketMessageEnum, UserMessage } from "../../../common/e import { DanakService } from "../../danak-services/entities/danak-service.entity"; import { NotificationsService } from "../../notifications/providers/notifications.service"; import { User } from "../../users/entities/user.entity"; +import { AdminsService } from "../../users/providers/admins.service"; import { UsersService } from "../../users/providers/users.service"; import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto"; import { CreateTicketMessageDto } from "../DTO/create-ticket-message.dto"; @@ -32,6 +33,7 @@ export class TicketsService { private readonly ticketsRepository: TicketsRepository, private readonly ticketMessagesRepository: TicketMessagesRepository, private readonly usersService: UsersService, + private readonly adminsService: AdminsService, private readonly referralService: ReferralService, private dataSource: DataSource, ) {} @@ -367,6 +369,7 @@ export class TicketsService { }, queryRunner, ); + await this.notifySuperAdminsForTicket(queryRunner, ticket); } //***************************** */ @@ -395,7 +398,7 @@ export class TicketsService { } //***************************** */ - private async saveAttachments(queryRunner: QueryRunner, attachmentUrls: string[], ticketMessage: TicketMessage): Promise { + private async saveAttachments(queryRunner: QueryRunner, attachmentUrls: string[], ticketMessage: TicketMessage) { const attachments = attachmentUrls.map((url) => queryRunner.manager.create(TicketMessageAttachment, { attachmentUrl: url, @@ -406,7 +409,7 @@ export class TicketsService { } //***************************** */ - private async sendNotificationForAdminResponse(queryRunner: QueryRunner, ticket: Ticket, admin: User): Promise { + private async sendNotificationForAdminResponse(queryRunner: QueryRunner, ticket: Ticket, admin: User) { await this.notificationsService.createAnswerTicketNotification( ticket.user.id, { @@ -419,4 +422,23 @@ export class TicketsService { queryRunner, ); } + //***************************** */ + + private async notifySuperAdminsForTicket(queryRunner: QueryRunner, ticket: Ticket) { + const superAdmins = await this.adminsService.getSuperAdmins(queryRunner); + + for (const admin of superAdmins) { + await this.notificationsService.createNewTicketGlobalNotification( + admin.id, + { + ticketId: ticket.numericId.toString(), + ticketSubject: ticket.subject, + userPhone: ticket.user.phone, + userEmail: ticket.user.email, + fullName: `${ticket.user.firstName} ${ticket.user.lastName}`, + }, + queryRunner, + ); + } + } } diff --git a/src/modules/users/providers/admins.service.ts b/src/modules/users/providers/admins.service.ts index f749a07..8e2b32d 100755 --- a/src/modules/users/providers/admins.service.ts +++ b/src/modules/users/providers/admins.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import slugify from "slugify"; -import { DataSource, In, Not } from "typeorm"; +import { DataSource, In, Not, QueryRunner } from "typeorm"; import { AdminMessage, AdsMessage, UserMessage } from "../../../common/enums/message.enum"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; @@ -223,7 +223,7 @@ export class AdminsService { }; } /************************************************************ */ - async getSuperAdmins() { - return this.userRepository.find({ where: { roles: { isAdmin: true, name: RoleEnum.SUPER_ADMIN } } }); + async getSuperAdmins(queryRunner: QueryRunner) { + return queryRunner.manager.find(this.userRepository.target, { where: { roles: { isAdmin: true, name: RoleEnum.SUPER_ADMIN } } }); } }