diff --git a/src/core/strings/constants/module.constants.ts b/src/core/strings/constants/module.constants.ts index a7b4aa0..8e8896e 100644 --- a/src/core/strings/constants/module.constants.ts +++ b/src/core/strings/constants/module.constants.ts @@ -31,4 +31,6 @@ export enum MODULE_NAME { TIME_GROUPS = 'time-groups', OTP_VERIFICATIONS = 'otp-verification', + + OTP_VERIFIER = 'otp-verifier', } diff --git a/src/database/migrations/1750045520332-add_enum_otp_action_type_and_update_column_otp_verifier.ts b/src/database/migrations/1750045520332-add_enum_otp_action_type_and_update_column_otp_verifier.ts new file mode 100644 index 0000000..963d8e8 --- /dev/null +++ b/src/database/migrations/1750045520332-add_enum_otp_action_type_and_update_column_otp_verifier.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddEnumOtpActionTypeAndUpdateColumnOtpVerifier1750045520332 + implements MigrationInterface +{ + name = 'AddEnumOtpActionTypeAndUpdateColumnOtpVerifier1750045520332'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "otp_verifier" ADD "is_all_action" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `CREATE TYPE "public"."otp_verifier_action_types_enum" AS ENUM('CREATE_DISCOUNT', 'CANCEL_TRANSACTION', 'REJECT_RECONCILIATION', 'ACTIVATE_ITEM', 'ACTIVATE_USER', 'UPDATE_ITEM_PRICE', 'UPDATE_ITEM_DETAILS', 'CONFIRM_TRANSACTION')`, + ); + await queryRunner.query( + `ALTER TABLE "otp_verifier" ADD "action_types" "public"."otp_verifier_action_types_enum" array`, + ); + 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', 'ACTIVATE_ITEM', 'ACTIVATE_USER', 'UPDATE_ITEM_PRICE', 'UPDATE_ITEM_DETAILS', 'CONFIRM_TRANSACTION')`, + ); + 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', 'REJECT_RECONCILIATION')`, + ); + 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"`, + ); + await queryRunner.query( + `ALTER TABLE "otp_verifier" DROP COLUMN "action_types"`, + ); + await queryRunner.query( + `DROP TYPE "public"."otp_verifier_action_types_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "otp_verifier" DROP COLUMN "is_all_action"`, + ); + } +} 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 index cbb7f3d..23f9bc3 100644 --- a/src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts +++ b/src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts @@ -1,5 +1,8 @@ import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; -import { OtpVerifierEntity } from '../../domain/entities/otp-verification.entity'; +import { + OTP_ACTION_TYPE, + OtpVerifierEntity, +} from '../../domain/entities/otp-verification.entity'; import { Column, Entity } from 'typeorm'; import { BaseModel } from 'src/core/modules/data/model/base.model'; @@ -13,4 +16,10 @@ export class OtpVerifierModel @Column({ type: 'varchar', nullable: false }) phone_number: string; + + @Column({ default: false }) + is_all_action: boolean; + + @Column({ type: 'enum', enum: OTP_ACTION_TYPE, array: true, nullable: true }) + action_types: OTP_ACTION_TYPE[] | null; } 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 e4d87ba..f79856a 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,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { OtpVerificationModel } from '../models/otp-verification.model'; import { + OTP_ACTION_TYPE, OtpRequestEntity, OtpVerificationEntity, OtpVerifierEntity, @@ -37,16 +38,28 @@ export class OtpVerificationService { return moment().valueOf(); // epoch millis verification time (now) } - private generateOTPMsgTemplate(payload) { - const { userRequest, newOtp } = payload; - const header = newOtp.action_type.split('_').join(' '); - const otpCode = newOtp?.otp_code; - const username = userRequest?.username; - const otpType = newOtp.action_type + private generateHeaderTemplate(payload) { + const label = payload.action_type; + + // Optional logic to override label based on action type. + // e.g., if (payload.action_type === OTP_ACTION_TYPE.CONFIRM_TRANSACTION) label = 'CONFIRM_BOOKING_TRANSACTION' + + const header = label.split('_').join(' '); + const type = label .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); + return { header, type }; + } + + private generateOTPMsgTemplate(payload) { + const { userRequest, newOtp } = payload; + const { header, type } = this.generateHeaderTemplate(newOtp); + + const otpCode = newOtp?.otp_code; + const username = userRequest?.username; + return { name: 'general_flow', language: { code: 'id' }, @@ -77,7 +90,7 @@ export class OtpVerificationService { { type: 'text', parameter_name: 'type', - text: otpType, + text: type, }, ], }, @@ -146,10 +159,12 @@ export class OtpVerificationService { // save otp to database await this.otpVerificationRepo.save(newOtp); - const verifiers: OtpVerifierEntity[] = await this.otpVerifierRepo.find(); + const verifiers: OtpVerifierEntity[] = await this.getVerifier([ + payload.action_type, + ]); const notificationService = new WhatsappService(); - verifiers.map((v) => { + verifiers?.map((v) => { notificationService.sendTemplateMessage({ phone: v.phone_number, templateMsg: this.generateOTPMsgTemplate({ userRequest, newOtp }), @@ -238,4 +253,14 @@ export class OtpVerificationService { ) .getOne(); } + + async getVerifier(actions: OTP_ACTION_TYPE[]) { + const tableALias = TABLE_NAME.OTP_VERIFIER; + const results = await this.otpVerifierRepo + .createQueryBuilder(tableALias) + .where(`${tableALias}.is_all_action = :isAll`, { isAll: true }) + .orWhere(`${tableALias}.action_types && :actions`, { actions }) + .getMany(); + return results ?? []; + } } diff --git a/src/modules/configuration/otp-verification/data/services/otp-verifier.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verifier.service.ts new file mode 100644 index 0000000..eb06bd8 --- /dev/null +++ b/src/modules/configuration/otp-verification/data/services/otp-verifier.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { OtpVerifierModel } from '../models/otp-verifier.model'; +import { OtpVerifierCreateDto } from '../../infrastructure/dto/otp-verification.dto'; +import * as moment from 'moment'; + +@Injectable() +export class OtpVerifierService { + constructor( + @InjectRepository(OtpVerifierModel) + private readonly otpVerifierRepo: Repository, + ) {} + + async create(payload: OtpVerifierCreateDto) { + const dateNow = moment().valueOf(); + + return this.otpVerifierRepo.save({ + ...payload, + created_at: dateNow, + updated_at: dateNow, + }); + } +} 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 951b4a9..6300834 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 @@ -4,6 +4,14 @@ export enum OTP_ACTION_TYPE { CREATE_DISCOUNT = 'CREATE_DISCOUNT', CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', REJECT_RECONCILIATION = 'REJECT_RECONCILIATION', + + ACTIVATE_USER = 'ACTIVATE_USER', + + ACTIVATE_ITEM = 'ACTIVATE_ITEM', + UPDATE_ITEM_PRICE = 'UPDATE_ITEM_PRICE', + UPDATE_ITEM_DETAILS = 'UPDATE_ITEM_DETAILS', + + CONFIRM_TRANSACTION = 'CONFIRM_TRANSACTION', } export enum OTP_SOURCE { @@ -37,4 +45,6 @@ export interface OtpVerifyEntity extends OtpRequestEntity { export interface OtpVerifierEntity { name: string; phone_number: string; + is_all_action?: boolean; + action_types?: OTP_ACTION_TYPE[] | null; } 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 cfd0097..ef721de 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,4 +1,13 @@ -import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { + IsArray, + IsBoolean, + IsEnum, + IsNotEmpty, + IsOptional, + IsPhoneNumber, + IsString, + ValidateIf, +} from 'class-validator'; import { OTP_ACTION_TYPE, OTP_SOURCE, @@ -6,6 +15,7 @@ import { OtpVerifyEntity, } from '../../domain/entities/otp-verification.entity'; import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; export class OtpRequestDto implements OtpRequestEntity { @ApiProperty({ @@ -53,3 +63,50 @@ export class OtpVerifyDto extends OtpRequestDto implements OtpVerifyEntity { @IsNotEmpty() otp_code: string; } + +export class OtpVerifierCreateDto { + @ApiProperty({ + example: 'Item Manager', + description: 'Nama verifier, opsional.', + }) + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({ + example: '6281234567890', + description: 'Nomor telepon verifier dalam format internasional (E.164).', + }) + @IsString() + @IsPhoneNumber('ID') + phone_number: string; + + @ApiProperty({ + example: false, + description: + 'True jika verifier boleh memverifikasi semua aksi tanpa batas.', + }) + @IsBoolean() + is_all_action: boolean; + + @ApiProperty({ + isArray: true, + enum: OTP_ACTION_TYPE, + example: [ + 'CREATE_DISCOUNT', + 'CANCEL_TRANSACTION', + 'REJECT_RECONCILIATION', + 'ACTIVATE_ITEM', + 'ACTIVATE_USER', + 'UPDATE_ITEM_PRICE', + 'UPDATE_ITEM_DETAILS', + 'CONFIRM_TRANSACTION', + ], + description: 'Daftar tipe aksi yang boleh diverifikasi, jika bukan semua.', + }) + @IsOptional() + @IsArray() + @IsEnum(OTP_ACTION_TYPE, { each: true }) + @Type(() => String) + action_types?: OTP_ACTION_TYPE[]; +} 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 bd249ac..1d1124f 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 @@ -7,14 +7,18 @@ import { Req, UseGuards, } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { Public } from 'src/core/guards'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ExcludePrivilege, 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'; +import { + OtpRequestDto, + OtpVerifierCreateDto, + OtpVerifyDto, +} from './dto/otp-verification.dto'; import { OtpAuthGuard } from './guards/otp-auth-guard'; +import { OtpVerifierService } from '../data/services/otp-verifier.service'; -//TODO implementation auth @ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.OTP_VERIFICATIONS}`) @Public() @@ -40,3 +44,17 @@ export class OtpVerificationController { return this.otpVerificationService.getActiveOtp(ref_or_target_id); } } + +@ApiTags(`${MODULE_NAME.OTP_VERIFIER.split('-').join(' ')} - data`) +@Controller(`v1/${MODULE_NAME.OTP_VERIFIER}`) +@ApiBearerAuth('JWT') +@Public(false) +export class OtpVerifierController { + constructor(private readonly otpVerifierService: OtpVerifierService) {} + + @Post() + @ExcludePrivilege() + async create(@Body() body: OtpVerifierCreateDto) { + return await this.otpVerifierService.create(body); + } +} diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index 1407b71..efdfba6 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -4,7 +4,10 @@ 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 { + OtpVerificationController, + OtpVerifierController, +} from './infrastructure/otp-verification-data.controller'; import { OtpVerificationService } from './data/services/otp-verification.service'; import { OtpVerifierModel } from './data/models/otp-verifier.model'; import { OtpAuthGuard } from './infrastructure/guards/otp-auth-guard'; @@ -12,6 +15,7 @@ import { OtpAuthGuard } from './infrastructure/guards/otp-auth-guard'; import { JwtModule } from '@nestjs/jwt'; import { JWT_EXPIRED } from 'src/core/sessions/constants'; import { JWT_SECRET } from 'src/core/sessions/constants'; +import { OtpVerifierService } from './data/services/otp-verifier.service'; @Module({ imports: [ @@ -27,7 +31,7 @@ import { JWT_SECRET } from 'src/core/sessions/constants'; signOptions: { expiresIn: JWT_EXPIRED }, }), ], - controllers: [OtpVerificationController], - providers: [OtpAuthGuard, OtpVerificationService], + controllers: [OtpVerificationController, OtpVerifierController], + providers: [OtpAuthGuard, OtpVerificationService, OtpVerifierService], }) export class OtpVerificationModule {} diff --git a/src/modules/item-related/item/infrastructure/item-data.controller.ts b/src/modules/item-related/item/infrastructure/item-data.controller.ts index fefb105..01ed1dd 100644 --- a/src/modules/item-related/item/infrastructure/item-data.controller.ts +++ b/src/modules/item-related/item/infrastructure/item-data.controller.ts @@ -41,16 +41,19 @@ export class ItemDataController { } @Patch(':id/active') + // TODO => simpan OTP update yang disikim dari request ini async active(@Param('id') dataId: string): Promise { return await this.orchestrator.active(dataId); } @Put('/batch-active') + // TODO => simpan OTP update yang disikim dari request ini async batchActive(@Body() body: BatchIdsDto): Promise { return await this.orchestrator.batchActive(body.ids); } @Patch(':id/confirm') + // TODO => simpan OTP update yang disikim dari request ini async confirm(@Param('id') dataId: string): Promise { return await this.orchestrator.confirm(dataId); } @@ -71,6 +74,7 @@ export class ItemDataController { } @Put(':id') + // TODO => simpan OTP update yang disikim dari request ini async update( @Param('id') dataId: string, @Body() data: ItemDto, diff --git a/src/modules/season-related/season-period/infrastructure/season-period-data.controller.ts b/src/modules/season-related/season-period/infrastructure/season-period-data.controller.ts index e44e0c3..20aa20a 100644 --- a/src/modules/season-related/season-period/infrastructure/season-period-data.controller.ts +++ b/src/modules/season-related/season-period/infrastructure/season-period-data.controller.ts @@ -80,6 +80,7 @@ export class SeasonPeriodDataController { } // pemisahan update data dengan update items dikarenakan payload (based on tampilan) berbeda + // TODO => simpan OTP update yang disikim dari request ini @Put(':id/items') async updateItems( @Param('id') dataId: string, diff --git a/src/modules/user-related/user/infrastructure/user-data.controller.ts b/src/modules/user-related/user/infrastructure/user-data.controller.ts index 6377695..d3b6d64 100644 --- a/src/modules/user-related/user/infrastructure/user-data.controller.ts +++ b/src/modules/user-related/user/infrastructure/user-data.controller.ts @@ -36,21 +36,25 @@ export class UserDataController { } @Patch(':id/active') + // TODO => simpan OTP update yang disikim dari request ini async active(@Param('id') dataId: string): Promise { return await this.orchestrator.active(dataId); } @Put('/batch-active') + // TODO => simpan OTP update yang disikim dari request ini async batchActive(@Body() body: BatchIdsDto): Promise { return await this.orchestrator.batchActive(body.ids); } @Patch(':id/confirm') + // TODO => simpan OTP update yang disikim dari request ini async confirm(@Param('id') dataId: string): Promise { return await this.orchestrator.confirm(dataId); } @Put('/batch-confirm') + // TODO => simpan OTP update yang disikim dari request ini async batchConfirm(@Body() body: BatchIdsDto): Promise { return await this.orchestrator.batchConfirm(body.ids); }