Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development

pull/158/head 1.6.21-alpha.2
shancheas 2025-06-17 14:26:50 +07:00
commit 6636c596f4
12 changed files with 231 additions and 18 deletions

View File

@ -31,4 +31,6 @@ export enum MODULE_NAME {
TIME_GROUPS = 'time-groups',
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 { 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;
}

View File

@ -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 ?? [];
}
}

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,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;
}

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 {
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[];
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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<string> {
return await this.orchestrator.active(dataId);
}
@Put('/batch-active')
// TODO => simpan OTP update yang disikim dari request ini
async batchActive(@Body() body: BatchIdsDto): Promise<BatchResult> {
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<string> {
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,

View File

@ -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,

View File

@ -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<string> {
return await this.orchestrator.active(dataId);
}
@Put('/batch-active')
// TODO => simpan OTP update yang disikim dari request ini
async batchActive(@Body() body: BatchIdsDto): Promise<BatchResult> {
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<string> {
return await this.orchestrator.confirm(dataId);
}
@Put('/batch-confirm')
// TODO => simpan OTP update yang disikim dari request ini
async batchConfirm(@Body() body: BatchIdsDto): Promise<BatchResult> {
return await this.orchestrator.batchConfirm(body.ids);
}