From 2e2154d7452f6ea2d6a693a2ec208f0d1bf7eb81 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 3 May 2025 16:30:22 +0330 Subject: [PATCH] refactor: the whole paymanet service --- package.json | 3 +- pnpm-lock.yaml | 6 +- .../decorators/inject-queue.decorator.ts | 2 +- src/common/enums/message.enum.ts | 14 +- src/configs/mailer.config.ts | 2 +- src/configs/sms.config.ts | 14 +- src/main.ts | 5 + .../interfaces/ISendNotificationData.ts | 5 + .../providers/notifications.service.ts | 132 ++-- src/modules/payments/constants/index.ts | 17 +- .../payments/factories/payment.factory.ts | 32 +- src/modules/payments/interfaces/IPayment.ts | 25 +- src/modules/payments/payments.module.ts | 2 +- .../payments/providers/payments.service.ts | 701 ++++++++++-------- .../payments/queue/payment.processor.ts | 235 +++++- .../payments/types/payment-job.type.ts | 3 + .../settings/enums/notif-settings.enum.ts | 4 + src/modules/utils/providers/email.service.ts | 125 ++++ src/modules/utils/providers/sms.service.ts | 61 +- src/monitoring/monitoring.module.ts | 1 - src/templates/.gitkeep | 0 src/templates/email/announcement.hbs | 24 + src/templates/{ => email}/email-verify.hbs | 0 src/templates/email/exam-cancellation.hbs | 23 + src/templates/email/invoice.hbs | 40 + src/templates/email/login.hbs | 23 + src/templates/email/payment-cancellation.hbs | 23 + src/templates/email/payment-reminder.hbs | 23 + src/templates/email/ticket.hbs | 42 ++ src/templates/email/wallet.hbs | 40 + 30 files changed, 1231 insertions(+), 396 deletions(-) create mode 100644 src/modules/payments/types/payment-job.type.ts create mode 100644 src/templates/.gitkeep create mode 100644 src/templates/email/announcement.hbs rename src/templates/{ => email}/email-verify.hbs (100%) create mode 100644 src/templates/email/exam-cancellation.hbs create mode 100644 src/templates/email/invoice.hbs create mode 100644 src/templates/email/login.hbs create mode 100644 src/templates/email/payment-cancellation.hbs create mode 100644 src/templates/email/payment-reminder.hbs create mode 100644 src/templates/email/ticket.hbs create mode 100644 src/templates/email/wallet.hbs diff --git a/package.json b/package.json index 92c6452..e39fd47 100755 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "@nestjs/swagger": "^11.1.5", "@nestjs/throttler": "^6.4.0", "@nestjs/typeorm": "^11.0.0", - "@types/bcrypt": "^5.0.2", "@willsoto/nestjs-prometheus": "^6.0.2", "aws-sdk": "^2.1692.0", "axios": "^1.8.4", @@ -74,12 +73,12 @@ "typeorm": "^0.3.22" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@commitlint/cli": "^19.8.0", "@commitlint/config-conventional": "^19.8.0", "@nestjs/cli": "^11.0.6", "@nestjs/schematics": "^11.0.5", "@nestjs/testing": "^11.0.20", - "@types/bcrypt": "^5.0.2", "@types/jest": "^29.5.14", "@types/node": "^20.17.30", "@types/nodemailer": "^6.4.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32393a8..363c433 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,6 @@ importers: '@nestjs/typeorm': specifier: ^11.0.0 version: 11.0.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.22(ioredis@5.6.1)(pg@8.14.1)(redis@4.7.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.8.3))) - '@types/bcrypt': - specifier: ^5.0.2 - version: 5.0.2 '@willsoto/nestjs-prometheus': specifier: ^6.0.2 version: 6.0.2(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) @@ -150,6 +147,9 @@ importers: '@nestjs/testing': specifier: ^11.0.20 version: 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 '@types/jest': specifier: ^29.5.14 version: 29.5.14 diff --git a/src/common/decorators/inject-queue.decorator.ts b/src/common/decorators/inject-queue.decorator.ts index 104c193..1c0ecec 100755 --- a/src/common/decorators/inject-queue.decorator.ts +++ b/src/common/decorators/inject-queue.decorator.ts @@ -2,4 +2,4 @@ import { InjectQueue } from "@nestjs/bullmq"; import { PAYMENT } from "../../modules/payments/constants"; -export const InjectPaymentQueue = () => InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME); +export const InjectPaymentQueue = () => InjectQueue(PAYMENT.QUEUE_NAME); diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index deb310e..0313c90 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -119,6 +119,7 @@ export const enum CommonMessage { ID_SHOULD_BE_UUID = "شناسه باید یک یو یو آی دی باشد", PaymentTypeQueryNotEmpty = "نوع پرداخت نمی‌تواند خالی باشد", SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد", + UPDATED = "با موفقیت آپدیت شد", } export const enum CategoryMessage { @@ -383,7 +384,7 @@ export const enum NotificationMessage { WALLET_CHARGE_MESSAGE = "کیف پول شما به مبلغ [amount] تومان شارژ شد", WALLET_DEDUCTION = "کسر از کیف پول", WALLET_DEDUCTION_MESSAGE = "مبلغ [amount] تومان از کیف پول شما کسر شد", - BILL_INVOICE_REMINDER = "یادآوری پرداخت", + BILL_INVOICE_REMINDER = "یادآوری پرداخت صورت حساب", BILL_INVOICE_REMINDER_MESSAGE = "صورت حساب شماره [invoiceNumber] هنوز پرداخت نشده است", BILL_INVOICE = "صورت حساب پرداختی", BILL_INVOICE_MESSAGE = "صورت حساب شماره [invoiceNumber] با موفقیت پرداخت شد", @@ -405,6 +406,10 @@ export const enum NotificationMessage { ASSIGN_TICKET = "اختصاص تیکت", RECURRING_INVOICE_MESSAGE = "صورت حساب دوره‌ای شماره [invoiceNumber] صادر شده است، لطفا نسبت به تایید یا ویرایش آن اقدام کنید", RECURRING_INVOICE = "صورت حساب دوره‌ای", + PAYMENT_REMINDER = "یادآوری پرداخت", + PAYMENT_REMINDER_MESSAGE = "لطفا مبلغ [amount] تومان را پرداخت کنید", + PAYMENT_CANCELLATION = "لغو پرداخت", + PAYMENT_CANCELLATION_MESSAGE = "پرداخت مبلغ [amount] تومان لغو شد", } export const enum SubscriptionMessage { @@ -555,6 +560,13 @@ export const enum DiscountMessage { export const enum EmailMessage { EMAIL_VERIFICATION = "تایید ایمیل", EMAIL_SENDING_FAILED = "ارسال ایمیل با خطا مواجه شد", + LOGIN = "ورود به سیستم", + INVOICE = "فاکتور", + WALLET = "اعلان کیف پول", + TICKET = "تیکت پشتیبانی", + ANNOUNCEMENT = "اعلان", + PAYMENT_REMINDER = "یادآوری پرداخت", + PAYMENT_CANCELLATION = "لغو پرداخت", } export const enum FinancialMessage { diff --git a/src/configs/mailer.config.ts b/src/configs/mailer.config.ts index 8858884..1de1e94 100755 --- a/src/configs/mailer.config.ts +++ b/src/configs/mailer.config.ts @@ -20,7 +20,7 @@ export function mailerConfig(): MailerAsyncOptions { }, template: { - dir: process.cwd() + "/src/templates", + dir: process.cwd() + "/src/templates/email", adapter: new HandlebarsAdapter(), options: { strict: true, diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index ee9135a..d96ab42 100755 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -19,11 +19,12 @@ export function smsConfigs() { SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"), SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow("SMS_PATTERN_INVOICE_APPROVED"), SMS_PATTERN_INVOICE_PAID: configService.getOrThrow("SMS_PATTERN_INVOICE_PAID"), - SMS_PATTERN_INVOICE_PAYMENT_REMINDER: configService.getOrThrow("SMS_PATTERN_INVOICE_PAYMENT_REMINDER"), - SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow("SMS_PATTERN_INVOICE_REMINDER"), - SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow("SMS_PATTERN_INVOICE_OVERDUE"), SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow("SMS_PATTERN_RECURRING_INVOICE_DRAFT"), SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow("SMS_PATTERN_SUBSCRIPTION_CANCELLED"), + SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow("SMS_PATTERN_INVOICE_REMINDER"), + SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow("SMS_PATTERN_INVOICE_OVERDUE"), + SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow("SMS_PATTERN_PAYMENT_REMINDER"), + SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow("SMS_PATTERN_PAYMENT_CANCELLATION"), }; }, }; @@ -44,9 +45,10 @@ export interface ISmsConfigs { SMS_PATTERN_TICKET_ASSIGNED_ADMIN: string; SMS_PATTERN_INVOICE_APPROVED: string; SMS_PATTERN_INVOICE_PAID: string; - SMS_PATTERN_INVOICE_PAYMENT_REMINDER: string; - SMS_PATTERN_INVOICE_REMINDER: string; - SMS_PATTERN_INVOICE_OVERDUE: string; SMS_PATTERN_RECURRING_INVOICE_DRAFT: string; SMS_PATTERN_SUBSCRIPTION_CANCELLED: string; + SMS_PATTERN_INVOICE_REMINDER: string; + SMS_PATTERN_INVOICE_OVERDUE: string; + SMS_PATTERN_PAYMENT_REMINDER: string; + SMS_PATTERN_PAYMENT_CANCELLATION: string; } diff --git a/src/main.ts b/src/main.ts index 35ed46f..0349b8c 100755 --- a/src/main.ts +++ b/src/main.ts @@ -13,12 +13,17 @@ async function bootstrap() { const fastifyAdapter = new FastifyAdapter({ bodyLimit: 10 * 1024 * 1024, + trustProxy: true, }); fastifyAdapter.getInstance().addContentTypeParser("multipart/form-data", (_request, _payload, done) => { done(null); }); + fastifyAdapter.getInstance().addContentTypeParser("text/plain", (_request, _payload, done) => { + done(null); + }); + const app = await NestFactory.create(AppModule, fastifyAdapter); app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index 43c02d9..ebce510 100755 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -45,3 +45,8 @@ export interface ISubscriptionNotificationData extends IBaseNotificationData { planName: string; invoiceId: string; } + +export interface IPaymentNotificationData extends IBaseNotificationData { + amount: number; + paymentId: string; +} diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index cdaa96e..59ac134 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -1,10 +1,10 @@ import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { InjectDataSource } from "@nestjs/typeorm"; import { DataSource, QueryRunner } from "typeorm"; import { CommonMessage, NotificationMessage } from "../../../common/enums/message.enum"; import { NotifType } from "../../settings/enums/notif-settings.enum"; import { UserSettingsService } from "../../settings/providers/user-settings.service"; +import { EmailService } from "../../utils/providers/email.service"; import { dateFormat, numberFormat } from "../../utils/providers/international.utils"; import { SmsService } from "../../utils/providers/sms.service"; import { CreateNotificationDto } from "../DTO/create-notification.dto"; @@ -14,6 +14,7 @@ import { IAnnouncementNotificationData, IBaseNotificationData, IInvoiceNotificationData, + IPaymentNotificationData, ISubscriptionNotificationData, ITicketNotificationData, IWalletNotificationData, @@ -24,10 +25,11 @@ import { NotificationRepository } from "../repositories/notifications.repository export class NotificationsService { private readonly logger = new Logger(NotificationsService.name); constructor( + private readonly dataSource: DataSource, private readonly notificationRepository: NotificationRepository, private readonly smsService: SmsService, private readonly userSettingsService: UserSettingsService, - @InjectDataSource() private readonly dataSource: DataSource, + private readonly emailService: EmailService, ) {} //************************ */ @@ -116,14 +118,14 @@ export class NotificationsService { //************************ */ - async createLoginNotification(recipientId: string, sendNotifData: IBaseNotificationData) { + async createLoginNotification(recipientId: string, data: IBaseNotificationData) { const loginDate = new Date().toLocaleString("fa-IR"); const message = NotificationMessage.LOGIN_MESSAGE.replace("[loginDate]", loginDate); //sent notification based on the userSettings const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.USER_LOGIN, recipientId); if (isActive) { - await this.smsService.sendLoginSms(sendNotifData.userPhone, loginDate); - //send email too + await this.smsService.sendLoginSms(data.userPhone, loginDate); + if (data.userEmail) await this.emailService.sendLoginEmail(data); } return this.createNotification({ title: NotificationMessage.LOGIN, type: NotifType.USER_LOGIN, message, recipientId }); } @@ -146,7 +148,7 @@ export class NotificationsService { dateFormat(data.createDate), dateFormat(data.dueDate), ); - //send email too + if (data.userEmail) await this.emailService.sendInvoiceEmail(data); } return this.createNotification( { title: NotificationMessage.INVOICE_CREATION, type: NotifType.CREATE_INVOICE, message, recipientId }, @@ -165,7 +167,7 @@ export class NotificationsService { ); if (isActive) { await this.smsService.sendAnnouncementSms(data.userPhone, data.title, data.description, dateFormat(data.date)); - //send email too + if (data.userEmail) await this.emailService.sendAnnouncementEmail(data); } return this.createNotification({ title: NotificationMessage.ANNOUNCEMENT, type: NotifType.ANNOUNCEMENT, message, recipientId }); } @@ -187,7 +189,7 @@ export class NotificationsService { numberFormat(data.balance), dateFormat(data.date), ); - //send email too + if (data.userEmail) await this.emailService.sendWalletEmail(data); } return this.createNotification({ title: NotificationMessage.WALLET_CHARGE, type: NotifType.WALLET_CHARGE, message, recipientId }); } @@ -210,7 +212,7 @@ export class NotificationsService { data.reason, dateFormat(data.date), ); - //send email too + if (data.userEmail) await this.emailService.sendWalletEmail(data); } return this.createNotification({ title: NotificationMessage.WALLET_DEDUCTION, type: NotifType.WALLET_DEDUCTION, message, recipientId }); } @@ -227,7 +229,7 @@ export class NotificationsService { ); if (isActive) { await this.smsService.sendAnswerTicketSms(data.userPhone, data.ticketId, data.subject); - //send email too + if (data.userEmail) await this.emailService.sendTicketEmail(data); } return this.createNotification({ title: NotificationMessage.ANSWER_TICKET, type: NotifType.ANSWER_TICKET, message, recipientId }); } @@ -244,24 +246,23 @@ export class NotificationsService { ); if (isActive) { await this.smsService.sendCreateTicketSms(data.userPhone, data.ticketId, data.subject, dateFormat(data.date)); - //send email too + if (data.userEmail) await this.emailService.sendTicketEmail(data); } return this.createNotification({ title: NotificationMessage.CREATE_TICKET, type: NotifType.CREATE_TICKET, message, recipientId }); } //************************ */ - //TODO: fix this cause this for admin async createAssignTicketNotificationForAdmin(recipientId: string, data: ITicketNotificationData, queryRunner: QueryRunner) { const message = NotificationMessage.ASSIGN_TICKET_MESSAGE.replace("[ticketId]", data.ticketId); //sent notification based on the userSettings - // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( - // NotifType.ANSWER_TICKET, - // recipientId, - // queryRunner, - // ); - // if (isActive) { - await this.smsService.sendTicketAssignedToAdminSms(data.userPhone, data.ticketId, data.subject, data.user!); - //send email too - // } + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.ASSIGN_TICKET, + recipientId, + queryRunner, + ); + if (isActive) { + await this.smsService.sendTicketAssignedToAdminSms(data.userPhone, data.ticketId, data.subject, data.user!); + if (data.userEmail) await this.emailService.sendTicketEmail(data); + } return this.createNotification( { title: NotificationMessage.ASSIGN_TICKET, type: NotifType.ASSIGN_TICKET, message, recipientId }, queryRunner, @@ -276,7 +277,7 @@ export class NotificationsService { const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE_REMINDER, recipientId); if (isActive) { await this.smsService.sendInvoicePaymentReminderSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate)); - //send email too + if (data.userEmail) await this.emailService.sendInvoiceEmail(data); } return this.createNotification( { title: NotificationMessage.BILL_INVOICE_REMINDER, type: NotifType.BILL_INVOICE_REMINDER, message, recipientId }, @@ -292,7 +293,7 @@ export class NotificationsService { const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE, recipientId); if (isActive) { await this.smsService.sendInvoicePaidSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.paidAt!)); - //send email too + if (data.userEmail) await this.emailService.sendInvoiceEmail(data); } return this.createNotification( { title: NotificationMessage.BILL_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, @@ -307,7 +308,7 @@ export class NotificationsService { const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.APPROVE_INVOICE, recipientId); if (isActive) { await this.smsService.sendInvoiceApprovedSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate)); - //send email too + if (data.userEmail) await this.emailService.sendInvoiceEmail(data); } return this.createNotification( { title: NotificationMessage.APPROVED_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, @@ -327,7 +328,7 @@ export class NotificationsService { const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.INVOICE_OVERDUE, recipientId); if (isActive) { await this.smsService.sendInvoiceOverdueSms(data.userPhone, data.invoiceId, data.price, data.lateFee || 0, dateFormat(data.dueDate)); - //send email too + if (data.userEmail) await this.emailService.sendInvoiceEmail(data); } return this.createNotification( { title: NotificationMessage.INVOICE_OVERDUE, type: NotifType.INVOICE_OVERDUE, message, recipientId }, @@ -343,13 +344,14 @@ export class NotificationsService { const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.RECURRING_INVOICE, recipientId); if (isActive) { await this.smsService.sendRecurringInvoiceDraftSms(data.userPhone, data.invoiceId, data.price); - //send email too + if (data.userEmail) await this.emailService.sendInvoiceEmail(data); } return this.createNotification( { title: NotificationMessage.RECURRING_INVOICE, type: NotifType.RECURRING_INVOICE, message, recipientId }, queryRunner, ); } + // //************************ */ async createBlockServiceNotification(recipientId: string, data: ISubscriptionNotificationData, queryRunner: QueryRunner) { const message = NotificationMessage.BLOCK_SERVICE_MESSAGE.replace("[serviceName]", data.planName); @@ -357,7 +359,14 @@ export class NotificationsService { const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BLOCK_SERVICE, recipientId); if (isActive) { await this.smsService.sendBlockServiceSms(data.userPhone, data.invoiceId, data.planName); - //send email too + if (data.userEmail) + await this.emailService.sendAnnouncementEmail({ + userEmail: data.userEmail, + userPhone: data.userPhone, + title: NotificationMessage.BLOCK_SERVICE, + description: message, + date: new Date(), + }); } return this.createNotification( { title: NotificationMessage.BLOCK_SERVICE, type: NotifType.BLOCK_SERVICE, message, recipientId }, @@ -365,29 +374,56 @@ export class NotificationsService { ); } - // async createServiceNotification(recipientId: string, sendNotifData: ISendNotificationData) { - // const message = NotificationMessage.CREATE_SERVICE_MESSAGE.replace("[serviceName]", sendNotifData.serviceName); - // //sent notification based on the userSettings - // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.CREATE_SERVICE, recipientId); - // if (isActive) { - // await this.smsService.sendCreateServiceSms(sendNotifData.userPhone, sendNotifData.serviceName); - // //send email too - // } - // return this.createNotification({ title: NotificationMessage.CREATE_SERVICE, message, recipientId }); - // } - // //************************ */ - // async createUnblockServiceNotification(recipientId: string, sendNotifData: ISendNotificationData) { - // const message = NotificationMessage.UNBLOCK_SERVICE_MESSAGE.replace("[serviceName]", sendNotifData.serviceName); - // //sent notification based on the userSettings - // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.UNBLOCK_SERVICE, recipientId); - // if (isActive) { - // await this.smsService.sendUnblockServiceSms(sendNotifData.userPhone, sendNotifData.serviceName); - // //send email too - // } - // return this.createNotification({ title: NotificationMessage.UNBLOCK_SERVICE, message, recipientId }); - // } + async createPaymentReminderNotification(recipientId: string, data: IPaymentNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.PAYMENT_REMINDER_MESSAGE.replace("[amount]", numberFormat(data.amount)); + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.PAYMENT_REMINDER, + recipientId, + queryRunner, + ); + + if (isActive) { + await this.smsService.sendPaymentReminderSms(data.userPhone, numberFormat(data.amount)); + if (data.userEmail) await this.emailService.sendPaymentReminderEmail(data); + } + + return this.createNotification( + { + title: NotificationMessage.PAYMENT_REMINDER, + type: NotifType.PAYMENT_REMINDER, + message, + recipientId, + }, + queryRunner, + ); + } // //************************ */ + + async createPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.PAYMENT_CANCELLATION_MESSAGE.replace("[amount]", numberFormat(data.amount)); + + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.PAYMENT_CANCELLATION, + recipientId, + queryRunner, + ); + + if (isActive) { + await this.smsService.sendPaymentCancellationSms(data.userPhone, numberFormat(data.amount)); + if (data.userEmail) await this.emailService.sendPaymentCancellationEmail(data); + } + + return this.createNotification( + { + title: NotificationMessage.PAYMENT_CANCELLATION, + type: NotifType.PAYMENT_CANCELLATION, + message, + recipientId, + }, + queryRunner, + ); + } } diff --git a/src/modules/payments/constants/index.ts b/src/modules/payments/constants/index.ts index c5b69f1..3c109c8 100755 --- a/src/modules/payments/constants/index.ts +++ b/src/modules/payments/constants/index.ts @@ -1,6 +1,19 @@ export const ZARINPAL_CONFIG = "ZARINPAL_CONFIG"; +//=============================================== export const PAYMENT = Object.freeze({ - PAYMENT_QUEUE_NAME: "payment", - PAYMENT_START: "payment_start", + QUEUE_NAME: "payment", + START: "payment_start", + FIRST_REMINDER: "payment_first_reminder", + SECOND_REMINDER: "payment_second_reminder", + CANCELLATION: "payment_cancellation", + + JOB_PRIORITY: 1, // high priority + JOB_ATTEMPTS: 3, // 3 times + JOB_BACKOFF: 5 * 1000, // ms + JOB_TIMEOUT: 10000, // timeout after 10 seconds + START_DELAY: 1 * 60 * 1000, // 1 minute delay in milliseconds + FIRST_REMINDER_DELAY: 5 * 60 * 1000, // 5 minutes + SECOND_REMINDER_DELAY: 10 * 60 * 1000, // 10 minutes + CANCELLATION_DELAY: 15 * 60 * 1000, // 15 minutes }); diff --git a/src/modules/payments/factories/payment.factory.ts b/src/modules/payments/factories/payment.factory.ts index 179476f..34046af 100755 --- a/src/modules/payments/factories/payment.factory.ts +++ b/src/modules/payments/factories/payment.factory.ts @@ -3,26 +3,32 @@ import { BadGatewayException, Injectable } from "@nestjs/common"; import { PaymentMessage } from "../../../common/enums/message.enum"; import { GatewayEnum } from "../enums/gateway.enum"; import { ZarinpalGateway } from "../gateways/zarinpal.gateway"; +import { IPaymentGateway, IPaymentGatewayFactory } from "../interfaces/IPayment"; import { GatewayType } from "../types/gateway.type"; @Injectable() -export class PaymentGatewayFactory { - constructor(private readonly ZarinpalGateway: ZarinpalGateway) {} +export class PaymentGatewayFactory implements IPaymentGatewayFactory { + private readonly gateways: Map; - //************************ * - getPaymentGateway(provider: GatewayType) { - switch (provider) { - case "zarinpal": - return this.ZarinpalGateway; - - default: - throw new BadGatewayException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND); - } + constructor(private readonly zarinpalGateway: ZarinpalGateway) { + this.gateways = new Map(); + this.initializeGateways(); + } + + private initializeGateways(): void { + this.gateways.set(GatewayEnum.ZARINPAL, this.zarinpalGateway); + } + + getPaymentGateway(provider: GatewayType): IPaymentGateway { + const gateway = this.gateways.get(provider); + if (!gateway) { + throw new BadGatewayException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND); + } + return gateway; } - //************************ */ getAvailablePaymentGateways() { - const gateways = [{ name: GatewayEnum.ZARINPAL }]; + const gateways = Array.from(this.gateways.keys()).map((name) => ({ name: name as GatewayEnum })); return { gateways }; } } diff --git a/src/modules/payments/interfaces/IPayment.ts b/src/modules/payments/interfaces/IPayment.ts index 07a9913..b198acc 100755 --- a/src/modules/payments/interfaces/IPayment.ts +++ b/src/modules/payments/interfaces/IPayment.ts @@ -2,6 +2,11 @@ import Decimal from "decimal.js"; +import { Payment } from "../entities/payment.entity"; +import { GatewayEnum } from "../enums/gateway.enum"; +import { PaymentStatus } from "../enums/payment-status.enum"; +import { GatewayType } from "../types/gateway.type"; + export interface IProcessPaymentParams { amount: number; description: string; @@ -18,9 +23,9 @@ export interface IProcessPaymentData { //------------------ Verify payment ------------------------- -//TODO: Define the IPaymentVerifyData interface -export interface IPaymentVerifyData { - [x: string]: unknown; +export interface IVerifyPayment { + code: number; + ref_id: number; } export interface IPaymentVerifyParams { @@ -73,15 +78,27 @@ export interface ZarinPalPGVerifyData { error: string[]; } +export interface PaymentVerificationResult { + status: PaymentStatus; + payment: Payment; + redirectUrl?: URL; + additionalParams?: Record; +} + //********************************************** */ //------------------abstract class---------------- export interface IPaymentGateway { processPayment(processPaymentParam: IProcessPaymentParams): Promise; - verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; + verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; } // export abstract class PaymentGateway { // abstract processPayment(processPaymentParam: IProcessPaymentParams): Promise; // abstract verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise; // } + +export interface IPaymentGatewayFactory { + getPaymentGateway(provider: GatewayType): IPaymentGateway; + getAvailablePaymentGateways(): { gateways: Array<{ name: GatewayEnum }> }; +} diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index cf6cb3c..5844df6 100755 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -24,7 +24,7 @@ import { ReferralsModule } from "../referrals/referrals.module"; @Module({ imports: [ TypeOrmModule.forFeature([Payment, PaymentGateway, BankAccount, DepositRequest]), - BullModule.registerQueue({ name: PAYMENT.PAYMENT_QUEUE_NAME }), + BullModule.registerQueue({ name: PAYMENT.QUEUE_NAME }), WalletsModule, NotificationModule, ReferralsModule, diff --git a/src/modules/payments/providers/payments.service.ts b/src/modules/payments/providers/payments.service.ts index a2f1dc7..34123fd 100755 --- a/src/modules/payments/providers/payments.service.ts +++ b/src/modules/payments/providers/payments.service.ts @@ -1,10 +1,10 @@ import { InjectQueue } from "@nestjs/bullmq"; -import { BadRequestException, HttpException, HttpStatus, Injectable, InternalServerErrorException } from "@nestjs/common"; +import { BadRequestException, HttpException, HttpStatus, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Queue } from "bullmq"; import dayjs from "dayjs"; import Decimal from "decimal.js"; -import { FastifyReply } from "fastify"; +import { FastifyReply as FRply } from "fastify"; import { DataSource, Not, QueryRunner } from "typeorm"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; @@ -30,17 +30,19 @@ import { PaymentGateway } from "../entities/payment-gateway.entity"; import { Payment } from "../entities/payment.entity"; import { DepositRequestStatus } from "../enums/deposit-request-status.enum"; import { PaymentStatus } from "../enums/payment-status.enum"; +import { TransferType } from "../enums/payment-type.enum"; import { PaymentGatewayFactory } from "../factories/payment.factory"; +import { IVerifyPayment } from "../interfaces/IPayment"; import { BankAccountsRepository } from "../repositories/bank-accounts.repository"; import { DepositRequestsRepository } from "../repositories/deposit-requests.repository"; import { PaymentGatewaysRepository } from "../repositories/payment-gateway.repository"; import { PaymentsRepository } from "../repositories/payments.repository"; import { GatewayType } from "../types/gateway.type"; - @Injectable() export class PaymentsService { + private readonly logger = new Logger(PaymentsService.name); constructor( - @InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue, + @InjectQueue(PAYMENT.QUEUE_NAME) private readonly paymentQueue: Queue, private readonly notificationsService: NotificationsService, private readonly configService: ConfigService, private readonly gatewayFactory: PaymentGatewayFactory, @@ -53,19 +55,17 @@ export class PaymentsService { private dataSource: DataSource, ) {} - //*********************************** */ + //=============================================== async chargeWalletWithGateway(chargeDto: GatewayDepositDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); + try { const { amount, gatewayId } = chargeDto; - const user = await queryRunner.manager.findOneBy(User, { id: userId }); - if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); - - const paymentGateway = await queryRunner.manager.findOneBy(PaymentGateway, { id: gatewayId }); - if (!paymentGateway) throw new BadRequestException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND); + const user = await this.getUserById(userId, queryRunner); + const paymentGateway = await this.getPaymentGatewayById(gatewayId, queryRunner); const gatewayData = await this.processPayment(paymentGateway.name, amount, WalletMessage.DEPOSIT_WALLET_IPG, user.email, user.phone); @@ -84,32 +84,20 @@ export class PaymentsService { await queryRunner.release(); } } + //=============================================== - //*********************************** */ async chargeWalletWithTransfer(depositDto: TransferDepositDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); - const { amount, bankAccountId, method, transferReceiptUrl } = depositDto; + try { - const bankAccount = await queryRunner.manager.findOneBy(BankAccount, { id: bankAccountId }); - if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); + const { amount, bankAccountId, method, transferReceiptUrl } = depositDto; - const user = await queryRunner.manager.findOneBy(User, { id: userId }); - if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + const { bankAccount } = await this.getBankAccountById(bankAccountId); + const user = await this.getUserById(userId, queryRunner); - // if (method === TransferType.GATEWAY) throw new BadRequestException(PaymentMessage.TRANSFER_METHOD_NOT_ALLOWED); - - // - const depositRequest = queryRunner.manager.create(DepositRequest, { - bankAccount, - user, - receiptUrl: transferReceiptUrl, - amount, - type: method, - }); - - await queryRunner.manager.save(DepositRequest, depositRequest); + const depositRequest = this.createDepositRequest(bankAccount, user, transferReceiptUrl, amount, method, queryRunner); await queryRunner.commitTransaction(); return { @@ -118,101 +106,267 @@ export class PaymentsService { }; } catch (error) { await queryRunner.rollbackTransaction(); - throw error; } finally { await queryRunner.release(); } } - //*********************************** */ - //*********************************** */ + //=============================================== + async processPayment(provider: GatewayType, amount: number, description: string, email?: string, mobile?: string) { const paymentGateway = this.gatewayFactory.getPaymentGateway(provider); return paymentGateway.processPayment({ amount, description, email, mobile }); } + //=============================================== - //*********************************** */ - //*********************************** */ - async verifyPayment(gateway: GatewayType, queryDto: VerifyQueryDto, rep: FastifyReply) { - const frontUrl = new URL(this.configService.getOrThrow("SITE_URL")); - frontUrl.pathname = "/payment"; + async verifyPayment(gateway: GatewayType, queryDto: VerifyQueryDto, rep: FRply) { + const frontUrl = this.buildFrontendRedirectUrl(); + const queryRunner = this.dataSource.createQueryRunner(); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const paymentGateway = this.gatewayFactory.getPaymentGateway(gateway); + const payment = await this.getPaymentByReference(queryDto.Authority, queryRunner); + + if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE); + + if (queryDto.Status !== "OK") await this.handleFailedPaymentRedirect(payment, queryRunner, rep, frontUrl); + + const verifyData = await paymentGateway.verifyPayment({ + reference: queryDto.Authority, + amount: payment.amount, + }); + + return await this.handlePaymentVerificationWithResponse(verifyData, payment, rep, frontUrl, queryRunner); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + //=============================================== + + async handlePaymentVerificationWithResponse(verifyData: IVerifyPayment, payment: Payment, rp: FRply, frontUrl: URL, qryRnr: QueryRunner) { + const result = await this.processVerificationResult(verifyData, payment, qryRnr); + + this.addPaymentParamsToUrl(frontUrl, result.payment, result.status); + + if ("additionalParams" in result) { + Object.entries(result.additionalParams).forEach(([key, value]) => { + frontUrl.searchParams.append(key, String(value)); + }); + } + + await qryRnr.commitTransaction(); + + return rp.status(HttpStatus.FOUND).redirect(frontUrl.toString()); + } + //=============================================== + + async processVerificationResult(verifyData: IVerifyPayment, payment: Payment, qryRnr: QueryRunner) { + if (verifyData.code === 100) { + return this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr); + } else if (verifyData.code === 101) { + return { status: PaymentStatus.PENDING, payment }; + } else { + await this.handleFailedPayment(payment.id, qryRnr); + return { status: PaymentStatus.FAILED, payment }; + } + } + //=============================================== + private async processSuccessfulVerification(payment: Payment, refId: number, queryRunner: QueryRunner) { + // Regular wallet charge + const { wallet } = await this.handleSuccessfulPayment(payment, payment.user.id, payment.amount, refId, queryRunner); + + //for future use + const additionalParams: Record = {}; + + // + await this.notificationsService.createWalletChargeNotification( + payment.user.id, + { + amount: new Decimal(payment.amount).toNumber(), + balance: new Decimal(wallet.balance).toNumber(), + date: payment.createdAt, + reason: WalletMessage.DEPOSIT_WALLET_IPG, + userPhone: payment.user.phone, + userEmail: payment.user.email, + }, + queryRunner, + ); + + return { + status: PaymentStatus.COMPLETED, + payment, + additionalParams, + }; + } + //=============================================== + + async createGatewayPaymentForUser(usr: User, amt: number, ref_Id: string, gatewayId: string, qryRnr: QueryRunner) { + const payment = qryRnr.manager.create(Payment, { + amount: amt, + reference: ref_Id, + user: usr, + paymentGateway: { id: gatewayId }, + }); + + await qryRnr.manager.save(Payment, payment); + this.logger.log(`Created payment ${payment.id} with reference ${ref_Id}`); + + await this.addPaymentReminderToQueue(payment); + return payment; + } + //=============================================== + async getAvailableGateways() { + const paymentGateways = await this.paymentGatewaysRepository.find({ where: { isActive: true } }); + return { paymentGateways }; + } + + //=============================================== + + async getPaymentWithReference(reference: string) { + const payment = await this.paymentsRepository.findOneWithReference(reference); + if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF); + + return { payment }; + } + + //=============================================== + async handleFailedPayment(paymentId: string, queryRunner: QueryRunner) { + await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.CANCELLED }); + } + + //=============================================== + async handleSuccessfulPayment(payment: Payment, userId: string, amount: Decimal, transactionId: number, queryRunner: QueryRunner) { + await queryRunner.manager.update( + Payment, + { id: payment.id }, + { status: PaymentStatus.COMPLETED, transactionId: transactionId.toString() }, + ); + + await this.handleReferralReward(payment, queryRunner); + return this.walletsService.createPaymentTransaction(userId, amount, payment, queryRunner); + } + + //=============================================== + + async addBankAccount(createDto: CreateBankAccountDto) { + await this.validateBankAccountDetails(createDto); + + const bankAccount = this.bankAccountsRepository.create({ ...createDto }); + await this.bankAccountsRepository.save(bankAccount); + + return { + message: CommonMessage.CREATED, + bankAccount, + }; + } + //=============================================== + + async updateBankAccount(updateDto: UpdateBankAccountDto, bankAccountId: string) { + const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId }); + if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); + + await this.validateBankAccountUpdate(updateDto, bankAccountId); + await this.bankAccountsRepository.save({ ...bankAccount, ...updateDto }); + + return { + message: CommonMessage.UPDATED, + }; + } + //=============================================== + + async getBankAccounts(isAdmin: boolean) { + const bankAccounts = isAdmin + ? await this.bankAccountsRepository.find({ where: { isActive: true } }) + : await this.bankAccountsRepository.find(); + + return { bankAccounts }; + } + + //=============================================== + + async getBankAccountById(bankAccountId: string) { + const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId }); + if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); + + return { + bankAccount, + }; + } + + //=============================================== + + async getTransactions(queryDto: SearchTransactionQueryDto) { + return this.walletsService.getTransactionsForAdmin(queryDto); + } + //=============================================== + + async getDepositRequests(queryDto: PaymentTransactionQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + const { type } = queryDto; + + const queryBuilder = this.buildDepositRequestsQuery(skip, limit, type); + const [depositRequests, count] = await queryBuilder.getManyAndCount(); + + return { + depositRequests, + count, + paginate: true, + }; + } + //=============================================== + + async getDepositGatewayPayment(queryDto: PaginationDto) { + const { limit, skip } = PaginationUtils(queryDto); + + const queryBuilder = this.buildDepositGatewayPaymentsQuery(skip, limit); + const [payments, count] = await queryBuilder.getManyAndCount(); + + return { + payments, + count, + paginate: true, + }; + } + + //=============================================== + + async getDepositRequestById(depositId: string) { + const depositRequest = await this.depositRequestsRepository.findOneBy({ id: depositId }); + if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); + + return { + depositRequest, + }; + } + + //=============================================== + async approverDepositRequest(depositId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { - const paymentGateway = this.gatewayFactory.getPaymentGateway(gateway); - // const payment = await queryRunner.manager.findOne(Payment, { - // where: { reference: queryDto.Authority }, - // relations: ["user"], - // }); + const depositRequest = await this.getDepositRequestWithLock(depositId, queryRunner); - const payment = await queryRunner.manager - .createQueryBuilder(Payment, "payment") - .innerJoinAndSelect("payment.user", "user") - .where("payment.reference = :reference", { reference: queryDto.Authority }) - .setLock("pessimistic_write") - .getOne(); - - if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF); - if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE); - - if (queryDto.Status !== "OK") { - await this.handleFailedPayment(payment.id, queryRunner); - await queryRunner.commitTransaction(); - frontUrl.searchParams.append("status", PaymentStatus.FAILED); - frontUrl.searchParams.append("id", payment.id); - frontUrl.searchParams.append("date", payment.createdAt.toISOString()); - frontUrl.searchParams.append("amount", payment.amount.toString()); - - return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString()); + if (depositRequest.status === DepositRequestStatus.APPROVED) { + throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_APPROVED); } - const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount }); + await this.updateDepositRequestStatus(depositRequest.id, DepositRequestStatus.APPROVED, null, queryRunner); - if (verifyData.code === 100) { - const { wallet } = await this.handleSuccessfulPayment( - payment, - payment.user.id, - payment.amount, - `${verifyData.ref_id}`, - queryRunner, - ); - await queryRunner.commitTransaction(); - frontUrl.searchParams.append("status", PaymentStatus.COMPLETED); - frontUrl.searchParams.append("id", payment.id); - frontUrl.searchParams.append("date", payment.createdAt.toISOString()); - frontUrl.searchParams.append("amount", payment.amount.toString()); - // - await this.notificationsService.createWalletChargeNotification( - payment.user.id, - { - amount: new Decimal(payment.amount).toNumber(), - balance: new Decimal(wallet.balance).toNumber(), - date: payment.createdAt, - reason: WalletMessage.DEPOSIT_WALLET_IPG, - userPhone: payment.user.phone, - userEmail: payment.user.email, - }, - queryRunner, - ); - } else if (verifyData.code === 101) { - await queryRunner.commitTransaction(); - frontUrl.searchParams.append("status", PaymentStatus.PENDING); - frontUrl.searchParams.append("id", payment.id); - frontUrl.searchParams.append("date", payment.createdAt.toISOString()); - frontUrl.searchParams.append("amount", payment.amount.toString()); - } else { - await this.handleFailedPayment(payment.id, queryRunner); - await queryRunner.commitTransaction(); - frontUrl.searchParams.append("status", PaymentStatus.FAILED); - frontUrl.searchParams.append("id", payment.id); - frontUrl.searchParams.append("date", payment.createdAt.toISOString()); - frontUrl.searchParams.append("amount", payment.amount.toString()); - } + await this.walletsService.createDepositTransaction(depositRequest.user.id, depositRequest.amount, depositRequest, queryRunner); - return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString()); + await queryRunner.commitTransaction(); + return { + message: PaymentMessage.DEPOSIT_APPROVED, + depositRequest: depositRequest.id, + }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; @@ -221,43 +375,49 @@ export class PaymentsService { } } - //*********************************** */ - //*********************************** */ - async createGatewayPaymentForUser(user: User, amount: number, reference: string, gatewayId: string, queryRunner: QueryRunner) { - // - const payment = queryRunner.manager.create(Payment, { - amount, - reference, - user, - paymentGateway: { id: gatewayId }, - }); - // - await queryRunner.manager.save(Payment, payment); - this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 }); + //=============================================== + async rejectDepositRequest(depositId: string, rejectDto: RejectDepositRequestDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - return payment; - } - //*********************************** */ - //*********************************** */ - async getAvailableGateways() { - const paymentGateways = await this.paymentGatewaysRepository.find({ where: { isActive: true } }); - return { paymentGateways }; - } - //*********************************** */ - //*********************************** */ - async getPaymentWithReference(reference: string) { - const payment = await this.paymentsRepository.findOneWithReference(reference); - if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF); + try { + const depositRequest = await queryRunner.manager.findOne(DepositRequest, { + where: { id: depositId }, + lock: { mode: "pessimistic_write" }, + }); - return { payment }; - } + if (!depositRequest) { + throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); + } - //*********************************** */ - //*********************************** */ - async handleFailedPayment(paymentId: string, queryRunner: QueryRunner) { - await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.CANCELLED }); + if (depositRequest.status === DepositRequestStatus.REJECTED) { + throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_REJECTED); + } + + await this.updateDepositRequestStatus(depositRequest.id, DepositRequestStatus.REJECTED, rejectDto.comment, queryRunner); + + await queryRunner.commitTransaction(); + return { + message: PaymentMessage.DEPOSIT_REJECTED, + depositRequest, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + //---------------------------------------- + //-private methods + //---------------------------------------- + + private buildFrontendRedirectUrl(): URL { + const frontUrl = new URL(this.configService.getOrThrow("SITE_URL")); + frontUrl.pathname = "/payment"; + return frontUrl; } - //*********************************** */ private async isFirstPurchase(userId: string, queryRunner: QueryRunner) { const paymentCount = await queryRunner.manager.count(Payment, { @@ -266,7 +426,7 @@ export class PaymentsService { return paymentCount === 0; } - //*********************************** */ + //=============================================== private async handleReferralReward(payment: Payment, queryRunner: QueryRunner) { const isFirstPurchase = await this.isFirstPurchase(payment.user.id, queryRunner); @@ -293,87 +453,100 @@ export class PaymentsService { referral.completedAt = dayjs().toDate(); await queryRunner.manager.save(referral); } - //*********************************** */ + //=============================================== - async handleSuccessfulPayment(payment: Payment, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) { - await queryRunner.manager.update(Payment, { id: payment.id }, { status: PaymentStatus.COMPLETED, transactionId }); - const result = await this.walletsService.createPaymentTransaction(userId, amount, payment, queryRunner); + private async handleFailedPaymentRedirect(payment: Payment, queryRunner: QueryRunner, rep: FRply, frontUrl: URL) { + await this.handleFailedPayment(payment.id, queryRunner); + await queryRunner.commitTransaction(); - // Handle referral reward if applicable - await this.handleReferralReward(payment, queryRunner); + this.addPaymentParamsToUrl(frontUrl, payment, PaymentStatus.FAILED); - return result; + return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString()); } - //*********************************** */ - async addBankAccount(createDto: CreateBankAccountDto) { - const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: createDto.cardNumber }); - if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST); - // - const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: createDto.IBan }); - if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST); - // - const bankAccount = this.bankAccountsRepository.create({ ...createDto }); - await this.bankAccountsRepository.save(bankAccount); - - return { - message: CommonMessage.CREATED, - bankAccount, - }; + //=============================================== + private addPaymentParamsToUrl(url: URL, payment: Payment, status: PaymentStatus) { + url.searchParams.append("status", status); + url.searchParams.append("id", payment.id); + url.searchParams.append("date", payment.createdAt.toISOString()); + url.searchParams.append("amount", payment.amount.toString()); } - //*********************************** */ - async updateBankAccount(updateDto: UpdateBankAccountDto, bankAccountId: string) { - const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId }); - if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); + //=============================================== - if (updateDto.cardNumber) { - const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: updateDto.cardNumber, id: Not(bankAccountId) }); + private async getUserById(userId: string, queryRunner: QueryRunner): Promise { + const user = await queryRunner.manager.findOneBy(User, { id: userId }); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + return user; + } + //=============================================== + + private async getPaymentGatewayById(gatewayId: string, queryRunner: QueryRunner): Promise { + const paymentGateway = await queryRunner.manager.findOneBy(PaymentGateway, { id: gatewayId }); + if (!paymentGateway) throw new BadRequestException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND); + return paymentGateway; + } + + //=============================================== + private createDepositRequest(bnkAcc: BankAccount, usr: User, recUrl: string, amt: number, typ: TransferType, qryRnr: QueryRunner) { + const depositRequest = qryRnr.manager.create(DepositRequest, { + bankAccount: bnkAcc, + user: usr, + receiptUrl: recUrl, + amount: amt, + type: typ, + }); + + qryRnr.manager.save(DepositRequest, depositRequest); + return depositRequest; + } + //=============================================== + + private async getPaymentByReference(reference: string, queryRunner: QueryRunner): Promise { + const payment = await queryRunner.manager + .createQueryBuilder(Payment, "payment") + .innerJoinAndSelect("payment.user", "user") + .where("payment.reference = :reference", { reference }) + .setLock("pessimistic_write") + .getOne(); + + if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF); + return payment; + } + + //=============================================== + private async validateBankAccountDetails(accountDetails: { cardNumber?: string; IBan?: string }) { + if (accountDetails.cardNumber) { + const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: accountDetails.cardNumber }); if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST); } - // - if (updateDto.IBan) { - const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: updateDto.IBan, id: Not(bankAccountId) }); + + if (accountDetails.IBan) { + const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: accountDetails.IBan }); if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST); } - - await this.bankAccountsRepository.save({ ...bankAccount, ...updateDto }); } - //*********************************** */ - async getBankAccounts(isAdmin: boolean) { - let bankAccounts: BankAccount[]; - if (isAdmin) { - bankAccounts = await this.bankAccountsRepository.find({ where: { isActive: true } }); - } else { - bankAccounts = await this.bankAccountsRepository.find(); + //=============================================== + private async validateBankAccountUpdate(updateDto: UpdateBankAccountDto, bankAccountId: string) { + if (updateDto.cardNumber) { + const existCardNumber = await this.bankAccountsRepository.findOneBy({ + cardNumber: updateDto.cardNumber, + id: Not(bankAccountId), + }); + if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST); } - return { - bankAccounts, - }; + if (updateDto.IBan) { + const existIBan = await this.bankAccountsRepository.findOneBy({ + IBan: updateDto.IBan, + id: Not(bankAccountId), + }); + if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST); + } } - //*********************************** */ - async getBankAccountById(bankAccountId: string) { - const bankAccount = await this.bankAccountsRepository.findOneBy({ id: bankAccountId }); - if (!bankAccount) throw new BadRequestException(PaymentMessage.BANK_ACCOUNT_NOT_FOUND); - - return { - bankAccount, - }; - } - - // async deleteBankAccount() - //*********************************** */ - async getTransactions(queryDto: SearchTransactionQueryDto) { - return this.walletsService.getTransactionsForAdmin(queryDto); - } - - //*********************************** */ - async getDepositRequests(queryDto: PaymentTransactionQueryDto) { - const { limit, skip } = PaginationUtils(queryDto); - const { type } = queryDto; - + //=============================================== + private buildDepositRequestsQuery(skip: number, limit: number, type?: TransferType) { const queryBuilder = this.depositRequestsRepository .createQueryBuilder("depositRequest") .leftJoinAndSelect("depositRequest.user", "user") @@ -386,117 +559,53 @@ export class PaymentsService { queryBuilder.andWhere("depositRequest.type = :type", { type }); } - const [depositRequests, count] = await queryBuilder.getManyAndCount(); - - return { - depositRequests, - count, - paginate: true, - }; + return queryBuilder; } - //*********************************** */ - async getDepositGatewayPayment(queryDto: PaginationDto) { - const { limit, skip } = PaginationUtils(queryDto); - - const queryBuilder = this.paymentsRepository + //=============================================== + private buildDepositGatewayPaymentsQuery(skip: number, limit: number) { + return this.paymentsRepository .createQueryBuilder("payment") .leftJoinAndSelect("payment.user", "user") .leftJoinAndSelect("payment.paymentGateway", "paymentGateway") .orderBy("payment.createdAt", "DESC") .skip(skip) .take(limit); - - const [payments, count] = await queryBuilder.getManyAndCount(); - - return { - payments, - count, - paginate: true, - }; } - //*********************************** */ + //=============================================== + private async getDepositRequestWithLock(depositId: string, queryRunner: QueryRunner) { + const depositRequest = await queryRunner.manager + .createQueryBuilder(DepositRequest, "deposit") + .innerJoinAndSelect("deposit.user", "user") + .where("deposit.id = :id", { id: depositId }) + .setLock("pessimistic_write") + .getOne(); - async getDepositRequestById(depositId: string) { - const depositRequest = await this.depositRequestsRepository.findOneBy({ id: depositId }); if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); - - return { - depositRequest, - }; + return depositRequest; } - //*********************************** */ - async approverDepositRequest(depositId: string) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - try { - // const depositRequest = await queryRunner.manager.findOne(DepositRequest, { - // where: { id: depositId }, - // lock: { mode: "pessimistic_write" }, - // }); - - const depositRequest = await queryRunner.manager - .createQueryBuilder(DepositRequest, "deposit") - .innerJoinAndSelect("deposit.user", "user") - .where("deposit.id = :id", { id: depositId }) - .setLock("pessimistic_write") - .getOne(); - - if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); - - if (depositRequest.status === DepositRequestStatus.APPROVED) throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_APPROVED); - - await queryRunner.manager.update(DepositRequest, { id: depositRequest.id }, { status: DepositRequestStatus.APPROVED }); - - await this.walletsService.createDepositTransaction(depositRequest.user.id, depositRequest.amount, depositRequest, queryRunner); - - await queryRunner.commitTransaction(); - return { - message: PaymentMessage.DEPOSIT_APPROVED, - depositRequest: depositRequest.id, - }; - } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); + //=============================================== + private async updateDepositRequestStatus(depositId: string, status: DepositRequestStatus, comment: string | null, qryRnr: QueryRunner) { + const updateData: Partial = { status }; + if (comment !== null) { + updateData.comment = comment; } + + await qryRnr.manager.update(DepositRequest, { id: depositId }, updateData); } - - //*********************************** */ - async rejectDepositRequest(depositId: string, rejectDto: RejectDepositRequestDto) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - try { - const depositRequest = await queryRunner.manager.findOne(DepositRequest, { - where: { id: depositId }, - lock: { mode: "pessimistic_write" }, - }); - if (!depositRequest) throw new BadRequestException(PaymentMessage.DEPOSIT_NOT_FOUND); - - if (depositRequest.status === DepositRequestStatus.REJECTED) throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_REJECTED); - - await queryRunner.manager.update( - DepositRequest, - { id: depositRequest.id }, - { status: DepositRequestStatus.REJECTED, comment: rejectDto.comment }, - ); - - await queryRunner.commitTransaction(); - - return { - message: PaymentMessage.DEPOSIT_REJECTED, - depositRequest, - }; - } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); - } + //=============================================== + private async addPaymentReminderToQueue(payment: Payment) { + this.paymentQueue.add( + PAYMENT.START, + { payment }, + { + delay: PAYMENT.START_DELAY, + priority: PAYMENT.JOB_PRIORITY, + attempts: PAYMENT.JOB_ATTEMPTS, + backoff: { type: "exponential", delay: PAYMENT.JOB_BACKOFF }, + }, + ); } } diff --git a/src/modules/payments/queue/payment.processor.ts b/src/modules/payments/queue/payment.processor.ts index 27fc2dc..d8836ce 100755 --- a/src/modules/payments/queue/payment.processor.ts +++ b/src/modules/payments/queue/payment.processor.ts @@ -1,16 +1,239 @@ -import { Processor } from "@nestjs/bullmq"; +import { InjectQueue, Processor } from "@nestjs/bullmq"; import { Logger } from "@nestjs/common"; -import { Job } from "bullmq"; +import { Job, Queue } from "bullmq"; +import Decimal from "decimal.js"; +import { DataSource, QueryRunner } from "typeorm"; import { WorkerProcessor } from "../../../common/queues/worker.processor"; +import { NotificationsService } from "../../notifications/providers/notifications.service"; import { PAYMENT } from "../constants"; +import { Payment } from "../entities/payment.entity"; +import { PaymentStatus } from "../enums/payment-status.enum"; +import { PaymentGatewayFactory } from "../factories/payment.factory"; +import { PaymentsService } from "../providers/payments.service"; +import { PaymentJobData } from "../types/payment-job.type"; -@Processor(PAYMENT.PAYMENT_QUEUE_NAME) +@Processor(PAYMENT.QUEUE_NAME) export class PaymentProcessor extends WorkerProcessor { protected readonly logger = new Logger(PaymentProcessor.name); - process(job: Job, token?: string) { - this.logger.log(job, token); - return Promise.resolve("test"); + constructor( + @InjectQueue(PAYMENT.QUEUE_NAME) private readonly paymentQueue: Queue, + private readonly gatewayFactory: PaymentGatewayFactory, + private readonly dataSource: DataSource, + private readonly notificationsService: NotificationsService, + private readonly paymentsService: PaymentsService, + ) { + super(); + } + //=============================================== + async process(job: Job) { + this.logger.log(`Processing payment job: ${job.name}`); + + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const result = await this.processJobWithTransaction(job, queryRunner); + + await queryRunner.commitTransaction(); + return result; + } catch (error) { + this.logger.error(`Error processing payment job: ${error}`); + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + //=============================================== + + private async processJobWithTransaction(job: Job, queryRunner: QueryRunner) { + switch (job.name) { + case PAYMENT.START: + return this.handlePaymentStart(job as Job<{ payment: Payment }>, queryRunner); + case PAYMENT.FIRST_REMINDER: + return this.handleFirstReminder(job as Job<{ paymentId: string }>, queryRunner); + case PAYMENT.SECOND_REMINDER: + return this.handleSecondReminder(job as Job<{ paymentId: string }>, queryRunner); + case PAYMENT.CANCELLATION: + return this.handlePaymentCancellation(job as Job<{ paymentId: string }>, queryRunner); + default: + this.logger.error(`Unknown job name: ${job.name}`); + return; + } + } + //=============================================== + + private async handlePaymentStart(job: Job<{ payment: Payment }>, _queryRunner: QueryRunner) { + const { payment } = job.data; + + await this.scheduleFirstReminder(payment.id); + return payment; + } + //=============================================== + + private async handleFirstReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) { + const payment = await this.fetchPayment(job.data.paymentId, queryRunner); + + if (!this.isPaymentPending(payment, queryRunner)) { + this.logger.log(`Payment ${payment.id} is no longer pending, skipping reminder`); + return payment; + } + + await this.sendPaymentReminderNotification(payment, "first", queryRunner); + await this.scheduleSecondReminder(payment.id); + + return payment; + } + //=============================================== + + private async handleSecondReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) { + const payment = await this.fetchPayment(job.data.paymentId, queryRunner); + + if (!this.isPaymentPending(payment, queryRunner)) { + this.logger.log(`Payment ${payment.id} is no longer pending, skipping reminder`); + return payment; + } + + await this.sendPaymentReminderNotification(payment, "second", queryRunner); + await this.schedulePaymentCancellation(payment.id); + + return payment; + } + //=============================================== + + private async handlePaymentCancellation(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) { + const payment = await this.fetchPayment(job.data.paymentId, queryRunner); + + if (!this.isPaymentPending(payment, queryRunner)) { + this.logger.log(`Payment ${payment.id} is no longer pending, skipping cancellation`); + return payment; + } + + await this.cancelPayment(payment, queryRunner); + + return payment; + } + //=============================================== + private async scheduleFirstReminder(paymentId: string) { + await this.paymentQueue.add( + PAYMENT.FIRST_REMINDER, + { paymentId }, + { + delay: PAYMENT.FIRST_REMINDER_DELAY, + priority: PAYMENT.JOB_PRIORITY, + attempts: PAYMENT.JOB_ATTEMPTS, + backoff: { type: "exponential", delay: PAYMENT.JOB_BACKOFF }, + }, + ); + } + //=============================================== + + private async scheduleSecondReminder(paymentId: string) { + await this.paymentQueue.add( + PAYMENT.SECOND_REMINDER, + { paymentId }, + { + delay: PAYMENT.SECOND_REMINDER_DELAY, + priority: PAYMENT.JOB_PRIORITY, + attempts: PAYMENT.JOB_ATTEMPTS, + backoff: { type: "exponential", delay: PAYMENT.JOB_BACKOFF }, + }, + ); + } + //=============================================== + + private async schedulePaymentCancellation(paymentId: string) { + await this.paymentQueue.add( + PAYMENT.CANCELLATION, + { paymentId }, + { + delay: PAYMENT.CANCELLATION_DELAY, + priority: PAYMENT.JOB_PRIORITY, + attempts: PAYMENT.JOB_ATTEMPTS, + backoff: { type: "exponential", delay: PAYMENT.JOB_BACKOFF }, + }, + ); + } + //=============================================== + + private async cancelPayment(payment: Payment, queryRunner: QueryRunner) { + await queryRunner.manager.update(Payment, { id: payment.id }, { status: PaymentStatus.CANCELLED }); + await this.sendPaymentCancellationNotification(payment, queryRunner); + } + + //=============================================== + + private async fetchPayment(paymentId: string, queryRunner: QueryRunner): Promise { + const payment = await queryRunner.manager.findOne(Payment, { + where: { id: paymentId }, + relations: { user: true }, + }); + + if (!payment) throw new Error(`Payment not found: ${paymentId}`); + + return payment; + } + //=============================================== + + private async isPaymentPending(payment: Payment, queryRunner: QueryRunner): Promise { + if (payment.status !== PaymentStatus.PENDING) { + this.logger.log(`Payment ${payment.id} is not in pending status`); + return false; + } + + this.logger.log(`Verifying pending status for payment ${payment.id}`); + + try { + const gateway = this.gatewayFactory.getPaymentGateway(payment.paymentGateway.name); + const verifyData = await gateway.verifyPayment({ reference: payment.reference, amount: payment.amount }); + + const result = await this.paymentsService.processVerificationResult(verifyData, payment, queryRunner); + if (result.status === PaymentStatus.FAILED || result.status === PaymentStatus.PENDING) { + this.logger.log(`Payment ${payment.id} verification returned code ${verifyData.code} (failed or pending)`); + return true; + } + + this.logger.log(`Payment ${payment.id} verification returned code ${verifyData.code} (completed)`); + return false; + } catch (error) { + this.logger.error(`Error verifying payment ${payment.id}: ${error instanceof Error ? error.message : "Unknown error"}`); + return true; + } + } + + //=============================================== + private async sendPaymentReminderNotification(payment: Payment, reminderType: "first" | "second", queryRunner: QueryRunner) { + this.logger.log(`Sending ${reminderType} payment reminder for payment ${payment.id}`); + + await this.notificationsService.createPaymentReminderNotification( + payment.user.id, + { + paymentId: payment.id, + amount: new Decimal(payment.amount).toNumber(), + userPhone: payment.user.phone, + userEmail: payment.user.email, + }, + queryRunner, + ); + } + //=============================================== + + private async sendPaymentCancellationNotification(payment: Payment, queryRunner: QueryRunner) { + this.logger.log(`Sending payment cancellation notification for payment ${payment.id}`); + + await this.notificationsService.createPaymentCancellationNotification( + payment.user.id, + { + paymentId: payment.id, + amount: new Decimal(payment.amount).toNumber(), + userPhone: payment.user.phone, + userEmail: payment.user.email, + }, + queryRunner, + ); } } diff --git a/src/modules/payments/types/payment-job.type.ts b/src/modules/payments/types/payment-job.type.ts new file mode 100644 index 0000000..a029ffd --- /dev/null +++ b/src/modules/payments/types/payment-job.type.ts @@ -0,0 +1,3 @@ +import { Payment } from "../entities/payment.entity"; + +export type PaymentJobData = { payment: Payment } | { paymentId: string }; diff --git a/src/modules/settings/enums/notif-settings.enum.ts b/src/modules/settings/enums/notif-settings.enum.ts index 4fbf77b..d9b3433 100755 --- a/src/modules/settings/enums/notif-settings.enum.ts +++ b/src/modules/settings/enums/notif-settings.enum.ts @@ -34,6 +34,8 @@ export enum NotifType { // ASSIGN_TICKET = "ASSIGN_TICKET", RECURRING_INVOICE = "RECURRING_INVOICE", + PAYMENT_REMINDER = "PAYMENT_REMINDER", + PAYMENT_CANCELLATION = "PAYMENT_CANCELLATION", } export const NotifDescriptions: Record = { @@ -42,6 +44,8 @@ export const NotifDescriptions: Record(`${this.smsConfigs.API_URL}/send/verify`, smsData, { + headers: { "X-API-KEY": this.smsConfigs.API_KEY }, + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error("error in sending payment reminder sms", err); + throw new InternalServerErrorException("error in sending payment reminder sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } //************************************************* */ - // sendBlockServiceSms(userPhone: string, serviceName: any) { - // throw new Error("Method not implemented."); - // } - //************************************************* */ + async sendPaymentCancellationSms(mobile: string, amount: string) { + const smsData: ISmsVerifyBody = { + Parameters: [{ name: "amount", value: amount }], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_CANCELLATION, + }; + + try { + const { data } = await firstValueFrom( + this.httpService + .post(`${this.smsConfigs.API_URL}/send/verify`, smsData, { + headers: { "X-API-KEY": this.smsConfigs.API_KEY }, + }) + .pipe( + catchError((err: AxiosError) => { + this.logger.error("error in sending payment cancellation sms", err); + throw new InternalServerErrorException("error in sending payment cancellation sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } async getSmsLines() { try { diff --git a/src/monitoring/monitoring.module.ts b/src/monitoring/monitoring.module.ts index e966d41..12a083d 100644 --- a/src/monitoring/monitoring.module.ts +++ b/src/monitoring/monitoring.module.ts @@ -7,7 +7,6 @@ import { PrometheusModule } from "@willsoto/nestjs-prometheus"; defaultMetrics: { enabled: true, }, - path: "/metrics", defaultLabels: { application: "danak_dsc_api", }, diff --git a/src/templates/.gitkeep b/src/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/templates/email/announcement.hbs b/src/templates/email/announcement.hbs new file mode 100644 index 0000000..4c0e284 --- /dev/null +++ b/src/templates/email/announcement.hbs @@ -0,0 +1,24 @@ + + + + + اعلان + + +
+

اعلان

+
+
+

سلام،

+
{{title}}
+
{{description}}
+
تاریخ: {{date}}
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file diff --git a/src/templates/email-verify.hbs b/src/templates/email/email-verify.hbs similarity index 100% rename from src/templates/email-verify.hbs rename to src/templates/email/email-verify.hbs diff --git a/src/templates/email/exam-cancellation.hbs b/src/templates/email/exam-cancellation.hbs new file mode 100644 index 0000000..5f08342 --- /dev/null +++ b/src/templates/email/exam-cancellation.hbs @@ -0,0 +1,23 @@ + + + + + لغو آزمون + + +
+

لغو آزمون

+
+
+

سلام،

+

آزمون زیر با موفقیت لغو شد:

+
{{examTitle}}
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file diff --git a/src/templates/email/invoice.hbs b/src/templates/email/invoice.hbs new file mode 100644 index 0000000..6f581a7 --- /dev/null +++ b/src/templates/email/invoice.hbs @@ -0,0 +1,40 @@ + + + + + فاکتور + + +
+

فاکتور

+
+
+

سلام،

+

جزئیات فاکتور شما به شرح زیر است:

+
+
+ مبلغ: + {{price}} تومان +
+
+ تاریخ ایجاد: + {{createDate}} +
+
+ تاریخ سررسید: + {{dueDate}} +
+
+ شناسه فاکتور: + {{invoiceId}} +
+
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file diff --git a/src/templates/email/login.hbs b/src/templates/email/login.hbs new file mode 100644 index 0000000..1b2ddcc --- /dev/null +++ b/src/templates/email/login.hbs @@ -0,0 +1,23 @@ + + + + + ورود به سیستم + + +
+

ورود به سیستم

+
+
+

سلام،

+

ورود به سیستم در تاریخ و ساعت زیر انجام شد:

+
{{date}}
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file diff --git a/src/templates/email/payment-cancellation.hbs b/src/templates/email/payment-cancellation.hbs new file mode 100644 index 0000000..5d6a040 --- /dev/null +++ b/src/templates/email/payment-cancellation.hbs @@ -0,0 +1,23 @@ + + + + + لغو پرداخت + + +
+

لغو پرداخت

+
+
+

سلام،

+

پرداخت مبلغ زیر به دلیل عدم پرداخت در زمان مقرر لغو شد:

+
{{amount}} تومان
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file diff --git a/src/templates/email/payment-reminder.hbs b/src/templates/email/payment-reminder.hbs new file mode 100644 index 0000000..89b691a --- /dev/null +++ b/src/templates/email/payment-reminder.hbs @@ -0,0 +1,23 @@ + + + + + یادآوری پرداخت + + +
+

یادآوری پرداخت

+
+
+

سلام،

+

لطفاً نسبت به پرداخت مبلغ زیر اقدام فرمایید:

+
{{amount}} تومان
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file diff --git a/src/templates/email/ticket.hbs b/src/templates/email/ticket.hbs new file mode 100644 index 0000000..12e6d58 --- /dev/null +++ b/src/templates/email/ticket.hbs @@ -0,0 +1,42 @@ + + + + + تیکت پشتیبانی + + +
+

تیکت پشتیبانی

+
+
+

سلام،

+

جزئیات تیکت پشتیبانی شما به شرح زیر است:

+
+
+ موضوع: + {{subject}} +
+
+ تاریخ: + {{date}} +
+
+ شناسه تیکت: + {{ticketId}} +
+ {{#if user}} +
+ کارشناس: + {{user}} +
+ {{/if}} +
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file diff --git a/src/templates/email/wallet.hbs b/src/templates/email/wallet.hbs new file mode 100644 index 0000000..7009407 --- /dev/null +++ b/src/templates/email/wallet.hbs @@ -0,0 +1,40 @@ + + + + + اعلان کیف پول + + +
+

اعلان کیف پول

+
+
+

سلام،

+

جزئیات تراکنش کیف پول شما به شرح زیر است:

+
+
+ مبلغ تراکنش: +
{{amount}} تومان
+
+
+ موجودی فعلی: +
{{balance}} تومان
+
+
+ تاریخ: + {{date}} +
+
+ دلیل: + {{reason}} +
+
+

با تشکر

+
+
+

این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.

+
+ + \ No newline at end of file