Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into feat/otp-cancel
commit
dc1fadbe1f
|
@ -105,6 +105,7 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti
|
||||||
import { OtpVerificationModule } from './modules/configuration/otp-verification/otp-verification.module';
|
import { OtpVerificationModule } from './modules/configuration/otp-verification/otp-verification.module';
|
||||||
import { OtpVerificationModel } from './modules/configuration/otp-verification/data/models/otp-verification.model';
|
import { OtpVerificationModel } from './modules/configuration/otp-verification/data/models/otp-verification.model';
|
||||||
import { OtpVerifierModel } from './modules/configuration/otp-verification/data/models/otp-verifier.model';
|
import { OtpVerifierModel } from './modules/configuration/otp-verification/data/models/otp-verifier.model';
|
||||||
|
import { RescheduleVerificationModel } from './modules/booking-online/order/data/models/reschedule-verification.model';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -170,6 +171,7 @@ import { OtpVerifierModel } from './modules/configuration/otp-verification/data/
|
||||||
|
|
||||||
// Booking Online
|
// Booking Online
|
||||||
VerificationModel,
|
VerificationModel,
|
||||||
|
RescheduleVerificationModel,
|
||||||
|
|
||||||
OtpVerificationModel,
|
OtpVerificationModel,
|
||||||
OtpVerifierModel,
|
OtpVerifierModel,
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class RescheduleOtp1749524993295 implements MigrationInterface {
|
||||||
|
name = 'RescheduleOtp1749524993295';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "reschedule_verification" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "phone_number" character varying NOT NULL, "booking_id" character varying NOT NULL, "reschedule_date" character varying NOT NULL, "code" integer NOT NULL, "tried" integer NOT NULL DEFAULT '0', "created_at" bigint NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()) * 1000, "updated_at" bigint NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()) * 1000, CONSTRAINT "PK_d4df453337ca12771eb223323d8" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "booking_verification" ALTER COLUMN "created_at" SET DEFAULT EXTRACT(EPOCH FROM NOW()) * 1000`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "booking_verification" ALTER COLUMN "updated_at" SET DEFAULT EXTRACT(EPOCH FROM NOW()) * 1000`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "booking_verification" ALTER COLUMN "updated_at" SET DEFAULT (EXTRACT(epoch FROM now()) * (1000))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "booking_verification" ALTER COLUMN "created_at" SET DEFAULT (EXTRACT(epoch FROM now()) * (1000))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "reschedule_verification"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
export function generateOtp(digits = 4): number {
|
||||||
|
if (digits < 1) {
|
||||||
|
throw new Error('OTP digits must be at least 1');
|
||||||
|
}
|
||||||
|
const min = Math.pow(10, digits - 1);
|
||||||
|
const max = Math.pow(10, digits) - 1;
|
||||||
|
const otp = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
return otp;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { RescheduleVerification } from '../../domain/entities/reschedule-verification.entity';
|
||||||
|
|
||||||
|
@Entity('reschedule_verification')
|
||||||
|
export class RescheduleVerificationModel implements RescheduleVerification {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
phone_number: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
booking_id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
reschedule_date: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
code: number;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
tried: number;
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', default: () => 'EXTRACT(EPOCH FROM NOW()) * 1000' })
|
||||||
|
created_at: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'bigint',
|
||||||
|
default: () => 'EXTRACT(EPOCH FROM NOW()) * 1000',
|
||||||
|
onUpdate: 'EXTRACT(EPOCH FROM NOW()) * 1000',
|
||||||
|
})
|
||||||
|
updated_at: number;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
export interface RescheduleVerification {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
phone_number: string;
|
||||||
|
booking_id: string;
|
||||||
|
reschedule_date: string;
|
||||||
|
code: number;
|
||||||
|
tried?: number;
|
||||||
|
created_at?: number;
|
||||||
|
updated_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RescheduleRequest {
|
||||||
|
booking_id: string;
|
||||||
|
reschedule_date: string;
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { RescheduleVerificationModel } from '../../../data/models/reschedule-verification.model';
|
||||||
|
import {
|
||||||
|
RescheduleRequest,
|
||||||
|
RescheduleVerification,
|
||||||
|
} from '../../entities/reschedule-verification.entity';
|
||||||
|
import { generateOtp } from 'src/modules/booking-online/helpers/generate-otp';
|
||||||
|
import { TransactionReadService } from 'src/modules/transaction/transaction/data/services/transaction-read.service';
|
||||||
|
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
|
||||||
|
import { WhatsappService } from 'src/services/whatsapp/whatsapp.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RescheduleVerificationManager {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(RescheduleVerificationModel)
|
||||||
|
private readonly rescheduleVerificationRepository: Repository<RescheduleVerificationModel>,
|
||||||
|
private readonly transactionService: TransactionReadService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async saveVerification(
|
||||||
|
request: RescheduleRequest,
|
||||||
|
): Promise<RescheduleVerificationModel> {
|
||||||
|
try {
|
||||||
|
const otp = generateOtp();
|
||||||
|
const transaction = await this.findDetailByBookingId(request.booking_id);
|
||||||
|
|
||||||
|
if (!transaction) {
|
||||||
|
throw new Error('Transaction not found for the provided booking id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Partial<RescheduleVerification> = {
|
||||||
|
code: otp,
|
||||||
|
booking_id: transaction.id,
|
||||||
|
name: transaction.customer_name,
|
||||||
|
phone_number: transaction.customer_phone,
|
||||||
|
reschedule_date: request.reschedule_date,
|
||||||
|
};
|
||||||
|
|
||||||
|
const existTransaction =
|
||||||
|
await this.rescheduleVerificationRepository.findOne({
|
||||||
|
where: {
|
||||||
|
booking_id: transaction.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const verification =
|
||||||
|
existTransaction ?? this.rescheduleVerificationRepository.create(data);
|
||||||
|
const result = await this.rescheduleVerificationRepository.save({
|
||||||
|
...verification,
|
||||||
|
code: otp,
|
||||||
|
});
|
||||||
|
|
||||||
|
const whatsapp = new WhatsappService();
|
||||||
|
whatsapp.sendOtpNotification({
|
||||||
|
phone: transaction.customer_phone,
|
||||||
|
code: otp.toString(),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
// You can customize the error handling as needed, e.g., throw HttpException for NestJS
|
||||||
|
throw new UnprocessableEntityException(
|
||||||
|
`Failed to save reschedule verification: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOtp(
|
||||||
|
booking_id: string,
|
||||||
|
code: number,
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
const verification = await this.rescheduleVerificationRepository.findOne({
|
||||||
|
where: { booking_id, code },
|
||||||
|
order: { created_at: 'DESC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!verification) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'No verification code found for this booking.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally, you can implement OTP expiration logic here
|
||||||
|
|
||||||
|
if (verification.code !== code) {
|
||||||
|
// Increment tried count
|
||||||
|
verification.tried = (verification.tried || 0) + 1;
|
||||||
|
await this.rescheduleVerificationRepository.save(verification);
|
||||||
|
return { success: false, message: 'Invalid verification code.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally, you can mark the verification as used or verified here
|
||||||
|
|
||||||
|
return { success: true, message: 'Verification successful.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async findDetailByBookingId(bookingId: string): Promise<TransactionEntity> {
|
||||||
|
const transaction = await this.transactionService.getOneByOptions({
|
||||||
|
where: { id: bookingId },
|
||||||
|
});
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { IsString, Matches } from 'class-validator';
|
||||||
|
import { RescheduleRequest } from 'src/modules/booking-online/order/domain/entities/reschedule-verification.entity';
|
||||||
|
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RescheduleRequestDTO implements RescheduleRequest {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
example: '123e4567-e89b-12d3-a456-426614174000',
|
||||||
|
description: 'The unique identifier of the booking',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
booking_id: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
example: '25-12-2024',
|
||||||
|
description: 'The new date for rescheduling in the format DD-MM-YYYY',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^(0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-\d{4}$/, {
|
||||||
|
message: 'reschedule_date must be in the format DD-MM-YYYY',
|
||||||
|
})
|
||||||
|
reschedule_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RescheduleVerificationOTP {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
example: '123e4567-e89b-12d3-a456-426614174000',
|
||||||
|
description: 'The unique identifier of the booking',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
booking_id: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
example: '123456',
|
||||||
|
description: 'The OTP code sent for verification',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
code: string;
|
||||||
|
}
|
|
@ -10,6 +10,11 @@ import { CreateBookingManager } from '../domain/usecases/managers/create-booking
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { Gate } from 'src/core/response/domain/decorators/pagination.response';
|
import { Gate } from 'src/core/response/domain/decorators/pagination.response';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
import {
|
||||||
|
RescheduleRequestDTO,
|
||||||
|
RescheduleVerificationOTP,
|
||||||
|
} from './dto/reschedule.dto';
|
||||||
|
import { RescheduleVerificationManager } from '../domain/usecases/managers/reschedule-verification.manager';
|
||||||
|
|
||||||
@ApiTags('Booking Order')
|
@ApiTags('Booking Order')
|
||||||
@Controller('v1/booking')
|
@Controller('v1/booking')
|
||||||
|
@ -19,6 +24,7 @@ export class BookingOrderController {
|
||||||
private createBooking: CreateBookingManager,
|
private createBooking: CreateBookingManager,
|
||||||
private serviceData: TransactionDataService,
|
private serviceData: TransactionDataService,
|
||||||
private midtransService: MidtransService,
|
private midtransService: MidtransService,
|
||||||
|
private rescheduleVerification: RescheduleVerificationManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@ -50,6 +56,27 @@ export class BookingOrderController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('reschedule')
|
||||||
|
async reschedule(@Body() data: RescheduleRequestDTO) {
|
||||||
|
const result = await this.rescheduleVerification.saveVerification(data);
|
||||||
|
const maskedPhoneNumber = result.phone_number.replace(/.(?=.{4})/g, '*');
|
||||||
|
result.phone_number = maskedPhoneNumber;
|
||||||
|
|
||||||
|
return `Verification code sent to ${maskedPhoneNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('reschedule/verification')
|
||||||
|
async verificationReschedule(@Body() data: RescheduleVerificationOTP) {
|
||||||
|
const result = await this.rescheduleVerification.verifyOtp(
|
||||||
|
data.booking_id,
|
||||||
|
+data.code,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transaction = await this.get(data.booking_id);
|
||||||
|
|
||||||
|
return { ...result, transaction };
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async get(@Param('id') transactionId: string) {
|
async get(@Param('id') transactionId: string) {
|
||||||
const data = await this.serviceData.getOneByOptions({
|
const data = await this.serviceData.getOneByOptions({
|
||||||
|
|
|
@ -11,16 +11,21 @@ import { BookingOrderController } from './infrastructure/order.controller';
|
||||||
import { CreateBookingManager } from './domain/usecases/managers/create-booking.manager';
|
import { CreateBookingManager } from './domain/usecases/managers/create-booking.manager';
|
||||||
import { MidtransModule } from 'src/modules/configuration/midtrans/midtrans.module';
|
import { MidtransModule } from 'src/modules/configuration/midtrans/midtrans.module';
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
|
import { RescheduleVerificationModel } from './data/models/reschedule-verification.model';
|
||||||
|
import { RescheduleVerificationManager } from './domain/usecases/managers/reschedule-verification.manager';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
TypeOrmModule.forFeature([ItemModel], CONNECTION_NAME.DEFAULT),
|
TypeOrmModule.forFeature(
|
||||||
|
[ItemModel, RescheduleVerificationModel],
|
||||||
|
CONNECTION_NAME.DEFAULT,
|
||||||
|
),
|
||||||
ItemModule,
|
ItemModule,
|
||||||
TransactionModule,
|
TransactionModule,
|
||||||
MidtransModule,
|
MidtransModule,
|
||||||
CqrsModule,
|
CqrsModule,
|
||||||
],
|
],
|
||||||
controllers: [ItemController, BookingOrderController],
|
controllers: [ItemController, BookingOrderController],
|
||||||
providers: [CreateBookingManager],
|
providers: [CreateBookingManager, RescheduleVerificationManager],
|
||||||
})
|
})
|
||||||
export class BookingOrderModule {}
|
export class BookingOrderModule {}
|
||||||
|
|
|
@ -7,7 +7,7 @@ export const BOOKING_QR_URL =
|
||||||
|
|
||||||
export const BOOKING_TICKET_URL =
|
export const BOOKING_TICKET_URL =
|
||||||
process.env.BOOKING_TICKET_URL ??
|
process.env.BOOKING_TICKET_URL ??
|
||||||
'https://booking.sky.eigen.co.id/app/ticket/';
|
'https://booking.sky.eigen.co.id/public/ticket/';
|
||||||
|
|
||||||
export const WHATSAPP_BUSINESS_VERSION =
|
export const WHATSAPP_BUSINESS_VERSION =
|
||||||
process.env.WHATSAPP_BUSINESS_VERSION ?? 'v22.0';
|
process.env.WHATSAPP_BUSINESS_VERSION ?? 'v22.0';
|
||||||
|
|
Loading…
Reference in New Issue