refactor: the whole paymanet service
This commit is contained in:
+1
-2
@@ -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",
|
||||
|
||||
Generated
+3
-3
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,11 +19,12 @@ export function smsConfigs() {
|
||||
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
|
||||
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
|
||||
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
|
||||
SMS_PATTERN_INVOICE_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAYMENT_REMINDER"),
|
||||
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
||||
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
||||
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
|
||||
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
|
||||
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
||||
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
||||
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
|
||||
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("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;
|
||||
}
|
||||
|
||||
@@ -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<NestFastifyApplication>(AppModule, fastifyAdapter);
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||
|
||||
@@ -45,3 +45,8 @@ export interface ISubscriptionNotificationData extends IBaseNotificationData {
|
||||
planName: string;
|
||||
invoiceId: string;
|
||||
}
|
||||
|
||||
export interface IPaymentNotificationData extends IBaseNotificationData {
|
||||
amount: number;
|
||||
paymentId: string;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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!);
|
||||
//send email too
|
||||
// }
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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<GatewayType, IPaymentGateway>;
|
||||
|
||||
//************************ *
|
||||
getPaymentGateway(provider: GatewayType) {
|
||||
switch (provider) {
|
||||
case "zarinpal":
|
||||
return this.ZarinpalGateway;
|
||||
constructor(private readonly zarinpalGateway: ZarinpalGateway) {
|
||||
this.gateways = new Map<GatewayType, IPaymentGateway>();
|
||||
this.initializeGateways();
|
||||
}
|
||||
|
||||
default:
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
//********************************************** */
|
||||
//------------------abstract class----------------
|
||||
|
||||
export interface IPaymentGateway {
|
||||
processPayment(processPaymentParam: IProcessPaymentParams): Promise<IProcessPaymentData>;
|
||||
verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IPaymentVerifyData>;
|
||||
verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IVerifyPayment>;
|
||||
}
|
||||
|
||||
// export abstract class PaymentGateway {
|
||||
// abstract processPayment(processPaymentParam: IProcessPaymentParams): Promise<IProcessPaymentData>;
|
||||
// abstract verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IPaymentVerifyData>;
|
||||
// }
|
||||
|
||||
export interface IPaymentGatewayFactory {
|
||||
getPaymentGateway(provider: GatewayType): IPaymentGateway;
|
||||
getAvailablePaymentGateways(): { gateways: Array<{ name: GatewayEnum }> };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,72 +106,84 @@ 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<string>("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();
|
||||
|
||||
try {
|
||||
const paymentGateway = this.gatewayFactory.getPaymentGateway(gateway);
|
||||
// const payment = await queryRunner.manager.findOne(Payment, {
|
||||
// where: { reference: queryDto.Authority },
|
||||
// relations: ["user"],
|
||||
// });
|
||||
const payment = await this.getPaymentByReference(queryDto.Authority, 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());
|
||||
if (queryDto.Status !== "OK") await this.handleFailedPaymentRedirect(payment, queryRunner, rep, frontUrl);
|
||||
|
||||
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount });
|
||||
await qryRnr.commitTransaction();
|
||||
|
||||
return rp.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
||||
}
|
||||
//===============================================
|
||||
|
||||
async processVerificationResult(verifyData: IVerifyPayment, payment: Payment, qryRnr: 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());
|
||||
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<string, string> = {};
|
||||
|
||||
//
|
||||
await this.notificationsService.createWalletChargeNotification(
|
||||
payment.user.id,
|
||||
@@ -197,22 +197,176 @@ export class PaymentsService {
|
||||
},
|
||||
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());
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
return rep.status(HttpStatus.FOUND).redirect(frontUrl.toString());
|
||||
//===============================================
|
||||
|
||||
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 depositRequest = await this.getDepositRequestWithLock(depositId, queryRunner);
|
||||
|
||||
if (depositRequest.status === DepositRequestStatus.APPROVED) {
|
||||
throw new BadRequestException(PaymentMessage.DEPOSIT_ALREADY_APPROVED);
|
||||
}
|
||||
|
||||
await this.updateDepositRequestStatus(depositRequest.id, DepositRequestStatus.APPROVED, null, queryRunner);
|
||||
|
||||
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;
|
||||
@@ -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 },
|
||||
//===============================================
|
||||
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" },
|
||||
});
|
||||
//
|
||||
await queryRunner.manager.save(Payment, payment);
|
||||
this.paymentQueue.add(PAYMENT.PAYMENT_START, { payment }, { delay: 1000 });
|
||||
|
||||
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 };
|
||||
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<string>("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 });
|
||||
//===============================================
|
||||
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());
|
||||
}
|
||||
//===============================================
|
||||
|
||||
private async getUserById(userId: string, queryRunner: QueryRunner): Promise<User> {
|
||||
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<PaymentGateway> {
|
||||
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<Payment> {
|
||||
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);
|
||||
//
|
||||
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,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
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 (accountDetails.IBan) {
|
||||
const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: accountDetails.IBan });
|
||||
if (existIBan) throw new BadRequestException(PaymentMessage.IBAN_EXIST);
|
||||
}
|
||||
}
|
||||
|
||||
//===============================================
|
||||
private async validateBankAccountUpdate(updateDto: UpdateBankAccountDto, bankAccountId: string) {
|
||||
if (updateDto.cardNumber) {
|
||||
const existCardNumber = await this.bankAccountsRepository.findOneBy({ cardNumber: updateDto.cardNumber, id: Not(bankAccountId) });
|
||||
const existCardNumber = await this.bankAccountsRepository.findOneBy({
|
||||
cardNumber: updateDto.cardNumber,
|
||||
id: Not(bankAccountId),
|
||||
});
|
||||
if (existCardNumber) throw new BadRequestException(PaymentMessage.CARD_NUMBER_EXIST);
|
||||
}
|
||||
//
|
||||
|
||||
if (updateDto.IBan) {
|
||||
const existIBan = await this.bankAccountsRepository.findOneBy({ IBan: updateDto.IBan, id: Not(bankAccountId) });
|
||||
const existIBan = await this.bankAccountsRepository.findOneBy({
|
||||
IBan: updateDto.IBan,
|
||||
id: Not(bankAccountId),
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
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 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,58 +559,22 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
|
||||
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 depositRequest = await queryRunner.manager.findOne(DepositRequest, {
|
||||
// where: { id: depositId },
|
||||
// lock: { mode: "pessimistic_write" },
|
||||
// });
|
||||
|
||||
//===============================================
|
||||
private async getDepositRequestWithLock(depositId: string, queryRunner: QueryRunner) {
|
||||
const depositRequest = await queryRunner.manager
|
||||
.createQueryBuilder(DepositRequest, "deposit")
|
||||
.innerJoinAndSelect("deposit.user", "user")
|
||||
@@ -446,57 +583,29 @@ export class PaymentsService {
|
||||
.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();
|
||||
}
|
||||
return depositRequest;
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
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);
|
||||
//===============================================
|
||||
private async updateDepositRequestStatus(depositId: string, status: DepositRequestStatus, comment: string | null, qryRnr: QueryRunner) {
|
||||
const updateData: Partial<DepositRequest> = { status };
|
||||
if (comment !== null) {
|
||||
updateData.comment = comment;
|
||||
}
|
||||
|
||||
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 qryRnr.manager.update(DepositRequest, { id: depositId }, updateData);
|
||||
}
|
||||
//===============================================
|
||||
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 },
|
||||
},
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
message: PaymentMessage.DEPOSIT_REJECTED,
|
||||
depositRequest,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PaymentJobData>) {
|
||||
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<PaymentJobData>, 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<Payment> {
|
||||
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<boolean> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Payment } from "../entities/payment.entity";
|
||||
|
||||
export type PaymentJobData = { payment: Payment } | { paymentId: string };
|
||||
@@ -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<NotifType, { fa: string; category: NotifCategory }> = {
|
||||
@@ -42,6 +44,8 @@ export const NotifDescriptions: Record<NotifType, { fa: string; category: NotifC
|
||||
[NotifType.ANNOUNCEMENT]: { fa: "اعلانات و اطلاعیه ها", category: NotifCategory.ACCOUNT },
|
||||
|
||||
// Finance category
|
||||
[NotifType.PAYMENT_REMINDER]: { fa: "یادآوری پرداخت", category: NotifCategory.FINANCE },
|
||||
[NotifType.PAYMENT_CANCELLATION]: { fa: "لغو پرداخت", category: NotifCategory.FINANCE },
|
||||
[NotifType.WALLET_CHARGE]: { fa: "شارژ کیف پول", category: NotifCategory.FINANCE },
|
||||
[NotifType.WALLET_DEDUCTION]: { fa: "کسر از کیف پول", category: NotifCategory.FINANCE },
|
||||
|
||||
|
||||
@@ -2,7 +2,16 @@ import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common
|
||||
import { MailerService } from "@nestjs-modules/mailer";
|
||||
import { SentMessageInfo } from "nodemailer";
|
||||
|
||||
import { dateFormat, numberFormat } from "./international.utils";
|
||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
IWalletNotificationData,
|
||||
} from "../../notifications/interfaces/ISendNotificationData";
|
||||
import { IBaseNotificationData } from "../../notifications/interfaces/ISendNotificationData";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
@@ -28,4 +37,120 @@ export class EmailService {
|
||||
throw new InternalServerErrorException(EmailMessage.EMAIL_SENDING_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
async sendLoginEmail(data: IBaseNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.LOGIN,
|
||||
template: "login",
|
||||
context: {
|
||||
date: dateFormat(new Date()),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending login email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendInvoiceEmail(data: IInvoiceNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.INVOICE,
|
||||
template: "invoice",
|
||||
context: {
|
||||
price: numberFormat(data.price),
|
||||
createDate: dateFormat(data.createDate),
|
||||
dueDate: dateFormat(data.dueDate),
|
||||
invoiceId: data.invoiceId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending invoice email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendWalletEmail(data: IWalletNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.WALLET,
|
||||
template: "wallet",
|
||||
context: {
|
||||
amount: numberFormat(data.amount),
|
||||
balance: numberFormat(data.balance),
|
||||
date: dateFormat(data.date),
|
||||
reason: data.reason,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending wallet email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendTicketEmail(data: ITicketNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.TICKET,
|
||||
template: "ticket",
|
||||
context: {
|
||||
ticketId: data.ticketId,
|
||||
subject: data.subject,
|
||||
date: data.date ? dateFormat(data.date) : dateFormat(new Date()),
|
||||
user: data.user,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending ticket email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendAnnouncementEmail(data: IAnnouncementNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.ANNOUNCEMENT,
|
||||
template: "announcement",
|
||||
context: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
date: dateFormat(data.date),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending announcement email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPaymentReminderEmail(data: IPaymentNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.PAYMENT_REMINDER,
|
||||
template: "payment-reminder",
|
||||
context: {
|
||||
amount: numberFormat(data.amount),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending payment reminder email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPaymentCancellationEmail(data: IPaymentNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.PAYMENT_CANCELLATION,
|
||||
template: "payment-cancellation",
|
||||
context: {
|
||||
amount: numberFormat(data.amount),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending payment cancellation email", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,21 +527,60 @@ export class SmsService {
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
// sendCreateServiceSms(userPhone: string, serviceName: any) {
|
||||
// throw new Error("Method not implemented.");
|
||||
// }
|
||||
//************************************************* */
|
||||
|
||||
// sendUnblockServiceSms(userPhone: string, serviceName: any) {
|
||||
// throw new Error("Method not implemented.");
|
||||
// }
|
||||
async sendPaymentReminderSms(mobile: string, amount: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "amount", value: amount }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_REMINDER,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${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<ISmsVerifyResponse>(`${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 {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PrometheusModule } from "@willsoto/nestjs-prometheus";
|
||||
defaultMetrics: {
|
||||
enabled: true,
|
||||
},
|
||||
path: "/metrics",
|
||||
defaultLabels: {
|
||||
application: "danak_dsc_api",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>اعلان</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>اعلان</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<div style="font-size: 24px; color: #2c3e50; text-align: center; margin: 20px 0;">{{title}}</div>
|
||||
<div style="margin: 20px 0; text-align: center;">{{description}}</div>
|
||||
<div style="text-align: center; color: #666; font-size: 14px;">تاریخ: {{date}}</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>لغو آزمون</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>لغو آزمون</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>آزمون زیر با موفقیت لغو شد:</p>
|
||||
<div style="font-size: 20px; color: #2c3e50; text-align: center; margin: 20px 0;">{{examTitle}}</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>فاکتور</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>فاکتور</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>جزئیات فاکتور شما به شرح زیر است:</p>
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>مبلغ:</span>
|
||||
<span>{{price}} تومان</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ ایجاد:</span>
|
||||
<span>{{createDate}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ سررسید:</span>
|
||||
<span>{{dueDate}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>شناسه فاکتور:</span>
|
||||
<span>{{invoiceId}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ورود به سیستم</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>ورود به سیستم</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>ورود به سیستم در تاریخ و ساعت زیر انجام شد:</p>
|
||||
<div style="font-size: 20px; color: #2c3e50; text-align: center; margin: 20px 0;">{{date}}</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>لغو پرداخت</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>لغو پرداخت</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>پرداخت مبلغ زیر به دلیل عدم پرداخت در زمان مقرر لغو شد:</p>
|
||||
<div style="font-size: 24px; color: #e74c3c; text-align: center; margin: 20px 0;">{{amount}} تومان</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>یادآوری پرداخت</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>یادآوری پرداخت</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>لطفاً نسبت به پرداخت مبلغ زیر اقدام فرمایید:</p>
|
||||
<div style="font-size: 24px; color: #e74c3c; text-align: center; margin: 20px 0;">{{amount}} تومان</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>تیکت پشتیبانی</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>تیکت پشتیبانی</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>جزئیات تیکت پشتیبانی شما به شرح زیر است:</p>
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>موضوع:</span>
|
||||
<span>{{subject}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ:</span>
|
||||
<span>{{date}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>شناسه تیکت:</span>
|
||||
<span>{{ticketId}}</span>
|
||||
</div>
|
||||
{{#if user}}
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>کارشناس:</span>
|
||||
<span>{{user}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<html dir="rtl" lang="fa">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>اعلان کیف پول</title>
|
||||
</head>
|
||||
<body
|
||||
style="font-family: 'IRANSans', Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;"
|
||||
>
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h1>اعلان کیف پول</h1>
|
||||
</div>
|
||||
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px;">
|
||||
<p>سلام،</p>
|
||||
<p>جزئیات تراکنش کیف پول شما به شرح زیر است:</p>
|
||||
<div style="margin: 20px 0;">
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>مبلغ تراکنش:</span>
|
||||
<div style="font-size: 24px; color: #2ecc71; text-align: center; margin: 20px 0;">{{amount}} تومان</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>موجودی فعلی:</span>
|
||||
<div style="font-size: 20px; color: #3498db; text-align: center; margin: 20px 0;">{{balance}} تومان</div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>تاریخ:</span>
|
||||
<span>{{date}}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin: 10px 0;">
|
||||
<span>دلیل:</span>
|
||||
<span>{{reason}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>با تشکر</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 30px; font-size: 12px; color: #666;">
|
||||
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user