From ffd8595ab26fa619261e0df621d17ffbe7af0d45 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:26:09 +0700 Subject: [PATCH 01/12] feat: add column top_code --- .../1748935417155-add_column_otp_code.ts | 21 +++++++++++++++++++ .../data/models/transaction.model.ts | 3 +++ .../vip-code/data/models/vip-code.model.ts | 3 +++ 3 files changed, 27 insertions(+) create mode 100644 src/database/migrations/1748935417155-add_column_otp_code.ts diff --git a/src/database/migrations/1748935417155-add_column_otp_code.ts b/src/database/migrations/1748935417155-add_column_otp_code.ts new file mode 100644 index 0000000..a7347fd --- /dev/null +++ b/src/database/migrations/1748935417155-add_column_otp_code.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddColumnOtpCode1748935417155 implements MigrationInterface { + name = 'AddColumnOtpCode1748935417155'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "vip_codes" ADD "otp_code" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" ADD "otp_code" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "otp_code"`, + ); + await queryRunner.query(`ALTER TABLE "vip_codes" DROP COLUMN "otp_code"`); + } +} diff --git a/src/modules/transaction/transaction/data/models/transaction.model.ts b/src/modules/transaction/transaction/data/models/transaction.model.ts index 053503c..83c4416 100644 --- a/src/modules/transaction/transaction/data/models/transaction.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction.model.ts @@ -274,4 +274,7 @@ export class TransactionModel onUpdate: 'CASCADE', }) refunds: RefundModel[]; + + @Column('varchar', { name: 'otp_code', nullable: true }) + otp_code: string; } diff --git a/src/modules/transaction/vip-code/data/models/vip-code.model.ts b/src/modules/transaction/vip-code/data/models/vip-code.model.ts index 314b047..57e45e0 100644 --- a/src/modules/transaction/vip-code/data/models/vip-code.model.ts +++ b/src/modules/transaction/vip-code/data/models/vip-code.model.ts @@ -27,4 +27,7 @@ export class VipCodeModel }) @JoinColumn({ name: 'vip_category_id' }) vip_category: VipCategoryModel; + + @Column('varchar', { name: 'otp_code', nullable: true }) + otp_code: string; } -- 2.40.1 From 5e328fda1ecc965e1a479154f6252e7082385649 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:27:30 +0700 Subject: [PATCH 02/12] feat: fix response time group --- .../domain/usecases/managers/create-time-group.manager.ts | 2 +- .../domain/usecases/managers/update-time-group.manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts index 8dc54a5..ce07b64 100644 --- a/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts +++ b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts @@ -37,7 +37,7 @@ export class CreateTimeGroupManager extends BaseCreateManager { if (max_usage_time.isBefore(end_time)) { throw new Error( - 'Waktu maksimum penggunaan harus lebih kecil dari waktu selesai.', + 'Waktu maksimum penggunaan harus lebih besar dari waktu selesai.', ); } return; diff --git a/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts index 904bfb4..f003866 100644 --- a/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts +++ b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts @@ -38,7 +38,7 @@ export class UpdateTimeGroupManager extends BaseUpdateManager { if (max_usage_time.isBefore(end_time)) { throw new Error( - 'Waktu maksimum penggunaan harus lebih kecil dari waktu selesai.', + 'Waktu maksimum penggunaan harus lebih besar dari waktu selesai.', ); } return; -- 2.40.1 From 5f08b4be66076ebf3fbc69becc148eed911cad72 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:48:10 +0700 Subject: [PATCH 03/12] feat: setup module otp verifications --- .../strings/constants/module.constants.ts | 1 + src/core/strings/constants/table.constants.ts | 1 + .../data/models/otp-verification.model.ts | 35 +++++++++++++++++++ .../services/otp-verification-data.service.ts | 0 .../services/otp-verification-read.service.ts | 0 .../entities/otp-verification.entity.ts | 21 +++++++++++ .../otp-verification-data.orchestrator.ts | 0 .../otp-verification-read.orchestrator.ts | 0 .../dto/otp-verification.dto.ts | 0 .../otp-verification-data.controller.ts | 0 .../otp-verification-read.controller.ts | 0 .../otp-verification.module.ts | 0 12 files changed, 58 insertions(+) create mode 100644 src/modules/configuration/otp-verification/data/models/otp-verification.model.ts create mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts create mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts create mode 100644 src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts create mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts create mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts create mode 100644 src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts create mode 100644 src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts create mode 100644 src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts create mode 100644 src/modules/configuration/otp-verification/otp-verification.module.ts diff --git a/src/core/strings/constants/module.constants.ts b/src/core/strings/constants/module.constants.ts index 032dace..a7b4aa0 100644 --- a/src/core/strings/constants/module.constants.ts +++ b/src/core/strings/constants/module.constants.ts @@ -30,4 +30,5 @@ export enum MODULE_NAME { QUEUE = 'queue', TIME_GROUPS = 'time-groups', + OTP_VERIFICATIONS = 'otp-verification', } diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts index dcdc1a3..edc0b7b 100644 --- a/src/core/strings/constants/table.constants.ts +++ b/src/core/strings/constants/table.constants.ts @@ -45,4 +45,5 @@ export enum TABLE_NAME { QUEUE_BUCKET = 'queue_bucket', TIME_GROUPS = 'time_groups', + OTP_VERIFICATIONS = 'otp_verifications', } diff --git a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts new file mode 100644 index 0000000..4db9191 --- /dev/null +++ b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts @@ -0,0 +1,35 @@ +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { + OPT_ACTION_TYPE, + OTP_SOURCE, + OtpVerificationEntity, +} from '../../domain/entities/otp-verification.entity'; +import { Column, Entity } from 'typeorm'; +import { BaseModel } from 'src/core/modules/data/model/base.model'; + +@Entity(TABLE_NAME.OTP_VERIFICATIONS) +export class OtpVerificationModel + extends BaseModel + implements OtpVerificationEntity +{ + @Column({ type: 'varchar', nullable: false }) + otp_code: string; + + @Column({ type: 'enum', enum: OPT_ACTION_TYPE }) + action_type: OPT_ACTION_TYPE; + + @Column({ type: 'varchar', nullable: true }) + target_id: string; + + @Column({ type: 'enum', enum: OTP_SOURCE }) + source: OTP_SOURCE; + + @Column({ default: false }) + is_used: boolean; + + @Column({ type: 'bigint', nullable: false }) + expired_at: number; // UNIX timestamp + + @Column({ type: 'bigint', nullable: true }) + verified_at: number; // UNIX timestamp or null +} diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts new file mode 100644 index 0000000..88e0c9f --- /dev/null +++ b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts @@ -0,0 +1,21 @@ +import { BaseEntity } from 'src/core/modules/domain/entities//base.entity'; + +export enum OPT_ACTION_TYPE { + CREATE_DISCOUNT = 'CREATE_DISCOUNT', + CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', +} + +export enum OTP_SOURCE { + POS = 'POS', + WEB = 'WEB', +} + +export interface OtpVerificationEntity extends BaseEntity { + otp_code: string; + action_type: OPT_ACTION_TYPE; + target_id: string; + source: OTP_SOURCE; + is_used: boolean; + expired_at: number; + verified_at: number; +} diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts b/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts new file mode 100644 index 0000000..e69de29 -- 2.40.1 From d8cfa9761209cc0287a80dbb0771a29cb2d12317 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:41:46 +0700 Subject: [PATCH 04/12] fix: add time group at item bundling --- .../item/domain/usecases/managers/detail-item.manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts index b091df9..21e3e4b 100644 --- a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts @@ -26,6 +26,7 @@ export class DetailItemManager extends BaseDetailManager { selectRelations: [ 'item_category', 'bundling_items', + 'bundling_items.time_group bundling_time_groups', 'tenant', 'time_group', ], @@ -64,6 +65,9 @@ export class DetailItemManager extends BaseDetailManager { 'bundling_items.hpp', 'bundling_items.base_price', + 'bundling_time_groups.id', + 'bundling_time_groups.name', + 'tenant.id', 'tenant.name', -- 2.40.1 From 538abb122f8c37476dff43c8f805b0d3d9e3e4f3 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:01:23 +0700 Subject: [PATCH 05/12] feat: implement module otp verification --- src/app.module.ts | 6 + src/core/helpers/otp/otp-service.ts | 57 +++++++ ...28279580-create-table-otp-cerifications.ts | 29 ++++ ...add_column_is_replaced_otp_verification.ts | 19 +++ .../data/models/otp-verification.model.ts | 6 + .../services/otp-verification-data.service.ts | 0 .../services/otp-verification-read.service.ts | 0 .../data/services/otp-verification.service.ts | 158 ++++++++++++++++++ .../entities/otp-verification.entity.ts | 12 ++ .../otp-verification-data.orchestrator.ts | 0 .../otp-verification-read.orchestrator.ts | 0 .../dto/otp-verification.dto.ts | 44 +++++ .../otp-verification-data.controller.ts | 30 ++++ .../otp-verification-read.controller.ts | 0 .../otp-verification.module.ts | 17 ++ 15 files changed, 378 insertions(+) create mode 100644 src/core/helpers/otp/otp-service.ts create mode 100644 src/database/migrations/1749028279580-create-table-otp-cerifications.ts create mode 100644 src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts delete mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts delete mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts create mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification.service.ts delete mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts delete mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts delete mode 100644 src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6bb3641..e8f891d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -101,6 +101,9 @@ import { BookingOnlineAuthModule } from './modules/booking-online/authentication import { BookingOrderModule } from './modules/booking-online/order/order.module'; import { TimeGroupModule } from './modules/item-related/time-group/time-group.module'; import { TimeGroupModel } from './modules/item-related/time-group/data/models/time-group.model'; + +import { OtpVerificationModule } from './modules/configuration/otp-verification/otp-verification.module'; +import { OtpVerificationModel } from './modules/configuration/otp-verification/data/models/otp-verification.model'; @Module({ imports: [ ApmModule.register(), @@ -165,6 +168,7 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti // Booking Online VerificationModel, + OtpVerificationModel, ], synchronize: false, }), @@ -230,6 +234,8 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti BookingOnlineAuthModule, BookingOrderModule, + + OtpVerificationModule, ], controllers: [], providers: [ diff --git a/src/core/helpers/otp/otp-service.ts b/src/core/helpers/otp/otp-service.ts new file mode 100644 index 0000000..e5f9433 --- /dev/null +++ b/src/core/helpers/otp/otp-service.ts @@ -0,0 +1,57 @@ +interface OtpServiceEntity { + length?: number; +} + +export class OtpService { + private readonly otpLength: number; + + constructor({ length = 4 }: OtpServiceEntity) { + this.otpLength = Math.max(length, 4); // Minimum of 4 digits + } + + private hasSequentialDigits(str: string): boolean { + for (let i = 0; i < str.length - 1; i++) { + const current = parseInt(str[i], 10); + const next = parseInt(str[i + 1], 10); + if (next === current + 1 || next === current - 1) { + return true; + } + } + return false; + } + + private hasRepeatedDigits(str: string): boolean { + return str.split('').every((char) => char === str[0]); + } + + private isPalindrome(str: string): boolean { + return str === str.split('').reverse().join(''); + } + + private hasPartiallyRepeatedDigits(str: string): boolean { + const counts: Record = {}; + for (const char of str) { + counts[char] = (counts[char] || 0) + 1; + } + + // Reject if any digit appears more than twice + return Object.values(counts).some((count) => count > 2); + } + + public generateSecureOTP(): string { + let otp: string; + + do { + otp = Array.from({ length: this.otpLength }, () => + Math.floor(Math.random() * 10).toString(), + ).join(''); + } while ( + this.hasSequentialDigits(otp) || + this.hasRepeatedDigits(otp) || + this.isPalindrome(otp) || + this.hasPartiallyRepeatedDigits(otp) + ); + + return otp; + } +} diff --git a/src/database/migrations/1749028279580-create-table-otp-cerifications.ts b/src/database/migrations/1749028279580-create-table-otp-cerifications.ts new file mode 100644 index 0000000..a1d2800 --- /dev/null +++ b/src/database/migrations/1749028279580-create-table-otp-cerifications.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateTableOtpCerifications1749028279580 + implements MigrationInterface +{ + name = 'CreateTableOtpCerifications1749028279580'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."otp_verifications_action_type_enum" AS ENUM('CREATE_DISCOUNT', 'CANCEL_TRANSACTION')`, + ); + await queryRunner.query( + `CREATE TYPE "public"."otp_verifications_source_enum" AS ENUM('POS', 'WEB')`, + ); + await queryRunner.query( + `CREATE TABLE "otp_verifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "creator_id" character varying(36), "creator_name" character varying(125), "editor_id" character varying(36), "editor_name" character varying(125), "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "otp_code" character varying NOT NULL, "action_type" "public"."otp_verifications_action_type_enum" NOT NULL, "target_id" character varying, "reference" character varying, "source" "public"."otp_verifications_source_enum" NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "expired_at" bigint NOT NULL, "verified_at" bigint, CONSTRAINT "PK_91d17e75ac3182dba6701869b39" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "otp_verifications"`); + await queryRunner.query( + `DROP TYPE "public"."otp_verifications_source_enum"`, + ); + await queryRunner.query( + `DROP TYPE "public"."otp_verifications_action_type_enum"`, + ); + } +} diff --git a/src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts b/src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts new file mode 100644 index 0000000..befa570 --- /dev/null +++ b/src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddColumnIsReplacedOtpVerification1749030419440 + implements MigrationInterface +{ + name = 'AddColumnIsReplacedOtpVerification1749030419440'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "otp_verifications" ADD "is_replaced" boolean NOT NULL DEFAULT false`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "otp_verifications" DROP COLUMN "is_replaced"`, + ); + } +} diff --git a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts index 4db9191..22032aa 100644 --- a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts +++ b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts @@ -21,12 +21,18 @@ export class OtpVerificationModel @Column({ type: 'varchar', nullable: true }) target_id: string; + @Column({ type: 'varchar', nullable: true }) + reference: string; + @Column({ type: 'enum', enum: OTP_SOURCE }) source: OTP_SOURCE; @Column({ default: false }) is_used: boolean; + @Column({ default: false }) + is_replaced: boolean; + @Column({ type: 'bigint', nullable: false }) expired_at: number; // UNIX timestamp diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts new file mode 100644 index 0000000..12c5c09 --- /dev/null +++ b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts @@ -0,0 +1,158 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { OtpVerificationModel } from '../models/otp-verification.model'; +import { + OTP_SOURCE, + OtpRequestEntity, + OtpVerificationEntity, + OtpVerifyEntity, +} from '../../domain/entities/otp-verification.entity'; +import * as moment from 'moment'; +import { OtpService } from 'src/core/helpers/otp/otp-service'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +@Injectable() +export class OtpVerificationService { + constructor( + @InjectRepository(OtpVerificationModel) + private readonly otpVerificationRepo: Repository, + ) {} + + private generateOtpExpiration(minutes = 5): number { + return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time + } + + private generateResendAvailableAt(seconds = 90): number { + return moment().add(seconds, 'seconds').valueOf(); // epoch millis + } + + private generateTimestamp(): number { + return moment().valueOf(); // epoch millis verification time (now) + } + + async requestOTP(payload: OtpRequestEntity) { + const otpService = new OtpService({ length: 4 }); + const otpCode = otpService.generateSecureOTP(); + const dateNow = this.generateTimestamp(); + const expiredAt = this.generateOtpExpiration(); + const source = OTP_SOURCE.WEB; + const creator = { + id: 'c59f811e-873c-4472-bd58-21c111902114', + name: 'dev', + }; + + const newOtp: OtpVerificationEntity = { + otp_code: otpCode, + action_type: payload.action_type, + target_id: payload.target_id, + reference: payload.reference, + source: source, + is_used: false, + is_replaced: false, + expired_at: expiredAt, + + creator_id: creator.id, + creator_name: creator.name, + created_at: dateNow, + verified_at: null, + + editor_id: creator.id, + editor_name: creator.name, + updated_at: dateNow, + }; + + const activeOTP = await this.getActiveOtp( + payload.target_id ?? payload.reference, + ); + + if (activeOTP) { + const createdAtMoment = moment(Number(activeOTP.created_at)); + const nowMoment = moment(Number(dateNow)); + const diffSeconds = nowMoment.diff(createdAtMoment, 'seconds'); + if (diffSeconds < 90) { + throw new BadRequestException( + 'An active OTP request was made recently. Please try again later.', + ); + } else { + // Update data is_replaced on database + this.otpVerificationRepo.save({ + ...activeOTP, + is_replaced: true, + }); + } + } + + // save otp to database + await this.otpVerificationRepo.save(newOtp); + + return { + message: `OTP has been sent to the admin's WhatsApp.`, + updated_at: expiredAt, + resend_available_at: this.generateResendAvailableAt(), + }; + } + + async verifyOTP(payload: OtpVerifyEntity) { + const { otp_code, action_type, target_id, reference } = payload; + const dateNow = this.generateTimestamp(); + + if (!target_id && !reference) { + throw new BadRequestException( + 'Either target_id or reference must be provided.', + ); + } + + // Build a where condition with OR between target_id and reference + const otp = await this.otpVerificationRepo.findOne({ + where: [ + { + otp_code, + action_type, + target_id, + is_used: false, + is_replaced: false, + }, + { + otp_code, + action_type, + reference, + is_used: false, + is_replaced: false, + }, + ], + }); + + if (!otp) { + throw new BadRequestException('Invalid or expired OTP.'); + } else if (otp.expired_at <= dateNow) { + throw new BadRequestException('OTP has expired.'); + } + + otp.is_used = true; + otp.verified_at = dateNow; + + // update otp to database + await this.otpVerificationRepo.save(otp); + return { message: 'OTP verified successfully.' }; + } + + async getActiveOtp(payload: string) { + const now = this.generateTimestamp(); + const tableName = TABLE_NAME.OTP_VERIFICATIONS; + + return this.otpVerificationRepo + .createQueryBuilder(tableName) + .where( + `(${tableName}.target_id = :payload OR ${tableName}.reference = :payload) + AND ${tableName}.is_used = false + AND ${tableName}.is_replaced = false + AND ${tableName}.expired_at > :now`, + { payload, now }, + ) + .orderBy( + `CASE WHEN ${tableName}.target_id = :payload THEN 0 ELSE 1 END`, + 'ASC', + ) + .getOne(); + } +} diff --git a/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts index 88e0c9f..78e8c68 100644 --- a/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts +++ b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts @@ -14,8 +14,20 @@ export interface OtpVerificationEntity extends BaseEntity { otp_code: string; action_type: OPT_ACTION_TYPE; target_id: string; + reference: string; source: OTP_SOURCE; is_used: boolean; + is_replaced: boolean; expired_at: number; verified_at: number; } + +export interface OtpRequestEntity { + action_type: OPT_ACTION_TYPE; + target_id: string; + reference: string; +} + +export interface OtpVerifyEntity extends OtpRequestEntity { + otp_code: string; +} diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts b/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts index e69de29..34786d2 100644 --- a/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts +++ b/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts @@ -0,0 +1,44 @@ +import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { + OPT_ACTION_TYPE, + OtpRequestEntity, + OtpVerifyEntity, +} from '../../domain/entities/otp-verification.entity'; +import { ApiProperty } from '@nestjs/swagger'; + +export class OtpRequestDto implements OtpRequestEntity { + @ApiProperty({ + type: String, + required: true, + example: OPT_ACTION_TYPE.CANCEL_TRANSACTION, + description: 'CANCEL_TRANSACTION || CREATE_DISCOUNT', + }) + @IsString() + @IsNotEmpty() + action_type: OPT_ACTION_TYPE; + + @ApiProperty({ + name: 'target_id', + example: 'bccc0c6a-51a0-437f-abc8-dc18851604ee', + }) + @IsString() + @ValidateIf((body) => body.target_id) + target_id: string; + + @ApiProperty({ name: 'reference', example: '0625N21' }) + @IsString() + @ValidateIf((body) => body.reference) + reference: string; +} + +export class OtpVerifyDto extends OtpRequestDto implements OtpVerifyEntity { + @ApiProperty({ + name: 'otp_code', + type: String, + required: true, + example: '2345', + }) + @IsString() + @IsNotEmpty() + otp_code: string; +} diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts index e69de29..0eb5957 100644 --- a/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts +++ b/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Public } from 'src/core/guards'; +import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; +import { OtpVerificationService } from '../data/services/otp-verification.service'; +import { OtpRequestDto, OtpVerifyDto } from './dto/otp-verification.dto'; + +@ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`) +@Controller(`v1/${MODULE_NAME.OTP_VERIFICATIONS}`) +@Public() +export class OtpVerificationController { + constructor( + private readonly otpVerificationService: OtpVerificationService, + ) {} + + @Post('request') + async request(@Body() body: OtpRequestDto) { + return await this.otpVerificationService.requestOTP(body); + } + + @Post('verify') + async verify(@Body() body: OtpVerifyDto) { + return await this.otpVerificationService.verifyOTP(body); + } + + @Get(':ref_or_target_id') + async getByPhoneNumber(@Param('ref_or_target_id') ref_or_target_id: string) { + return this.otpVerificationService.getActiveOtp(ref_or_target_id); + } +} diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index e69de29..1fd4aeb 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -0,0 +1,17 @@ +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; + +import { ConfigModule } from '@nestjs/config'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { OtpVerificationModel } from './data/models/otp-verification.model'; +import { OtpVerificationController } from './infrastructure/otp-verification-data.controller'; +import { OtpVerificationService } from './data/services/otp-verification.service'; +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forFeature([OtpVerificationModel], CONNECTION_NAME.DEFAULT), + ], + controllers: [OtpVerificationController], + providers: [OtpVerificationService], +}) +export class OtpVerificationModule {} -- 2.40.1 From ee52a35af2d49c08f75daf8541842004a6eb03cf Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:08:55 +0700 Subject: [PATCH 06/12] feat: implement module otp verification --- .../data/models/otp-verification.model.ts | 6 +++--- .../data/services/otp-verification.service.ts | 10 ++++------ .../domain/entities/otp-verification.entity.ts | 7 ++++--- .../infrastructure/dto/otp-verification.dto.ts | 17 ++++++++++++++--- .../otp-verification-data.controller.ts | 1 + 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts index 22032aa..eec21fb 100644 --- a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts +++ b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts @@ -1,6 +1,6 @@ import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { - OPT_ACTION_TYPE, + OTP_ACTION_TYPE, OTP_SOURCE, OtpVerificationEntity, } from '../../domain/entities/otp-verification.entity'; @@ -15,8 +15,8 @@ export class OtpVerificationModel @Column({ type: 'varchar', nullable: false }) otp_code: string; - @Column({ type: 'enum', enum: OPT_ACTION_TYPE }) - action_type: OPT_ACTION_TYPE; + @Column({ type: 'enum', enum: OTP_ACTION_TYPE }) + action_type: OTP_ACTION_TYPE; @Column({ type: 'varchar', nullable: true }) target_id: string; diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts index 12c5c09..a5b8d7c 100644 --- a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts +++ b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts @@ -35,18 +35,16 @@ export class OtpVerificationService { const otpCode = otpService.generateSecureOTP(); const dateNow = this.generateTimestamp(); const expiredAt = this.generateOtpExpiration(); - const source = OTP_SOURCE.WEB; - const creator = { - id: 'c59f811e-873c-4472-bd58-21c111902114', - name: 'dev', - }; + + //TODO implementation from auth + const creator = { id: null, name: null }; const newOtp: OtpVerificationEntity = { otp_code: otpCode, action_type: payload.action_type, target_id: payload.target_id, reference: payload.reference, - source: source, + source: payload.source, is_used: false, is_replaced: false, expired_at: expiredAt, diff --git a/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts index 78e8c68..3d0dc5a 100644 --- a/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts +++ b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts @@ -1,6 +1,6 @@ import { BaseEntity } from 'src/core/modules/domain/entities//base.entity'; -export enum OPT_ACTION_TYPE { +export enum OTP_ACTION_TYPE { CREATE_DISCOUNT = 'CREATE_DISCOUNT', CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', } @@ -12,7 +12,7 @@ export enum OTP_SOURCE { export interface OtpVerificationEntity extends BaseEntity { otp_code: string; - action_type: OPT_ACTION_TYPE; + action_type: OTP_ACTION_TYPE; target_id: string; reference: string; source: OTP_SOURCE; @@ -23,7 +23,8 @@ export interface OtpVerificationEntity extends BaseEntity { } export interface OtpRequestEntity { - action_type: OPT_ACTION_TYPE; + action_type: OTP_ACTION_TYPE; + source: OTP_SOURCE; target_id: string; reference: string; } diff --git a/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts b/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts index 34786d2..cfd0097 100644 --- a/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts +++ b/src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts @@ -1,6 +1,7 @@ import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { - OPT_ACTION_TYPE, + OTP_ACTION_TYPE, + OTP_SOURCE, OtpRequestEntity, OtpVerifyEntity, } from '../../domain/entities/otp-verification.entity'; @@ -10,12 +11,22 @@ export class OtpRequestDto implements OtpRequestEntity { @ApiProperty({ type: String, required: true, - example: OPT_ACTION_TYPE.CANCEL_TRANSACTION, + example: OTP_ACTION_TYPE.CANCEL_TRANSACTION, description: 'CANCEL_TRANSACTION || CREATE_DISCOUNT', }) @IsString() @IsNotEmpty() - action_type: OPT_ACTION_TYPE; + action_type: OTP_ACTION_TYPE; + + @ApiProperty({ + type: String, + required: true, + example: OTP_SOURCE.POS, + description: 'POS || WEB', + }) + @IsString() + @IsNotEmpty() + source: OTP_SOURCE; @ApiProperty({ name: 'target_id', diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts index 0eb5957..783f109 100644 --- a/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts +++ b/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts @@ -5,6 +5,7 @@ import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; import { OtpVerificationService } from '../data/services/otp-verification.service'; import { OtpRequestDto, OtpVerifyDto } from './dto/otp-verification.dto'; +//TODO implementation auth @ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.OTP_VERIFICATIONS}`) @Public() -- 2.40.1 From 798476aaf53c876bc4b722f2defa6de43d1345ef Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:15:24 +0700 Subject: [PATCH 07/12] feat: implement module otp verification --- .../data/services/otp-verification.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts index a5b8d7c..87ae630 100644 --- a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts +++ b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts @@ -91,7 +91,7 @@ export class OtpVerificationService { } async verifyOTP(payload: OtpVerifyEntity) { - const { otp_code, action_type, target_id, reference } = payload; + const { otp_code, action_type, target_id, reference, source } = payload; const dateNow = this.generateTimestamp(); if (!target_id && !reference) { @@ -107,6 +107,7 @@ export class OtpVerificationService { otp_code, action_type, target_id, + source, is_used: false, is_replaced: false, }, @@ -114,6 +115,7 @@ export class OtpVerificationService { otp_code, action_type, reference, + source, is_used: false, is_replaced: false, }, -- 2.40.1 From 6a0b1f6e0589fa7ee35948c6068d8fe05712791d Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:27:34 +0700 Subject: [PATCH 08/12] feat: setup verifier --- src/app.module.ts | 5 ++- src/core/strings/constants/table.constants.ts | 1 + .../1749043616622-add_table_otp_verifier.ts | 15 ++++++++ ...49046285398-update_enum_otp_action_type.ts | 37 +++++++++++++++++++ .../data/models/otp-verifier.model.ts | 16 ++++++++ .../data/services/otp-verification.service.ts | 18 ++++++++- .../entities/otp-verification.entity.ts | 6 +++ .../otp-verification.module.ts | 6 ++- 8 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/database/migrations/1749043616622-add_table_otp_verifier.ts create mode 100644 src/database/migrations/1749046285398-update_enum_otp_action_type.ts create mode 100644 src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts diff --git a/src/app.module.ts b/src/app.module.ts index e8f891d..75ab21a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -104,6 +104,8 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti import { OtpVerificationModule } from './modules/configuration/otp-verification/otp-verification.module'; import { OtpVerificationModel } from './modules/configuration/otp-verification/data/models/otp-verification.model'; +import { OtpVerifierModel } from './modules/configuration/otp-verification/data/models/otp-verifier.model'; + @Module({ imports: [ ApmModule.register(), @@ -168,7 +170,9 @@ import { OtpVerificationModel } from './modules/configuration/otp-verification/d // Booking Online VerificationModel, + OtpVerificationModel, + OtpVerifierModel, ], synchronize: false, }), @@ -234,7 +238,6 @@ import { OtpVerificationModel } from './modules/configuration/otp-verification/d BookingOnlineAuthModule, BookingOrderModule, - OtpVerificationModule, ], controllers: [], diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts index edc0b7b..2139ba6 100644 --- a/src/core/strings/constants/table.constants.ts +++ b/src/core/strings/constants/table.constants.ts @@ -46,4 +46,5 @@ export enum TABLE_NAME { TIME_GROUPS = 'time_groups', OTP_VERIFICATIONS = 'otp_verifications', + OTP_VERIFIER = 'otp_verifier', } diff --git a/src/database/migrations/1749043616622-add_table_otp_verifier.ts b/src/database/migrations/1749043616622-add_table_otp_verifier.ts new file mode 100644 index 0000000..b2085c4 --- /dev/null +++ b/src/database/migrations/1749043616622-add_table_otp_verifier.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTableOtpVerifier1749043616622 implements MigrationInterface { + name = 'AddTableOtpVerifier1749043616622'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "otp_verifier" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "creator_id" character varying(36), "creator_name" character varying(125), "editor_id" character varying(36), "editor_name" character varying(125), "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "name" character varying, "phone_number" character varying NOT NULL, CONSTRAINT "PK_884e2d0873fc589a1bdc477b2ea" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "otp_verifier"`); + } +} diff --git a/src/database/migrations/1749046285398-update_enum_otp_action_type.ts b/src/database/migrations/1749046285398-update_enum_otp_action_type.ts new file mode 100644 index 0000000..e107607 --- /dev/null +++ b/src/database/migrations/1749046285398-update_enum_otp_action_type.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateEnumOtpActionType1749046285398 + implements MigrationInterface +{ + name = 'UpdateEnumOtpActionType1749046285398'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "public"."otp_verifications_action_type_enum" RENAME TO "otp_verifications_action_type_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "public"."otp_verifications_action_type_enum" AS ENUM('CREATE_DISCOUNT', 'CANCEL_TRANSACTION', 'REJECT_RECONCILIATION')`, + ); + await queryRunner.query( + `ALTER TABLE "otp_verifications" ALTER COLUMN "action_type" TYPE "public"."otp_verifications_action_type_enum" USING "action_type"::"text"::"public"."otp_verifications_action_type_enum"`, + ); + await queryRunner.query( + `DROP TYPE "public"."otp_verifications_action_type_enum_old"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."otp_verifications_action_type_enum_old" AS ENUM('CREATE_DISCOUNT', 'CANCEL_TRANSACTION')`, + ); + await queryRunner.query( + `ALTER TABLE "otp_verifications" ALTER COLUMN "action_type" TYPE "public"."otp_verifications_action_type_enum_old" USING "action_type"::"text"::"public"."otp_verifications_action_type_enum_old"`, + ); + await queryRunner.query( + `DROP TYPE "public"."otp_verifications_action_type_enum"`, + ); + await queryRunner.query( + `ALTER TYPE "public"."otp_verifications_action_type_enum_old" RENAME TO "otp_verifications_action_type_enum"`, + ); + } +} diff --git a/src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts b/src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts new file mode 100644 index 0000000..cbb7f3d --- /dev/null +++ b/src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts @@ -0,0 +1,16 @@ +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { OtpVerifierEntity } from '../../domain/entities/otp-verification.entity'; +import { Column, Entity } from 'typeorm'; +import { BaseModel } from 'src/core/modules/data/model/base.model'; + +@Entity(TABLE_NAME.OTP_VERIFIER) +export class OtpVerifierModel + extends BaseModel + implements OtpVerifierEntity +{ + @Column({ type: 'varchar', nullable: true }) + name: string; + + @Column({ type: 'varchar', nullable: false }) + phone_number: string; +} diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts index 87ae630..fc9a7e5 100644 --- a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts +++ b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts @@ -3,19 +3,26 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { OtpVerificationModel } from '../models/otp-verification.model'; import { - OTP_SOURCE, OtpRequestEntity, OtpVerificationEntity, + OtpVerifierEntity, + // OtpVerifierEntity, OtpVerifyEntity, } from '../../domain/entities/otp-verification.entity'; import * as moment from 'moment'; import { OtpService } from 'src/core/helpers/otp/otp-service'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; +import { OtpVerifierModel } from '../models/otp-verifier.model'; + @Injectable() export class OtpVerificationService { constructor( @InjectRepository(OtpVerificationModel) private readonly otpVerificationRepo: Repository, + + @InjectRepository(OtpVerifierModel) + private readonly otpVerifierRepo: Repository, ) {} private generateOtpExpiration(minutes = 5): number { @@ -82,6 +89,15 @@ export class OtpVerificationService { // save otp to database await this.otpVerificationRepo.save(newOtp); + const verifiers: OtpVerifierEntity[] = await this.otpVerifierRepo.find(); + const notificationService = new WhatsappService(); + + // verifiers.map((v) => { + // notificationService.sendOtpNotification({ + // phone: v.phone_number, + // code: otpCode, + // }); + // }); return { message: `OTP has been sent to the admin's WhatsApp.`, diff --git a/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts index 3d0dc5a..951b4a9 100644 --- a/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts +++ b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts @@ -3,6 +3,7 @@ import { BaseEntity } from 'src/core/modules/domain/entities//base.entity'; export enum OTP_ACTION_TYPE { CREATE_DISCOUNT = 'CREATE_DISCOUNT', CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', + REJECT_RECONCILIATION = 'REJECT_RECONCILIATION', } export enum OTP_SOURCE { @@ -32,3 +33,8 @@ export interface OtpRequestEntity { export interface OtpVerifyEntity extends OtpRequestEntity { otp_code: string; } + +export interface OtpVerifierEntity { + name: string; + phone_number: string; +} diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index 1fd4aeb..6e1f02d 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -6,10 +6,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { OtpVerificationModel } from './data/models/otp-verification.model'; import { OtpVerificationController } from './infrastructure/otp-verification-data.controller'; import { OtpVerificationService } from './data/services/otp-verification.service'; +import { OtpVerifierModel } from './data/models/otp-verifier.model'; @Module({ imports: [ ConfigModule.forRoot(), - TypeOrmModule.forFeature([OtpVerificationModel], CONNECTION_NAME.DEFAULT), + TypeOrmModule.forFeature( + [OtpVerificationModel, OtpVerifierModel], + CONNECTION_NAME.DEFAULT, + ), ], controllers: [OtpVerificationController], providers: [OtpVerificationService], -- 2.40.1 From f9025faf0bdccfa1b1fa722cc967a92570faf7bc Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:22:14 +0700 Subject: [PATCH 09/12] feat: setup message --- .../data/services/otp-verification.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts index fc9a7e5..7905606 100644 --- a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts +++ b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts @@ -92,12 +92,12 @@ export class OtpVerificationService { const verifiers: OtpVerifierEntity[] = await this.otpVerifierRepo.find(); const notificationService = new WhatsappService(); - // verifiers.map((v) => { - // notificationService.sendOtpNotification({ - // phone: v.phone_number, - // code: otpCode, - // }); - // }); + verifiers.map((v) => { + notificationService.sendOtpNotification({ + phone: v.phone_number, + code: otpCode, + }); + }); return { message: `OTP has been sent to the admin's WhatsApp.`, -- 2.40.1 From cc78dfbd06408c611b20df5fe1d2d90959846b10 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:17:06 +0700 Subject: [PATCH 10/12] feat: monitor log data mapper create discount --- .../domain/usecases/handlers/create-vip-code.handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts b/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts index 2c07dcb..006a81b 100644 --- a/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts +++ b/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts @@ -29,7 +29,7 @@ export class CreateVipCodeHandler implements IEventHandler { id: data._id ?? data.id, vip_category_id: data.vip_category?._id ?? data.vip_category?.id, }; - + console.log({ dataMapped }); try { await this.dataService.create(queryRunner, VipCodeModel, dataMapped); } catch (error) { -- 2.40.1 From 88753546b6da6e8617a1b61f9439590e45d69430 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:12:31 +0700 Subject: [PATCH 11/12] feat: add otp column at report, cancel transaction, giving discount, reconciliation, vip_code --- .../transaction-report/configs/cancel-transaction.ts | 7 +++++++ .../transaction-report/configs/giving-discounts.ts | 8 ++++++++ .../configs/transaction-report/configs/reconciliation.ts | 7 +++++++ .../shared/configs/transaction-report/configs/vip_code.ts | 7 +++++++ 4 files changed, 29 insertions(+) diff --git a/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts b/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts index 03467f4..f1d195c 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts @@ -84,6 +84,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__otp_code', + query: 'main.otp_code', + label: 'Kode OTP', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'main__payment_code', query: `CASE WHEN main.type = 'counter' THEN main.invoice_code ELSE main.payment_code END`, diff --git a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts index 99a71ee..94167e2 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts @@ -119,6 +119,14 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'vip__otp_code', + query: 'vip.otp_code', + label: 'Kode OTP Pemberi Diskon', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { column: 'privilege__name', query: 'privilege.name', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts b/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts index 8063ad3..acee33b 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts @@ -50,6 +50,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__otp_code', + query: 'main.otp_code', + label: 'Kode OTP Reject', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'main__payment_date', query: `CASE WHEN main.payment_date is not null THEN to_char(main.payment_date, 'DD-MM-YYYY') ELSE null END`, diff --git a/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts b/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts index a23ece7..a8a4d5b 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts @@ -35,6 +35,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'main__otp_code', + query: 'main.otp_code', + label: 'Kode OTP', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'main__discount', query: 'CASE WHEN main.discount > 0 THEN main.discount ELSE null END', -- 2.40.1 From d86f4075d4443ceecc95f613fb885120a4490c1d Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:40:13 +0700 Subject: [PATCH 12/12] feat: rename label kode otp at giving discount report --- .../configs/transaction-report/configs/giving-discounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts index 94167e2..e46ec43 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts @@ -122,7 +122,7 @@ export default { { column: 'vip__otp_code', query: 'vip.otp_code', - label: 'Kode OTP Pemberi Diskon', + label: 'Kode OTP', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, -- 2.40.1