feat(SPG-1234): add action_type at verifier

pull/157/head
Firman Ramdhani 2025-06-16 11:48:56 +07:00
parent d6717c9c60
commit ec5229645f
9 changed files with 219 additions and 18 deletions

View File

@ -31,4 +31,6 @@ export enum MODULE_NAME {
TIME_GROUPS = 'time-groups', TIME_GROUPS = 'time-groups',
OTP_VERIFICATIONS = 'otp-verification', OTP_VERIFICATIONS = 'otp-verification',
OTP_VERIFIER = 'otp-verifier',
} }

View File

@ -0,0 +1,55 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddEnumOtpActionTypeAndUpdateColumnOtpVerifier1750045520332
implements MigrationInterface
{
name = 'AddEnumOtpActionTypeAndUpdateColumnOtpVerifier1750045520332';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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"`,
);
}
}

View File

@ -1,5 +1,8 @@
import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; 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 { Column, Entity } from 'typeorm';
import { BaseModel } from 'src/core/modules/data/model/base.model'; import { BaseModel } from 'src/core/modules/data/model/base.model';
@ -13,4 +16,10 @@ export class OtpVerifierModel
@Column({ type: 'varchar', nullable: false }) @Column({ type: 'varchar', nullable: false })
phone_number: string; 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;
} }

View File

@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { OtpVerificationModel } from '../models/otp-verification.model'; import { OtpVerificationModel } from '../models/otp-verification.model';
import { import {
OTP_ACTION_TYPE,
OtpRequestEntity, OtpRequestEntity,
OtpVerificationEntity, OtpVerificationEntity,
OtpVerifierEntity, OtpVerifierEntity,
@ -37,16 +38,28 @@ export class OtpVerificationService {
return moment().valueOf(); // epoch millis verification time (now) return moment().valueOf(); // epoch millis verification time (now)
} }
private generateOTPMsgTemplate(payload) { private generateHeaderTemplate(payload) {
const { userRequest, newOtp } = payload; const label = payload.action_type;
const header = newOtp.action_type.split('_').join(' ');
const otpCode = newOtp?.otp_code; // Optional logic to override label based on action type.
const username = userRequest?.username; // e.g., if (payload.action_type === OTP_ACTION_TYPE.CONFIRM_TRANSACTION) label = 'CONFIRM_BOOKING_TRANSACTION'
const otpType = newOtp.action_type
const header = label.split('_').join(' ');
const type = label
.split('_') .split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' '); .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 { return {
name: 'general_flow', name: 'general_flow',
language: { code: 'id' }, language: { code: 'id' },
@ -77,7 +90,7 @@ export class OtpVerificationService {
{ {
type: 'text', type: 'text',
parameter_name: 'type', parameter_name: 'type',
text: otpType, text: type,
}, },
], ],
}, },
@ -146,10 +159,12 @@ export class OtpVerificationService {
// save otp to database // save otp to database
await this.otpVerificationRepo.save(newOtp); 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(); const notificationService = new WhatsappService();
verifiers.map((v) => { verifiers?.map((v) => {
notificationService.sendTemplateMessage({ notificationService.sendTemplateMessage({
phone: v.phone_number, phone: v.phone_number,
templateMsg: this.generateOTPMsgTemplate({ userRequest, newOtp }), templateMsg: this.generateOTPMsgTemplate({ userRequest, newOtp }),
@ -238,4 +253,14 @@ export class OtpVerificationService {
) )
.getOne(); .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 ?? [];
}
} }

View File

@ -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<OtpVerifierModel>,
) {}
async create(payload: OtpVerifierCreateDto) {
const dateNow = moment().valueOf();
return this.otpVerifierRepo.save({
...payload,
created_at: dateNow,
updated_at: dateNow,
});
}
}

View File

@ -4,6 +4,11 @@ export enum OTP_ACTION_TYPE {
CREATE_DISCOUNT = 'CREATE_DISCOUNT', CREATE_DISCOUNT = 'CREATE_DISCOUNT',
CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', CANCEL_TRANSACTION = 'CANCEL_TRANSACTION',
REJECT_RECONCILIATION = 'REJECT_RECONCILIATION', REJECT_RECONCILIATION = 'REJECT_RECONCILIATION',
ACTIVATE_ITEM = 'ACTIVATE_ITEM',
ACTIVATE_USER = 'ACTIVATE_USER',
UPDATE_ITEM_PRICE = 'UPDATE_ITEM_PRICE',
UPDATE_ITEM_DETAILS = 'UPDATE_ITEM_DETAILS',
CONFIRM_TRANSACTION = 'CONFIRM_TRANSACTION',
} }
export enum OTP_SOURCE { export enum OTP_SOURCE {
@ -37,4 +42,6 @@ export interface OtpVerifyEntity extends OtpRequestEntity {
export interface OtpVerifierEntity { export interface OtpVerifierEntity {
name: string; name: string;
phone_number: string; phone_number: string;
is_all_action?: boolean;
action_types?: OTP_ACTION_TYPE[] | null;
} }

View File

@ -1,4 +1,13 @@
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsOptional,
IsPhoneNumber,
IsString,
ValidateIf,
} from 'class-validator';
import { import {
OTP_ACTION_TYPE, OTP_ACTION_TYPE,
OTP_SOURCE, OTP_SOURCE,
@ -6,6 +15,7 @@ import {
OtpVerifyEntity, OtpVerifyEntity,
} from '../../domain/entities/otp-verification.entity'; } from '../../domain/entities/otp-verification.entity';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class OtpRequestDto implements OtpRequestEntity { export class OtpRequestDto implements OtpRequestEntity {
@ApiProperty({ @ApiProperty({
@ -53,3 +63,50 @@ export class OtpVerifyDto extends OtpRequestDto implements OtpVerifyEntity {
@IsNotEmpty() @IsNotEmpty()
otp_code: string; 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[];
}

View File

@ -7,14 +7,18 @@ import {
Req, Req,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards'; import { ExcludePrivilege, Public } from 'src/core/guards';
import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants';
import { OtpVerificationService } from '../data/services/otp-verification.service'; 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 { 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`) @ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`)
@Controller(`v1/${MODULE_NAME.OTP_VERIFICATIONS}`) @Controller(`v1/${MODULE_NAME.OTP_VERIFICATIONS}`)
@Public() @Public()
@ -40,3 +44,17 @@ export class OtpVerificationController {
return this.otpVerificationService.getActiveOtp(ref_or_target_id); 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);
}
}

View File

@ -4,7 +4,10 @@ import { ConfigModule } from '@nestjs/config';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { OtpVerificationModel } from './data/models/otp-verification.model'; 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 { OtpVerificationService } from './data/services/otp-verification.service';
import { OtpVerifierModel } from './data/models/otp-verifier.model'; import { OtpVerifierModel } from './data/models/otp-verifier.model';
import { OtpAuthGuard } from './infrastructure/guards/otp-auth-guard'; 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 { JwtModule } from '@nestjs/jwt';
import { JWT_EXPIRED } from 'src/core/sessions/constants'; import { JWT_EXPIRED } from 'src/core/sessions/constants';
import { JWT_SECRET } from 'src/core/sessions/constants'; import { JWT_SECRET } from 'src/core/sessions/constants';
import { OtpVerifierService } from './data/services/otp-verifier.service';
@Module({ @Module({
imports: [ imports: [
@ -27,7 +31,7 @@ import { JWT_SECRET } from 'src/core/sessions/constants';
signOptions: { expiresIn: JWT_EXPIRED }, signOptions: { expiresIn: JWT_EXPIRED },
}), }),
], ],
controllers: [OtpVerificationController], controllers: [OtpVerificationController, OtpVerifierController],
providers: [OtpAuthGuard, OtpVerificationService], providers: [OtpAuthGuard, OtpVerificationService, OtpVerifierService],
}) })
export class OtpVerificationModule {} export class OtpVerificationModule {}