Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
commit
10a553ac9d
|
@ -101,6 +101,11 @@ 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';
|
||||
import { OtpVerifierModel } from './modules/configuration/otp-verification/data/models/otp-verifier.model';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApmModule.register(),
|
||||
|
@ -165,6 +170,9 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti
|
|||
|
||||
// Booking Online
|
||||
VerificationModel,
|
||||
|
||||
OtpVerificationModel,
|
||||
OtpVerifierModel,
|
||||
],
|
||||
synchronize: false,
|
||||
}),
|
||||
|
@ -230,6 +238,7 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti
|
|||
|
||||
BookingOnlineAuthModule,
|
||||
BookingOrderModule,
|
||||
OtpVerificationModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
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<string, number> = {};
|
||||
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) ||
|
||||
otp?.split('')?.length < this.otpLength
|
||||
);
|
||||
|
||||
return otp;
|
||||
}
|
||||
}
|
|
@ -30,4 +30,5 @@ export enum MODULE_NAME {
|
|||
QUEUE = 'queue',
|
||||
|
||||
TIME_GROUPS = 'time-groups',
|
||||
OTP_VERIFICATIONS = 'otp-verification',
|
||||
}
|
||||
|
|
|
@ -45,4 +45,6 @@ export enum TABLE_NAME {
|
|||
QUEUE_BUCKET = 'queue_bucket',
|
||||
|
||||
TIME_GROUPS = 'time_groups',
|
||||
OTP_VERIFICATIONS = 'otp_verifications',
|
||||
OTP_VERIFIER = 'otp_verifier',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddColumnOtpCode1748935417155 implements MigrationInterface {
|
||||
name = 'AddColumnOtpCode1748935417155';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" DROP COLUMN "otp_code"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "vip_codes" DROP COLUMN "otp_code"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateTableOtpCerifications1749028279580
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'CreateTableOtpCerifications1749028279580';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddColumnIsReplacedOtpVerification1749030419440
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddColumnIsReplacedOtpVerification1749030419440';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "otp_verifications" ADD "is_replaced" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "otp_verifications" DROP COLUMN "is_replaced"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTableOtpVerifier1749043616622 implements MigrationInterface {
|
||||
name = 'AddTableOtpVerifier1749043616622';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(`DROP TABLE "otp_verifier"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateEnumOtpActionType1749046285398
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'UpdateEnumOtpActionType1749046285398';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
||||
import {
|
||||
OTP_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<OtpVerificationEntity>
|
||||
implements OtpVerificationEntity
|
||||
{
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
otp_code: string;
|
||||
|
||||
@Column({ type: 'enum', enum: OTP_ACTION_TYPE })
|
||||
action_type: OTP_ACTION_TYPE;
|
||||
|
||||
@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
|
||||
|
||||
@Column({ type: 'bigint', nullable: true })
|
||||
verified_at: number; // UNIX timestamp or null
|
||||
}
|
|
@ -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<OtpVerifierEntity>
|
||||
implements OtpVerifierEntity
|
||||
{
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
phone_number: string;
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { OtpVerificationModel } from '../models/otp-verification.model';
|
||||
import {
|
||||
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<OtpVerificationModel>,
|
||||
|
||||
@InjectRepository(OtpVerifierModel)
|
||||
private readonly otpVerifierRepo: Repository<OtpVerifierModel>,
|
||||
) {}
|
||||
|
||||
private generateOtpExpiration(minutes = 5): number {
|
||||
return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time
|
||||
}
|
||||
|
||||
private generateResendAvailableAt(seconds = 60): 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();
|
||||
|
||||
//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: payload.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.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 < 60) {
|
||||
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);
|
||||
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.`,
|
||||
updated_at: expiredAt,
|
||||
resend_available_at: this.generateResendAvailableAt(),
|
||||
};
|
||||
}
|
||||
|
||||
async verifyOTP(payload: OtpVerifyEntity) {
|
||||
const { otp_code, action_type, target_id, reference, source } = 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,
|
||||
source,
|
||||
is_used: false,
|
||||
is_replaced: false,
|
||||
},
|
||||
{
|
||||
otp_code,
|
||||
action_type,
|
||||
reference,
|
||||
source,
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
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 {
|
||||
POS = 'POS',
|
||||
WEB = 'WEB',
|
||||
}
|
||||
|
||||
export interface OtpVerificationEntity extends BaseEntity {
|
||||
otp_code: string;
|
||||
action_type: OTP_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: OTP_ACTION_TYPE;
|
||||
source: OTP_SOURCE;
|
||||
target_id: string;
|
||||
reference: string;
|
||||
}
|
||||
|
||||
export interface OtpVerifyEntity extends OtpRequestEntity {
|
||||
otp_code: string;
|
||||
}
|
||||
|
||||
export interface OtpVerifierEntity {
|
||||
name: string;
|
||||
phone_number: string;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';
|
||||
import {
|
||||
OTP_ACTION_TYPE,
|
||||
OTP_SOURCE,
|
||||
OtpRequestEntity,
|
||||
OtpVerifyEntity,
|
||||
} from '../../domain/entities/otp-verification.entity';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class OtpRequestDto implements OtpRequestEntity {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
example: OTP_ACTION_TYPE.CANCEL_TRANSACTION,
|
||||
description: 'CANCEL_TRANSACTION || CREATE_DISCOUNT',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
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',
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
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';
|
||||
|
||||
//TODO implementation auth
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
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';
|
||||
import { OtpVerifierModel } from './data/models/otp-verifier.model';
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
TypeOrmModule.forFeature(
|
||||
[OtpVerificationModel, OtpVerifierModel],
|
||||
CONNECTION_NAME.DEFAULT,
|
||||
),
|
||||
],
|
||||
controllers: [OtpVerificationController],
|
||||
providers: [OtpVerificationService],
|
||||
})
|
||||
export class OtpVerificationModule {}
|
|
@ -26,6 +26,7 @@ export class DetailItemManager extends BaseDetailManager<ItemEntity> {
|
|||
selectRelations: [
|
||||
'item_category',
|
||||
'bundling_items',
|
||||
'bundling_items.time_group bundling_time_groups',
|
||||
'tenant',
|
||||
'time_group',
|
||||
],
|
||||
|
@ -64,6 +65,9 @@ export class DetailItemManager extends BaseDetailManager<ItemEntity> {
|
|||
'bundling_items.hpp',
|
||||
'bundling_items.base_price',
|
||||
|
||||
'bundling_time_groups.id',
|
||||
'bundling_time_groups.name',
|
||||
|
||||
'tenant.id',
|
||||
'tenant.name',
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export class CreateTimeGroupManager extends BaseCreateManager<TimeGroupEntity> {
|
|||
|
||||
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;
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseIndexManager } from 'src/core/modules/domain/usecase/managers/base-index.manager';
|
||||
import { TimeGroupEntity } from '../../entities/time-group.entity';
|
||||
import { SelectQueryBuilder } from 'typeorm';
|
||||
import {
|
||||
Param,
|
||||
RelationParam,
|
||||
} from 'src/core/modules/domain/entities/base-filter.entity';
|
||||
|
||||
// TODO:
|
||||
// Implementasikan filter by start_time, end_timen, dan max_usage_time
|
||||
|
||||
@Injectable()
|
||||
export class IndexPublicTimeGroupManager extends BaseIndexManager<TimeGroupEntity> {
|
||||
async prepareData(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeProcess(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async afterProcess(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
get relations(): RelationParam {
|
||||
return {
|
||||
joinRelations: ['items'],
|
||||
selectRelations: [],
|
||||
countRelations: ['items'],
|
||||
};
|
||||
}
|
||||
|
||||
get selects(): string[] {
|
||||
return [
|
||||
`${this.tableName}.id`,
|
||||
`${this.tableName}.status`,
|
||||
`${this.tableName}.name`,
|
||||
`${this.tableName}.start_time`,
|
||||
`${this.tableName}.end_time`,
|
||||
`${this.tableName}.max_usage_time`,
|
||||
`${this.tableName}.created_at`,
|
||||
`${this.tableName}.creator_name`,
|
||||
`${this.tableName}.updated_at`,
|
||||
`${this.tableName}.editor_name`,
|
||||
];
|
||||
}
|
||||
|
||||
get specificFilter(): Param[] {
|
||||
return [
|
||||
{
|
||||
cols: `${this.tableName}.name`,
|
||||
data: this.filterParam.names,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
setQueryFilter(
|
||||
queryBuilder: SelectQueryBuilder<TimeGroupEntity>,
|
||||
): SelectQueryBuilder<TimeGroupEntity> {
|
||||
queryBuilder.andWhere(`items.id is not null`);
|
||||
return queryBuilder;
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ export class UpdateTimeGroupManager extends BaseUpdateManager<TimeGroupEntity> {
|
|||
|
||||
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;
|
||||
|
|
|
@ -6,11 +6,13 @@ import { PaginationResponse } from 'src/core/response/domain/ok-response.interfa
|
|||
import { BaseReadOrchestrator } from 'src/core/modules/domain/usecase/orchestrators/base-read.orchestrator';
|
||||
import { DetailTimeGroupManager } from './managers/detail-time-group.manager';
|
||||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
||||
import { IndexPublicTimeGroupManager } from './managers/index-public-time-group.manager';
|
||||
|
||||
@Injectable()
|
||||
export class TimeGroupReadOrchestrator extends BaseReadOrchestrator<TimeGroupEntity> {
|
||||
constructor(
|
||||
private indexManager: IndexTimeGroupManager,
|
||||
private indexPublicManager: IndexPublicTimeGroupManager,
|
||||
private detailManager: DetailTimeGroupManager,
|
||||
private serviceData: TimeGroupReadService,
|
||||
) {
|
||||
|
@ -24,6 +26,16 @@ export class TimeGroupReadOrchestrator extends BaseReadOrchestrator<TimeGroupEnt
|
|||
return this.indexManager.getResult();
|
||||
}
|
||||
|
||||
async indexPublic(params): Promise<PaginationResponse<TimeGroupEntity>> {
|
||||
this.indexPublicManager.setFilterParam(params);
|
||||
this.indexPublicManager.setService(
|
||||
this.serviceData,
|
||||
TABLE_NAME.TIME_GROUPS,
|
||||
);
|
||||
await this.indexPublicManager.execute();
|
||||
return this.indexPublicManager.getResult();
|
||||
}
|
||||
|
||||
async detail(dataId: string): Promise<TimeGroupEntity> {
|
||||
this.detailManager.setData(dataId);
|
||||
this.detailManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS);
|
||||
|
|
|
@ -28,3 +28,19 @@ export class TimeGroupReadController {
|
|||
return await this.orchestrator.detail(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiTags(`${MODULE_NAME.TIME_GROUPS.split('-').join(' ')} List- read`)
|
||||
// @Controller(`v1/${MODULE_NAME.TIME_GROUPS}-list`)
|
||||
@Controller(``)
|
||||
@Public()
|
||||
export class TimeGroupPublicReadController {
|
||||
constructor(private orchestrator: TimeGroupReadOrchestrator) {}
|
||||
|
||||
@Get('v1/time-group-list-by-items')
|
||||
@Pagination()
|
||||
async indexPublic(
|
||||
@Query() params: FilterTimeGroupDto,
|
||||
): Promise<PaginationResponse<TimeGroupEntity>> {
|
||||
return await this.orchestrator.indexPublic(params);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
|
||||
import { TimeGroupDataService } from './data/services/time-group-data.service';
|
||||
import { TimeGroupReadService } from './data/services/time-group-read.service';
|
||||
import { TimeGroupReadController } from './infrastructure/time-group-read.controller';
|
||||
import {
|
||||
TimeGroupPublicReadController,
|
||||
TimeGroupReadController,
|
||||
} from './infrastructure/time-group-read.controller';
|
||||
import { TimeGroupReadOrchestrator } from './domain/usecases/time-group-read.orchestrator';
|
||||
import { TimeGroupDataController } from './infrastructure/time-group-data.controller';
|
||||
import { TimeGroupDataOrchestrator } from './domain/usecases/time-group-data.orchestrator';
|
||||
|
@ -22,6 +25,7 @@ import { BatchActiveTimeGroupManager } from './domain/usecases/managers/batch-ac
|
|||
import { BatchConfirmTimeGroupManager } from './domain/usecases/managers/batch-confirm-time-group.manager';
|
||||
import { BatchInactiveTimeGroupManager } from './domain/usecases/managers/batch-inactive-time-group.manager';
|
||||
import { TimeGroupModel } from './data/models/time-group.model';
|
||||
import { IndexPublicTimeGroupManager } from './domain/usecases/managers/index-public-time-group.manager';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -29,8 +33,13 @@ import { TimeGroupModel } from './data/models/time-group.model';
|
|||
TypeOrmModule.forFeature([TimeGroupModel], CONNECTION_NAME.DEFAULT),
|
||||
CqrsModule,
|
||||
],
|
||||
controllers: [TimeGroupDataController, TimeGroupReadController],
|
||||
controllers: [
|
||||
TimeGroupDataController,
|
||||
TimeGroupReadController,
|
||||
TimeGroupPublicReadController,
|
||||
],
|
||||
providers: [
|
||||
IndexPublicTimeGroupManager,
|
||||
IndexTimeGroupManager,
|
||||
DetailTimeGroupManager,
|
||||
CreateTimeGroupManager,
|
||||
|
|
|
@ -84,6 +84,13 @@ export default <ReportConfigEntity>{
|
|||
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`,
|
||||
|
|
|
@ -119,6 +119,14 @@ export default <ReportConfigEntity>{
|
|||
type: DATA_TYPE.DIMENSION,
|
||||
format: DATA_FORMAT.TEXT,
|
||||
},
|
||||
{
|
||||
column: 'vip__otp_code',
|
||||
query: 'vip.otp_code',
|
||||
label: 'Kode OTP',
|
||||
type: DATA_TYPE.DIMENSION,
|
||||
format: DATA_FORMAT.TEXT,
|
||||
},
|
||||
|
||||
{
|
||||
column: 'privilege__name',
|
||||
query: 'privilege.name',
|
||||
|
|
|
@ -50,6 +50,13 @@ export default <ReportConfigEntity>{
|
|||
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`,
|
||||
|
|
|
@ -35,6 +35,13 @@ export default <ReportConfigEntity>{
|
|||
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',
|
||||
|
|
|
@ -274,4 +274,7 @@ export class TransactionModel
|
|||
onUpdate: 'CASCADE',
|
||||
})
|
||||
refunds: RefundModel[];
|
||||
|
||||
@Column('varchar', { name: 'otp_code', nullable: true })
|
||||
otp_code: string;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class CreateVipCodeHandler implements IEventHandler<ChangeDocEvent> {
|
|||
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) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { BaseDto } from 'src/core/modules/infrastructure/dto/base.dto';
|
||||
import { VipCodeEntity } from '../../domain/entities/vip-code.entity';
|
||||
import { IsNumber, IsObject, IsString } from 'class-validator';
|
||||
import { IsNumber, IsObject, IsString, ValidateIf } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class VipCodeDto extends BaseDto implements VipCodeEntity {
|
||||
|
@ -29,6 +29,7 @@ export class VipCodeDto extends BaseDto implements VipCodeEntity {
|
|||
example: 25000,
|
||||
})
|
||||
@IsNumber()
|
||||
@ValidateIf((v) => v.discount_value)
|
||||
discount_value: number;
|
||||
|
||||
@ApiProperty({
|
||||
|
|
Loading…
Reference in New Issue