From 5b1b574d41b6380768513e1171f52dd96389dd0d Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Fri, 28 Feb 2025 16:34:11 +0330 Subject: [PATCH] refactor: the user setting and change to be cateogry based --- ...421-AddBusinessFieldsToUserSubscription.ts | 73 -------------- ...742921699-SetInvoiceNumericIdStartValue.ts | 11 +++ database/seeders/notif-setting.seeder.ts | 35 +++---- .../transformers/decimal.transformer.ts | 4 +- src/configs/sms.config.ts | 4 +- src/modules/auth/DTO/verify-otp.dto.ts | 4 +- .../invoices/entities/invoice.entity.ts | 3 + src/modules/invoices/invoices.controller.ts | 19 +--- .../invoices/providers/invoices.service.ts | 69 ++++---------- .../settings/entities/notif-setting.entity.ts | 17 ---- .../entities/notification-setting.entity.ts | 27 ++++++ .../settings/entities/user-setting.entity.ts | 17 ++-- .../settings/enums/notif-settings.enum.ts | 71 ++++++++++---- src/modules/settings/enums/user-settings.ts | 68 +------------- .../providers/user-settings.service.ts | 94 +++++++++---------- .../repositories/notif-settings.repository.ts | 6 +- src/modules/settings/settings.controller.ts | 14 ++- src/modules/settings/settings.module.ts | 7 +- src/modules/utils/interfaces/IOtpKey.ts | 2 +- src/modules/utils/interfaces/ISms.ts | 2 +- src/modules/utils/providers/sms.service.ts | 36 +++++++ 21 files changed, 242 insertions(+), 341 deletions(-) delete mode 100644 database/migrations/1740574122421-AddBusinessFieldsToUserSubscription.ts create mode 100644 database/migrations/1740742921699-SetInvoiceNumericIdStartValue.ts delete mode 100755 src/modules/settings/entities/notif-setting.entity.ts create mode 100755 src/modules/settings/entities/notification-setting.entity.ts diff --git a/database/migrations/1740574122421-AddBusinessFieldsToUserSubscription.ts b/database/migrations/1740574122421-AddBusinessFieldsToUserSubscription.ts deleted file mode 100644 index 1ce35a6..0000000 --- a/database/migrations/1740574122421-AddBusinessFieldsToUserSubscription.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; - -export class AddBusinessFieldsToUserSubscription1740574122421 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.addColumns("user_subscription", [ - new TableColumn({ - name: "businessName", - type: "varchar", - length: "250", - isNullable: true, - default: "'Unknown Business'", - }), - new TableColumn({ - name: "businessPhone", - type: "varchar", - length: "50", - isNullable: true, - default: "''", - }), - new TableColumn({ - name: "description", - type: "text", - // length: "250", - isNullable: true, - default: "''", - }), - ]); - - await queryRunner.query( - `UPDATE "user_subscription" SET "businessName" = 'Unknown Business', "businessPhone" = '', "description" = '' WHERE "businessName" IS NULL`, - ); - - await queryRunner.changeColumn( - "user_subscription", - "businessName", - new TableColumn({ - name: "businessName", - type: "varchar", - length: "250", - isNullable: false, - }), - ); - - await queryRunner.changeColumn( - "user_subscription", - "businessPhone", - new TableColumn({ - name: "businessPhone", - type: "varchar", - length: "50", - isNullable: false, - }), - ); - - await queryRunner.changeColumn( - "user_subscription", - "description", - new TableColumn({ - name: "description", - type: "text", - isNullable: false, - }), - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumns("user_subscription", [ - new TableColumn({ name: "businessName", type: "varchar" }), - new TableColumn({ name: "businessPhone", type: "varchar" }), - new TableColumn({ name: "description", type: "text" }), - ]); - } -} diff --git a/database/migrations/1740742921699-SetInvoiceNumericIdStartValue.ts b/database/migrations/1740742921699-SetInvoiceNumericIdStartValue.ts new file mode 100644 index 0000000..c9783b5 --- /dev/null +++ b/database/migrations/1740742921699-SetInvoiceNumericIdStartValue.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class SetInvoiceNumericIdStartValue1740742921699 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "invoice" ALTER COLUMN "numericId" RESTART WITH 1000;`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "invoice" ALTER COLUMN "numericId" RESTART WITH 1;`); + } +} diff --git a/database/seeders/notif-setting.seeder.ts b/database/seeders/notif-setting.seeder.ts index 1061d2b..d3600c3 100755 --- a/database/seeders/notif-setting.seeder.ts +++ b/database/seeders/notif-setting.seeder.ts @@ -1,31 +1,24 @@ import { Logger } from "@nestjs/common"; import { DataSource } from "typeorm"; -import { NotifSetting } from "../../src/modules/settings/entities/notif-setting.entity"; -import { FarsiNotifSettingsEnum, NotifSettingsEnum } from "../../src/modules/settings/enums/notif-settings.enum"; - -const notifSettings = [ - { title: NotifSettingsEnum.ANNOUNCE_NOTIF, title_fa: FarsiNotifSettingsEnum.ANNOUNCE_NOTIF }, - { title: NotifSettingsEnum.ANSWER_TICKET, title_fa: FarsiNotifSettingsEnum.ANSWER_TICKET }, - { title: NotifSettingsEnum.BILL_PAYMENT, title_fa: FarsiNotifSettingsEnum.BILL_PAYMENT }, - { title: NotifSettingsEnum.BILL_PAYMENT_REMINDER, title_fa: FarsiNotifSettingsEnum.BILL_PAYMENT_REMINDER }, - { title: NotifSettingsEnum.BLOCKING_SERVICE, title_fa: FarsiNotifSettingsEnum.BLOCKING_SERVICE }, - { title: NotifSettingsEnum.CREATE_INVOICE, title_fa: FarsiNotifSettingsEnum.CREATE_INVOICE }, - { title: NotifSettingsEnum.CREATE_TICKET, title_fa: FarsiNotifSettingsEnum.CREATE_TICKET }, - { title: NotifSettingsEnum.UNBLOCKING_SERVICE, title_fa: FarsiNotifSettingsEnum.UNBLOCKING_SERVICE }, - { title: NotifSettingsEnum.CREATE_SERVICE, title_fa: FarsiNotifSettingsEnum.CREATE_SERVICE }, -]; +import { NotificationSetting } from "../../src/modules/settings/entities/notification-setting.entity"; +import { NotifDescriptions, NotifType } from "../../src/modules/settings/enums/notif-settings.enum"; export const seedNotifSettings = async (dataSource: DataSource, logger: Logger) => { try { - const notifSettingRepo = dataSource.getRepository(NotifSetting); - for (const setting of notifSettings) { - const newSetting = notifSettingRepo.create(setting); - await notifSettingRepo.save(newSetting); - } - logger.log("Notif Settings Successfully"); + const notifSettingRepo = dataSource.getRepository(NotificationSetting); + + const notifSettings = Object.entries(NotifDescriptions).map(([type, { category, fa }]) => ({ + type: type as NotifType, + category, + description: fa, + })); + + await notifSettingRepo.insert(notifSettings); + + logger.log(`Successfully seeded ${notifSettings.length} notification settings.`); } catch (error) { - logger.error("Error in seeding settings", error); + logger.error("Error in seeding notification settings", error); process.exit(1); } }; diff --git a/src/common/transformers/decimal.transformer.ts b/src/common/transformers/decimal.transformer.ts index bbfda1b..07b5d86 100755 --- a/src/common/transformers/decimal.transformer.ts +++ b/src/common/transformers/decimal.transformer.ts @@ -12,11 +12,11 @@ export class DecimalTransformer implements ValueTransformer { // } to(value: Decimal | null): string | null { - return value ? value.toString() : "0"; // Ensure it's always a valid number + return value ? value.toString() : "0"; } from(value: string | null): number { - return value ? new Decimal(value).toNumber() : new Decimal(0).toNumber(); // Default to 0 if null + return value ? new Decimal(value).toNumber() : new Decimal(0).toNumber(); } } diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index 70db6dc..12b375d 100755 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -7,8 +7,8 @@ export function smsConfigs() { return { API_URL: configService.getOrThrow("SMS_API_URL"), API_KEY: configService.getOrThrow("SMS_API_KEY"), - // SECRET_KEY: configService.getOrThrow("SMS_SECRET"), SMS_PATTERN_OTP: configService.getOrThrow("SMS_PATTERN_OTP"), + SMS_PATTERN_INVOICE: configService.getOrThrow("SMS_PATTERN_INVOICE"), }; }, }; @@ -17,6 +17,6 @@ export function smsConfigs() { export interface ISmsConfigs { API_URL: string; API_KEY: string; - // SECRET_KEY: string; SMS_PATTERN_OTP: string; + SMS_PATTERN_INVOICE: string; } diff --git a/src/modules/auth/DTO/verify-otp.dto.ts b/src/modules/auth/DTO/verify-otp.dto.ts index 1181730..599e39f 100755 --- a/src/modules/auth/DTO/verify-otp.dto.ts +++ b/src/modules/auth/DTO/verify-otp.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, PickType } from "@nestjs/swagger"; import { IsMobilePhone, IsNotEmpty, IsNumberString, Length } from "class-validator"; import { AuthMessage } from "../../../common/enums/message.enum"; @@ -16,3 +16,5 @@ export class VerifyOtpDto { @Length(5, 5, { message: AuthMessage.OTP_FORMAT_INVALID }) code: string; } + +export class VerifyOtpWithUserId extends PickType(VerifyOtpDto, ["code"]) {} diff --git a/src/modules/invoices/entities/invoice.entity.ts b/src/modules/invoices/entities/invoice.entity.ts index 7db1666..2952e00 100755 --- a/src/modules/invoices/entities/invoice.entity.ts +++ b/src/modules/invoices/entities/invoice.entity.ts @@ -11,6 +11,9 @@ import { InvoiceStatus } from "../enums/invoice-status.enum"; @Entity() export class Invoice extends BaseEntity { + @Column({ type: "int", generated: "identity", insert: false }) + numericId: number; + @ManyToOne(() => User, (user) => user.invoices, { nullable: true, onDelete: "RESTRICT" }) user: User; diff --git a/src/modules/invoices/invoices.controller.ts b/src/modules/invoices/invoices.controller.ts index 384a071..84e277e 100755 --- a/src/modules/invoices/invoices.controller.ts +++ b/src/modules/invoices/invoices.controller.ts @@ -10,24 +10,22 @@ import { Pagination } from "../../common/decorators/pagination.decorator"; import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; -import { RequestOtpDto } from "../auth/DTO/request-otp.dto"; -import { VerifyOtpDto } from "../auth/DTO/verify-otp.dto"; +import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto"; import { PermissionEnum } from "../users/enums/permission.enum"; @Controller("invoices") +@AuthGuards() export class InvoicesController { constructor(private readonly invoiceService: InvoicesService) {} @ApiOperation({ summary: "create an invoice ==> admin route" }) @PermissionsDec(PermissionEnum.INVOICES) - @AuthGuards() @Post() createInvoice(@Body() createDto: CreateInvoiceDto) { return this.invoiceService.createInvoiceAdmin(createDto); } @ApiOperation({ summary: "get all invoices ==> admin route" }) - @AuthGuards() @PermissionsDec(PermissionEnum.INVOICES) @Pagination() @Get() @@ -36,7 +34,6 @@ export class InvoicesController { } @ApiOperation({ summary: "get all user invoices" }) - @AuthGuards() @Pagination() @Get("user") getUserInvoices(@Query() queryDto: UserInvoicesSearchQueryDto, @UserDec("id") userId: string) { @@ -44,42 +41,36 @@ export class InvoicesController { } @ApiOperation({ summary: "get single invoice by Id " }) - @AuthGuards() @Get(":id") getInvoiceById(@Param() paramDto: ParamDto, @UserDec("isAdmin") isAdmin: boolean, @UserDec("id") userId: string) { return this.invoiceService.getInvoiceById(paramDto.id, isAdmin, userId); } @ApiOperation({ summary: "approve request invoice by user" }) - @AuthGuards() @Post(":id/approve/request") - approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() updateDto: RequestOtpDto) { - return this.invoiceService.approveRequest(paramDto.id, userId, updateDto); + approveRequestInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { + return this.invoiceService.approveInvoiceRequest(paramDto.id, userId); } @ApiOperation({ summary: "approve invoice by user" }) - @AuthGuards() @Patch(":id/approve") - approveInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() verifyOtpDto: VerifyOtpDto) { + approveInvoiceByUser(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() verifyOtpDto: VerifyOtpWithUserId) { return this.invoiceService.approveInvoiceByUser(paramDto.id, userId, verifyOtpDto); } @ApiOperation({ summary: "apply discount on invoice by user" }) - @AuthGuards() @Post(":id/apply-discount") async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) { return this.invoiceService.applyDiscount(paramDto.id, applyDiscountDto, userId); } @ApiOperation({ summary: "cancel discount by user" }) - @AuthGuards() @Post(":id/cancel-discount") cancelDiscount(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { return this.invoiceService.cancelDiscount(paramDto.id, userId); } @ApiOperation({ summary: "pay invoice by user" }) - @AuthGuards() @Post(":id/pay") async payInvoice(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { return this.invoiceService.payInvoice(paramDto.id, userId); diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index b9efcf2..2637e54 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -5,9 +5,8 @@ import dayjs from "dayjs"; import Decimal from "decimal.js"; import { Between, DataSource, QueryRunner } from "typeorm"; -import { AuthMessage, DiscountMessage, InvoiceMessage, UserMessage, WalletMessage } from "../../../common/enums/message.enum"; -import { RequestOtpDto } from "../../auth/DTO/request-otp.dto"; -import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto"; +import { AuthMessage, DiscountMessage, InvoiceMessage, WalletMessage } from "../../../common/enums/message.enum"; +import { VerifyOtpWithUserId } from "../../auth/DTO/verify-otp.dto"; import { DiscountRepository } from "../../discounts/repositories/discount.repository"; import { UsageDiscountRepository } from "../../discounts/repositories/usage-discount.repository"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; @@ -22,7 +21,6 @@ import { CreateInvoiceDto } from "../DTO/create-invoice.dto"; import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "../DTO/invoices-search-query.dto"; import { Invoice } from "../entities/invoice.entity"; import { InvoiceStatus } from "../enums/invoice-status.enum"; -// import { InvoiceItemsRepository } from "../repositories/invoice-items.repository"; import { InvoicesRepository } from "../repositories/invoices.repository"; @Injectable() @@ -38,7 +36,6 @@ export class InvoicesService { private readonly otpService: OTPService, private readonly smsService: SmsService, private readonly dataSource: DataSource, - // private readonly invoiceItemsRepository: InvoiceItemsRepository, ) {} ///********************************** */ @@ -80,12 +77,10 @@ export class InvoicesService { //********************************** */ - async approveRequest(invoiceId: string, userId: string, requestOtpDto: RequestOtpDto) { - const { phone } = requestOtpDto; - const user = await this.usersService.findOneById(userId); - if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + async approveInvoiceRequest(invoiceId: string, userId: string) { + const { user } = await this.usersService.findOneById(userId); - const invoice = await this.invoiceRepository.findOneBy({ id: invoiceId, user: { id: userId } }); + const invoice = await this.invoiceRepository.findOne({ where: { id: invoiceId, user: { id: userId } }, relations: { items: true } }); if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); if (invoice.status === InvoiceStatus.APPROVED) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED); @@ -94,7 +89,7 @@ export class InvoicesService { if (invoice.status !== InvoiceStatus.PENDING) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED); - const existCode = await this.otpService.checkExistOtp(phone, "LOGIN"); + const existCode = await this.otpService.checkExistOtp(user.phone, "INVOICE_VERIFY"); if (existCode) { return { message: AuthMessage.OTP_ALREADY_SENT, @@ -102,10 +97,11 @@ export class InvoicesService { }; } - const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN"); + const otpCode = await this.otpService.generateAndSetInCache(user.phone, "INVOICE_VERIFY"); + const items = invoice.items.map((item) => item.name).join(", "); // - await this.smsService.sendSmsVerifyCode(phone, otpCode); - this.logger.debug(`OTP sent to ${phone}: ${otpCode}`); + await this.smsService.sendInvoiceVerifyCode(user.phone, otpCode, invoice.numericId, new Decimal(invoice.totalPrice).toNumber(), items); + this.logger.debug(`OTP sent to ${user.phone}: ${otpCode}`); return { message: AuthMessage.OTP_SENT, @@ -115,15 +111,18 @@ export class InvoicesService { //********************************** */ - async approveInvoiceByUser(invoiceId: string, userId: string, verifyOtpDto: VerifyOtpDto) { - const { code, phone } = verifyOtpDto; - - const user = await this.checkUserVerifyCredentialWithPhone(phone, code, userId); - if (!user) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_APPROVED); + async approveInvoiceByUser(invoiceId: string, userId: string, verifyOtpDto: VerifyOtpWithUserId) { + const { code } = verifyOtpDto; const invoice = await this.invoiceRepository.findOneBy({ id: invoiceId, user: { id: userId } }); if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); + const { user } = await this.usersService.findOneById(userId); + + const isValid = await this.otpService.verifyOtp(user.phone, code, "INVOICE_VERIFY"); + if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); + + if (invoice.status === InvoiceStatus.REJECTED) throw new BadRequestException(InvoiceMessage.INVOICE_IS_REJECTED); if (invoice.status === InvoiceStatus.APPROVED) throw new BadRequestException(InvoiceMessage.ALREADY_APPROVED); if (invoice.status === InvoiceStatus.PAID) throw new BadRequestException(InvoiceMessage.INVOICE_ALREADY_PAID); if (dayjs().isAfter(invoice.dueDate)) throw new BadRequestException(InvoiceMessage.INVOICE_IS_OVERDUE); @@ -407,36 +406,4 @@ export class InvoicesService { }); return !!usage; } - - //*********************************** */ - - private async checkUserVerifyCredentialWithPhone(phone: string, otpCode: string, userId: string) { - //TODO: Change key from LOGIN to something else - const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN"); - if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - - await this.otpService.delOtpFormCache(phone, "LOGIN"); - - const { user } = await this.usersService.findOneById(userId); - if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); - - return user; - } - - //*********************************** */ - - // async calculateFinalPrice(invoiceId: string): Promise { - // const invoice = await this.invoiceRepository.findOneBy({ id: invoiceId }); - // if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); - - // let discountValue = new Decimal(0); - // if (invoice.discount) { - // if (invoice.discount.calculationType === "PERCENTAGE") { - // discountValue = invoice.totalPrice.mul(invoice.discount.amount).div(100); - // } else if (invoice.discount.calculationType === "FIXED") { - // discountValue = new Decimal(invoice.discount.amount); - // } - // } - // return invoice.totalPrice.minus(discountValue); - // } } diff --git a/src/modules/settings/entities/notif-setting.entity.ts b/src/modules/settings/entities/notif-setting.entity.ts deleted file mode 100755 index 6397084..0000000 --- a/src/modules/settings/entities/notif-setting.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Column, Entity, OneToMany } from "typeorm"; - -import { UserSetting } from "./user-setting.entity"; -import { BaseEntity } from "../../../common/entities/base.entity"; -import { FarsiNotifSettingsEnum, NotifSettingsEnum } from "../enums/notif-settings.enum"; - -@Entity() -export class NotifSetting extends BaseEntity { - @Column({ type: "enum", enum: NotifSettingsEnum, unique: true }) - title: string; - - @Column({ type: "enum", enum: FarsiNotifSettingsEnum }) - title_fa: string; - - @OneToMany(() => UserSetting, (userSettings) => userSettings.notifSetting) - userSettings: UserSetting[]; -} diff --git a/src/modules/settings/entities/notification-setting.entity.ts b/src/modules/settings/entities/notification-setting.entity.ts new file mode 100755 index 0000000..1b6440b --- /dev/null +++ b/src/modules/settings/entities/notification-setting.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity, OneToMany } from "typeorm"; + +import { UserSetting } from "./user-setting.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { NotifCategory, NotifType } from "../enums/notif-settings.enum"; + +@Entity() +export class NotificationSetting extends BaseEntity { + @Column({ type: "enum", enum: NotifType, unique: true }) + type: NotifType; + + @Column({ type: "enum", enum: NotifCategory }) + category: NotifCategory; + + @Column({ type: "text" }) + description: string; + + @OneToMany(() => UserSetting, (userSetting) => userSetting.notificationSetting) + userSettings: UserSetting[]; + + // constructor(type: NotifType) { + // super(); + // this.type = type; + // this.category = NotifDescriptions[type].category; + // this.description = NotifDescriptions[type].fa; + // } +} diff --git a/src/modules/settings/entities/user-setting.entity.ts b/src/modules/settings/entities/user-setting.entity.ts index 48320c8..ca4a59c 100755 --- a/src/modules/settings/entities/user-setting.entity.ts +++ b/src/modules/settings/entities/user-setting.entity.ts @@ -1,19 +1,22 @@ import { Column, Entity, ManyToOne } from "typeorm"; -import { NotifSetting } from "./notif-setting.entity"; +import { NotificationSetting } from "./notification-setting.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { User } from "../../users/entities/user.entity"; @Entity() export class UserSetting extends BaseEntity { + // @Column({ type: "boolean", default: true }) + // sms: boolean; + + // @Column({ type: "boolean", default: false }) + // email: boolean; + @Column({ type: "boolean", default: true }) - sms: boolean; + isActive: boolean; - @Column({ type: "boolean", default: false }) - email: boolean; - - @ManyToOne(() => NotifSetting, (notifSetting) => notifSetting.userSettings) - notifSetting: NotifSetting; + @ManyToOne(() => NotificationSetting, (notifSetting) => notifSetting.userSettings, { nullable: false }) + notificationSetting: NotificationSetting; @ManyToOne(() => User, (user) => user.settings, { nullable: false }) user: User; diff --git a/src/modules/settings/enums/notif-settings.enum.ts b/src/modules/settings/enums/notif-settings.enum.ts index faf49c4..d1d3679 100755 --- a/src/modules/settings/enums/notif-settings.enum.ts +++ b/src/modules/settings/enums/notif-settings.enum.ts @@ -1,23 +1,56 @@ -export enum NotifSettingsEnum { - ANNOUNCE_NOTIF = "ANNOUNCE_NOTIF", - BILL_PAYMENT = "BILL_PAYMENT", - CREATE_SERVICE = "CREATE_SERVICE", - UNBLOCKING_SERVICE = "UNBLOCKING_SERVICE", - ANSWER_TICKET = "ANSWER_TICKET", +export enum NotifCategory { + SUPPORT = "SUPPORT", //TICKET + SERVICE = "SERVICE", + INVOICE = "INVOICE", + ACCOUNT = "ACCOUNT", + FINANCE = "FINANCE", //WALLET AND ... + // ANNOUNCEMENT = "ANNOUNCEMENT", +} + +export enum NotifType { + // Account category + USER_LOGIN = "USER_LOGIN", + ANNOUNCEMENT = "ANNOUNCEMENT", + + // Finance category + WALLET_CHARGE = "WALLET_CHARGE", + WALLET_DEDUCTION = "WALLET_DEDUCTION", + + //Invoice + BILL_INVOICE_REMINDER = "BILL_INVOICE_REMINDER", + BILL_INVOICE = "BILL_INVOICE", CREATE_INVOICE = "CREATE_INVOICE", - BILL_PAYMENT_REMINDER = "BILL_PAYMENT_REMINDER", - BLOCKING_SERVICE = "BLOCKING_SERVICE", + + // Service category + CREATE_SERVICE = "CREATE_SERVICE", + UNBLOCK_SERVICE = "UNBLOCK_SERVICE", + BLOCK_SERVICE = "BLOCK_SERVICE", + + // Support category + ANSWER_TICKET = "ANSWER_TICKET", CREATE_TICKET = "CREATE_TICKET", } -export enum FarsiNotifSettingsEnum { - ANNOUNCE_NOTIF = "اعلانات و اطلاعیه ها", - BILL_PAYMENT = "پرداخت صورت حساب", - CREATE_SERVICE = "ایجاد سرویس", - UNBLOCKING_SERVICE = "رفع انسداد سرویس", - ANSWER_TICKET = "پاسخ تیکت", - CREATE_INVOICE = "ایجاد صورت حساب", - BILL_PAYMENT_REMINDER = "یادآوری پرداخت صورت حساب", - BLOCKING_SERVICE = "انسداد سرویس", - CREATE_TICKET = "ثبت تیکت جدبد", -} +export const NotifDescriptions: Record = { + // Account category + [NotifType.USER_LOGIN]: { fa: "ورود به ناحیه کاربری", category: NotifCategory.ACCOUNT }, + [NotifType.ANNOUNCEMENT]: { fa: "اعلانات و اطلاعیه ها", category: NotifCategory.ACCOUNT }, + + // Finance category + [NotifType.WALLET_CHARGE]: { fa: "شارژ کیف پول", category: NotifCategory.FINANCE }, + [NotifType.WALLET_DEDUCTION]: { fa: "کسر از کیف پول", category: NotifCategory.FINANCE }, + + // Invoice category + [NotifType.CREATE_INVOICE]: { fa: "ایجاد صورت حساب", category: NotifCategory.INVOICE }, + [NotifType.BILL_INVOICE_REMINDER]: { fa: "یادآوری پرداخت صورت حساب", category: NotifCategory.INVOICE }, + [NotifType.BILL_INVOICE]: { fa: "پرداخت صورت حساب", category: NotifCategory.INVOICE }, + + // Service category + [NotifType.CREATE_SERVICE]: { fa: "ایجاد سرویس", category: NotifCategory.SERVICE }, + [NotifType.UNBLOCK_SERVICE]: { fa: "رفع انسداد سرویس", category: NotifCategory.SERVICE }, + [NotifType.BLOCK_SERVICE]: { fa: "انسداد سرویس", category: NotifCategory.SERVICE }, + + // Support category + [NotifType.ANSWER_TICKET]: { fa: "پاسخ تیکت", category: NotifCategory.SUPPORT }, + [NotifType.CREATE_TICKET]: { fa: "ثبت تیکت جدید", category: NotifCategory.SUPPORT }, +}; diff --git a/src/modules/settings/enums/user-settings.ts b/src/modules/settings/enums/user-settings.ts index 97232bb..2f1b719 100755 --- a/src/modules/settings/enums/user-settings.ts +++ b/src/modules/settings/enums/user-settings.ts @@ -1,67 +1 @@ -import { NotifSettingsEnum } from "./notif-settings.enum"; - -export const userSettings = [ - { - title: NotifSettingsEnum.ANNOUNCE_NOTIF, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.ANSWER_TICKET, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.BILL_PAYMENT, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.BILL_PAYMENT_REMINDER, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.BLOCKING_SERVICE, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.CREATE_INVOICE, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.CREATE_SERVICE, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.CREATE_TICKET, - fields: { - sms: true, - email: false, - }, - }, - { - title: NotifSettingsEnum.UNBLOCKING_SERVICE, - fields: { - sms: true, - email: false, - }, - }, -]; +export const userSettings = []; diff --git a/src/modules/settings/providers/user-settings.service.ts b/src/modules/settings/providers/user-settings.service.ts index a7b9843..437a2d9 100755 --- a/src/modules/settings/providers/user-settings.service.ts +++ b/src/modules/settings/providers/user-settings.service.ts @@ -1,12 +1,11 @@ import { BadRequestException, Injectable } from "@nestjs/common"; -import { QueryRunner } from "typeorm"; +import { In, QueryRunner } from "typeorm"; import { CommonMessage, SettingMessageEnum, UserMessage } from "../../../common/enums/message.enum"; import { User } from "../../users/entities/user.entity"; -import { UpdateUserSettingsDto } from "../DTO/update-settings.dto"; -import { NotifSetting } from "../entities/notif-setting.entity"; +import { NotificationSetting } from "../entities/notification-setting.entity"; import { UserSetting } from "../entities/user-setting.entity"; -import { userSettings } from "../enums/user-settings"; +import { NotifType } from "../enums/notif-settings.enum"; import { UserSettingsRepository } from "../repositories/user-settings.repository"; @Injectable() @@ -15,66 +14,61 @@ export class UserSettingsService { //*+******************************************* async createUserSettings(userId: string, queryRunner: QueryRunner) { - const user = await queryRunner.manager.findOneBy(User, { - id: userId, - }); + const user = await queryRunner.manager.findOneBy(User, { id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); - for (const setting of userSettings) { - const notifSetting = await queryRunner.manager.findOneBy(NotifSetting, { - title: setting.title, - }); - if (!notifSetting) throw new BadRequestException(SettingMessageEnum.NOTIF_NOT_FOUND); - const userSetting = queryRunner.manager.create(UserSetting, { - ...setting, - notifSetting, + const notifSettings = await queryRunner.manager.find(NotificationSetting, { + where: { type: In(Object.values(NotifType)) }, + }); + + if (notifSettings.length !== Object.values(NotifType).length) throw new BadRequestException(SettingMessageEnum.NOTIF_NOT_FOUND); + + const userSettings = notifSettings.map((notifSetting) => + queryRunner.manager.create(UserSetting, { + isActive: true, + notificationSetting: notifSetting, user, - }); - await queryRunner.manager.save(userSetting); - } - return { - message: CommonMessage.CREATED, - }; + }), + ); + + await queryRunner.manager.save(UserSetting, userSettings); + + return { message: CommonMessage.CREATED }; } //*+******************************************* async getSettings(userId: string) { - const settings = await this.userSettingsRepository.find({ - where: { - user: { - id: userId, - }, - }, - relations: ["notifSetting"], - order: { - createdAt: "ASC", - }, - }); - return { settings }; + const groupedSettings = await this.userSettingsRepository + .createQueryBuilder("userSetting") + .innerJoin("userSetting.notificationSetting", "notif") + .where("userSetting.userId = :userId", { userId }) + .select([ + "notif.category AS category", + "jsonb_agg(jsonb_build_object('id', userSetting.id, 'type', notif.type, 'description', notif.description, 'isActive', userSetting.isActive)) AS settings", + ]) + .groupBy("notif.category") + .getRawMany(); + + // const settings = groupedSettings.reduce( + // (acc, row) => { + // acc[row.category] = row.settings; + // return acc; + // }, + // {} as Record, + // ); + + return { settings: groupedSettings }; } //*+******************************************* - async updateUserSetting(settingId: string, updateDto: UpdateUserSettingsDto) { - const setting = await this.userSettingsRepository.findOne({ - where: { - id: settingId, - }, - order: { - createdAt: "ASC", - }, - }); + async toggleUserNotificationSetting(settingId: string) { + const setting = await this.userSettingsRepository.findOneBy({ id: settingId }); if (!setting) throw new BadRequestException(SettingMessageEnum.NOT_FOUND); - if (updateDto.sms !== undefined) { - setting.sms = updateDto.sms; - } - - if (updateDto.email !== undefined) { - setting.email = updateDto.email; - } + setting.isActive = !setting.isActive; await this.userSettingsRepository.save(setting); - return setting; + return { message: CommonMessage.UPDATE_SUCCESS, setting }; } } diff --git a/src/modules/settings/repositories/notif-settings.repository.ts b/src/modules/settings/repositories/notif-settings.repository.ts index 9922248..db48f15 100755 --- a/src/modules/settings/repositories/notif-settings.repository.ts +++ b/src/modules/settings/repositories/notif-settings.repository.ts @@ -2,11 +2,11 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; -import { NotifSetting } from "../entities/notif-setting.entity"; +import { NotificationSetting } from "../entities/notification-setting.entity"; @Injectable() -export class NotifSettingsRepository extends Repository { - constructor(@InjectRepository(NotifSetting) notifSettingsRepository: Repository) { +export class NotifSettingsRepository extends Repository { + constructor(@InjectRepository(NotificationSetting) notifSettingsRepository: Repository) { super(notifSettingsRepository.target, notifSettingsRepository.manager, notifSettingsRepository.queryRunner); } } diff --git a/src/modules/settings/settings.controller.ts b/src/modules/settings/settings.controller.ts index cb772ab..3b883a0 100755 --- a/src/modules/settings/settings.controller.ts +++ b/src/modules/settings/settings.controller.ts @@ -1,12 +1,10 @@ -import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch } from "@nestjs/common"; +import { Controller, Get, HttpCode, HttpStatus, Param, Patch } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; -import { UpdateUserSettingsDto } from "./DTO/update-settings.dto"; import { UserSettingsService } from "./providers/user-settings.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; -import { User } from "../users/entities/user.entity"; @Controller("settings") @ApiTags("Settings") @@ -16,15 +14,15 @@ export class SettingsController { @AuthGuards() @ApiOperation({ summary: "Get all settings" }) @Get() - async getSettings(@UserDec() user: User) { - return await this.userSettingsService.getSettings(user.id); + async getSettings(@UserDec("id") userId: string) { + return await this.userSettingsService.getSettings(userId); } @AuthGuards() @ApiOperation({ summary: "update status of setting" }) @HttpCode(HttpStatus.OK) - @Patch(":id/update") - updateStatus(@Param() paramDto: ParamDto, @Body() createDto: UpdateUserSettingsDto) { - return this.userSettingsService.updateUserSetting(paramDto.id, createDto); + @Patch(":id/toggle") + updateStatus(@Param() paramDto: ParamDto) { + return this.userSettingsService.toggleUserNotificationSetting(paramDto.id); } } diff --git a/src/modules/settings/settings.module.ts b/src/modules/settings/settings.module.ts index 6ca6b5d..940be55 100755 --- a/src/modules/settings/settings.module.ts +++ b/src/modules/settings/settings.module.ts @@ -1,17 +1,16 @@ -import { Module, forwardRef } from "@nestjs/common"; +import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { NotifSetting } from "./entities/notif-setting.entity"; +import { NotificationSetting } from "./entities/notification-setting.entity"; import { UserSetting } from "./entities/user-setting.entity"; import { NotifSettingsService } from "./providers/notif-settings.service"; import { UserSettingsService } from "./providers/user-settings.service"; import { NotifSettingsRepository } from "./repositories/notif-settings.repository"; import { UserSettingsRepository } from "./repositories/user-settings.repository"; import { SettingsController } from "./settings.controller"; -import { UsersModule } from "../users/users.module"; @Module({ - imports: [TypeOrmModule.forFeature([UserSetting, NotifSetting]), forwardRef(() => UsersModule)], + imports: [TypeOrmModule.forFeature([UserSetting, NotificationSetting])], providers: [UserSettingsService, NotifSettingsService, UserSettingsRepository, NotifSettingsRepository], controllers: [SettingsController], exports: [UserSettingsService, UserSettingsRepository], diff --git a/src/modules/utils/interfaces/IOtpKey.ts b/src/modules/utils/interfaces/IOtpKey.ts index 565fad8..8389763 100755 --- a/src/modules/utils/interfaces/IOtpKey.ts +++ b/src/modules/utils/interfaces/IOtpKey.ts @@ -1 +1 @@ -export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD"; +export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY"; diff --git a/src/modules/utils/interfaces/ISms.ts b/src/modules/utils/interfaces/ISms.ts index 65539f9..a4e3d78 100755 --- a/src/modules/utils/interfaces/ISms.ts +++ b/src/modules/utils/interfaces/ISms.ts @@ -23,4 +23,4 @@ export interface ISmsVerifyBody { TemplateId: string; } -export type TemplateParams = "VERIFICATIONCODE" | "code"; +export type TemplateParams = "VERIFICATIONCODE" | "code" | "invoiceId" | "price" | "items"; diff --git a/src/modules/utils/providers/sms.service.ts b/src/modules/utils/providers/sms.service.ts index 450fbd3..239674d 100755 --- a/src/modules/utils/providers/sms.service.ts +++ b/src/modules/utils/providers/sms.service.ts @@ -48,6 +48,42 @@ export class SmsService { this.logger.error("error in sending sms", error); } } + //************************************************* */ + + async sendInvoiceVerifyCode(mobile: string, otpCode: string, invoiceId: number, price: number, items: string) { + const smsData: ISmsVerifyBody = { + Parameters: [ + { name: "code", value: otpCode }, + { name: "invoiceId", value: invoiceId.toString() }, + { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, + { name: "items", value: items }, + ], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE, + }; + 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"); + } + } //************************************************* */