diff --git a/backup_file.dump b/backup_file.dump deleted file mode 100644 index 3ab482a..0000000 Binary files a/backup_file.dump and /dev/null differ diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index a4fa537..2663b68 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -323,8 +323,30 @@ export const enum NotificationMessage { USERID_IS_UUID = "شناسه کاربر باید یک UUID معتبر باشد", USERID_IS_STRING = "شناسه کاربر باید یک رشته متنی باشد", LOGIN = "لاگین", - LOGIN_MESSAGE = "یک لاگین به حساب کاربری شما صورت گرفت", + LOGIN_MESSAGE = "یک لاگین در تاریخ [loginDate] به حساب کاربری شما صورت گرفت", NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی غیرمجاز است", + INVOICE_CREATION = "صدور فاکتور", + INVOICE_CREATION_MESSAGE = "یک فاکتور به شماره [id] جدید برای شما صادر شده است", + ANNOUNCEMENT = "اطلاعیه", + ANNOUNCEMENT_MESSAGE = "یک اطلاعیه جدید برای شما ارسال شده است", + WALLET_CHARGE = "شارژ کیف پول", + WALLET_CHARGE_MESSAGE = "کیف پول شما به مبلغ [amount] تومان شارژ شد", + WALLET_DEDUCTION = "کسر از کیف پول", + WALLET_DEDUCTION_MESSAGE = "مبلغ [amount] تومان از کیف پول شما کسر شد", + BILL_INVOICE_REMINDER = "یادآوری پرداخت", + BILL_INVOICE_REMINDER_MESSAGE = "فاکتور شماره [invoiceNumber] هنوز پرداخت نشده است", + BILL_INVOICE = "فاکتور پرداختی", + BILL_INVOICE_MESSAGE = "فاکتور شماره [invoiceNumber] با موفقیت پرداخت شد", + CREATE_SERVICE = "ایجاد سرویس", + CREATE_SERVICE_MESSAGE = "سرویس جدیدی با عنوان [serviceName] ایجاد شد", + UNBLOCK_SERVICE = "فعال‌سازی سرویس", + UNBLOCK_SERVICE_MESSAGE = "سرویس [serviceName] مجددا فعال شده است", + BLOCK_SERVICE = "غیرفعال‌سازی سرویس", + BLOCK_SERVICE_MESSAGE = "سرویس [serviceName] غیرفعال شده است", + ANSWER_TICKET = "پاسخ تیکت", + ANSWER_TICKET_MESSAGE = "پاسخ جدیدی برای تیکت [ticketId] دریافت کردید", + CREATE_TICKET = "ایجاد تیکت", + CREATE_TICKET_MESSAGE = "تیکت شما با شماره [ticketNumber] ثبت شد", } export const enum SubscriptionMessage { diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index 12b375d..c3f0b38 100755 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -9,6 +9,13 @@ export function smsConfigs() { API_KEY: configService.getOrThrow("SMS_API_KEY"), SMS_PATTERN_OTP: configService.getOrThrow("SMS_PATTERN_OTP"), SMS_PATTERN_INVOICE: configService.getOrThrow("SMS_PATTERN_INVOICE"), + SMS_PATTERN_LOGIN: configService.getOrThrow("SMS_PATTERN_LOGIN"), + SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow("SMS_PATTERN_INVOICE_CREATED"), + SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow("SMS_PATTERN_ANNOUNCEMENT"), + SMS_PATTERN_WALLET_CHARGE: configService.getOrThrow("SMS_PATTERN_WALLET_CHARGE"), + SMS_PATTERN_WALLET_DEDUCTION: configService.getOrThrow("SMS_PATTERN_WALLET_DEDUCTION"), + SMS_PATTERN_TICKET_CREATED: configService.getOrThrow("SMS_PATTERN_TICKET_CREATED"), + SMS_PATTERN_TICKET_ANSWERED: configService.getOrThrow("SMS_PATTERN_WALLET_DEDUCTION"), }; }, }; @@ -19,4 +26,11 @@ export interface ISmsConfigs { API_KEY: string; SMS_PATTERN_OTP: string; SMS_PATTERN_INVOICE: string; + SMS_PATTERN_LOGIN: string; + SMS_PATTERN_INVOICE_CREATED: string; + SMS_PATTERN_ANNOUNCEMENT: string; + SMS_PATTERN_WALLET_CHARGE: string; + SMS_PATTERN_WALLET_DEDUCTION: string; + SMS_PATTERN_TICKET_CREATED: string; + SMS_PATTERN_TICKET_ANSWERED: string; } diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 9d51229..c1cd720 100755 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -95,7 +95,7 @@ export class AuthService { const tokens = this.generateAccessAndRefreshToken(user); - await this.notificationService.createLoginNotification(user.id); + await this.notificationService.createLoginNotification(user.id, { userPhone: user.phone }); return { message: AuthMessage.PASSWORD_LOGIN_SUCCESS, @@ -176,7 +176,7 @@ export class AuthService { const tokens = this.generateAccessAndRefreshToken(user); - await this.notificationService.createLoginNotification(user.id); + await this.notificationService.createLoginNotification(user.id, { userPhone: user.phone }); return { message: AuthMessage.LOGIN_SUCCESS, ...tokens, diff --git a/src/modules/invoices/invoices.module.ts b/src/modules/invoices/invoices.module.ts index 34c16ae..c9b075f 100755 --- a/src/modules/invoices/invoices.module.ts +++ b/src/modules/invoices/invoices.module.ts @@ -10,12 +10,19 @@ import { Discount } from "../discounts/entities/discount.entity"; import { UsageDiscount } from "../discounts/entities/usage-discount.entity"; import { DiscountRepository } from "../discounts/repositories/discount.repository"; import { UsageDiscountRepository } from "../discounts/repositories/usage-discount.repository"; +import { NotificationModule } from "../notifications/notifications.module"; import { UsersModule } from "../users/users.module"; import { UtilsModule } from "../utils/utils.module"; import { WalletsModule } from "../wallets/wallets.module"; @Module({ - imports: [TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]), UsersModule, WalletsModule, UtilsModule], + imports: [ + TypeOrmModule.forFeature([Invoice, InvoiceItem, Discount, UsageDiscount]), + UsersModule, + WalletsModule, + UtilsModule, + NotificationModule, + ], providers: [InvoicesService, InvoicesRepository, DiscountRepository, UsageDiscountRepository], controllers: [InvoicesController], exports: [InvoicesService], diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 2637e54..e023a53 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -9,6 +9,7 @@ import { AuthMessage, DiscountMessage, InvoiceMessage, WalletMessage } from "../ import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto"; import { DiscountRepository } from "../../discounts/repositories/discount.repository"; import { UsageDiscountRepository } from "../../discounts/repositories/usage-discount.repository"; +import { NotificationsService } from "../../notifications/providers/notifications.service"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { UsersService } from "../../users/providers/users.service"; import { OTPService } from "../../utils/providers/otp.service"; @@ -28,6 +29,7 @@ export class InvoicesService { private readonly logger = new Logger(InvoicesService.name); constructor( + private readonly notificationsService: NotificationsService, private readonly invoiceRepository: InvoicesRepository, private readonly discountRepository: DiscountRepository, private readonly usageDiscountRepository: UsageDiscountRepository, @@ -41,38 +43,63 @@ export class InvoicesService { ///********************************** */ async createInvoiceAdmin(createDto: CreateInvoiceDto) { - // - const invoiceItems = createDto.items.map((item) => { - return { + const queryRunner = this.dataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const invoiceItems = createDto.items.map((item) => ({ name: item.name, count: item.count, unitPrice: item.unitPrice, discount: item.discount || 0, totalPrice: item.unitPrice * item.count - (item.unitPrice * item.count * item.discount) / 100, - }; - }); - // - const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0)); - const tax = totalPrice.mul(0.1); - // - const dueDate = new Date(); - dueDate.setDate(dueDate.getDate() + 5); - // - const invoice = this.invoiceRepository.create({ - user: { id: createDto.userId }, - totalPrice: totalPrice.toNumber(), - items: invoiceItems, - tax: tax.toNumber(), - dueDate, - }); - // - await this.invoiceRepository.save(invoice); - //TODO: notify user + })); - return { - message: InvoiceMessage.CREATED, - invoice, - }; + const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0)); + const tax = totalPrice.mul(0.1); + + const dueDate = new Date(); + dueDate.setDate(dueDate.getDate() + 5); + + const invoice = queryRunner.manager.create(Invoice, { + user: { id: createDto.userId }, + totalPrice: totalPrice.toNumber(), + items: invoiceItems, + tax: tax.toNumber(), + dueDate, + }); + + await queryRunner.manager.save(Invoice, invoice); + + const user = await this.usersService.findOneByIdWithQueryRunner(createDto.userId, queryRunner); + + await this.notificationsService.createInvoiceCreationNotification( + createDto.userId, + { + invoiceId: invoice.numericId.toString(), + dueDate: invoice.dueDate, + price: new Decimal(invoice.totalPrice).toNumber(), + userPhone: user.phone, + userEmail: user.email, + items: invoiceItems.map((item) => item.name).join(", "), + }, + queryRunner, + ); + + await queryRunner.commitTransaction(); + + return { + message: InvoiceMessage.CREATED, + invoice, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } //********************************** */ diff --git a/src/modules/notifications/DTO/search-notification-query.dto.ts b/src/modules/notifications/DTO/search-notification-query.dto.ts index 416346f..7f9570c 100755 --- a/src/modules/notifications/DTO/search-notification-query.dto.ts +++ b/src/modules/notifications/DTO/search-notification-query.dto.ts @@ -1,12 +1,13 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsOptional } from "class-validator"; +import { IsIn, IsOptional } from "class-validator"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; export class SearchNotificationQueryDto extends PaginationDto { @IsOptional() @Type(() => Number) + @IsIn([0, 1]) @ApiPropertyOptional({ description: "Notification status", example: 1 }) isRead?: number; } diff --git a/src/modules/notifications/enums/notification-type.enum.ts b/src/modules/notifications/enums/notification-type.enum.ts index a90fbd5..c33b192 100755 --- a/src/modules/notifications/enums/notification-type.enum.ts +++ b/src/modules/notifications/enums/notification-type.enum.ts @@ -1,5 +1,5 @@ export enum NotificationType { LOGIN = "LOGIN", - PASSWORD_CAHNGED = "PASSWORD_CHANGED", + PASSWORD_CHANGED = "PASSWORD_CHANGED", WALLET_CHARGED = "WALLER_CHARGED", } diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts new file mode 100644 index 0000000..0268769 --- /dev/null +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -0,0 +1,38 @@ +export interface IBaseNotificationData { + userPhone: string; + userEmail?: string; +} + +export interface IInvoiceNotificationData extends IBaseNotificationData { + price: number; + items: string; + dueDate: Date; + invoiceId: string; +} + +export interface IServiceNotificationData extends IBaseNotificationData { + serviceName: string; +} + +export interface ITicketNotificationData extends IBaseNotificationData { + ticketId: string; + subject: string; + date: Date; +} + +export interface IWalletNotificationData extends IBaseNotificationData { + amount: number; + balance: number; + date: Date; + reason: string; +} + +export interface IGenericNotificationData extends IBaseNotificationData { + message: string; +} + +export interface IAnnouncementNotificationData extends IBaseNotificationData { + title: string; + description: string; + date: Date; +} diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/notifications.controller.ts index f1de3fa..cb71250 100755 --- a/src/modules/notifications/notifications.controller.ts +++ b/src/modules/notifications/notifications.controller.ts @@ -10,6 +10,7 @@ import { ParamDto } from "../../common/DTO/param.dto"; @Controller("notifications") @ApiTags("Notifications") +@AuthGuards() export class NotificationController { constructor(private readonly notificationService: NotificationsService) {} @@ -17,7 +18,6 @@ export class NotificationController { @ApiOperation({ summary: "all notifications by user" }) @Pagination() - @AuthGuards() @Get() getAllNotifications(@UserDec("id") userId: string, @Query() queryDto: SearchNotificationQueryDto) { return this.notificationService.getAllNotifications(queryDto, userId); @@ -26,7 +26,6 @@ export class NotificationController { //********************* */ @ApiOperation({ summary: "mark user notification as read" }) - @AuthGuards() @Patch(":id/read") markAsRead(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { return this.notificationService.markAsRead(paramDto.id, userId); @@ -35,7 +34,6 @@ export class NotificationController { //********************* */ @ApiOperation({ summary: "mark user all notification as read" }) - @AuthGuards() @Patch("read-all") markAllAsRead(@UserDec("id") userId: string) { return this.notificationService.markAllAsRead(userId); diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts index 2d5030d..97fc8f7 100755 --- a/src/modules/notifications/notifications.module.ts +++ b/src/modules/notifications/notifications.module.ts @@ -5,11 +5,11 @@ import { Notification } from "./entities/notification.entity"; import { NotificationController } from "./notifications.controller"; import { NotificationsService } from "./providers/notifications.service"; import { NotificationRepository } from "./repositories/notifications.repository"; -import { User } from "../users/entities/user.entity"; -import { UsersModule } from "../users/users.module"; +import { SettingModule } from "../settings/settings.module"; +import { UtilsModule } from "../utils/utils.module"; @Module({ - imports: [TypeOrmModule.forFeature([Notification, User]), UsersModule], + imports: [TypeOrmModule.forFeature([Notification]), UtilsModule, SettingModule], providers: [NotificationRepository, NotificationsService], controllers: [NotificationController], exports: [NotificationRepository, NotificationsService], diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index 7e9eeae..9fb7848 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -1,30 +1,73 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; +import { InjectDataSource } from "@nestjs/typeorm"; +import { DataSource, QueryRunner } from "typeorm"; -import { CommonMessage, NotificationMessage, UserMessage } from "../../../common/enums/message.enum"; -import { UsersService } from "../../users/providers/users.service"; +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 { dateFormat, numberFormat } from "../../utils/providers/international.utils"; +import { SmsService } from "../../utils/providers/sms.service"; import { CreateNotificationDto } from "../DTO/create-notification.dto"; import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto"; +import { Notification } from "../entities/notification.entity"; +import { + IAnnouncementNotificationData, + IBaseNotificationData, + IInvoiceNotificationData, + ITicketNotificationData, + IWalletNotificationData, +} from "../interfaces/ISendNotificationData"; import { NotificationRepository } from "../repositories/notifications.repository"; @Injectable() export class NotificationsService { + private readonly logger = new Logger(NotificationsService.name); constructor( private readonly notificationRepository: NotificationRepository, - private readonly usersService: UsersService, + private readonly smsService: SmsService, + private readonly userSettingsService: UserSettingsService, + @InjectDataSource() private readonly dataSource: DataSource, ) {} //************************ */ - async createNotification(createNotificationDto: CreateNotificationDto) { - const { user } = await this.usersService.findOneById(createNotificationDto.recipientId); - if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); - const notification = this.notificationRepository.create({ - ...createNotificationDto, - recipient: user, - }); - await this.notificationRepository.save(notification); + async createNotification(createNotificationDto: CreateNotificationDto, queryRunner?: QueryRunner) { + if (!queryRunner) { + const localQueryRunner = this.dataSource.createQueryRunner(); - return { notification }; + await localQueryRunner.connect(); + await localQueryRunner.startTransaction(); + try { + // + const notification = localQueryRunner.manager.create(Notification, { + ...createNotificationDto, + recipient: { id: createNotificationDto.recipientId }, + }); + await localQueryRunner.manager.save(notification); + + await localQueryRunner.commitTransaction(); + + return notification; + // + } catch (error) { + this.logger.error("error in createNotification", error); + await localQueryRunner.rollbackTransaction(); + throw error; + // + } finally { + await localQueryRunner.release(); + } + // + } else { + // + const notification = queryRunner.manager.create(Notification, { + ...createNotificationDto, + recipient: { id: createNotificationDto.recipientId }, + }); + // + await queryRunner.manager.save(notification); + return notification; + } } //************************ */ @@ -42,20 +85,13 @@ export class NotificationsService { //************************ */ - async createLoginNotification(recipientId: string) { - const message = NotificationMessage.LOGIN_MESSAGE; - return this.createNotification({ title: NotificationMessage.LOGIN, message, recipientId }); - } - - //************************ */ - async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) { return await this.notificationRepository.getAllNotifications(queryDto, recipientId); } //************************ */ - async getOneNotification(id: string) { + async getNotificationById(id: string) { const notification = await this.notificationRepository.findOneBy({ id }); if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED); return { notification }; @@ -64,18 +100,199 @@ export class NotificationsService { //************************ */ async markAllAsRead(userId: string) { - await this.notificationRepository.update( - { - recipient: { - id: userId, - }, - }, - { - isRead: true, - }, - ); + await this.notificationRepository.update({ recipient: { id: userId } }, { isRead: true }); return { message: CommonMessage.UPDATE_SUCCESS, }; } + + //************************ */ + + async createLoginNotification(recipientId: string, sendNotifData: 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 + } + return this.createNotification({ title: NotificationMessage.LOGIN, message, recipientId }); + } + + //************************ */ + async createInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.INVOICE_CREATION_MESSAGE.replace("[id]", data.invoiceId); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.CREATE_INVOICE, + recipientId, + queryRunner, + ); + if (isActive) { + await this.smsService.sendInvoiceCreationSms(data.userPhone, data.invoiceId, data.price, data.items, dateFormat(data.dueDate)); + //send email too + } + return this.createNotification({ title: NotificationMessage.INVOICE_CREATION, message, recipientId }, queryRunner); + } + //************************ */ + + async createAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.ANNOUNCEMENT_MESSAGE; + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.ANNOUNCEMENT, + recipientId, + queryRunner, + ); + if (isActive) { + await this.smsService.sendAnnouncementSms(data.userPhone, data.title, data.description, dateFormat(data.date)); + //send email too + } + return this.createNotification({ title: NotificationMessage.ANNOUNCEMENT, message, recipientId }); + } + + //************************ */ + + async createWalletChargeNotification(recipientId: string, data: IWalletNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.WALLET_CHARGE_MESSAGE.replace("[amount]", numberFormat(data.amount)); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.WALLET_CHARGE, + recipientId, + queryRunner, + ); + if (isActive) { + await this.smsService.sendWalletChargeSms( + data.userPhone, + numberFormat(data.amount), + numberFormat(data.balance), + dateFormat(data.date), + ); + //send email too + } + return this.createNotification({ title: NotificationMessage.WALLET_CHARGE, message, recipientId }); + } + + //************************ */ + + async createWalletDeductionNotification(recipientId: string, data: IWalletNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.WALLET_DEDUCTION_MESSAGE.replace("[amount]", numberFormat(data.amount)); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.WALLET_DEDUCTION, + recipientId, + queryRunner, + ); + if (isActive) { + await this.smsService.sendWalletDeductionSms( + data.userPhone, + numberFormat(data.amount), + numberFormat(data.balance), + data.reason, + dateFormat(data.date), + ); + //send email too + } + return this.createNotification({ title: NotificationMessage.WALLET_DEDUCTION, message, recipientId }); + } + + // //************************ */ + + async createAnswerTicketNotification(recipientId: string, data: ITicketNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.ANSWER_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.sendAnswerTicketSms(data.userPhone, data.ticketId, data.subject); + //send email too + } + return this.createNotification({ title: NotificationMessage.ANSWER_TICKET, message, recipientId }); + } + + // //************************ */ + + async createTicketNotification(recipientId: string, data: ITicketNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.CREATE_TICKET_MESSAGE.replace("[ticketId]", data.ticketId); + //sent notification based on the userSettings + const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType( + NotifType.CREATE_TICKET, + recipientId, + queryRunner, + ); + if (isActive) { + await this.smsService.sendCreateTicketSms(data.userPhone, data.ticketId, data.subject, dateFormat(data.date)); + //send email too + } + return this.createNotification({ title: NotificationMessage.CREATE_TICKET, message, recipientId }); + } + + //************************ */ + + // async createBillInvoiceReminderNotification(recipientId: string, sendNotifData: ISendNotificationData) { + // const message = NotificationMessage.BILL_INVOICE_REMINDER_MESSAGE; + // //sent notification based on the userSettings + // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE_REMINDER, recipientId); + // if (isActive) { + // await this.smsService.sendBillInvoiceReminderSms(sendNotifData.userPhone, sendNotifData.dueDate, sendNotifData.invoiceId); + // //send email too + // } + // return this.createNotification({ title: NotificationMessage.BILL_INVOICE_REMINDER, message, recipientId }); + // } + + //************************ */ + + // async createBillInvoiceNotification(recipientId: string, sendNotifData: ISendNotificationData) { + // const message = NotificationMessage.BILL_INVOICE_MESSAGE; + // //sent notification based on the userSettings + // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BILL_INVOICE, recipientId); + // if (isActive) { + // await this.smsService.sendBillInvoiceSms(sendNotifData.userPhone, sendNotifData.amount, sendNotifData.invoiceId); + // //send email too + // } + // return this.createNotification({ title: NotificationMessage.BILL_INVOICE, message, recipientId }); + // } + + // //************************ */ + + // 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 createBlockServiceNotification(recipientId: string, sendNotifData: ISendNotificationData) { + // const message = NotificationMessage.BLOCK_SERVICE_MESSAGE.replace("[serviceName]", sendNotifData.serviceName); + // //sent notification based on the userSettings + // const { isActive } = await this.userSettingsService.getUserNotificationSettingWithType(NotifType.BLOCK_SERVICE, recipientId); + // if (isActive) { + // await this.smsService.sendBlockServiceSms(sendNotifData.userPhone, sendNotifData.serviceName); + // //send email too + // } + // return this.createNotification({ title: NotificationMessage.BLOCK_SERVICE, message, recipientId }); + // } } diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index ac13b57..fae25c2 100755 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -18,12 +18,14 @@ import { DepositRequest } from "./entities/deposit-request.entity"; import { PaymentGateway } from "./entities/payment-gateway.entity"; import { DepositRequestsRepository } from "./repositories/deposit-requests.repository"; import { PaymentGatewaysRepository } from "./repositories/payment-gateway.repository"; +import { NotificationModule } from "../notifications/notifications.module"; @Module({ imports: [ TypeOrmModule.forFeature([Payment, PaymentGateway, BankAccount, DepositRequest]), BullModule.registerQueue({ name: PAYMENT.PAYMENT_QUEUE_NAME }), WalletsModule, + NotificationModule, ], controllers: [PaymentsController], providers: [ diff --git a/src/modules/payments/providers/payments.service.ts b/src/modules/payments/providers/payments.service.ts index affeb53..629df31 100755 --- a/src/modules/payments/providers/payments.service.ts +++ b/src/modules/payments/providers/payments.service.ts @@ -9,6 +9,7 @@ import { DataSource, Not, QueryRunner } from "typeorm"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { CommonMessage, PaymentMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum"; +import { NotificationsService } from "../../notifications/providers/notifications.service"; import { User } from "../../users/entities/user.entity"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { WalletsService } from "../../wallets/providers/wallets.service"; @@ -37,6 +38,7 @@ import { GatewayType } from "../types/gateway.type"; export class PaymentsService { constructor( @InjectQueue(PAYMENT.PAYMENT_QUEUE_NAME) private readonly paymentQueue: Queue, + private readonly notificationsService: NotificationsService, private readonly configService: ConfigService, private readonly gatewayFactory: PaymentGatewayFactory, private readonly paymentGatewaysRepository: PaymentGatewaysRepository, @@ -159,12 +161,31 @@ export class PaymentsService { const verifyData = await paymentGateway.verifyPayment({ reference: queryDto.Authority, amount: payment.amount }); if (verifyData.code === 100) { - await this.handleSuccessfulPayment(payment.id, payment.user.id, payment.amount, `${verifyData.ref_id}`, queryRunner); + const { wallet } = await this.handleSuccessfulPayment( + payment.id, + 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); @@ -229,8 +250,7 @@ export class PaymentsService { //*********************************** */ async handleSuccessfulPayment(paymentId: string, userId: string, amount: Decimal, transactionId: string, queryRunner: QueryRunner) { await queryRunner.manager.update(Payment, { id: paymentId }, { status: PaymentStatus.COMPLETED, transactionId }); - const transaction = await this.walletsService.createPaymentTransaction(userId, amount, queryRunner); - return transaction; + return this.walletsService.createPaymentTransaction(userId, amount, queryRunner); } //*********************************** */ diff --git a/src/modules/settings/providers/user-settings.service.ts b/src/modules/settings/providers/user-settings.service.ts index 437a2d9..4d50136 100755 --- a/src/modules/settings/providers/user-settings.service.ts +++ b/src/modules/settings/providers/user-settings.service.ts @@ -60,6 +60,23 @@ export class UserSettingsService { } //*+******************************************* + async getUserNotificationSettingWithType(type: NotifType, userId: string, queryRunner?: QueryRunner) { + const repository = queryRunner ? queryRunner.manager.getRepository(UserSetting) : this.userSettingsRepository; + + const setting = await repository.findOne({ + where: { user: { id: userId }, notificationSetting: { type } }, + relations: { + notificationSetting: true, + }, + }); + + if (!setting) throw new BadRequestException(SettingMessageEnum.NOT_FOUND); + + return setting; + } + + //*+******************************************* + async toggleUserNotificationSetting(settingId: string) { const setting = await this.userSettingsRepository.findOneBy({ id: settingId }); diff --git a/src/modules/utils/interfaces/ISms.ts b/src/modules/utils/interfaces/ISms.ts index a4e3d78..f0d20e6 100755 --- a/src/modules/utils/interfaces/ISms.ts +++ b/src/modules/utils/interfaces/ISms.ts @@ -23,4 +23,21 @@ export interface ISmsVerifyBody { TemplateId: string; } -export type TemplateParams = "VERIFICATIONCODE" | "code" | "invoiceId" | "price" | "items"; +export type TemplateParams = + | "VERIFICATIONCODE" + | "code" + | "invoiceId" + | "price" + | "items" + | "loginDate" + | "invoiceDate" + | "title" + | "description" + | "ticketId" + | "serviceName" + | "date" + | "amount" + | "newBalance" + | "reason" + | "subject" + | "message"; diff --git a/src/modules/utils/providers/international.utils.ts b/src/modules/utils/providers/international.utils.ts new file mode 100644 index 0000000..cb2026e --- /dev/null +++ b/src/modules/utils/providers/international.utils.ts @@ -0,0 +1,7 @@ +export function numberFormat(number: number, locale: string = "fa-IR") { + return Intl.NumberFormat(locale).format(number); +} + +export function dateFormat(date: Date, locale: string = "fa-IR") { + return new Intl.DateTimeFormat(locale).format(date); +} diff --git a/src/modules/utils/providers/sms.service.ts b/src/modules/utils/providers/sms.service.ts index 239674d..919d54f 100755 --- a/src/modules/utils/providers/sms.service.ts +++ b/src/modules/utils/providers/sms.service.ts @@ -84,6 +84,254 @@ export class SmsService { throw new InternalServerErrorException("error in sending sms"); } } + //************************************************* */ + + async sendLoginSms(mobile: string, loginDate: string) { + const smsData: ISmsVerifyBody = { + Parameters: [{ name: "loginDate", value: loginDate }], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_LOGIN, + }; + + 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 verify sms", err); + throw new InternalServerErrorException("error in sending verify sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + // throw new InternalServerErrorException("error in sending sms"); + } + } + //************************************************* */ + + async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "invoiceId", value: invoiceId }, + { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, + { name: "items", value: items }, + { name: "invoiceDate", value: invoiceDate }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_CREATED, + }; + + 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 invoice created sms", err); + throw new InternalServerErrorException("error in sending invoice created sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } + //************************************************* */ + async sendAnnouncementSms(mobile: string, title: string, description: string, date: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "title", value: title }, + { name: "description", value: description }, + { name: "date", value: date }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_ANNOUNCEMENT, + }; + + 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 announcement sms", err); + throw new InternalServerErrorException("error in sending announcement sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } + //************************************************* */ + + async sendWalletChargeSms(mobile: string, amount: string, newBalance: string, date: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "amount", value: amount }, + { name: "newBalance", value: newBalance }, + { name: "date", value: date }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_WALLET_CHARGE, + }; + + 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 wallet charge sms", err); + throw new InternalServerErrorException("error in sending wallet charge sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } + //************************************************* */ + + async sendWalletDeductionSms(mobile: string, amount: string, newBalance: string, reason: string, date: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "amount", value: amount }, + { name: "newBalance", value: newBalance }, + { name: "reason", value: reason }, + { name: "date", value: date }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_WALLET_DEDUCTION, + }; + + 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 wallet deduction sms", err); + throw new InternalServerErrorException("error in sending wallet deduction sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } + + //************************************************* */ + + async sendCreateTicketSms(mobile: string, ticketId: string, subject: string, date: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "ticketId", value: ticketId }, + { name: "subject", value: subject }, + { name: "date", value: date }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_CREATED, + }; + + 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 ticket created sms", err); + throw new InternalServerErrorException("error in sending ticket created sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } + //************************************************* */ + + async sendAnswerTicketSms(mobile: string, ticketId: string, subject: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "ticketId", value: ticketId }, + { name: "subject", value: subject }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ANSWERED, + }; + + 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 ticket answered sms", err); + throw new InternalServerErrorException("error in sending ticket answered sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + throw new InternalServerErrorException("error in sending sms"); + } + } + + //************************************************* */ + + // sendBillInvoiceReminderSms(userPhone: string, dueDate: any, invoiceId: any) { + // throw new Error("Method not implemented."); + // } + // //************************************************* */ + + // sendBillInvoiceSms(userPhone: string, amount: any, invoiceId: any) { + // throw new Error("Method not implemented."); + // } + // //************************************************* */ + + // sendCreateServiceSms(userPhone: string, serviceName: any) { + // throw new Error("Method not implemented."); + // } + // //************************************************* */ + + // sendUnblockServiceSms(userPhone: string, serviceName: any) { + // throw new Error("Method not implemented."); + // } + // //************************************************* */ + + // sendBlockServiceSms(userPhone: string, serviceName: any) { + // throw new Error("Method not implemented."); + // } + // //************************************************* */ //************************************************* */ diff --git a/src/modules/wallets/providers/wallets.service.ts b/src/modules/wallets/providers/wallets.service.ts index bdc41ae..dc37a9c 100755 --- a/src/modules/wallets/providers/wallets.service.ts +++ b/src/modules/wallets/providers/wallets.service.ts @@ -56,7 +56,7 @@ export class WalletsService { await queryRunner.manager.save(Wallet, wallet); await queryRunner.manager.save(WalletTransaction, transaction); - return transaction; + return { transaction, wallet }; } //*********************************** */