From 10cd1a711e1c5e66e5645d0835eaf3d8192e575d Mon Sep 17 00:00:00 2001 From: Firman Ramdhani Date: Fri, 16 May 2025 18:37:40 +0700 Subject: [PATCH 01/83] feat: merubah tanggal pendapat di report pembalatan ke created_at --- .../configs/cancel-transaction.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts b/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts index 468eee6..03467f4 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts @@ -42,9 +42,16 @@ export default { }, column_configs: [ + // { + // column: 'main__payment_date', + // query: `to_char(main.payment_date, 'DD-MM-YYYY')`, + // label: 'Tgl. Pendapatan', + // type: DATA_TYPE.DIMENSION, + // format: DATA_FORMAT.TEXT, + // }, { column: 'main__payment_date', - query: `to_char(main.payment_date, 'DD-MM-YYYY')`, + query: `to_char(cast(to_timestamp(main.created_at/1000) as date),'DD-MM-YYYY')`, label: 'Tgl. Pendapatan', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, @@ -282,15 +289,22 @@ export default { }, ], filter_configs: [ + // { + // filed_label: 'Tgl. Pembatalan', + // filter_column: 'main__payment_date', + // field_type: FILTER_FIELD_TYPE.date_range_picker, + // filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, + // // date_format: 'DD-MM-YYYY', + // date_format: 'YYYY-MM-DD', + // }, + { filed_label: 'Tgl. Pembatalan', filter_column: 'main__payment_date', field_type: FILTER_FIELD_TYPE.date_range_picker, filter_type: FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP, - // date_format: 'DD-MM-YYYY', - date_format: 'YYYY-MM-DD', + date_format: 'DD-MM-YYYY', }, - { filed_label: 'Sumber', filter_column: 'main__type', @@ -392,8 +406,9 @@ export default { }, ], customQueryColumn(column) { - if (column === 'main__payment_date') return 'main.payment_date'; - else if (column === 'refund__refund_date') return 'refund.refund_date'; + // if (column === 'main__payment_date') return 'main.payment_date'; + // else if (column === 'refund__refund_date') return 'refund.refund_date'; + if (column === 'refund__refund_date') return 'refund.refund_date'; return; }, }; -- 2.40.1 From 027025935cc0348fddccb17e4d33ac3911898a4a Mon Sep 17 00:00:00 2001 From: Firman Ramdhani Date: Mon, 19 May 2025 17:42:35 +0700 Subject: [PATCH 02/83] feat: add column and filter payment reference at report income --- .../transaction-report/configs/income.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/modules/reports/shared/configs/transaction-report/configs/income.ts b/src/modules/reports/shared/configs/transaction-report/configs/income.ts index b66bdf9..bb611ba 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/income.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/income.ts @@ -255,7 +255,14 @@ export default { { column: 'main__payment_card_information', query: 'main.payment_card_information', - label: 'Information', + label: 'Card Information', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { + column: 'main__payment_code_reference', + query: 'main.payment_code_reference', + label: 'Payment Reference', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, @@ -334,6 +341,18 @@ export default { field_type: FILTER_FIELD_TYPE.input_tag, filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, }, + { + filed_label: 'Card Information', + filter_column: 'main__payment_card_information', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_IN_MEMBER, + }, + { + filed_label: 'Payment Reference', + filter_column: 'main__payment_code_reference', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_IN_MEMBER, + }, { filed_label: 'Tgl. Pengembalian', filter_column: 'refund__refund_date', -- 2.40.1 From 7c5b1b3aa8987c6c6ad1fbaf0bcf0348c2920029 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 28 May 2025 12:32:11 +0700 Subject: [PATCH 03/83] feat: api for booking online --- .../managers/create-booking.manager.ts | 41 ++++++ .../infrastructure/dto/booking-order.dto.ts | 121 ++++++++++++++++++ .../order/infrastructure/item.controller.ts | 15 +-- .../order/infrastructure/order.controller.ts | 36 ++++++ .../booking-online/order/order.module.ts | 12 +- .../transaction/transaction.module.ts | 6 +- 6 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts create mode 100644 src/modules/booking-online/order/infrastructure/dto/booking-order.dto.ts create mode 100644 src/modules/booking-online/order/infrastructure/order.controller.ts diff --git a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts new file mode 100644 index 0000000..3febbcb --- /dev/null +++ b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts @@ -0,0 +1,41 @@ +import { HttpStatus } from '@nestjs/common'; +import { UnprocessableEntityException } from '@nestjs/common'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { CreateTransactionManager } from 'src/modules/transaction/transaction/domain/usecases/managers/create-transaction.manager'; +import { generateInvoiceCodeHelper } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/generate-invoice-code.helper'; + +export class CreateBookingManager extends CreateTransactionManager { + async beforeProcess(): Promise { + await super.beforeProcess(); + + const invoiceCode = await generateInvoiceCodeHelper( + this.dataService, + 'BOOK', + ); + + try { + const { token, redirect_url } = await this.dataServiceFirstOpt.create({ + ...this.data, + id: invoiceCode, + }); + Object.assign(this.data, { + payment_midtrans_token: token, + payment_midtrans_url: redirect_url, + }); + } catch (error) { + console.log({ error }); + throw new UnprocessableEntityException({ + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + message: `Gagal! transaksi telah terbuat, silahkan periksa email untuk melanjutkan pembayaran`, + error: 'Unprocessable Entity', + }); + } + + Object.assign(this.data, { + invoice_code: invoiceCode, + status: STATUS.PENDING, + invoice_date: new Date(), + }); + return; + } +} diff --git a/src/modules/booking-online/order/infrastructure/dto/booking-order.dto.ts b/src/modules/booking-online/order/infrastructure/dto/booking-order.dto.ts new file mode 100644 index 0000000..9c725ab --- /dev/null +++ b/src/modules/booking-online/order/infrastructure/dto/booking-order.dto.ts @@ -0,0 +1,121 @@ +import { BaseStatusDto } from 'src/core/modules/infrastructure/dto/base-status.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { + IsArray, + IsNumber, + IsObject, + IsString, + ValidateIf, +} from 'class-validator'; +import { SeasonPeriodEntity } from 'src/modules/season-related/season-period/domain/entities/season-period.entity'; +import { TransactionItemEntity } from 'src/modules/transaction/transaction/domain/entities/transaction-item.entity'; +import { + TransactionPaymentType, + TransactionUserType, +} from 'src/modules/transaction/transaction/constants'; + +export class TransactionDto extends BaseStatusDto { + @ApiProperty({ + type: Object, + required: false, + example: { + id: 'uuid', + season_type: { + id: 'uuid', + name: 'high season', + }, + }, + }) + @IsObject() + @ValidateIf((body) => body.season_period) + season_period: SeasonPeriodEntity; + + @ApiProperty({ + type: String, + required: true, + example: TransactionUserType.GROUP, + }) + @IsString() + customer_type: TransactionUserType; + + @ApiProperty({ + type: String, + required: true, + example: 'Andika', + }) + @IsString() + customer_name: string; + + @ApiProperty({ + type: String, + required: false, + example: '0823...', + }) + @ValidateIf((body) => body.customer_phone) + customer_phone: string; + + @ApiProperty({ + type: Date, + required: true, + example: '2024-01-01', + }) + booking_date: Date; + + @ApiProperty({ + type: String, + required: false, + example: TransactionPaymentType.MIDTRANS, + }) + payment_type: TransactionPaymentType; + + @ApiProperty({ + type: Number, + required: true, + example: 7000000, + }) + @IsNumber() + payment_sub_total: number; + + @ApiProperty({ + type: Number, + required: true, + example: 3500000, + }) + @IsNumber() + payment_total: number; + + @ApiProperty({ + type: [Object], + required: true, + example: [ + { + item: { + id: '68aa12f7-2cce-422b-9bae-185eb1343b94', + created_at: '1718876384378', + status: 'active', + name: 'tes', + item_type: 'bundling', + hpp: '100000', + base_price: '100000', + limit_type: 'no limit', + limit_value: 0, + item_category: { + id: 'ab15981a-a656-4efc-856c-b2abfbe30979', + name: 'Kategori Bundling 2', + }, + bundling_items: [ + { + id: 'bd5a7a38-df25-4203-a1cd-bf94867946b2', + name: 'Wahana 21 panjangggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg', + }, + ], + tenant: null, + }, + qty: 40, + total_price: 4000000, + }, + ], + }) + @IsArray() + items: TransactionItemEntity[]; +} diff --git a/src/modules/booking-online/order/infrastructure/item.controller.ts b/src/modules/booking-online/order/infrastructure/item.controller.ts index 7a70a07..a3bf89c 100644 --- a/src/modules/booking-online/order/infrastructure/item.controller.ts +++ b/src/modules/booking-online/order/infrastructure/item.controller.ts @@ -21,15 +21,10 @@ export class ItemController { async index( @Query() params: FilterItemDto, ): Promise> { - try { - params.show_to_booking = true; - this.indexManager.setFilterParam(params); - this.indexManager.setService(this.serviceData, TABLE_NAME.ITEM); - await this.indexManager.execute(); - return this.indexManager.getResult(); - } catch (error) { - console.log(error); - throw error; - } + params.show_to_booking = true; + this.indexManager.setFilterParam(params); + this.indexManager.setService(this.serviceData, TABLE_NAME.ITEM); + await this.indexManager.execute(); + return this.indexManager.getResult(); } } diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts new file mode 100644 index 0000000..d8c1741 --- /dev/null +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -0,0 +1,36 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Public } from 'src/core/guards'; +import { TransactionDto } from './dto/booking-order.dto'; +import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity'; +import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { TransactionType } from 'src/modules/transaction/transaction/constants'; +import { MidtransService } from 'src/modules/configuration/midtrans/data/services/midtrans.service'; +import { CreateBookingManager } from '../domain/usecases/managers/create-booking.manager'; + +@ApiTags('Booking Order') +@Controller('v1/booking') +@Public(true) +export class BookingOrderController { + constructor( + private createBooking: CreateBookingManager, + private serviceData: TransactionDataService, + private midtransService: MidtransService, + ) {} + + @Post() + async create(@Body() data: TransactionDto): Promise { + const payload: Partial = data; + payload.type = TransactionType.ONLINE; + + this.createBooking.setData(payload as any); + this.createBooking.setService( + this.serviceData, + TABLE_NAME.TRANSACTION, + this.midtransService, + ); + await this.createBooking.execute(); + return this.createBooking.getResult(); + } +} diff --git a/src/modules/booking-online/order/order.module.ts b/src/modules/booking-online/order/order.module.ts index 7a2203b..6a5c4b4 100644 --- a/src/modules/booking-online/order/order.module.ts +++ b/src/modules/booking-online/order/order.module.ts @@ -6,13 +6,21 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; import { ItemModule } from 'src/modules/item-related/item/item.module'; import { ItemController } from './infrastructure/item.controller'; +import { TransactionModule } from 'src/modules/transaction/transaction/transaction.module'; +import { BookingOrderController } from './infrastructure/order.controller'; +import { CreateBookingManager } from './domain/usecases/managers/create-booking.manager'; +import { MidtransModule } from 'src/modules/configuration/midtrans/midtrans.module'; +import { CqrsModule } from '@nestjs/cqrs'; @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forFeature([ItemModel], CONNECTION_NAME.DEFAULT), ItemModule, + TransactionModule, + MidtransModule, + CqrsModule, ], - controllers: [ItemController], - providers: [], + controllers: [ItemController, BookingOrderController], + providers: [CreateBookingManager], }) export class BookingOrderModule {} diff --git a/src/modules/transaction/transaction/transaction.module.ts b/src/modules/transaction/transaction/transaction.module.ts index 7803e20..af14bef 100644 --- a/src/modules/transaction/transaction/transaction.module.ts +++ b/src/modules/transaction/transaction/transaction.module.ts @@ -49,7 +49,11 @@ import { ItemModel } from 'src/modules/item-related/item/data/models/item.model' import { CouchModule } from 'src/modules/configuration/couch/couch.module'; @Module({ - exports: [TransactionReadService], + exports: [ + TransactionReadService, + TransactionDataService, + CreateTransactionManager, + ], imports: [ ConfigModule.forRoot(), TypeOrmModule.forFeature( -- 2.40.1 From b4f39d5ccf5d3de165c05e3d7e8003993022c6da Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 28 May 2025 13:35:19 +0700 Subject: [PATCH 04/83] feat: booking online response add midtrans url --- .../managers/create-booking.manager.ts | 4 +++- .../order/infrastructure/order.controller.ts | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts index 3febbcb..f148c93 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts @@ -1,12 +1,14 @@ import { HttpStatus } from '@nestjs/common'; import { UnprocessableEntityException } from '@nestjs/common'; import { STATUS } from 'src/core/strings/constants/base.constants'; +import { TransactionType } from 'src/modules/transaction/transaction/constants'; import { CreateTransactionManager } from 'src/modules/transaction/transaction/domain/usecases/managers/create-transaction.manager'; import { generateInvoiceCodeHelper } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/generate-invoice-code.helper'; +import { mappingRevertTransaction } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/mapping-transaction.helper'; export class CreateBookingManager extends CreateTransactionManager { async beforeProcess(): Promise { - await super.beforeProcess(); + mappingRevertTransaction(this.data, TransactionType.ONLINE); const invoiceCode = await generateInvoiceCodeHelper( this.dataService, diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index d8c1741..63f906e 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -20,9 +20,8 @@ export class BookingOrderController { ) {} @Post() - async create(@Body() data: TransactionDto): Promise { + async create(@Body() data: TransactionDto) { const payload: Partial = data; - payload.type = TransactionType.ONLINE; this.createBooking.setData(payload as any); this.createBooking.setService( @@ -31,6 +30,19 @@ export class BookingOrderController { this.midtransService, ); await this.createBooking.execute(); - return this.createBooking.getResult(); + const result = await this.createBooking.getResult(); + const { + invoice_code, + status, + payment_midtrans_token, + payment_midtrans_url, + } = result; + + return { + invoice_code, + status, + payment_midtrans_token, + payment_midtrans_url, + }; } } -- 2.40.1 From 3b362d2822986ce3f914fe2aae7cab2ecfce04ad Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 28 May 2025 14:52:26 +0700 Subject: [PATCH 05/83] feat: crete API CRUD feature time group --- src/app.module.ts | 4 + .../strings/constants/module.constants.ts | 2 + src/core/strings/constants/table.constants.ts | 2 + .../1748409891706-create-time-group-table.ts | 27 ++++ .../item/data/models/item.model.ts | 12 ++ .../data/models/time-group.model.ts | 29 +++++ .../data/services/time-group-data.service.ts | 17 +++ .../data/services/time-group-read.service.ts | 17 +++ .../event/time-group-change-status.event.ts | 5 + .../event/time-group-created.event.ts | 5 + .../event/time-group-deleted.event.ts | 5 + .../event/time-group-updated.event.ts | 5 + .../entities/filter-time-group.entity.ts | 11 ++ .../domain/entities/time-group.entity.ts | 8 ++ .../managers/active-time-group.manager.ts | 45 +++++++ .../batch-active-time-group.manager.ts | 45 +++++++ .../batch-confirm-time-group.manager.ts | 45 +++++++ .../batch-delete-time-group.manager.ts | 51 ++++++++ .../batch-inactive-time-group.manager.ts | 51 ++++++++ .../managers/confirm-time-group.manager.ts | 45 +++++++ .../managers/create-time-group.manager.ts | 75 +++++++++++ .../managers/delete-time-group.manager.ts | 51 ++++++++ .../managers/detail-time-group.manager.ts | 48 +++++++ .../managers/inactive-time-group.manager.ts | 51 ++++++++ .../managers/index-time-group.manager.ts | 64 ++++++++++ .../managers/update-time-group.manager.ts | 75 +++++++++++ .../usecases/time-group-data.orchestrator.ts | 119 ++++++++++++++++++ .../usecases/time-group-read.orchestrator.ts | 33 +++++ .../dto/filter-time-group.dto.ts | 33 +++++ .../infrastructure/dto/time-group.dto.ts | 47 +++++++ .../time-group-data.controller.ts | 78 ++++++++++++ .../time-group-read.controller.ts | 30 +++++ .../time-group/time-group.module.ts | 54 ++++++++ 33 files changed, 1189 insertions(+) create mode 100644 src/database/migrations/1748409891706-create-time-group-table.ts create mode 100644 src/modules/item-related/time-group/data/models/time-group.model.ts create mode 100644 src/modules/item-related/time-group/data/services/time-group-data.service.ts create mode 100644 src/modules/item-related/time-group/data/services/time-group-read.service.ts create mode 100644 src/modules/item-related/time-group/domain/entities/event/time-group-change-status.event.ts create mode 100644 src/modules/item-related/time-group/domain/entities/event/time-group-created.event.ts create mode 100644 src/modules/item-related/time-group/domain/entities/event/time-group-deleted.event.ts create mode 100644 src/modules/item-related/time-group/domain/entities/event/time-group-updated.event.ts create mode 100644 src/modules/item-related/time-group/domain/entities/filter-time-group.entity.ts create mode 100644 src/modules/item-related/time-group/domain/entities/time-group.entity.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/active-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/batch-active-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/batch-confirm-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/batch-delete-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/batch-inactive-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/confirm-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/delete-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/detail-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/inactive-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/index-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/time-group-data.orchestrator.ts create mode 100644 src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts create mode 100644 src/modules/item-related/time-group/infrastructure/dto/filter-time-group.dto.ts create mode 100644 src/modules/item-related/time-group/infrastructure/dto/time-group.dto.ts create mode 100644 src/modules/item-related/time-group/infrastructure/time-group-data.controller.ts create mode 100644 src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts create mode 100644 src/modules/item-related/time-group/time-group.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 5266aa2..6bb3641 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -99,6 +99,8 @@ import { QueueBucketModel } from './modules/queue/data/models/queue-bucket.model import { VerificationModel } from './modules/booking-online/authentication/data/models/verification.model'; import { BookingOnlineAuthModule } from './modules/booking-online/authentication/auth.module'; 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'; @Module({ imports: [ ApmModule.register(), @@ -123,6 +125,7 @@ import { BookingOrderModule } from './modules/booking-online/order/order.module' ItemCategoryModel, ItemRateModel, ItemQueueModel, + TimeGroupModel, LogModel, LogUserLoginModel, NewsModel, @@ -188,6 +191,7 @@ import { BookingOrderModule } from './modules/booking-online/order/order.module' ItemModule, ItemRateModule, ItemQueueModule, + TimeGroupModule, // transaction PaymentMethodModule, diff --git a/src/core/strings/constants/module.constants.ts b/src/core/strings/constants/module.constants.ts index b05838e..032dace 100644 --- a/src/core/strings/constants/module.constants.ts +++ b/src/core/strings/constants/module.constants.ts @@ -28,4 +28,6 @@ export enum MODULE_NAME { REPORT_SUMMARY = 'report-summary', QUEUE = 'queue', + + TIME_GROUPS = 'time-groups', } diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts index 4725e10..dcdc1a3 100644 --- a/src/core/strings/constants/table.constants.ts +++ b/src/core/strings/constants/table.constants.ts @@ -43,4 +43,6 @@ export enum TABLE_NAME { QUEUE_TICKET = 'queue_tickets', QUEUE_ITEM = 'queue_items', QUEUE_BUCKET = 'queue_bucket', + + TIME_GROUPS = 'time_groups', } diff --git a/src/database/migrations/1748409891706-create-time-group-table.ts b/src/database/migrations/1748409891706-create-time-group-table.ts new file mode 100644 index 0000000..5c83f84 --- /dev/null +++ b/src/database/migrations/1748409891706-create-time-group-table.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateTimeGroupTable1748409891706 implements MigrationInterface { + name = 'CreateTimeGroupTable1748409891706'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."time_groups_status_enum" AS ENUM('active', 'cancel', 'confirmed', 'draft', 'expired', 'inactive', 'partial refund', 'pending', 'proses refund', 'refunded', 'rejected', 'settled', 'waiting')`, + ); + await queryRunner.query( + `CREATE TABLE "time_groups" ("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, "status" "public"."time_groups_status_enum" NOT NULL DEFAULT 'draft', "name" character varying NOT NULL, "start_time" TIME NOT NULL, "end_time" TIME NOT NULL, "max_usage_time" TIME NOT NULL, CONSTRAINT "PK_083d02988db7bedfe3b7c869b50" PRIMARY KEY ("id"))`, + ); + await queryRunner.query(`ALTER TABLE "items" ADD "time_group_id" uuid`); + await queryRunner.query( + `ALTER TABLE "items" ADD CONSTRAINT "FK_f44f222e1808448dca1b6cc4557" FOREIGN KEY ("time_group_id") REFERENCES "time_groups"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" DROP CONSTRAINT "FK_f44f222e1808448dca1b6cc4557"`, + ); + await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "time_group_id"`); + await queryRunner.query(`DROP TABLE "time_groups"`); + await queryRunner.query(`DROP TYPE "public"."time_groups_status_enum"`); + } +} diff --git a/src/modules/item-related/item/data/models/item.model.ts b/src/modules/item-related/item/data/models/item.model.ts index 62e9c83..b565c4d 100644 --- a/src/modules/item-related/item/data/models/item.model.ts +++ b/src/modules/item-related/item/data/models/item.model.ts @@ -17,6 +17,7 @@ import { UserModel } from 'src/modules/user-related/user/data/models/user.model' import { ItemRateModel } from 'src/modules/item-related/item-rate/data/models/item-rate.model'; import { GateModel } from 'src/modules/web-information/gate/data/models/gate.model'; import { ItemQueueModel } from 'src/modules/item-related/item-queue/data/models/item-queue.model'; +import { TimeGroupModel } from 'src/modules/item-related/time-group/data/models/time-group.model'; @Entity(TABLE_NAME.ITEM) export class ItemModel @@ -86,6 +87,17 @@ export class ItemModel @JoinColumn({ name: 'item_category_id' }) item_category: ItemCategoryModel; + // start relation to time group + @Column('varchar', { name: 'time_group_id', nullable: true }) + time_group_id: number; + @ManyToOne(() => TimeGroupModel, (model) => model.items, { + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'time_group_id' }) + time_group: TimeGroupModel; + // end relation to time group + @ManyToOne(() => ItemQueueModel, (model) => model.items, { onUpdate: 'CASCADE', onDelete: 'SET NULL', diff --git a/src/modules/item-related/time-group/data/models/time-group.model.ts b/src/modules/item-related/time-group/data/models/time-group.model.ts new file mode 100644 index 0000000..e4332e0 --- /dev/null +++ b/src/modules/item-related/time-group/data/models/time-group.model.ts @@ -0,0 +1,29 @@ +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { TimeGroupEntity } from '../../domain/entities/time-group.entity'; +import { Column, Entity, OneToMany } from 'typeorm'; +import { BaseStatusModel } from 'src/core/modules/data/model/base-status.model'; +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; + +@Entity(TABLE_NAME.TIME_GROUPS) +export class TimeGroupModel + extends BaseStatusModel + implements TimeGroupEntity +{ + @Column('varchar', { name: 'name' }) + name: string; + + @Column({ type: 'time' }) + start_time: string; + + @Column({ type: 'time' }) + end_time: string; + + @Column({ type: 'time' }) + max_usage_time: string; + + @OneToMany(() => ItemModel, (model) => model.time_group, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + items: ItemModel[]; +} diff --git a/src/modules/item-related/time-group/data/services/time-group-data.service.ts b/src/modules/item-related/time-group/data/services/time-group-data.service.ts new file mode 100644 index 0000000..823d81a --- /dev/null +++ b/src/modules/item-related/time-group/data/services/time-group-data.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { BaseDataService } from 'src/core/modules/data/service/base-data.service'; +import { TimeGroupEntity } from '../../domain/entities/time-group.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { TimeGroupModel } from '../models/time-group.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { Repository } from 'typeorm'; + +@Injectable() +export class TimeGroupDataService extends BaseDataService { + constructor( + @InjectRepository(TimeGroupModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(repo); + } +} diff --git a/src/modules/item-related/time-group/data/services/time-group-read.service.ts b/src/modules/item-related/time-group/data/services/time-group-read.service.ts new file mode 100644 index 0000000..a528bd3 --- /dev/null +++ b/src/modules/item-related/time-group/data/services/time-group-read.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { TimeGroupEntity } from '../../domain/entities/time-group.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { TimeGroupModel } from '../models/time-group.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { Repository } from 'typeorm'; +import { BaseReadService } from 'src/core/modules/data/service/base-read.service'; + +@Injectable() +export class TimeGroupReadService extends BaseReadService { + constructor( + @InjectRepository(TimeGroupModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(repo); + } +} diff --git a/src/modules/item-related/time-group/domain/entities/event/time-group-change-status.event.ts b/src/modules/item-related/time-group/domain/entities/event/time-group-change-status.event.ts new file mode 100644 index 0000000..4aa911f --- /dev/null +++ b/src/modules/item-related/time-group/domain/entities/event/time-group-change-status.event.ts @@ -0,0 +1,5 @@ +import { IEvent } from 'src/core/strings/constants/interface.constants'; + +export class TimeGroupChangeStatusEvent { + constructor(public readonly data: IEvent) {} +} diff --git a/src/modules/item-related/time-group/domain/entities/event/time-group-created.event.ts b/src/modules/item-related/time-group/domain/entities/event/time-group-created.event.ts new file mode 100644 index 0000000..27c35a0 --- /dev/null +++ b/src/modules/item-related/time-group/domain/entities/event/time-group-created.event.ts @@ -0,0 +1,5 @@ +import { IEvent } from 'src/core/strings/constants/interface.constants'; + +export class TimeGroupCreatedEvent { + constructor(public readonly data: IEvent) {} +} diff --git a/src/modules/item-related/time-group/domain/entities/event/time-group-deleted.event.ts b/src/modules/item-related/time-group/domain/entities/event/time-group-deleted.event.ts new file mode 100644 index 0000000..a1f8030 --- /dev/null +++ b/src/modules/item-related/time-group/domain/entities/event/time-group-deleted.event.ts @@ -0,0 +1,5 @@ +import { IEvent } from 'src/core/strings/constants/interface.constants'; + +export class TimeGroupDeletedEvent { + constructor(public readonly data: IEvent) {} +} diff --git a/src/modules/item-related/time-group/domain/entities/event/time-group-updated.event.ts b/src/modules/item-related/time-group/domain/entities/event/time-group-updated.event.ts new file mode 100644 index 0000000..21d9c80 --- /dev/null +++ b/src/modules/item-related/time-group/domain/entities/event/time-group-updated.event.ts @@ -0,0 +1,5 @@ +import { IEvent } from 'src/core/strings/constants/interface.constants'; + +export class TimeGroupUpdatedEvent { + constructor(public readonly data: IEvent) {} +} diff --git a/src/modules/item-related/time-group/domain/entities/filter-time-group.entity.ts b/src/modules/item-related/time-group/domain/entities/filter-time-group.entity.ts new file mode 100644 index 0000000..c286d7c --- /dev/null +++ b/src/modules/item-related/time-group/domain/entities/filter-time-group.entity.ts @@ -0,0 +1,11 @@ +import { BaseFilterEntity } from 'src/core/modules/domain/entities/base-filter.entity'; + +export interface FilterITimeGroupEntity extends BaseFilterEntity { + names: string[]; + start_time_from: string; + start_time_to: string; + end_time_from: string; + end_time_to: string; + max_usage_time_from: string; + max_usage_time_to: string; +} diff --git a/src/modules/item-related/time-group/domain/entities/time-group.entity.ts b/src/modules/item-related/time-group/domain/entities/time-group.entity.ts new file mode 100644 index 0000000..7281552 --- /dev/null +++ b/src/modules/item-related/time-group/domain/entities/time-group.entity.ts @@ -0,0 +1,8 @@ +import { BaseStatusEntity } from 'src/core/modules/domain/entities/base-status.entity'; + +export interface TimeGroupEntity extends BaseStatusEntity { + name: string; + start_time: string; + end_time: string; + max_usage_time: string; +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/active-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/active-time-group.manager.ts new file mode 100644 index 0000000..6f3e3e7 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/active-time-group.manager.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupChangeStatusEvent } from '../../entities/event/time-group-change-status.event'; + +@Injectable() +export class ActiveTimeGroupManager extends BaseUpdateStatusManager { + getResult(): string { + return `Success active data ${this.result.name}`; + } + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupChangeStatusEvent, + data: this.data, + }, + ]; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/batch-active-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/batch-active-time-group.manager.ts new file mode 100644 index 0000000..464c346 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/batch-active-time-group.manager.ts @@ -0,0 +1,45 @@ +import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupChangeStatusEvent } from '../../entities/event/time-group-change-status.event'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchActiveTimeGroupManager extends BaseBatchUpdateStatusManager { + validateData(data: TimeGroupEntity): Promise { + return; + } + + beforeProcess(): Promise { + return; + } + + afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupChangeStatusEvent, + }, + ]; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/batch-confirm-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/batch-confirm-time-group.manager.ts new file mode 100644 index 0000000..2d2482c --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/batch-confirm-time-group.manager.ts @@ -0,0 +1,45 @@ +import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupChangeStatusEvent } from '../../entities/event/time-group-change-status.event'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchConfirmTimeGroupManager extends BaseBatchUpdateStatusManager { + validateData(data: TimeGroupEntity): Promise { + return; + } + + beforeProcess(): Promise { + return; + } + + afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupChangeStatusEvent, + }, + ]; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/batch-delete-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/batch-delete-time-group.manager.ts new file mode 100644 index 0000000..95bb85b --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/batch-delete-time-group.manager.ts @@ -0,0 +1,51 @@ +import { BaseBatchDeleteManager } from 'src/core/modules/domain/usecase/managers/base-batch-delete.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupDeletedEvent } from '../../entities/event/time-group-deleted.event'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchDeleteTimeGroupManager extends BaseBatchDeleteManager { + async beforeProcess(): Promise { + return; + } + + async validateData(data: TimeGroupEntity): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return [ + { + relation: 'items', + message: + 'Gagal! tidak dapat menghapus time group karena sudah berelasi dengan item', + }, + ]; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupDeletedEvent, + }, + ]; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/batch-inactive-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/batch-inactive-time-group.manager.ts new file mode 100644 index 0000000..ecb437f --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/batch-inactive-time-group.manager.ts @@ -0,0 +1,51 @@ +import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupChangeStatusEvent } from '../../entities/event/time-group-change-status.event'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchInactiveTimeGroupManager extends BaseBatchUpdateStatusManager { + validateData(data: TimeGroupEntity): Promise { + return; + } + + beforeProcess(): Promise { + return; + } + + afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return [ + { + relation: 'items', + message: + 'Gagal! tidak dapat mengubah status time group karena sudah berelasi dengan item', + }, + ]; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupChangeStatusEvent, + }, + ]; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/confirm-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/confirm-time-group.manager.ts new file mode 100644 index 0000000..76e207f --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/confirm-time-group.manager.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupChangeStatusEvent } from '../../entities/event/time-group-change-status.event'; + +@Injectable() +export class ConfirmTimeGroupManager extends BaseUpdateStatusManager { + getResult(): string { + return `Success active data ${this.result.name}`; + } + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupChangeStatusEvent, + data: this.data, + }, + ]; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts new file mode 100644 index 0000000..efa3885 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; +import { + EventTopics, + columnUniques, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { BaseCreateManager } from 'src/core/modules/domain/usecase/managers/base-create.manager'; +import { TimeGroupCreatedEvent } from '../../entities/event/time-group-created.event'; +import * as moment from 'moment'; + +@Injectable() +export class CreateTimeGroupManager extends BaseCreateManager { + async beforeProcess(): Promise { + const queryBuilder = this.dataService + .getRepository() + .createQueryBuilder(this.tableName); + + const overlapping = await queryBuilder + .where(`${this.tableName}.start_time <= :end_time`, { + end_time: this.data.end_time, + }) + .andWhere(`${this.tableName}.end_time >= :start_time`, { + start_time: this.data.start_time, + }) + .getOne(); + + if (overlapping) { + throw new Error( + 'Rentang waktu yang dimasukkan beririsan dengan data lain.', + ); + } else if (this.data.max_usage_time) { + const format = 'HH:mm'; + const end_time = moment(this.data.end_time, format); + const max_usage_time = moment(this.data.max_usage_time, format); + + if (max_usage_time.isBefore(end_time)) { + throw new Error( + 'Waktu maksimum penggunaan harus lebih kecil dari waktu selesai.', + ); + } + } + return; + } + + async afterProcess(): Promise { + return; + } + + async generateConfig(): Promise { + // TODO: Implement logic here + } + + get validateRelations(): validateRelations[] { + return []; + } + + get uniqueColumns(): columnUniques[] { + return [{ column: 'name' }]; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupCreatedEvent, + data: this.data, + }, + ]; + } + + get entityTarget(): any { + return TimeGroupModel; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/delete-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/delete-time-group.manager.ts new file mode 100644 index 0000000..5195904 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/delete-time-group.manager.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; +import { BaseDeleteManager } from 'src/core/modules/domain/usecase/managers/base-delete.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupDeletedEvent } from '../../entities/event/time-group-deleted.event'; + +@Injectable() +export class DeleteTimeGroupManager extends BaseDeleteManager { + getResult(): string { + return `Success`; + } + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return [ + { + relation: 'items', + message: + 'Gagal! tidak dapat menghapus time group karena sudah berelasi dengan item', + }, + ]; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupDeletedEvent, + data: this.data, + }, + ]; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/detail-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/detail-time-group.manager.ts new file mode 100644 index 0000000..c900594 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/detail-time-group.manager.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { BaseDetailManager } from 'src/core/modules/domain/usecase/managers/base-detail.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { RelationParam } from 'src/core/modules/domain/entities/base-filter.entity'; + +@Injectable() +export class DetailTimeGroupManager extends BaseDetailManager { + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get relations(): RelationParam { + return { + joinRelations: [], + selectRelations: [], + countRelations: [], + }; + } + + 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 setFindProperties(): any { + return { + id: this.dataId, + }; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/inactive-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/inactive-time-group.manager.ts new file mode 100644 index 0000000..d6dca30 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/inactive-time-group.manager.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupChangeStatusEvent } from '../../entities/event/time-group-change-status.event'; + +@Injectable() +export class InactiveTimeGroupManager extends BaseUpdateStatusManager { + getResult(): string { + return `Success inactive data ${this.result.name}`; + } + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return [ + { + relation: 'items', + message: + 'Gagal! tidak dapat mengubah status time group karena sudah berelasi dengan item', + }, + ]; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupChangeStatusEvent, + data: this.data, + }, + ]; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/index-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/index-time-group.manager.ts new file mode 100644 index 0000000..3ffdd56 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/index-time-group.manager.ts @@ -0,0 +1,64 @@ +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 IndexTimeGroupManager extends BaseIndexManager { + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get relations(): RelationParam { + return { + joinRelations: [], + selectRelations: [], + countRelations: [], + }; + } + + 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, + ): SelectQueryBuilder { + return queryBuilder; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts new file mode 100644 index 0000000..399130a --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateManager } from 'src/core/modules/domain/usecase/managers/base-update.manager'; +import { TimeGroupEntity } from '../../entities/time-group.entity'; +import { TimeGroupModel } from '../../../data/models/time-group.model'; +import { TimeGroupUpdatedEvent } from '../../entities/event/time-group-updated.event'; +import { + EventTopics, + columnUniques, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import * as moment from 'moment'; + +@Injectable() +export class UpdateTimeGroupManager extends BaseUpdateManager { + async validateProcess(): Promise { + const queryBuilder = this.dataService + .getRepository() + .createQueryBuilder(this.tableName); + + const overlapping = await queryBuilder + .where(`${this.tableName}.start_time <= :end_time`, { + end_time: this.data.end_time, + }) + .andWhere(`${this.tableName}.end_time >= :start_time`, { + start_time: this.data.start_time, + }) + .andWhere(`${this.tableName}.id != :id`, { id: this.dataId ?? null }) + .getOne(); + + if (overlapping) { + throw new Error( + 'Rentang waktu yang dimasukkan beririsan dengan data lain.', + ); + } else if (this.data.max_usage_time) { + const format = 'HH:mm'; + const end_time = moment(this.data.end_time, format); + const max_usage_time = moment(this.data.max_usage_time, format); + + if (max_usage_time.isBefore(end_time)) { + throw new Error( + 'Waktu maksimum penggunaan harus lebih kecil dari waktu selesai.', + ); + } + } + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get uniqueColumns(): columnUniques[] { + return [{ column: 'name' }]; + } + + get entityTarget(): any { + return TimeGroupModel; + } + + get eventTopics(): EventTopics[] { + return [ + { + topic: TimeGroupUpdatedEvent, + }, + ]; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/time-group-data.orchestrator.ts b/src/modules/item-related/time-group/domain/usecases/time-group-data.orchestrator.ts new file mode 100644 index 0000000..d474676 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/time-group-data.orchestrator.ts @@ -0,0 +1,119 @@ +import { Injectable } from '@nestjs/common'; +import { CreateTimeGroupManager } from './managers/create-time-group.manager'; +import { TimeGroupDataService } from '../../data/services/time-group-data.service'; +import { TimeGroupEntity } from '../entities/time-group.entity'; +import { DeleteTimeGroupManager } from './managers/delete-time-group.manager'; +import { UpdateTimeGroupManager } from './managers/update-time-group.manager'; +import { BaseDataTransactionOrchestrator } from 'src/core/modules/domain/usecase/orchestrators/base-data-transaction.orchestrator'; +import { ActiveTimeGroupManager } from './managers/active-time-group.manager'; +import { InactiveTimeGroupManager } from './managers/inactive-time-group.manager'; +import { ConfirmTimeGroupManager } from './managers/confirm-time-group.manager'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { BatchConfirmTimeGroupManager } from './managers/batch-confirm-time-group.manager'; +import { BatchInactiveTimeGroupManager } from './managers/batch-inactive-time-group.manager'; +import { BatchActiveTimeGroupManager } from './managers/batch-active-time-group.manager'; +import { BatchDeleteTimeGroupManager } from './managers/batch-delete-time-group.manager'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; + +@Injectable() +export class TimeGroupDataOrchestrator extends BaseDataTransactionOrchestrator { + constructor( + private createManager: CreateTimeGroupManager, + private updateManager: UpdateTimeGroupManager, + private deleteManager: DeleteTimeGroupManager, + private activeManager: ActiveTimeGroupManager, + private confirmManager: ConfirmTimeGroupManager, + private inactiveManager: InactiveTimeGroupManager, + private batchDeleteManager: BatchDeleteTimeGroupManager, + private batchActiveManager: BatchActiveTimeGroupManager, + private batchConfirmManager: BatchConfirmTimeGroupManager, + private batchInactiveManager: BatchInactiveTimeGroupManager, + private serviceData: TimeGroupDataService, + ) { + super(); + } + + async create(data): Promise { + this.createManager.setData(data); + this.createManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.createManager.execute(); + await this.createManager.generateConfig(); + return this.createManager.getResult(); + } + + async update(dataId, data): Promise { + this.updateManager.setData(dataId, data); + this.updateManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.updateManager.execute(); + return this.updateManager.getResult(); + } + + async delete(dataId): Promise { + this.deleteManager.setData(dataId); + this.deleteManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.deleteManager.execute(); + return this.deleteManager.getResult(); + } + + async batchDelete(dataIds: string[]): Promise { + this.batchDeleteManager.setData(dataIds); + this.batchDeleteManager.setService( + this.serviceData, + TABLE_NAME.TIME_GROUPS, + ); + await this.batchDeleteManager.execute(); + return this.batchDeleteManager.getResult(); + } + + async active(dataId): Promise { + this.activeManager.setData(dataId, STATUS.ACTIVE); + this.activeManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.activeManager.execute(); + return this.activeManager.getResult(); + } + + async batchActive(dataIds: string[]): Promise { + this.batchActiveManager.setData(dataIds, STATUS.ACTIVE); + this.batchActiveManager.setService( + this.serviceData, + TABLE_NAME.TIME_GROUPS, + ); + await this.batchActiveManager.execute(); + return this.batchActiveManager.getResult(); + } + + async confirm(dataId): Promise { + this.confirmManager.setData(dataId, STATUS.ACTIVE); + this.confirmManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.confirmManager.execute(); + return this.confirmManager.getResult(); + } + + async batchConfirm(dataIds: string[]): Promise { + this.batchConfirmManager.setData(dataIds, STATUS.ACTIVE); + this.batchConfirmManager.setService( + this.serviceData, + TABLE_NAME.TIME_GROUPS, + ); + await this.batchConfirmManager.execute(); + return this.batchConfirmManager.getResult(); + } + + async inactive(dataId): Promise { + this.inactiveManager.setData(dataId, STATUS.INACTIVE); + this.inactiveManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.inactiveManager.execute(); + return this.inactiveManager.getResult(); + } + + async batchInactive(dataIds: string[]): Promise { + this.batchInactiveManager.setData(dataIds, STATUS.INACTIVE); + this.batchInactiveManager.setService( + this.serviceData, + TABLE_NAME.TIME_GROUPS, + ); + await this.batchInactiveManager.execute(); + return this.batchInactiveManager.getResult(); + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts b/src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts new file mode 100644 index 0000000..aa6b6e3 --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { IndexTimeGroupManager } from './managers/index-time-group.manager'; +import { TimeGroupReadService } from '../../data/services/time-group-read.service'; +import { TimeGroupEntity } from '../entities/time-group.entity'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +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'; + +@Injectable() +export class TimeGroupReadOrchestrator extends BaseReadOrchestrator { + constructor( + private indexManager: IndexTimeGroupManager, + private detailManager: DetailTimeGroupManager, + private serviceData: TimeGroupReadService, + ) { + super(); + } + + async index(params): Promise> { + this.indexManager.setFilterParam(params); + this.indexManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.indexManager.execute(); + return this.indexManager.getResult(); + } + + async detail(dataId: string): Promise { + this.detailManager.setData(dataId); + this.detailManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); + await this.detailManager.execute(); + return this.detailManager.getResult(); + } +} diff --git a/src/modules/item-related/time-group/infrastructure/dto/filter-time-group.dto.ts b/src/modules/item-related/time-group/infrastructure/dto/filter-time-group.dto.ts new file mode 100644 index 0000000..b4b6cd9 --- /dev/null +++ b/src/modules/item-related/time-group/infrastructure/dto/filter-time-group.dto.ts @@ -0,0 +1,33 @@ +import { BaseFilterDto } from 'src/core/modules/infrastructure/dto/base-filter.dto'; +import { FilterITimeGroupEntity } from '../../domain/entities/filter-time-group.entity'; +import { ApiProperty } from '@nestjs/swagger'; +import { ValidateIf } from 'class-validator'; + +export class FilterTimeGroupDto + extends BaseFilterDto + implements FilterITimeGroupEntity +{ + @ApiProperty({ type: 'string', required: false }) + @ValidateIf((body) => body.start_time_from) + start_time_from: string; + + @ApiProperty({ type: 'string', required: false }) + @ValidateIf((body) => body.start_time_to) + start_time_to: string; + + @ApiProperty({ type: 'string', required: false }) + @ValidateIf((body) => body.end_time_from) + end_time_from: string; + + @ApiProperty({ type: 'string', required: false }) + @ValidateIf((body) => body.end_time_to) + end_time_to: string; + + @ApiProperty({ type: 'string', required: false }) + @ValidateIf((body) => body.max_usage_time_from) + max_usage_time_from: string; + + @ApiProperty({ type: 'string', required: false }) + @ValidateIf((body) => body.max_usage_time_to) + max_usage_time_to: string; +} diff --git a/src/modules/item-related/time-group/infrastructure/dto/time-group.dto.ts b/src/modules/item-related/time-group/infrastructure/dto/time-group.dto.ts new file mode 100644 index 0000000..f7a5bd9 --- /dev/null +++ b/src/modules/item-related/time-group/infrastructure/dto/time-group.dto.ts @@ -0,0 +1,47 @@ +import { BaseStatusDto } from 'src/core/modules/infrastructure/dto/base-status.dto'; +import { TimeGroupEntity } from '../../domain/entities/time-group.entity'; +import { IsString, ValidateIf } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateTimeGroupDto + extends BaseStatusDto + implements TimeGroupEntity +{ + @ApiProperty({ name: 'name', required: true, example: 'Morning' }) + @IsString() + name: string; + + @ApiProperty({ name: 'start_time', required: true, example: '09:00' }) + @IsString() + start_time: string; + + @ApiProperty({ name: 'end_time', required: true, example: '10:00' }) + @IsString() + end_time: string; + + @ApiProperty({ name: 'max_usage_time', required: true, example: '10:30' }) + @IsString() + max_usage_time: string; +} + +export class EditTimeGroupDto extends BaseStatusDto implements TimeGroupEntity { + @ApiProperty({ name: 'name', example: 'Morning' }) + @IsString() + @ValidateIf((body) => body.name) + name: string; + + @ApiProperty({ name: 'start_time', example: '09:00' }) + @IsString() + @ValidateIf((body) => body.start_time) + start_time: string; + + @ApiProperty({ name: 'end_time', example: '10:00' }) + @IsString() + @ValidateIf((body) => body.end_time) + end_time: string; + + @ApiProperty({ name: 'max_usage_time', example: '10:30' }) + @IsString() + @ValidateIf((body) => body.max_usage_time) + max_usage_time: string; +} diff --git a/src/modules/item-related/time-group/infrastructure/time-group-data.controller.ts b/src/modules/item-related/time-group/infrastructure/time-group-data.controller.ts new file mode 100644 index 0000000..4de60fa --- /dev/null +++ b/src/modules/item-related/time-group/infrastructure/time-group-data.controller.ts @@ -0,0 +1,78 @@ +import { + Body, + Controller, + Delete, + Param, + Patch, + Post, + Put, +} from '@nestjs/common'; +import { TimeGroupDataOrchestrator } from '../domain/usecases/time-group-data.orchestrator'; +import { CreateTimeGroupDto, EditTimeGroupDto } from './dto/time-group.dto'; +import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { TimeGroupEntity } from '../domain/entities/time-group.entity'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto'; +import { Public } from 'src/core/guards'; + +@ApiTags(`${MODULE_NAME.TIME_GROUPS.split('-').join(' ')} - data`) +@Controller(`v1/${MODULE_NAME.TIME_GROUPS}`) +@Public(false) +@ApiBearerAuth('JWT') +export class TimeGroupDataController { + constructor(private orchestrator: TimeGroupDataOrchestrator) {} + + @Post() + async create(@Body() data: CreateTimeGroupDto): Promise { + return await this.orchestrator.create(data); + } + + @Put('/batch-delete') + async batchDeleted(@Body() body: BatchIdsDto): Promise { + return await this.orchestrator.batchDelete(body.ids); + } + + @Patch(':id/active') + async active(@Param('id') dataId: string): Promise { + return await this.orchestrator.active(dataId); + } + + @Put('/batch-active') + async batchActive(@Body() body: BatchIdsDto): Promise { + return await this.orchestrator.batchActive(body.ids); + } + + @Patch(':id/confirm') + async confirm(@Param('id') dataId: string): Promise { + return await this.orchestrator.confirm(dataId); + } + + @Put('/batch-confirm') + async batchConfirm(@Body() body: BatchIdsDto): Promise { + return await this.orchestrator.batchConfirm(body.ids); + } + + @Patch(':id/inactive') + async inactive(@Param('id') dataId: string): Promise { + return await this.orchestrator.inactive(dataId); + } + + @Put('/batch-inactive') + async batchInactive(@Body() body: BatchIdsDto): Promise { + return await this.orchestrator.batchInactive(body.ids); + } + + @Put(':id') + async update( + @Param('id') dataId: string, + @Body() data: EditTimeGroupDto, + ): Promise { + return await this.orchestrator.update(dataId, data); + } + + @Delete(':id') + async delete(@Param('id') dataId: string): Promise { + return await this.orchestrator.delete(dataId); + } +} diff --git a/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts b/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts new file mode 100644 index 0000000..212558d --- /dev/null +++ b/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { FilterTimeGroupDto } from './dto/filter-time-group.dto'; +import { Pagination } from 'src/core/response'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +import { TimeGroupEntity } from '../domain/entities/time-group.entity'; +import { TimeGroupReadOrchestrator } from '../domain/usecases/time-group-read.orchestrator'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; +import { Public } from 'src/core/guards'; + +@ApiTags(`${MODULE_NAME.TIME_GROUPS.split('-').join(' ')} - read`) +@Controller(`v1/${MODULE_NAME.TIME_GROUPS}`) +@Public(false) +@ApiBearerAuth('JWT') +export class TimeGroupReadController { + constructor(private orchestrator: TimeGroupReadOrchestrator) {} + + @Get() + @Pagination() + async index( + @Query() params: FilterTimeGroupDto, + ): Promise> { + return await this.orchestrator.index(params); + } + + @Get(':id') + async detail(@Param('id') id: string): Promise { + return await this.orchestrator.detail(id); + } +} diff --git a/src/modules/item-related/time-group/time-group.module.ts b/src/modules/item-related/time-group/time-group.module.ts new file mode 100644 index 0000000..45455c0 --- /dev/null +++ b/src/modules/item-related/time-group/time-group.module.ts @@ -0,0 +1,54 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +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 { 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'; +import { CreateTimeGroupManager } from './domain/usecases/managers/create-time-group.manager'; +import { CqrsModule } from '@nestjs/cqrs'; +import { IndexTimeGroupManager } from './domain/usecases/managers/index-time-group.manager'; +import { DeleteTimeGroupManager } from './domain/usecases/managers/delete-time-group.manager'; +import { UpdateTimeGroupManager } from './domain/usecases/managers/update-time-group.manager'; +import { ActiveTimeGroupManager } from './domain/usecases/managers/active-time-group.manager'; +import { ConfirmTimeGroupManager } from './domain/usecases/managers/confirm-time-group.manager'; +import { InactiveTimeGroupManager } from './domain/usecases/managers/inactive-time-group.manager'; +import { DetailTimeGroupManager } from './domain/usecases/managers/detail-time-group.manager'; +import { BatchDeleteTimeGroupManager } from './domain/usecases/managers/batch-delete-time-group.manager'; +import { BatchActiveTimeGroupManager } from './domain/usecases/managers/batch-active-time-group.manager'; +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'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forFeature([TimeGroupModel], CONNECTION_NAME.DEFAULT), + CqrsModule, + ], + controllers: [TimeGroupDataController, TimeGroupReadController], + providers: [ + IndexTimeGroupManager, + DetailTimeGroupManager, + CreateTimeGroupManager, + DeleteTimeGroupManager, + UpdateTimeGroupManager, + ActiveTimeGroupManager, + ConfirmTimeGroupManager, + InactiveTimeGroupManager, + BatchDeleteTimeGroupManager, + BatchActiveTimeGroupManager, + BatchConfirmTimeGroupManager, + BatchInactiveTimeGroupManager, + + TimeGroupDataService, + TimeGroupReadService, + + TimeGroupDataOrchestrator, + TimeGroupReadOrchestrator, + ], +}) +export class TimeGroupModule {} -- 2.40.1 From a80e2e341941749d8d329a109060f269b7b1ddaf Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 28 May 2025 15:08:38 +0700 Subject: [PATCH 06/83] feat: memasangkan time group di item rate, item, dan item antrian --- .../usecases/managers/detail-item-queue.manager.ts | 5 ++++- .../usecases/managers/index-item-queue.manager.ts | 5 ++++- .../usecases/managers/index-item-rate.manager.ts | 4 ++++ .../domain/usecases/managers/detail-item.manager.ts | 10 +++++++++- .../domain/usecases/managers/index-item.manager.ts | 10 +++++++++- 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts index 2686425..7c62c0c 100644 --- a/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts +++ b/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts @@ -20,7 +20,7 @@ export class DetailItemQueueManager extends BaseDetailManager { get relations(): RelationParam { return { joinRelations: [], - selectRelations: ['items'], + selectRelations: ['items', 'items.time_group'], countRelations: [], }; } @@ -53,6 +53,9 @@ export class DetailItemQueueManager extends BaseDetailManager { `items.share_profit`, `items.play_estimation`, `items.video_url`, + + 'time_group.id', + 'time_group.name', ]; } diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts index a0c94b5..d3ba4d8 100644 --- a/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts +++ b/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts @@ -24,7 +24,7 @@ export class IndexItemQueueManager extends BaseIndexManager { get relations(): RelationParam { return { joinRelations: [], - selectRelations: ['items'], + selectRelations: ['items', 'items.time_group'], countRelations: [], }; } @@ -55,6 +55,9 @@ export class IndexItemQueueManager extends BaseIndexManager { `items.base_price`, `items.share_profit`, `items.play_estimation`, + + 'time_group.id', + 'time_group.name', ]; } diff --git a/src/modules/item-related/item-rate/domain/usecases/managers/index-item-rate.manager.ts b/src/modules/item-related/item-rate/domain/usecases/managers/index-item-rate.manager.ts index bb403c1..34c5314 100644 --- a/src/modules/item-related/item-rate/domain/usecases/managers/index-item-rate.manager.ts +++ b/src/modules/item-related/item-rate/domain/usecases/managers/index-item-rate.manager.ts @@ -76,6 +76,7 @@ export class IndexItemRateManager extends BaseIndexManager { 'item_rates', 'item_rates.season_period', 'season_period.season_type', + 'time_group', ], // relation yang hanya ingin dihitung (akan return number) @@ -113,6 +114,9 @@ export class IndexItemRateManager extends BaseIndexManager { 'season_type.id', 'season_type.name', + + 'time_group.id', + 'time_group.name', ]; } diff --git a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts index 3488d9d..b091df9 100644 --- a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts @@ -23,7 +23,12 @@ export class DetailItemManager extends BaseDetailManager { joinRelations: [], // relation join and select (relasi yang ingin ditampilkan), - selectRelations: ['item_category', 'bundling_items', 'tenant'], + selectRelations: [ + 'item_category', + 'bundling_items', + 'tenant', + 'time_group', + ], // relation yang hanya ingin dihitung (akan return number) countRelations: [], @@ -61,6 +66,9 @@ export class DetailItemManager extends BaseDetailManager { 'tenant.id', 'tenant.name', + + 'time_group.id', + 'time_group.name', ]; } diff --git a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts index b34b1ca..41c12a3 100644 --- a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts @@ -27,7 +27,12 @@ export class IndexItemManager extends BaseIndexManager { joinRelations: [], // relation join and select (relasi yang ingin ditampilkan), - selectRelations: ['item_category', 'bundling_items', 'tenant'], + selectRelations: [ + 'item_category', + 'bundling_items', + 'tenant', + 'time_group', + ], // relation yang hanya ingin dihitung (akan return number) countRelations: [], @@ -57,6 +62,9 @@ export class IndexItemManager extends BaseIndexManager { 'tenant.id', 'tenant.name', + + 'time_group.id', + 'time_group.name', ]; } -- 2.40.1 From 339b2bdab6b2805af57c85dbe3382f3772087f38 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 28 May 2025 15:48:36 +0700 Subject: [PATCH 07/83] feat: menambahkan validasi end time untuk create dan update time group --- .../usecases/managers/create-time-group.manager.ts | 10 ++++++++++ .../usecases/managers/update-time-group.manager.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts index efa3885..8dc54a5 100644 --- a/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts +++ b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts @@ -40,6 +40,16 @@ export class CreateTimeGroupManager extends BaseCreateManager { 'Waktu maksimum penggunaan harus lebih kecil dari waktu selesai.', ); } + return; + } else if (this.data.start_time && this.data.end_time) { + const format = 'HH:mm'; + const start_time = moment(this.data.start_time, format); + const end_time = moment(this.data.end_time, format); + + if (end_time.isBefore(start_time)) { + throw new Error('Waktu akhir harus lebih besar dari waktu mulai.'); + } + return; } return; } diff --git a/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts index 399130a..904bfb4 100644 --- a/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts +++ b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts @@ -41,6 +41,16 @@ export class UpdateTimeGroupManager extends BaseUpdateManager { 'Waktu maksimum penggunaan harus lebih kecil dari waktu selesai.', ); } + return; + } else if (this.data.start_time && this.data.end_time) { + const format = 'HH:mm'; + const start_time = moment(this.data.start_time, format); + const end_time = moment(this.data.end_time, format); + + if (end_time.isBefore(start_time)) { + throw new Error('Waktu akhir harus lebih besar dari waktu mulai.'); + } + return; } return; } -- 2.40.1 From 16bbb1f02bd40490d300f515b0ec12ec3e9b0688 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 28 May 2025 16:00:58 +0700 Subject: [PATCH 08/83] feat(SPG-1140): menambahkan validasi end time untuk create dan update time group --- .../managers/index-season-period-item.manager.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/season-related/season-period/domain/usecases/managers/index-season-period-item.manager.ts b/src/modules/season-related/season-period/domain/usecases/managers/index-season-period-item.manager.ts index e082a3f..7a35c5d 100644 --- a/src/modules/season-related/season-period/domain/usecases/managers/index-season-period-item.manager.ts +++ b/src/modules/season-related/season-period/domain/usecases/managers/index-season-period-item.manager.ts @@ -28,7 +28,12 @@ export class IndexSeasonPeriodeItemManager extends BaseIndexManager Date: Wed, 28 May 2025 17:04:26 +0700 Subject: [PATCH 09/83] feat: add relation time group at handle update item --- .../configuration/couch/domain/managers/item.handler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/configuration/couch/domain/managers/item.handler.ts b/src/modules/configuration/couch/domain/managers/item.handler.ts index 1c02076..e94a99c 100644 --- a/src/modules/configuration/couch/domain/managers/item.handler.ts +++ b/src/modules/configuration/couch/domain/managers/item.handler.ts @@ -48,6 +48,7 @@ export class ItemUpdatedHandler 'item_rates.item', 'item_rates.season_period', 'item_rates.season_period.season_type', + 'time_group', ], }); @@ -109,6 +110,7 @@ export class ItemPriceUpdatedHandler 'item_rates.item', 'item_rates.season_period', 'item_rates.season_period.season_type', + 'time_group', ], }); @@ -150,6 +152,7 @@ export class ItemRateUpdatedHandler 'item_rates.item', 'item_rates.season_period', 'item_rates.season_period.season_type', + 'time_group', ], }); -- 2.40.1 From 9e5d59baacb88d411233b523fd8c0f3c52f91db9 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 28 May 2025 17:13:13 +0700 Subject: [PATCH 10/83] feat: add table relation at event handler item --- .../configuration/couch/domain/managers/item.handler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/configuration/couch/domain/managers/item.handler.ts b/src/modules/configuration/couch/domain/managers/item.handler.ts index e94a99c..75e4c78 100644 --- a/src/modules/configuration/couch/domain/managers/item.handler.ts +++ b/src/modules/configuration/couch/domain/managers/item.handler.ts @@ -44,6 +44,7 @@ export class ItemUpdatedHandler 'item_category', 'bundling_items', 'bundling_items.item_category', + 'bundling_items.time_group', 'item_rates', 'item_rates.item', 'item_rates.season_period', @@ -106,6 +107,7 @@ export class ItemPriceUpdatedHandler 'item_category', 'bundling_items', 'bundling_items.item_category', + 'bundling_items.time_group', 'item_rates', 'item_rates.item', 'item_rates.season_period', @@ -148,6 +150,7 @@ export class ItemRateUpdatedHandler 'item_category', 'bundling_items', 'bundling_items.item_category', + 'bundling_items.time_group', 'item_rates', 'item_rates.item', 'item_rates.season_period', -- 2.40.1 From 845e0547ab7b7d691f775cd2812b6692e768a7b0 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 28 May 2025 18:26:52 +0700 Subject: [PATCH 11/83] feat(SPG-1133): penambahan kolom dan filter time group di report income per item dan report income per item master --- .../configs/income-per-item-master.ts | 14 ++++++++++++++ .../transaction-report/configs/income-per-item.ts | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/modules/reports/shared/configs/transaction-report/configs/income-per-item-master.ts b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item-master.ts index 62d46e8..9ab4af5 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/income-per-item-master.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item-master.ts @@ -19,6 +19,7 @@ export default { LEFT JOIN refunds refund ON refund.transaction_id = main.id LEFT JOIN refund_items refund_item ON refund_item.refund_item_id = tr_item.item_id::uuid LEFT JOIN items item ON item.id::text = tr_item.item_id::text + LEFT JOIN time_groups tg on tg.id = item.time_group_id LEFT JOIN users tenant ON tenant.id::text = item.tenant_id::text`, main_table_alias: 'main', whereDefaultConditions: [ @@ -111,6 +112,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'tg__name', + query: 'tg.name', + label: 'Time Group', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'tr_item__item_name', query: `CASE WHEN tr_item.item_type = 'bundling' THEN tr_item_bundling.item_name ELSE tr_item.item_name END`, @@ -338,6 +346,12 @@ export default { field_type: FILTER_FIELD_TYPE.input_tag, filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, }, + { + filed_label: 'Time Group', + filter_column: 'tg__name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, { filed_label: 'Tipe Pelanggan', filter_column: 'main__customer_type', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts index e091d99..277b090 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/income-per-item.ts @@ -18,6 +18,7 @@ export default { LEFT JOIN refunds refund ON refund.transaction_id = main.id LEFT JOIN refund_items refund_item ON refund_item.refund_item_id = tr_item.item_id::uuid LEFT JOIN items item ON item.id::text = tr_item.item_id::text + LEFT JOIN time_groups tg on tg.id = item.time_group_id LEFT JOIN users tenant ON tenant.id::text = item.tenant_id::text`, main_table_alias: 'main', whereDefaultConditions: [ @@ -109,6 +110,13 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'tg__name', + query: 'tg.name', + label: 'Time Group', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, { column: 'main__customer_type', query: 'main.customer_type', @@ -296,6 +304,12 @@ export default { field_type: FILTER_FIELD_TYPE.input_tag, filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, }, + { + filed_label: 'Time Group', + filter_column: 'tg__name', + field_type: FILTER_FIELD_TYPE.input_tag, + filter_type: FILTER_TYPE.TEXT_MULTIPLE_CONTAINS, + }, { filed_label: 'Tipe Pelanggan', filter_column: 'main__customer_type', -- 2.40.1 From bc8a0407a6302269a432ac0c3ff1c7048d261b78 Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 2 Jun 2025 14:03:58 +0700 Subject: [PATCH 12/83] feat: check booking status by id --- .../order/infrastructure/order.controller.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 63f906e..9498070 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -1,11 +1,10 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Public } from 'src/core/guards'; import { TransactionDto } from './dto/booking-order.dto'; import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity'; import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; -import { TransactionType } from 'src/modules/transaction/transaction/constants'; import { MidtransService } from 'src/modules/configuration/midtrans/data/services/midtrans.service'; import { CreateBookingManager } from '../domain/usecases/managers/create-booking.manager'; @@ -36,13 +35,40 @@ export class BookingOrderController { status, payment_midtrans_token, payment_midtrans_url, + id, } = result; return { + id, invoice_code, status, payment_midtrans_token, payment_midtrans_url, }; } + + @Get(':id') + async get(@Param('id') transactionId: string) { + const data = await this.serviceData.getOneByOptions({ + where: { id: transactionId }, + }); + + const { + customer_name, + customer_phone, + booking_date, + invoice_code, + status, + id, + } = data; + + return { + customer_name, + customer_phone, + booking_date, + invoice_code, + status, + id, + }; + } } -- 2.40.1 From 63e43a7ba0f1952aa271036126c810fe3d0ce5db Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 2 Jun 2025 15:33:22 +0700 Subject: [PATCH 13/83] fix: change midtrans id to uuid --- .../order/domain/usecases/managers/create-booking.manager.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts index f148c93..16bbd05 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts @@ -5,11 +5,13 @@ import { TransactionType } from 'src/modules/transaction/transaction/constants'; import { CreateTransactionManager } from 'src/modules/transaction/transaction/domain/usecases/managers/create-transaction.manager'; import { generateInvoiceCodeHelper } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/generate-invoice-code.helper'; import { mappingRevertTransaction } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/mapping-transaction.helper'; +import { v4 as uuidv4 } from 'uuid'; export class CreateBookingManager extends CreateTransactionManager { async beforeProcess(): Promise { mappingRevertTransaction(this.data, TransactionType.ONLINE); + const id = uuidv4(); const invoiceCode = await generateInvoiceCodeHelper( this.dataService, 'BOOK', @@ -18,7 +20,7 @@ export class CreateBookingManager extends CreateTransactionManager { try { const { token, redirect_url } = await this.dataServiceFirstOpt.create({ ...this.data, - id: invoiceCode, + id, }); Object.assign(this.data, { payment_midtrans_token: token, @@ -34,6 +36,7 @@ export class CreateBookingManager extends CreateTransactionManager { } Object.assign(this.data, { + id, invoice_code: invoiceCode, status: STATUS.PENDING, invoice_date: new Date(), -- 2.40.1 From ffd8595ab26fa619261e0df621d17ffbe7af0d45 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:26:09 +0700 Subject: [PATCH 14/83] feat: add column top_code --- .../1748935417155-add_column_otp_code.ts | 21 +++++++++++++++++++ .../data/models/transaction.model.ts | 3 +++ .../vip-code/data/models/vip-code.model.ts | 3 +++ 3 files changed, 27 insertions(+) create mode 100644 src/database/migrations/1748935417155-add_column_otp_code.ts diff --git a/src/database/migrations/1748935417155-add_column_otp_code.ts b/src/database/migrations/1748935417155-add_column_otp_code.ts new file mode 100644 index 0000000..a7347fd --- /dev/null +++ b/src/database/migrations/1748935417155-add_column_otp_code.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddColumnOtpCode1748935417155 implements MigrationInterface { + name = 'AddColumnOtpCode1748935417155'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "otp_code"`, + ); + await queryRunner.query(`ALTER TABLE "vip_codes" DROP COLUMN "otp_code"`); + } +} diff --git a/src/modules/transaction/transaction/data/models/transaction.model.ts b/src/modules/transaction/transaction/data/models/transaction.model.ts index 053503c..83c4416 100644 --- a/src/modules/transaction/transaction/data/models/transaction.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction.model.ts @@ -274,4 +274,7 @@ export class TransactionModel onUpdate: 'CASCADE', }) refunds: RefundModel[]; + + @Column('varchar', { name: 'otp_code', nullable: true }) + otp_code: string; } diff --git a/src/modules/transaction/vip-code/data/models/vip-code.model.ts b/src/modules/transaction/vip-code/data/models/vip-code.model.ts index 314b047..57e45e0 100644 --- a/src/modules/transaction/vip-code/data/models/vip-code.model.ts +++ b/src/modules/transaction/vip-code/data/models/vip-code.model.ts @@ -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; } -- 2.40.1 From 5e328fda1ecc965e1a479154f6252e7082385649 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:27:30 +0700 Subject: [PATCH 15/83] feat: fix response time group --- .../domain/usecases/managers/create-time-group.manager.ts | 2 +- .../domain/usecases/managers/update-time-group.manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts index 8dc54a5..ce07b64 100644 --- a/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts +++ b/src/modules/item-related/time-group/domain/usecases/managers/create-time-group.manager.ts @@ -37,7 +37,7 @@ export class CreateTimeGroupManager extends BaseCreateManager { 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; diff --git a/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts index 904bfb4..f003866 100644 --- a/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts +++ b/src/modules/item-related/time-group/domain/usecases/managers/update-time-group.manager.ts @@ -38,7 +38,7 @@ export class UpdateTimeGroupManager extends BaseUpdateManager { 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; -- 2.40.1 From 5f08b4be66076ebf3fbc69becc148eed911cad72 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 09:48:10 +0700 Subject: [PATCH 16/83] feat: setup module otp verifications --- .../strings/constants/module.constants.ts | 1 + src/core/strings/constants/table.constants.ts | 1 + .../data/models/otp-verification.model.ts | 35 +++++++++++++++++++ .../services/otp-verification-data.service.ts | 0 .../services/otp-verification-read.service.ts | 0 .../entities/otp-verification.entity.ts | 21 +++++++++++ .../otp-verification-data.orchestrator.ts | 0 .../otp-verification-read.orchestrator.ts | 0 .../dto/otp-verification.dto.ts | 0 .../otp-verification-data.controller.ts | 0 .../otp-verification-read.controller.ts | 0 .../otp-verification.module.ts | 0 12 files changed, 58 insertions(+) create mode 100644 src/modules/configuration/otp-verification/data/models/otp-verification.model.ts create mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts create mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts create mode 100644 src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts create mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts create mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts create mode 100644 src/modules/configuration/otp-verification/infrastructure/dto/otp-verification.dto.ts create mode 100644 src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts create mode 100644 src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts create mode 100644 src/modules/configuration/otp-verification/otp-verification.module.ts diff --git a/src/core/strings/constants/module.constants.ts b/src/core/strings/constants/module.constants.ts index 032dace..a7b4aa0 100644 --- a/src/core/strings/constants/module.constants.ts +++ b/src/core/strings/constants/module.constants.ts @@ -30,4 +30,5 @@ export enum MODULE_NAME { QUEUE = 'queue', TIME_GROUPS = 'time-groups', + OTP_VERIFICATIONS = 'otp-verification', } diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts index dcdc1a3..edc0b7b 100644 --- a/src/core/strings/constants/table.constants.ts +++ b/src/core/strings/constants/table.constants.ts @@ -45,4 +45,5 @@ export enum TABLE_NAME { QUEUE_BUCKET = 'queue_bucket', TIME_GROUPS = 'time_groups', + OTP_VERIFICATIONS = 'otp_verifications', } diff --git a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts new file mode 100644 index 0000000..4db9191 --- /dev/null +++ b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts @@ -0,0 +1,35 @@ +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { + OPT_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 + implements OtpVerificationEntity +{ + @Column({ type: 'varchar', nullable: false }) + otp_code: string; + + @Column({ type: 'enum', enum: OPT_ACTION_TYPE }) + action_type: OPT_ACTION_TYPE; + + @Column({ type: 'varchar', nullable: true }) + target_id: string; + + @Column({ type: 'enum', enum: OTP_SOURCE }) + source: OTP_SOURCE; + + @Column({ default: false }) + is_used: boolean; + + @Column({ type: 'bigint', nullable: false }) + expired_at: number; // UNIX timestamp + + @Column({ type: 'bigint', nullable: true }) + verified_at: number; // UNIX timestamp or null +} diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..88e0c9f --- /dev/null +++ b/src/modules/configuration/otp-verification/domain/entities/otp-verification.entity.ts @@ -0,0 +1,21 @@ +import { BaseEntity } from 'src/core/modules/domain/entities//base.entity'; + +export enum OPT_ACTION_TYPE { + CREATE_DISCOUNT = 'CREATE_DISCOUNT', + CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', +} + +export enum OTP_SOURCE { + POS = 'POS', + WEB = 'WEB', +} + +export interface OtpVerificationEntity extends BaseEntity { + otp_code: string; + action_type: OPT_ACTION_TYPE; + target_id: string; + source: OTP_SOURCE; + is_used: boolean; + expired_at: number; + verified_at: number; +} diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts new file mode 100644 index 0000000..e69de29 -- 2.40.1 From d8cfa9761209cc0287a80dbb0771a29cb2d12317 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:41:46 +0700 Subject: [PATCH 17/83] fix: add time group at item bundling --- .../item/domain/usecases/managers/detail-item.manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts index b091df9..21e3e4b 100644 --- a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts @@ -26,6 +26,7 @@ export class DetailItemManager extends BaseDetailManager { selectRelations: [ 'item_category', 'bundling_items', + 'bundling_items.time_group bundling_time_groups', 'tenant', 'time_group', ], @@ -64,6 +65,9 @@ export class DetailItemManager extends BaseDetailManager { 'bundling_items.hpp', 'bundling_items.base_price', + 'bundling_time_groups.id', + 'bundling_time_groups.name', + 'tenant.id', 'tenant.name', -- 2.40.1 From 36b6ee733f57c4b526b746556a4f8fa48cfdec56 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 4 Jun 2025 15:32:05 +0700 Subject: [PATCH 18/83] feat: add OTP whatsapp notification --- .../data/services/verification.service.ts | 33 +++++++---- .../order/infrastructure/order.controller.ts | 37 +++++++++++- src/services/whatsapp/whatsapp.constant.ts | 2 +- src/services/whatsapp/whatsapp.service.ts | 57 +++++++++++++++++++ 4 files changed, 117 insertions(+), 12 deletions(-) diff --git a/src/modules/booking-online/authentication/data/services/verification.service.ts b/src/modules/booking-online/authentication/data/services/verification.service.ts index 85ce140..acbc186 100644 --- a/src/modules/booking-online/authentication/data/services/verification.service.ts +++ b/src/modules/booking-online/authentication/data/services/verification.service.ts @@ -4,6 +4,7 @@ import { VerificationModel } from '../models/verification.model'; import { BookingVerification } from '../../domain/entities/booking-verification.entity'; import { UnprocessableEntityException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; export class VerificationService { constructor( @InjectRepository(VerificationModel) @@ -25,21 +26,25 @@ export class VerificationService { async register(data: BookingVerification) { const currentTime = Math.floor(Date.now()); // current time in seconds - if ( - data.created_at && - currentTime - data.created_at > this.expiredTimeRegister - ) { - throw new UnprocessableEntityException('Please try again in 1 minute'); - } + // Generate a 4 digit OTP code - data.code = Math.floor(1000 + Math.random() * 9000).toString(); - data.tried = 0; - data.updated_at = currentTime; + const otpCode = Math.floor(1000 + Math.random() * 9000).toString(); let verification = await this.verificationRepository.findOne({ where: { phone_number: data.phone_number }, }); + if ( + verification.updated_at && + currentTime - verification.updated_at < this.expiredTimeRegister + ) { + throw new UnprocessableEntityException('Please try again in 1 minute'); + } + + data.code = otpCode; + data.tried = 0; + data.updated_at = currentTime; + if (verification) { // Update existing record verification = this.verificationRepository.merge(verification, data); @@ -47,7 +52,15 @@ export class VerificationService { // Create new record verification = this.verificationRepository.create(data); } - return this.verificationRepository.save(verification); + const payload = await this.verificationRepository.save(verification); + + const notificationService = new WhatsappService(); + notificationService.sendOtpNotification({ + phone: data.phone_number, + code: otpCode, + }); + + return payload; } async findByPhoneNumber(phoneNumber: string) { diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 9498070..9a31e01 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -50,6 +50,7 @@ export class BookingOrderController { @Get(':id') async get(@Param('id') transactionId: string) { const data = await this.serviceData.getOneByOptions({ + relations: ['items'], where: { id: transactionId }, }); @@ -60,15 +61,49 @@ export class BookingOrderController { invoice_code, status, id, + items, } = data; + const usageItems = items.map((item) => { + const { + id, + item_id, + item_name, + item_price, + item_category_name, + total_price, + total_net_price, + qty, + qty_remaining, + } = item; + return { + id, + item_id, + item_name, + item_price, + item_category_name, + total_price, + total_net_price, + qty, + qty_remaining, + }; + }); + + // Mask customer_phone with * and keep last 4 numbers + let maskedCustomerPhone = customer_phone; + if (typeof customer_phone === 'string' && customer_phone.length > 4) { + const last4 = customer_phone.slice(-4); + maskedCustomerPhone = '*'.repeat(customer_phone.length - 4) + last4; + } + return { customer_name, - customer_phone, + customer_phone: maskedCustomerPhone, booking_date, invoice_code, status, id, + items: usageItems, }; } } diff --git a/src/services/whatsapp/whatsapp.constant.ts b/src/services/whatsapp/whatsapp.constant.ts index 0f568ca..cc04926 100644 --- a/src/services/whatsapp/whatsapp.constant.ts +++ b/src/services/whatsapp/whatsapp.constant.ts @@ -2,7 +2,7 @@ export const WHATSAPP_BUSINESS_API_URL = process.env.WHATSAPP_BUSINESS_API_URL ?? 'https://graph.facebook.com/'; export const WHATSAPP_BUSINESS_VERSION = - process.env.WHATSAPP_BUSINESS_VERSION ?? 'v21.0'; + process.env.WHATSAPP_BUSINESS_VERSION ?? 'v22.0'; export const WHATSAPP_BUSINESS_QUEUE_URL = process.env.WHATSAPP_BUSINESS_QUEUE_URL ?? 'auth/login'; diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index efd855f..8d22282 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -30,6 +30,19 @@ export class WhatsappService { const response = await axios(config); return response.data; } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response) { + console.error('Axios error response:', { + status: error.response.status, + data: error.response.data, + headers: error.response.headers, + }); + } else if (error.request) { + console.error('Axios error request:', error.request); + } else { + console.error('Axios error message:', error.message); + } + } Logger.error(error); apm?.captureError(error); return null; @@ -105,6 +118,50 @@ export class WhatsappService { ); } + async sendOtpNotification(data: { phone: string; code: string }) { + // Compose the WhatsApp message payload for OTP using Facebook WhatsApp API + const payload = { + messaging_product: 'whatsapp', + to: data.phone, // recipient's phone number in international format + type: 'template', + template: { + name: 'booking_otp', // Make sure this template is approved in WhatsApp Business Manager + language: { + code: 'id', // or 'en' if you want English + }, + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + text: parseInt(data.code), // OTP code + }, + ], + }, + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [ + { + type: 'text', + text: `${data.code}`, + }, + ], + }, + ], + }, + }; + + const response = await this.sendMessage(payload); + if (response) { + Logger.log( + `OTP notification for code ${data.code} sent to ${data.phone}`, + ); + } + } + async queueProcess(data: WhatsappQueue) { const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`; const payload = { -- 2.40.1 From 538abb122f8c37476dff43c8f805b0d3d9e3e4f3 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:01:23 +0700 Subject: [PATCH 19/83] feat: implement module otp verification --- src/app.module.ts | 6 + src/core/helpers/otp/otp-service.ts | 57 +++++++ ...28279580-create-table-otp-cerifications.ts | 29 ++++ ...add_column_is_replaced_otp_verification.ts | 19 +++ .../data/models/otp-verification.model.ts | 6 + .../services/otp-verification-data.service.ts | 0 .../services/otp-verification-read.service.ts | 0 .../data/services/otp-verification.service.ts | 158 ++++++++++++++++++ .../entities/otp-verification.entity.ts | 12 ++ .../otp-verification-data.orchestrator.ts | 0 .../otp-verification-read.orchestrator.ts | 0 .../dto/otp-verification.dto.ts | 44 +++++ .../otp-verification-data.controller.ts | 30 ++++ .../otp-verification-read.controller.ts | 0 .../otp-verification.module.ts | 17 ++ 15 files changed, 378 insertions(+) create mode 100644 src/core/helpers/otp/otp-service.ts create mode 100644 src/database/migrations/1749028279580-create-table-otp-cerifications.ts create mode 100644 src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts delete mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts delete mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts create mode 100644 src/modules/configuration/otp-verification/data/services/otp-verification.service.ts delete mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts delete mode 100644 src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts delete mode 100644 src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6bb3641..e8f891d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -101,6 +101,9 @@ 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'; @Module({ imports: [ ApmModule.register(), @@ -165,6 +168,7 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti // Booking Online VerificationModel, + OtpVerificationModel, ], synchronize: false, }), @@ -230,6 +234,8 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti BookingOnlineAuthModule, BookingOrderModule, + + OtpVerificationModule, ], controllers: [], providers: [ diff --git a/src/core/helpers/otp/otp-service.ts b/src/core/helpers/otp/otp-service.ts new file mode 100644 index 0000000..e5f9433 --- /dev/null +++ b/src/core/helpers/otp/otp-service.ts @@ -0,0 +1,57 @@ +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 = {}; + 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) + ); + + return otp; + } +} diff --git a/src/database/migrations/1749028279580-create-table-otp-cerifications.ts b/src/database/migrations/1749028279580-create-table-otp-cerifications.ts new file mode 100644 index 0000000..a1d2800 --- /dev/null +++ b/src/database/migrations/1749028279580-create-table-otp-cerifications.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateTableOtpCerifications1749028279580 + implements MigrationInterface +{ + name = 'CreateTableOtpCerifications1749028279580'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`, + ); + } +} diff --git a/src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts b/src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts new file mode 100644 index 0000000..befa570 --- /dev/null +++ b/src/database/migrations/1749030419440-add_column_is_replaced_otp_verification.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddColumnIsReplacedOtpVerification1749030419440 + implements MigrationInterface +{ + name = 'AddColumnIsReplacedOtpVerification1749030419440'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "otp_verifications" ADD "is_replaced" boolean NOT NULL DEFAULT false`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "otp_verifications" DROP COLUMN "is_replaced"`, + ); + } +} diff --git a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts index 4db9191..22032aa 100644 --- a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts +++ b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts @@ -21,12 +21,18 @@ export class OtpVerificationModel @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 diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-data.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification-read.service.ts deleted file mode 100644 index e69de29..0000000 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 new file mode 100644 index 0000000..12c5c09 --- /dev/null +++ b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts @@ -0,0 +1,158 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { OtpVerificationModel } from '../models/otp-verification.model'; +import { + OTP_SOURCE, + OtpRequestEntity, + OtpVerificationEntity, + 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'; +@Injectable() +export class OtpVerificationService { + constructor( + @InjectRepository(OtpVerificationModel) + private readonly otpVerificationRepo: Repository, + ) {} + + private generateOtpExpiration(minutes = 5): number { + return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time + } + + private generateResendAvailableAt(seconds = 90): 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(); + const source = OTP_SOURCE.WEB; + const creator = { + id: 'c59f811e-873c-4472-bd58-21c111902114', + name: 'dev', + }; + + const newOtp: OtpVerificationEntity = { + otp_code: otpCode, + action_type: payload.action_type, + target_id: payload.target_id, + reference: payload.reference, + source: 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.reference, + ); + + if (activeOTP) { + const createdAtMoment = moment(Number(activeOTP.created_at)); + const nowMoment = moment(Number(dateNow)); + const diffSeconds = nowMoment.diff(createdAtMoment, 'seconds'); + if (diffSeconds < 90) { + 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); + + 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 } = 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, + is_used: false, + is_replaced: false, + }, + { + otp_code, + action_type, + reference, + 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(); + } +} 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 88e0c9f..78e8c68 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 @@ -14,8 +14,20 @@ export interface OtpVerificationEntity extends BaseEntity { otp_code: string; action_type: OPT_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: OPT_ACTION_TYPE; + target_id: string; + reference: string; +} + +export interface OtpVerifyEntity extends OtpRequestEntity { + otp_code: string; +} diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-data.orchestrator.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts b/src/modules/configuration/otp-verification/domain/usecases/otp-verification-read.orchestrator.ts deleted file mode 100644 index e69de29..0000000 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 e69de29..34786d2 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 @@ -0,0 +1,44 @@ +import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { + OPT_ACTION_TYPE, + OtpRequestEntity, + OtpVerifyEntity, +} from '../../domain/entities/otp-verification.entity'; +import { ApiProperty } from '@nestjs/swagger'; + +export class OtpRequestDto implements OtpRequestEntity { + @ApiProperty({ + type: String, + required: true, + example: OPT_ACTION_TYPE.CANCEL_TRANSACTION, + description: 'CANCEL_TRANSACTION || CREATE_DISCOUNT', + }) + @IsString() + @IsNotEmpty() + action_type: OPT_ACTION_TYPE; + + @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; +} 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 e69de29..0eb5957 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 @@ -0,0 +1,30 @@ +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'; + +@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); + } +} diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-read.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index e69de29..1fd4aeb 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -0,0 +1,17 @@ +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'; +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forFeature([OtpVerificationModel], CONNECTION_NAME.DEFAULT), + ], + controllers: [OtpVerificationController], + providers: [OtpVerificationService], +}) +export class OtpVerificationModule {} -- 2.40.1 From ee52a35af2d49c08f75daf8541842004a6eb03cf Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:08:55 +0700 Subject: [PATCH 20/83] feat: implement module otp verification --- .../data/models/otp-verification.model.ts | 6 +++--- .../data/services/otp-verification.service.ts | 10 ++++------ .../domain/entities/otp-verification.entity.ts | 7 ++++--- .../infrastructure/dto/otp-verification.dto.ts | 17 ++++++++++++++--- .../otp-verification-data.controller.ts | 1 + 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts index 22032aa..eec21fb 100644 --- a/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts +++ b/src/modules/configuration/otp-verification/data/models/otp-verification.model.ts @@ -1,6 +1,6 @@ import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { - OPT_ACTION_TYPE, + OTP_ACTION_TYPE, OTP_SOURCE, OtpVerificationEntity, } from '../../domain/entities/otp-verification.entity'; @@ -15,8 +15,8 @@ export class OtpVerificationModel @Column({ type: 'varchar', nullable: false }) otp_code: string; - @Column({ type: 'enum', enum: OPT_ACTION_TYPE }) - action_type: OPT_ACTION_TYPE; + @Column({ type: 'enum', enum: OTP_ACTION_TYPE }) + action_type: OTP_ACTION_TYPE; @Column({ type: 'varchar', nullable: true }) target_id: string; 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 12c5c09..a5b8d7c 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 @@ -35,18 +35,16 @@ export class OtpVerificationService { const otpCode = otpService.generateSecureOTP(); const dateNow = this.generateTimestamp(); const expiredAt = this.generateOtpExpiration(); - const source = OTP_SOURCE.WEB; - const creator = { - id: 'c59f811e-873c-4472-bd58-21c111902114', - name: 'dev', - }; + + //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: source, + source: payload.source, is_used: false, is_replaced: false, expired_at: expiredAt, 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 78e8c68..3d0dc5a 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 @@ -1,6 +1,6 @@ import { BaseEntity } from 'src/core/modules/domain/entities//base.entity'; -export enum OPT_ACTION_TYPE { +export enum OTP_ACTION_TYPE { CREATE_DISCOUNT = 'CREATE_DISCOUNT', CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', } @@ -12,7 +12,7 @@ export enum OTP_SOURCE { export interface OtpVerificationEntity extends BaseEntity { otp_code: string; - action_type: OPT_ACTION_TYPE; + action_type: OTP_ACTION_TYPE; target_id: string; reference: string; source: OTP_SOURCE; @@ -23,7 +23,8 @@ export interface OtpVerificationEntity extends BaseEntity { } export interface OtpRequestEntity { - action_type: OPT_ACTION_TYPE; + action_type: OTP_ACTION_TYPE; + source: OTP_SOURCE; target_id: string; reference: string; } 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 34786d2..cfd0097 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,6 +1,7 @@ import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { - OPT_ACTION_TYPE, + OTP_ACTION_TYPE, + OTP_SOURCE, OtpRequestEntity, OtpVerifyEntity, } from '../../domain/entities/otp-verification.entity'; @@ -10,12 +11,22 @@ export class OtpRequestDto implements OtpRequestEntity { @ApiProperty({ type: String, required: true, - example: OPT_ACTION_TYPE.CANCEL_TRANSACTION, + example: OTP_ACTION_TYPE.CANCEL_TRANSACTION, description: 'CANCEL_TRANSACTION || CREATE_DISCOUNT', }) @IsString() @IsNotEmpty() - action_type: OPT_ACTION_TYPE; + 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', 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 0eb5957..783f109 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 @@ -5,6 +5,7 @@ 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() -- 2.40.1 From 798476aaf53c876bc4b722f2defa6de43d1345ef Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:15:24 +0700 Subject: [PATCH 21/83] feat: implement module otp verification --- .../data/services/otp-verification.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 a5b8d7c..87ae630 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 @@ -91,7 +91,7 @@ export class OtpVerificationService { } async verifyOTP(payload: OtpVerifyEntity) { - const { otp_code, action_type, target_id, reference } = payload; + const { otp_code, action_type, target_id, reference, source } = payload; const dateNow = this.generateTimestamp(); if (!target_id && !reference) { @@ -107,6 +107,7 @@ export class OtpVerificationService { otp_code, action_type, target_id, + source, is_used: false, is_replaced: false, }, @@ -114,6 +115,7 @@ export class OtpVerificationService { otp_code, action_type, reference, + source, is_used: false, is_replaced: false, }, -- 2.40.1 From 6a0b1f6e0589fa7ee35948c6068d8fe05712791d Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 21:27:34 +0700 Subject: [PATCH 22/83] feat: setup verifier --- src/app.module.ts | 5 ++- src/core/strings/constants/table.constants.ts | 1 + .../1749043616622-add_table_otp_verifier.ts | 15 ++++++++ ...49046285398-update_enum_otp_action_type.ts | 37 +++++++++++++++++++ .../data/models/otp-verifier.model.ts | 16 ++++++++ .../data/services/otp-verification.service.ts | 18 ++++++++- .../entities/otp-verification.entity.ts | 6 +++ .../otp-verification.module.ts | 6 ++- 8 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/database/migrations/1749043616622-add_table_otp_verifier.ts create mode 100644 src/database/migrations/1749046285398-update_enum_otp_action_type.ts create mode 100644 src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts diff --git a/src/app.module.ts b/src/app.module.ts index e8f891d..75ab21a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -104,6 +104,8 @@ import { TimeGroupModel } from './modules/item-related/time-group/data/models/ti 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(), @@ -168,7 +170,9 @@ import { OtpVerificationModel } from './modules/configuration/otp-verification/d // Booking Online VerificationModel, + OtpVerificationModel, + OtpVerifierModel, ], synchronize: false, }), @@ -234,7 +238,6 @@ import { OtpVerificationModel } from './modules/configuration/otp-verification/d BookingOnlineAuthModule, BookingOrderModule, - OtpVerificationModule, ], controllers: [], diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts index edc0b7b..2139ba6 100644 --- a/src/core/strings/constants/table.constants.ts +++ b/src/core/strings/constants/table.constants.ts @@ -46,4 +46,5 @@ export enum TABLE_NAME { TIME_GROUPS = 'time_groups', OTP_VERIFICATIONS = 'otp_verifications', + OTP_VERIFIER = 'otp_verifier', } diff --git a/src/database/migrations/1749043616622-add_table_otp_verifier.ts b/src/database/migrations/1749043616622-add_table_otp_verifier.ts new file mode 100644 index 0000000..b2085c4 --- /dev/null +++ b/src/database/migrations/1749043616622-add_table_otp_verifier.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTableOtpVerifier1749043616622 implements MigrationInterface { + name = 'AddTableOtpVerifier1749043616622'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE "otp_verifier"`); + } +} diff --git a/src/database/migrations/1749046285398-update_enum_otp_action_type.ts b/src/database/migrations/1749046285398-update_enum_otp_action_type.ts new file mode 100644 index 0000000..e107607 --- /dev/null +++ b/src/database/migrations/1749046285398-update_enum_otp_action_type.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateEnumOtpActionType1749046285398 + implements MigrationInterface +{ + name = 'UpdateEnumOtpActionType1749046285398'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`, + ); + } +} 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 new file mode 100644 index 0000000..cbb7f3d --- /dev/null +++ b/src/modules/configuration/otp-verification/data/models/otp-verifier.model.ts @@ -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 + implements OtpVerifierEntity +{ + @Column({ type: 'varchar', nullable: true }) + name: string; + + @Column({ type: 'varchar', nullable: false }) + phone_number: string; +} 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 87ae630..fc9a7e5 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,19 +3,26 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { OtpVerificationModel } from '../models/otp-verification.model'; import { - OTP_SOURCE, 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, + + @InjectRepository(OtpVerifierModel) + private readonly otpVerifierRepo: Repository, ) {} private generateOtpExpiration(minutes = 5): number { @@ -82,6 +89,15 @@ export class OtpVerificationService { // 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.`, 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 3d0dc5a..951b4a9 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 @@ -3,6 +3,7 @@ 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 { @@ -32,3 +33,8 @@ export interface OtpRequestEntity { export interface OtpVerifyEntity extends OtpRequestEntity { otp_code: string; } + +export interface OtpVerifierEntity { + name: string; + phone_number: string; +} diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index 1fd4aeb..6e1f02d 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -6,10 +6,14 @@ 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], CONNECTION_NAME.DEFAULT), + TypeOrmModule.forFeature( + [OtpVerificationModel, OtpVerifierModel], + CONNECTION_NAME.DEFAULT, + ), ], controllers: [OtpVerificationController], providers: [OtpVerificationService], -- 2.40.1 From f9025faf0bdccfa1b1fa722cc967a92570faf7bc Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:22:14 +0700 Subject: [PATCH 23/83] feat: setup message --- .../data/services/otp-verification.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 fc9a7e5..7905606 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 @@ -92,12 +92,12 @@ export class OtpVerificationService { const verifiers: OtpVerifierEntity[] = await this.otpVerifierRepo.find(); const notificationService = new WhatsappService(); - // verifiers.map((v) => { - // notificationService.sendOtpNotification({ - // phone: v.phone_number, - // code: otpCode, - // }); - // }); + verifiers.map((v) => { + notificationService.sendOtpNotification({ + phone: v.phone_number, + code: otpCode, + }); + }); return { message: `OTP has been sent to the admin's WhatsApp.`, -- 2.40.1 From cc78dfbd06408c611b20df5fe1d2d90959846b10 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:17:06 +0700 Subject: [PATCH 24/83] feat: monitor log data mapper create discount --- .../domain/usecases/handlers/create-vip-code.handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts b/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts index 2c07dcb..006a81b 100644 --- a/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts +++ b/src/modules/transaction/vip-code/domain/usecases/handlers/create-vip-code.handler.ts @@ -29,7 +29,7 @@ export class CreateVipCodeHandler implements IEventHandler { 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) { -- 2.40.1 From 88753546b6da6e8617a1b61f9439590e45d69430 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:12:31 +0700 Subject: [PATCH 25/83] feat: add otp column at report, cancel transaction, giving discount, reconciliation, vip_code --- .../transaction-report/configs/cancel-transaction.ts | 7 +++++++ .../transaction-report/configs/giving-discounts.ts | 8 ++++++++ .../configs/transaction-report/configs/reconciliation.ts | 7 +++++++ .../shared/configs/transaction-report/configs/vip_code.ts | 7 +++++++ 4 files changed, 29 insertions(+) diff --git a/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts b/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts index 03467f4..f1d195c 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/cancel-transaction.ts @@ -84,6 +84,13 @@ export default { 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`, diff --git a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts index 99a71ee..94167e2 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts @@ -119,6 +119,14 @@ export default { type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, + { + column: 'vip__otp_code', + query: 'vip.otp_code', + label: 'Kode OTP Pemberi Diskon', + type: DATA_TYPE.DIMENSION, + format: DATA_FORMAT.TEXT, + }, + { column: 'privilege__name', query: 'privilege.name', diff --git a/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts b/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts index 8063ad3..acee33b 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/reconciliation.ts @@ -50,6 +50,13 @@ export default { 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`, diff --git a/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts b/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts index a23ece7..a8a4d5b 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/vip_code.ts @@ -35,6 +35,13 @@ export default { 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', -- 2.40.1 From d86f4075d4443ceecc95f613fb885120a4490c1d Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 12:40:13 +0700 Subject: [PATCH 26/83] feat: rename label kode otp at giving discount report --- .../configs/transaction-report/configs/giving-discounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts index 94167e2..e46ec43 100644 --- a/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts +++ b/src/modules/reports/shared/configs/transaction-report/configs/giving-discounts.ts @@ -122,7 +122,7 @@ export default { { column: 'vip__otp_code', query: 'vip.otp_code', - label: 'Kode OTP Pemberi Diskon', + label: 'Kode OTP', type: DATA_TYPE.DIMENSION, format: DATA_FORMAT.TEXT, }, -- 2.40.1 From ca886069f1324dde637b5e2f508d142150abc170 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 16:45:58 +0700 Subject: [PATCH 27/83] feat: update validation otp --- src/core/helpers/otp/otp-service.ts | 3 ++- .../data/services/otp-verification.service.ts | 6 +++--- .../transaction/vip-code/infrastructure/dto/vip-code.dto.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/helpers/otp/otp-service.ts b/src/core/helpers/otp/otp-service.ts index e5f9433..f63057b 100644 --- a/src/core/helpers/otp/otp-service.ts +++ b/src/core/helpers/otp/otp-service.ts @@ -49,7 +49,8 @@ export class OtpService { this.hasSequentialDigits(otp) || this.hasRepeatedDigits(otp) || this.isPalindrome(otp) || - this.hasPartiallyRepeatedDigits(otp) + this.hasPartiallyRepeatedDigits(otp) || + otp?.split('')?.length < this.otpLength ); return otp; 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 7905606..11420d2 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 @@ -29,7 +29,7 @@ export class OtpVerificationService { return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time } - private generateResendAvailableAt(seconds = 90): number { + private generateResendAvailableAt(seconds = 60): number { return moment().add(seconds, 'seconds').valueOf(); // epoch millis } @@ -67,14 +67,14 @@ export class OtpVerificationService { }; const activeOTP = await this.getActiveOtp( - payload.target_id ?? payload.reference, + 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 < 90) { + if (diffSeconds < 60) { throw new BadRequestException( 'An active OTP request was made recently. Please try again later.', ); diff --git a/src/modules/transaction/vip-code/infrastructure/dto/vip-code.dto.ts b/src/modules/transaction/vip-code/infrastructure/dto/vip-code.dto.ts index 1666c7f..be762ae 100644 --- a/src/modules/transaction/vip-code/infrastructure/dto/vip-code.dto.ts +++ b/src/modules/transaction/vip-code/infrastructure/dto/vip-code.dto.ts @@ -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({ -- 2.40.1 From c97399ae8f85b46b7845f64062b39a745e567996 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:08:39 +0700 Subject: [PATCH 28/83] feat: add api get time group items public --- .../index-public-time-group.manager.ts | 65 +++++++++++++++++++ .../usecases/time-group-read.orchestrator.ts | 12 ++++ .../time-group-read.controller.ts | 15 +++++ .../time-group/time-group.module.ts | 13 +++- 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 src/modules/item-related/time-group/domain/usecases/managers/index-public-time-group.manager.ts diff --git a/src/modules/item-related/time-group/domain/usecases/managers/index-public-time-group.manager.ts b/src/modules/item-related/time-group/domain/usecases/managers/index-public-time-group.manager.ts new file mode 100644 index 0000000..c626f1d --- /dev/null +++ b/src/modules/item-related/time-group/domain/usecases/managers/index-public-time-group.manager.ts @@ -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 { + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + 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, + ): SelectQueryBuilder { + queryBuilder.andWhere(`items.id is not null`); + return queryBuilder; + } +} diff --git a/src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts b/src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts index aa6b6e3..b0ef50f 100644 --- a/src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts +++ b/src/modules/item-related/time-group/domain/usecases/time-group-read.orchestrator.ts @@ -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 { constructor( private indexManager: IndexTimeGroupManager, + private indexPublicManager: IndexPublicTimeGroupManager, private detailManager: DetailTimeGroupManager, private serviceData: TimeGroupReadService, ) { @@ -24,6 +26,16 @@ export class TimeGroupReadOrchestrator extends BaseReadOrchestrator> { + 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 { this.detailManager.setData(dataId); this.detailManager.setService(this.serviceData, TABLE_NAME.TIME_GROUPS); diff --git a/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts b/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts index 212558d..860ff92 100644 --- a/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts +++ b/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts @@ -28,3 +28,18 @@ 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`) +@Public() +export class TimeGroupPublicReadController { + constructor(private orchestrator: TimeGroupReadOrchestrator) {} + + @Get('items') + @Pagination() + async indexPublic( + @Query() params: FilterTimeGroupDto, + ): Promise> { + return await this.orchestrator.indexPublic(params); + } +} diff --git a/src/modules/item-related/time-group/time-group.module.ts b/src/modules/item-related/time-group/time-group.module.ts index 45455c0..87009a2 100644 --- a/src/modules/item-related/time-group/time-group.module.ts +++ b/src/modules/item-related/time-group/time-group.module.ts @@ -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, -- 2.40.1 From 0d8ec858eb0598910a856bd1f188e71925320594 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:10:33 +0700 Subject: [PATCH 29/83] feat: add api get time group items public --- .../time-group/infrastructure/time-group-read.controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts b/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts index 860ff92..73b3a14 100644 --- a/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts +++ b/src/modules/item-related/time-group/infrastructure/time-group-read.controller.ts @@ -30,12 +30,13 @@ export class TimeGroupReadController { } @ApiTags(`${MODULE_NAME.TIME_GROUPS.split('-').join(' ')} List- read`) -@Controller(`v1/${MODULE_NAME.TIME_GROUPS}-list`) +// @Controller(`v1/${MODULE_NAME.TIME_GROUPS}-list`) +@Controller(``) @Public() export class TimeGroupPublicReadController { constructor(private orchestrator: TimeGroupReadOrchestrator) {} - @Get('items') + @Get('v1/time-group-list-by-items') @Pagination() async indexPublic( @Query() params: FilterTimeGroupDto, -- 2.40.1 From d95f8fd6e5b5448cf6095fdc7b65efbddb479847 Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 9 Jun 2025 10:11:16 +0700 Subject: [PATCH 30/83] feat: add time_group_ids filter to item management and DTO --- .../item/domain/usecases/managers/index-item.manager.ts | 9 +++++++++ .../item/infrastructure/dto/filter-item.dto.ts | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts index 41c12a3..28ef6f9 100644 --- a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts @@ -106,6 +106,15 @@ export class IndexItemManager extends BaseIndexManager { queryBuilder.andWhere(`${this.tableName}.tenant_id Is Null`); } + if (this.filterParam.time_group_ids?.length) { + queryBuilder.andWhere( + `${this.tableName}.time_group_id In (:...timeGroupIds)`, + { + timeGroupIds: this.filterParam.time_group_ids, + }, + ); + } + if (this.filterParam.show_to_booking) { queryBuilder.andWhere(`${this.tableName}.show_to_booking = true`); } diff --git a/src/modules/item-related/item/infrastructure/dto/filter-item.dto.ts b/src/modules/item-related/item/infrastructure/dto/filter-item.dto.ts index b87dc33..be87a1b 100644 --- a/src/modules/item-related/item/infrastructure/dto/filter-item.dto.ts +++ b/src/modules/item-related/item/infrastructure/dto/filter-item.dto.ts @@ -16,6 +16,12 @@ export class FilterItemDto extends BaseFilterDto implements FilterItemEntity { }) season_period_ids: string[]; + @ApiProperty({ type: ['string'], required: false }) + @Transform((body) => { + return Array.isArray(body.value) ? body.value : [body.value]; + }) + time_group_ids: string[]; + @ApiProperty({ type: ['string'], required: false }) @Transform((body) => { return Array.isArray(body.value) ? body.value : [body.value]; -- 2.40.1 From d4d605d16887bf02204dd67779ca3ccc5f9b0d0e Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 9 Jun 2025 16:55:55 +0700 Subject: [PATCH 31/83] feat: add whatsapp notification for booking created --- package.json | 2 + .../data/services/verification.service.ts | 2 + .../order/infrastructure/order.controller.ts | 17 +++- .../midtrans-transaction-callback.handler.ts | 30 ++++--- .../whatsapp/entity/booking.entity.ts | 7 ++ src/services/whatsapp/whatsapp.constant.ts | 8 ++ src/services/whatsapp/whatsapp.service.ts | 81 +++++++++++++++++++ yarn.lock | 45 ++++++++++- 8 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 src/services/whatsapp/entity/booking.entity.ts diff --git a/package.json b/package.json index 9b6f5a4..ef1f732 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "pdfmake": "^0.2.10", "pg": "^8.11.5", "plop": "^4.0.1", + "qrcode": "^1.5.4", "reflect-metadata": "^0.2.0", "rxjs": "^7.5.0", "typeorm": "^0.3.20", @@ -71,6 +72,7 @@ "@types/express": "^4.17.13", "@types/jest": "29.5.12", "@types/node": "^20.12.13", + "@types/qrcode": "^1.5.5", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/src/modules/booking-online/authentication/data/services/verification.service.ts b/src/modules/booking-online/authentication/data/services/verification.service.ts index acbc186..69a5540 100644 --- a/src/modules/booking-online/authentication/data/services/verification.service.ts +++ b/src/modules/booking-online/authentication/data/services/verification.service.ts @@ -25,6 +25,7 @@ export class VerificationService { } async register(data: BookingVerification) { + const isProduction = process.env.NODE_ENV === 'true'; const currentTime = Math.floor(Date.now()); // current time in seconds // Generate a 4 digit OTP code @@ -35,6 +36,7 @@ export class VerificationService { }); if ( + isProduction && verification.updated_at && currentTime - verification.updated_at < this.expiredTimeRegister ) { diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 9a31e01..6b87ca2 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Public } from 'src/core/guards'; import { TransactionDto } from './dto/booking-order.dto'; @@ -7,6 +7,9 @@ import { TransactionDataService } from 'src/modules/transaction/transaction/data import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { MidtransService } from 'src/modules/configuration/midtrans/data/services/midtrans.service'; import { CreateBookingManager } from '../domain/usecases/managers/create-booking.manager'; +import * as QRCode from 'qrcode'; +import { Gate } from 'src/core/response/domain/decorators/pagination.response'; +import { Response } from 'express'; @ApiTags('Booking Order') @Controller('v1/booking') @@ -106,4 +109,16 @@ export class BookingOrderController { items: usageItems, }; } + + @Gate() + @Get('qrcode/:id') + async getQRcode(@Param('id') id: string, @Res() res: Response) { + console.log(QRCode); + const qrData = id; + const data = await QRCode.toDataURL(qrData); + res.setHeader('Content-Type', 'image/png'); + const base64Data = data.split(',')[1]; + const buffer = Buffer.from(base64Data, 'base64'); + res.send(buffer); + } } diff --git a/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts b/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts index a441d4b..35f9214 100644 --- a/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts +++ b/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts @@ -11,6 +11,9 @@ import { TransactionChangeStatusEvent } from '../../entities/event/transaction-c import * as _ from 'lodash'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { generateInvoiceCodeHelper } from '../managers/helpers/generate-invoice-code.helper'; +import { TransactionType } from '../../../constants'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; +import * as moment from 'moment'; @EventsHandler(MidtransCallbackEvent) export class MidtransCallbackHandler @@ -22,7 +25,6 @@ export class MidtransCallbackHandler ) {} async handle(event: MidtransCallbackEvent) { - console.log('callbak mid', event); const data_id = event.data.id; const data = event.data.data; let old_data = undefined; @@ -58,16 +60,22 @@ export class MidtransCallbackHandler .manager.connection.createQueryRunner(); await this.dataService.create(queryRunner, TransactionModel, transaction); - console.log('update change status to tr', { - id: data_id, - old: old_data, - data: { ...data, status: transaction.status }, - user: BLANK_USER, - description: 'Midtrans Callback', - module: TABLE_NAME.TRANSACTION, - op: OPERATION.UPDATE, - }); - console.log({ data, old_data }); + + if ( + transaction.status === STATUS.SETTLED && + transaction.type === TransactionType.ONLINE + ) { + const whatsappService = new WhatsappService(); + const formattedDate = moment(transaction.booking_date); + const payload = { + id: transaction.id, + phone: transaction.customer_phone, + code: transaction.invoice_code, + name: transaction.customer_name, + time: formattedDate.valueOf(), + }; + await whatsappService.bookingCreated(payload); + } this.eventBus.publish( new TransactionChangeStatusEvent({ diff --git a/src/services/whatsapp/entity/booking.entity.ts b/src/services/whatsapp/entity/booking.entity.ts new file mode 100644 index 0000000..90f5b95 --- /dev/null +++ b/src/services/whatsapp/entity/booking.entity.ts @@ -0,0 +1,7 @@ +export interface WhatsappBookingCreate { + id: string; + phone: string; + code: string; + name: string; + time: number; +} diff --git a/src/services/whatsapp/whatsapp.constant.ts b/src/services/whatsapp/whatsapp.constant.ts index cc04926..2b8f472 100644 --- a/src/services/whatsapp/whatsapp.constant.ts +++ b/src/services/whatsapp/whatsapp.constant.ts @@ -1,6 +1,14 @@ export const WHATSAPP_BUSINESS_API_URL = process.env.WHATSAPP_BUSINESS_API_URL ?? 'https://graph.facebook.com/'; +export const BOOKING_QR_URL = + process.env.BOOKING_QR_URL ?? + 'https://www.drupal.org/files/project-images/qrcode-module_0.png?'; + +export const BOOKING_TICKET_URL = + process.env.BOOKING_TICKET_URL ?? + 'https://booking.sky.eigen.co.id/app/ticket/'; + export const WHATSAPP_BUSINESS_VERSION = process.env.WHATSAPP_BUSINESS_VERSION ?? 'v22.0'; diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index 8d22282..cad1b36 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -4,6 +4,8 @@ import { } from 'src/modules/queue/domain/helpers/time.helper'; import { WhatsappQueue } from './entity/whatsapp-queue.entity'; import { + BOOKING_QR_URL, + BOOKING_TICKET_URL, WHATSAPP_BUSINESS_ACCESS_TOKEN, WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID, WHATSAPP_BUSINESS_API_URL, @@ -13,6 +15,8 @@ import { import axios from 'axios'; import { Logger } from '@nestjs/common'; import { apm } from 'src/core/apm'; +import { WhatsappBookingCreate } from './entity/booking.entity'; +import * as moment from 'moment'; export class WhatsappService { async sendMessage(data) { @@ -36,6 +40,7 @@ export class WhatsappService { status: error.response.status, data: error.response.data, headers: error.response.headers, + error: error.response.data?.error?.error_data, }); } else if (error.request) { console.error('Axios error request:', error.request); @@ -118,6 +123,82 @@ export class WhatsappService { ); } + async bookingCreated(data: WhatsappBookingCreate) { + const ticketUrl = `${BOOKING_TICKET_URL}${data.id}`; + const imageUrl = `${BOOKING_QR_URL}${data.id}`; + + const momentDate = moment(data.time); + const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); + // const dayOfWeek = momentDate.day(); + // const dayOfMonth = momentDate.date(); + // const year = momentDate.year(); + // const month = momentDate.month() + 1; + // const hour = momentDate.hour(); + // const minute = momentDate.minute(); + + const payload = { + messaging_product: 'whatsapp', + to: phoneNumberOnly(data.phone), // recipient's phone number + type: 'template', + template: { + name: 'booking_online', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'header', + parameters: [ + { + type: 'image', + image: { + link: imageUrl, + }, + }, + ], + }, + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'customer', + text: data.name, // replace with name variable + }, + { + type: 'text', + parameter_name: 'booking_code', + text: data.code, // replace with queue_code variable + }, + { + type: 'text', + parameter_name: 'booking_date', + text: fallbackValue, + }, + ], + }, + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [ + { + type: 'text', + text: ticketUrl, // replace with dynamic URL + }, + ], + }, + ], + }, + }; + + const response = await this.sendMessage(payload); + if (response) + Logger.log( + `Notification register Booking for ${data.code} send to ${data.phone}`, + ); + } + async sendOtpNotification(data: { phone: string; code: string }) { // Compose the WhatsApp message payload for OTP using Facebook WhatsApp API const payload = { diff --git a/yarn.lock b/yarn.lock index ddf1626..a4abb7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1239,6 +1239,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== +"@types/qrcode@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" + integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg== + dependencies: + "@types/node" "*" + "@types/qs@*": version "6.9.15" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" @@ -2861,6 +2868,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +dijkstrajs@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" + integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -6363,6 +6375,11 @@ png-js@^1.0.0: resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -6464,6 +6481,15 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +qrcode@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" + integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== + dependencies: + dijkstrajs "^1.0.1" + pngjs "^5.0.0" + yargs "^15.3.1" + qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -8070,7 +8096,7 @@ yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-parser@^18.1.1: +yargs-parser@^18.1.1, yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -8108,6 +8134,23 @@ yargs@15.3.1: y18n "^4.0.0" yargs-parser "^18.1.1" +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yargs@^16.0.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" -- 2.40.1 From 3661d9d1717fe6f2b2a41602468a2d06d4513dc5 Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 9 Jun 2025 17:26:57 +0700 Subject: [PATCH 32/83] feat: increase item limit to 1000 and enhance query for time group filtering --- .../booking-online/order/infrastructure/item.controller.ts | 1 + .../item/domain/usecases/managers/index-item.manager.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/infrastructure/item.controller.ts b/src/modules/booking-online/order/infrastructure/item.controller.ts index a3bf89c..061edcf 100644 --- a/src/modules/booking-online/order/infrastructure/item.controller.ts +++ b/src/modules/booking-online/order/infrastructure/item.controller.ts @@ -21,6 +21,7 @@ export class ItemController { async index( @Query() params: FilterItemDto, ): Promise> { + params.limit = 1000; params.show_to_booking = true; this.indexManager.setFilterParam(params); this.indexManager.setService(this.serviceData, TABLE_NAME.ITEM); diff --git a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts index 28ef6f9..02204ed 100644 --- a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts @@ -53,6 +53,7 @@ export class IndexItemManager extends BaseIndexManager { `${this.tableName}.share_profit`, `${this.tableName}.breakdown_bundling`, `${this.tableName}.play_estimation`, + `${this.tableName}.show_to_booking`, `item_category.id`, `item_category.name`, @@ -108,7 +109,7 @@ export class IndexItemManager extends BaseIndexManager { if (this.filterParam.time_group_ids?.length) { queryBuilder.andWhere( - `${this.tableName}.time_group_id In (:...timeGroupIds)`, + `${this.tableName}.time_group_id In (:...timeGroupIds) OR ${this.tableName}.time_group_id Is Null`, { timeGroupIds: this.filterParam.time_group_ids, }, -- 2.40.1 From 81923960852ba7af7b699d657cfe8402c5134ca4 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 10 Jun 2025 10:26:36 +0700 Subject: [PATCH 33/83] feat: implement reschedule verification process with OTP functionality --- src/app.module.ts | 2 + .../1749524993295-reschedule-otp.ts | 28 +++++ .../booking-online/helpers/generate-otp.ts | 9 ++ .../models/reschedule-verification.model.ts | 36 ++++++ .../reschedule-verification.entity.ts | 16 +++ .../reschedule-verification.manager.ts | 105 ++++++++++++++++++ .../infrastructure/dto/reschedule.dto.ts | 47 ++++++++ .../order/infrastructure/order.controller.ts | 27 +++++ .../booking-online/order/order.module.ts | 9 +- src/services/whatsapp/whatsapp.constant.ts | 2 +- 10 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 src/database/migrations/1749524993295-reschedule-otp.ts create mode 100644 src/modules/booking-online/helpers/generate-otp.ts create mode 100644 src/modules/booking-online/order/data/models/reschedule-verification.model.ts create mode 100644 src/modules/booking-online/order/domain/entities/reschedule-verification.entity.ts create mode 100644 src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts create mode 100644 src/modules/booking-online/order/infrastructure/dto/reschedule.dto.ts diff --git a/src/app.module.ts b/src/app.module.ts index 75ab21a..7a8461f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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 { OtpVerificationModel } from './modules/configuration/otp-verification/data/models/otp-verification.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({ imports: [ @@ -170,6 +171,7 @@ import { OtpVerifierModel } from './modules/configuration/otp-verification/data/ // Booking Online VerificationModel, + RescheduleVerificationModel, OtpVerificationModel, OtpVerifierModel, diff --git a/src/database/migrations/1749524993295-reschedule-otp.ts b/src/database/migrations/1749524993295-reschedule-otp.ts new file mode 100644 index 0000000..b9051ff --- /dev/null +++ b/src/database/migrations/1749524993295-reschedule-otp.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RescheduleOtp1749524993295 implements MigrationInterface { + name = 'RescheduleOtp1749524993295'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } +} diff --git a/src/modules/booking-online/helpers/generate-otp.ts b/src/modules/booking-online/helpers/generate-otp.ts new file mode 100644 index 0000000..5b737cf --- /dev/null +++ b/src/modules/booking-online/helpers/generate-otp.ts @@ -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; +} diff --git a/src/modules/booking-online/order/data/models/reschedule-verification.model.ts b/src/modules/booking-online/order/data/models/reschedule-verification.model.ts new file mode 100644 index 0000000..a3935fc --- /dev/null +++ b/src/modules/booking-online/order/data/models/reschedule-verification.model.ts @@ -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; +} diff --git a/src/modules/booking-online/order/domain/entities/reschedule-verification.entity.ts b/src/modules/booking-online/order/domain/entities/reschedule-verification.entity.ts new file mode 100644 index 0000000..482d4ba --- /dev/null +++ b/src/modules/booking-online/order/domain/entities/reschedule-verification.entity.ts @@ -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; +} diff --git a/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts new file mode 100644 index 0000000..168b6f6 --- /dev/null +++ b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts @@ -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, + private readonly transactionService: TransactionReadService, + ) {} + + async saveVerification( + request: RescheduleRequest, + ): Promise { + 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 = { + 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 { + const transaction = await this.transactionService.getOneByOptions({ + where: { id: bookingId }, + }); + return transaction; + } +} diff --git a/src/modules/booking-online/order/infrastructure/dto/reschedule.dto.ts b/src/modules/booking-online/order/infrastructure/dto/reschedule.dto.ts new file mode 100644 index 0000000..21bb027 --- /dev/null +++ b/src/modules/booking-online/order/infrastructure/dto/reschedule.dto.ts @@ -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; +} diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 6b87ca2..6e423d0 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -10,6 +10,11 @@ import { CreateBookingManager } from '../domain/usecases/managers/create-booking import * as QRCode from 'qrcode'; import { Gate } from 'src/core/response/domain/decorators/pagination.response'; import { Response } from 'express'; +import { + RescheduleRequestDTO, + RescheduleVerificationOTP, +} from './dto/reschedule.dto'; +import { RescheduleVerificationManager } from '../domain/usecases/managers/reschedule-verification.manager'; @ApiTags('Booking Order') @Controller('v1/booking') @@ -19,6 +24,7 @@ export class BookingOrderController { private createBooking: CreateBookingManager, private serviceData: TransactionDataService, private midtransService: MidtransService, + private rescheduleVerification: RescheduleVerificationManager, ) {} @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') async get(@Param('id') transactionId: string) { const data = await this.serviceData.getOneByOptions({ diff --git a/src/modules/booking-online/order/order.module.ts b/src/modules/booking-online/order/order.module.ts index 6a5c4b4..84c8626 100644 --- a/src/modules/booking-online/order/order.module.ts +++ b/src/modules/booking-online/order/order.module.ts @@ -11,16 +11,21 @@ import { BookingOrderController } from './infrastructure/order.controller'; import { CreateBookingManager } from './domain/usecases/managers/create-booking.manager'; import { MidtransModule } from 'src/modules/configuration/midtrans/midtrans.module'; import { CqrsModule } from '@nestjs/cqrs'; +import { RescheduleVerificationModel } from './data/models/reschedule-verification.model'; +import { RescheduleVerificationManager } from './domain/usecases/managers/reschedule-verification.manager'; @Module({ imports: [ ConfigModule.forRoot(), - TypeOrmModule.forFeature([ItemModel], CONNECTION_NAME.DEFAULT), + TypeOrmModule.forFeature( + [ItemModel, RescheduleVerificationModel], + CONNECTION_NAME.DEFAULT, + ), ItemModule, TransactionModule, MidtransModule, CqrsModule, ], controllers: [ItemController, BookingOrderController], - providers: [CreateBookingManager], + providers: [CreateBookingManager, RescheduleVerificationManager], }) export class BookingOrderModule {} diff --git a/src/services/whatsapp/whatsapp.constant.ts b/src/services/whatsapp/whatsapp.constant.ts index 2b8f472..07d8b76 100644 --- a/src/services/whatsapp/whatsapp.constant.ts +++ b/src/services/whatsapp/whatsapp.constant.ts @@ -7,7 +7,7 @@ export const BOOKING_QR_URL = export const 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 = process.env.WHATSAPP_BUSINESS_VERSION ?? 'v22.0'; -- 2.40.1 From a1ed81eec5a1a0edc01a641a9477a65c0634a7c7 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:20:53 +0700 Subject: [PATCH 34/83] feat: change length otp checker --- src/core/helpers/otp/otp-service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/helpers/otp/otp-service.ts b/src/core/helpers/otp/otp-service.ts index f63057b..9b0e33e 100644 --- a/src/core/helpers/otp/otp-service.ts +++ b/src/core/helpers/otp/otp-service.ts @@ -50,9 +50,8 @@ export class OtpService { this.hasRepeatedDigits(otp) || this.isPalindrome(otp) || this.hasPartiallyRepeatedDigits(otp) || - otp?.split('')?.length < this.otpLength + otp?.length < this.otpLength ); - return otp; } } -- 2.40.1 From ffc75ba1746d91bdbdb8d595d3e9bb59b35e30d6 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 10 Jun 2025 13:28:58 +0700 Subject: [PATCH 35/83] feat: integrate WhatsApp notifications for booking registration and rescheduling --- .../managers/create-booking.manager.ts | 17 +++ .../reschedule-verification.manager.ts | 5 +- src/services/whatsapp/whatsapp.service.ts | 112 ++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts index 16bbd05..750006b 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts @@ -5,6 +5,7 @@ import { TransactionType } from 'src/modules/transaction/transaction/constants'; import { CreateTransactionManager } from 'src/modules/transaction/transaction/domain/usecases/managers/create-transaction.manager'; import { generateInvoiceCodeHelper } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/generate-invoice-code.helper'; import { mappingRevertTransaction } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/mapping-transaction.helper'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; import { v4 as uuidv4 } from 'uuid'; export class CreateBookingManager extends CreateTransactionManager { @@ -43,4 +44,20 @@ export class CreateBookingManager extends CreateTransactionManager { }); return; } + + async afterProcess(): Promise { + const whatsapp = new WhatsappService(); + console.log(`/snap/v4/redirection/${this.data.payment_midtrans_token}`); + console.log(this.data.payment_midtrans_url); + await whatsapp.bookingRegister( + { + phone: this.data.customer_phone, + code: this.data.invoice_code, + name: this.data.customer_name, + time: this.data.booking_date, + id: this.data.id, + }, + `snap/v4/redirection/${this.data.payment_midtrans_token}`, + ); + } } diff --git a/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts index 168b6f6..3ab8717 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts @@ -53,9 +53,12 @@ export class RescheduleVerificationManager { }); const whatsapp = new WhatsappService(); - whatsapp.sendOtpNotification({ + whatsapp.bookingReschedule({ phone: transaction.customer_phone, code: otp.toString(), + name: transaction.customer_name, + time: new Date(request.reschedule_date).getTime(), + id: transaction.id, }); return result; } catch (error) { diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index cad1b36..3f95eae 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -199,6 +199,118 @@ export class WhatsappService { ); } + async bookingRegister(data: WhatsappBookingCreate, paymentUrl: string) { + const momentDate = moment(data.time); + const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); + + const payload = { + messaging_product: 'whatsapp', + to: phoneNumberOnly(data.phone), // recipient's phone number + type: 'template', + template: { + name: 'booking_register', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'customer', + text: data.name, // replace with name variable + }, + { + type: 'text', + parameter_name: 'booking_date', + text: fallbackValue, + }, + ], + }, + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [ + { + type: 'text', + text: paymentUrl, // replace with dynamic URL + }, + ], + }, + ], + }, + }; + + const response = await this.sendMessage(payload); + if (response) + Logger.log( + `Notification register Booking for ${data.code} send to ${data.phone}`, + ); + } + + async bookingReschedule(data: WhatsappBookingCreate) { + const momentDate = moment(data.time); + const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); + + const payload = { + messaging_product: 'whatsapp', + to: phoneNumberOnly(data.phone), // recipient's phone number + type: 'template', + template: { + name: 'booking_reschedule', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'customer', + text: data.name, // replace with name variable + }, + { + type: 'text', + parameter_name: 'booking_code', + text: data.code, // replace with queue_code variable + }, + { + type: 'text', + parameter_name: 'booking_date', + text: fallbackValue, + }, + { + type: 'text', + parameter_name: 'otp', + text: data.code, + }, + ], + }, + { + type: 'button', + sub_type: 'copy_code', + index: '0', + parameters: [ + { + type: 'coupon_code', + coupon_code: data.code, + }, + ], + }, + ], + }, + }; + + const response = await this.sendMessage(payload); + if (response) + Logger.log( + `Notification reschedule Booking for ${data.code} send to ${data.phone}`, + ); + } + async sendOtpNotification(data: { phone: string; code: string }) { // Compose the WhatsApp message payload for OTP using Facebook WhatsApp API const payload = { -- 2.40.1 From 6a7ab72e128acc1c73c7c229486d8a9223acaf27 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 10 Jun 2025 14:29:46 +0700 Subject: [PATCH 36/83] feat: add booking description field to item model and database --- ...7252986-add-booking-description-to-item.ts | 19 +++++++++++++++++++ .../item/data/models/item.model.ts | 3 +++ .../item/domain/entities/item.entity.ts | 1 + .../usecases/managers/detail-item.manager.ts | 1 + .../usecases/managers/index-item.manager.ts | 1 + .../item/infrastructure/dto/item.dto.ts | 11 +++++++++++ 6 files changed, 36 insertions(+) create mode 100644 src/database/migrations/1749537252986-add-booking-description-to-item.ts diff --git a/src/database/migrations/1749537252986-add-booking-description-to-item.ts b/src/database/migrations/1749537252986-add-booking-description-to-item.ts new file mode 100644 index 0000000..f8456a8 --- /dev/null +++ b/src/database/migrations/1749537252986-add-booking-description-to-item.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddBookingDescriptionToItem1749537252986 + implements MigrationInterface +{ + name = 'AddBookingDescriptionToItem1749537252986'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" ADD "booking_description" text`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" DROP COLUMN "booking_description"`, + ); + } +} diff --git a/src/modules/item-related/item/data/models/item.model.ts b/src/modules/item-related/item/data/models/item.model.ts index b565c4d..ca7951e 100644 --- a/src/modules/item-related/item/data/models/item.model.ts +++ b/src/modules/item-related/item/data/models/item.model.ts @@ -27,6 +27,9 @@ export class ItemModel @Column('varchar', { name: 'name', unique: true }) name: string; + @Column('text', { name: 'booking_description', nullable: true }) + booking_description: string; + @Column('varchar', { name: 'image_url', nullable: true }) image_url: string; diff --git a/src/modules/item-related/item/domain/entities/item.entity.ts b/src/modules/item-related/item/domain/entities/item.entity.ts index 995b113..8d4a0a7 100644 --- a/src/modules/item-related/item/domain/entities/item.entity.ts +++ b/src/modules/item-related/item/domain/entities/item.entity.ts @@ -18,4 +18,5 @@ export interface ItemEntity extends BaseStatusEntity { use_queue: boolean; show_to_booking: boolean; breakdown_bundling?: boolean; + booking_description?: string; } diff --git a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts index 21e3e4b..7aae478 100644 --- a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts @@ -56,6 +56,7 @@ export class DetailItemManager extends BaseDetailManager { `${this.tableName}.show_to_booking`, `${this.tableName}.breakdown_bundling`, `${this.tableName}.play_estimation`, + `${this.tableName}.booking_description`, `item_category.id`, `item_category.name`, diff --git a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts index 02204ed..1e5101a 100644 --- a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts @@ -54,6 +54,7 @@ export class IndexItemManager extends BaseIndexManager { `${this.tableName}.breakdown_bundling`, `${this.tableName}.play_estimation`, `${this.tableName}.show_to_booking`, + `${this.tableName}.booking_description`, `item_category.id`, `item_category.name`, diff --git a/src/modules/item-related/item/infrastructure/dto/item.dto.ts b/src/modules/item-related/item/infrastructure/dto/item.dto.ts index c1add0f..b91f3f5 100644 --- a/src/modules/item-related/item/infrastructure/dto/item.dto.ts +++ b/src/modules/item-related/item/infrastructure/dto/item.dto.ts @@ -138,6 +138,17 @@ export class ItemDto extends BaseStatusDto implements ItemEntity { @ValidateIf((body) => body.show_to_booking) show_to_booking: boolean; + @ApiProperty({ + type: String, + required: false, + example: '...', + }) + @ValidateIf((body) => body.show_to_booking) + @IsString({ + message: 'Booking description is required when show to booking is enabled.', + }) + booking_description: string; + @ApiProperty({ name: 'bundling_items', type: [Object], -- 2.40.1 From b8dd2a4e01ec72c6ff1aeb5beb95e5120f46a6a6 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 10 Jun 2025 14:30:50 +0700 Subject: [PATCH 37/83] temp: update WhatsApp notification method for rescheduling to use OTP notification --- .../managers/reschedule-verification.manager.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts index 3ab8717..098b92b 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts @@ -53,13 +53,17 @@ export class RescheduleVerificationManager { }); const whatsapp = new WhatsappService(); - whatsapp.bookingReschedule({ + whatsapp.sendOtpNotification({ phone: transaction.customer_phone, code: otp.toString(), - name: transaction.customer_name, - time: new Date(request.reschedule_date).getTime(), - id: transaction.id, }); + // whatsapp.bookingReschedule({ + // phone: transaction.customer_phone, + // code: otp.toString(), + // name: transaction.customer_name, + // time: new Date(request.reschedule_date).getTime(), + // id: transaction.id, + // }); return result; } catch (error) { // You can customize the error handling as needed, e.g., throw HttpException for NestJS -- 2.40.1 From 8497a5779da7f3adad74cbdd9dd8b817edde6631 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:02:43 +0700 Subject: [PATCH 38/83] feat: fix validation generate otp --- src/core/helpers/otp/otp-service.ts | 8 +++++-- .../data/services/otp-verification.service.ts | 22 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/core/helpers/otp/otp-service.ts b/src/core/helpers/otp/otp-service.ts index 9b0e33e..0ba5e26 100644 --- a/src/core/helpers/otp/otp-service.ts +++ b/src/core/helpers/otp/otp-service.ts @@ -38,6 +38,10 @@ export class OtpService { return Object.values(counts).some((count) => count > 2); } + private hasMatchLength(str: string) { + return str.length !== this.otpLength; + } + public generateSecureOTP(): string { let otp: string; @@ -46,11 +50,11 @@ export class OtpService { Math.floor(Math.random() * 10).toString(), ).join(''); } while ( + this.hasMatchLength(otp) || this.hasSequentialDigits(otp) || this.hasRepeatedDigits(otp) || this.isPalindrome(otp) || - this.hasPartiallyRepeatedDigits(otp) || - otp?.length < this.otpLength + this.hasPartiallyRepeatedDigits(otp) ); return otp; } 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 11420d2..148325e 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 @@ -74,7 +74,9 @@ export class OtpVerificationService { const createdAtMoment = moment(Number(activeOTP.created_at)); const nowMoment = moment(Number(dateNow)); const diffSeconds = nowMoment.diff(createdAtMoment, 'seconds'); - if (diffSeconds < 60) { + const isProduction = process.env.NODE_ENV === 'true'; + + if (diffSeconds < 60 && isProduction) { throw new BadRequestException( 'An active OTP request was made recently. Please try again later.', ); @@ -116,10 +118,13 @@ export class OtpVerificationService { ); } + let otp: any; + // Build a where condition with OR between target_id and reference - const otp = await this.otpVerificationRepo.findOne({ - where: [ - { + + if (target_id) { + otp = await this.otpVerificationRepo.findOne({ + where: { otp_code, action_type, target_id, @@ -127,7 +132,10 @@ export class OtpVerificationService { is_used: false, is_replaced: false, }, - { + }); + } else if (reference) { + otp = await this.otpVerificationRepo.findOne({ + where: { otp_code, action_type, reference, @@ -135,8 +143,8 @@ export class OtpVerificationService { is_used: false, is_replaced: false, }, - ], - }); + }); + } if (!otp) { throw new BadRequestException('Invalid or expired OTP.'); -- 2.40.1 From 16df6945b7564f88bc9916f9149b68f550b474c8 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:17:35 +0700 Subject: [PATCH 39/83] feat: fix validation generate otp --- src/core/helpers/otp/otp-service.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/helpers/otp/otp-service.ts b/src/core/helpers/otp/otp-service.ts index 0ba5e26..0b4e3e1 100644 --- a/src/core/helpers/otp/otp-service.ts +++ b/src/core/helpers/otp/otp-service.ts @@ -38,10 +38,14 @@ export class OtpService { return Object.values(counts).some((count) => count > 2); } - private hasMatchLength(str: string) { + private hasNoMatchLength(str: string) { return str.length !== this.otpLength; } + private hasStartWithZero(str: string) { + return str.split('')[0] === '0'; + } + public generateSecureOTP(): string { let otp: string; @@ -50,11 +54,12 @@ export class OtpService { Math.floor(Math.random() * 10).toString(), ).join(''); } while ( - this.hasMatchLength(otp) || + this.hasNoMatchLength(otp) || this.hasSequentialDigits(otp) || this.hasRepeatedDigits(otp) || this.isPalindrome(otp) || - this.hasPartiallyRepeatedDigits(otp) + this.hasPartiallyRepeatedDigits(otp) || + this.hasStartWithZero(otp) ); return otp; } -- 2.40.1 From 94fbec0c78751222fbac6e4cb0f1107736ce353f Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:02:05 +0700 Subject: [PATCH 40/83] feat: save otp code when reject reconciliation --- .../managers/cancel-reconciliation.manager.ts | 9 +++++++++ .../usecases/reconciliation-data.orchestrator.ts | 3 ++- .../infrastructure/dto/cancel-top-dto.ts | 14 ++++++++++++++ .../reconciliation-data.controller.ts | 8 ++++++-- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts diff --git a/src/modules/transaction/reconciliation/domain/usecases/managers/cancel-reconciliation.manager.ts b/src/modules/transaction/reconciliation/domain/usecases/managers/cancel-reconciliation.manager.ts index 0782fa6..71a71be 100644 --- a/src/modules/transaction/reconciliation/domain/usecases/managers/cancel-reconciliation.manager.ts +++ b/src/modules/transaction/reconciliation/domain/usecases/managers/cancel-reconciliation.manager.ts @@ -15,6 +15,12 @@ import { TransactionEntity } from 'src/modules/transaction/transaction/domain/en @Injectable() export class CancelReconciliationManager extends BaseUpdateStatusManager { + protected payloadBody: any; + + setCustomBodyRequest(body) { + this.payloadBody = body; + } + getResult(): string { return `Success active data ${this.result.id}`; } @@ -50,6 +56,7 @@ export class CancelReconciliationManager extends BaseUpdateStatusManager { if (this.data.is_recap_transaction) { Object.assign(this.data, { + otp_code: this.payloadBody?.otp_code, reconciliation_confirm_by: null, reconciliation_confirm_date: null, reconciliation_status: STATUS.PENDING, @@ -58,6 +65,7 @@ export class CancelReconciliationManager extends BaseUpdateStatusManager { + async cancel(dataId, body): Promise { this.cancelManager.setData(dataId, STATUS.REJECTED); this.cancelManager.setService(this.serviceData, TABLE_NAME.TRANSACTION); + this.cancelManager.setCustomBodyRequest(body); await this.cancelManager.execute(); return this.cancelManager.getResult(); } diff --git a/src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts b/src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts new file mode 100644 index 0000000..042a863 --- /dev/null +++ b/src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class OtpVerifyDto { + @ApiProperty({ + name: 'otp_code', + type: String, + required: true, + example: '2345', + }) + @IsString() + @IsNotEmpty() + otp_code: string; +} diff --git a/src/modules/transaction/reconciliation/infrastructure/reconciliation-data.controller.ts b/src/modules/transaction/reconciliation/infrastructure/reconciliation-data.controller.ts index c983d2f..4f76225 100644 --- a/src/modules/transaction/reconciliation/infrastructure/reconciliation-data.controller.ts +++ b/src/modules/transaction/reconciliation/infrastructure/reconciliation-data.controller.ts @@ -16,6 +16,7 @@ import { Public } from 'src/core/guards'; import { TransactionEntity } from '../../transaction/domain/entities/transaction.entity'; import { UpdateReconciliationDto } from './dto/reconciliation.dto'; import { RecapReconciliationDto } from './dto/recap.dto'; +import { OtpVerifyDto } from './dto/cancel-top-dto'; @ApiTags(`${MODULE_NAME.RECONCILIATION.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.RECONCILIATION}`) @@ -40,8 +41,11 @@ export class ReconciliationDataController { } @Patch(':id/cancel') - async cancel(@Param('id') dataId: string): Promise { - return await this.orchestrator.cancel(dataId); + async cancel( + @Param('id') dataId: string, + @Body() body: OtpVerifyDto, + ): Promise { + return await this.orchestrator.cancel(dataId, body); } @Put('/batch-cancel') -- 2.40.1 From baeb72fe7d8757ef95223b55ec8c13f72dce7818 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 10 Jun 2025 16:33:36 +0700 Subject: [PATCH 41/83] feat: add filtering option for items based on time group presence --- .../domain/usecases/managers/index-item.manager.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts index 1e5101a..f32ac74 100644 --- a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts @@ -121,6 +121,16 @@ export class IndexItemManager extends BaseIndexManager { queryBuilder.andWhere(`${this.tableName}.show_to_booking = true`); } + if (this.filterParam.without_time_group != null) { + const withoutTimeGroup = this.filterParam.without_time_group + ? 'Is Null' + : 'Is Not Null'; + + queryBuilder.andWhere( + `${this.tableName}.time_group_id ${withoutTimeGroup}`, + ); + } + return queryBuilder; } } -- 2.40.1 From fdbd667b7dcec800a39cdfd3d0cc8899c398cab3 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 10 Jun 2025 16:34:00 +0700 Subject: [PATCH 42/83] feat: enhance loginQueue method to filter orders by date range --- src/modules/queue/data/services/ticket.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/queue/data/services/ticket.service.ts b/src/modules/queue/data/services/ticket.service.ts index 24827e4..c3d1f11 100644 --- a/src/modules/queue/data/services/ticket.service.ts +++ b/src/modules/queue/data/services/ticket.service.ts @@ -40,11 +40,14 @@ export class TicketDataService extends BaseDataService { } async loginQueue(id: string): Promise { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + const order = await this.order.findOne({ relations: ['tickets'], where: [ - { transaction_id: id }, - { code: id, transaction_id: Not(IsNull()) }, + { transaction_id: id, date: Between(start, end) }, + { code: id, transaction_id: Not(IsNull()), date: Between(start, end) }, ], }); -- 2.40.1 From 6da2118ab5b1fc777a70a9e24df72f45875b39d7 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:38:06 +0700 Subject: [PATCH 43/83] feat: sync time group to couch --- src/modules/configuration/couch/constants.ts | 1 + .../domain/managers/time-group.handle.ts | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/modules/configuration/couch/domain/managers/time-group.handle.ts diff --git a/src/modules/configuration/couch/constants.ts b/src/modules/configuration/couch/constants.ts index 8171752..fa8be79 100644 --- a/src/modules/configuration/couch/constants.ts +++ b/src/modules/configuration/couch/constants.ts @@ -3,4 +3,5 @@ export const DatabaseListen = [ 'vip_code', 'pos_activity', 'pos_cash_activity', + 'time_groups', ]; diff --git a/src/modules/configuration/couch/domain/managers/time-group.handle.ts b/src/modules/configuration/couch/domain/managers/time-group.handle.ts new file mode 100644 index 0000000..b93fe04 --- /dev/null +++ b/src/modules/configuration/couch/domain/managers/time-group.handle.ts @@ -0,0 +1,65 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { CouchService } from '../../data/services/couch.service'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { TimeGroupDeletedEvent } from 'src/modules/item-related/time-group/domain/entities/event/time-group-deleted.event'; +import { TimeGroupChangeStatusEvent } from 'src/modules/item-related/time-group/domain/entities/event/time-group-change-status.event'; +import { TimeGroupUpdatedEvent } from 'src/modules/item-related/time-group/domain/entities/event/time-group-updated.event'; + +@EventsHandler(TimeGroupDeletedEvent) +export class TimeGroupDeletedHandler + implements IEventHandler +{ + constructor(private couchService: CouchService) {} + + async handle(event: TimeGroupDeletedEvent) { + const data = await this.couchService.deleteDoc( + { + _id: event.data.id, + ...event.data.data, + }, + 'time_groups', + ); + } +} + +@EventsHandler(TimeGroupChangeStatusEvent, TimeGroupUpdatedEvent) +export class TimeGroupUpdatedHandler + implements IEventHandler +{ + constructor(private couchService: CouchService) {} + + async handle(event: TimeGroupChangeStatusEvent) { + const dataOld = event.data.old; + const data = event.data.data; + + // change status to active + if (dataOld?.status != data.status && data.status == STATUS.ACTIVE) { + await this.couchService.createDoc( + { + _id: data.id, + ...data, + }, + 'time_groups', + ); + } else if (dataOld?.status != data.status) { + await this.couchService.deleteDoc( + { + _id: data.id, + ...data, + }, + 'time_groups', + ); + } + + // update + else { + await this.couchService.updateDoc( + { + _id: data.id, + ...data, + }, + 'time_groups', + ); + } + } +} -- 2.40.1 From 2f2fc27965de46f5f9b1ce50de58760c65c9c05a Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:44:58 +0700 Subject: [PATCH 44/83] feat: sync time group to couch --- src/modules/configuration/couch/couch.module.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/modules/configuration/couch/couch.module.ts b/src/modules/configuration/couch/couch.module.ts index 6fd848f..060254c 100644 --- a/src/modules/configuration/couch/couch.module.ts +++ b/src/modules/configuration/couch/couch.module.ts @@ -52,6 +52,10 @@ import { SeasonPeriodDataService } from 'src/modules/season-related/season-perio import { SeasonPeriodModel } from 'src/modules/season-related/season-period/data/models/season-period.model'; import { TransactionDemographyModel } from 'src/modules/transaction/transaction/data/models/transaction-demography.model'; import { UserLoginModel } from 'src/modules/user-related/user/data/models/user-login.model'; +import { + TimeGroupDeletedHandler, + TimeGroupUpdatedHandler, +} from './domain/managers/time-group.handle'; @Module({ imports: [ @@ -83,6 +87,10 @@ import { UserLoginModel } from 'src/modules/user-related/user/data/models/user-l VipCodeCreatedHandler, VipCategoryDeletedHandler, VipCategoryUpdatedHandler, + + TimeGroupDeletedHandler, + TimeGroupUpdatedHandler, + SeasonPeriodDeletedHandler, SeasonPeriodUpdatedHandler, ItemUpdatedHandler, -- 2.40.1 From 464f5cb49ec004f3346d726a6d962824b6fd427c Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 08:53:12 +0700 Subject: [PATCH 45/83] feat: add booking parent relationship to transactions and implement rescheduling functionality --- ...39749-add-booking-parent-to-transaction.ts | 24 ++++ .../reschedule-verification.manager.ts | 17 +-- .../usecases/managers/reschedule.manager.ts | 116 ++++++++++++++++++ .../order/infrastructure/order.controller.ts | 13 +- .../booking-online/order/order.module.ts | 7 +- .../data/models/transaction.model.ts | 16 +++ .../domain/entities/transaction.entity.ts | 2 + src/services/whatsapp/whatsapp.service.ts | 80 +++++++++++- 8 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 src/database/migrations/1749604239749-add-booking-parent-to-transaction.ts create mode 100644 src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts diff --git a/src/database/migrations/1749604239749-add-booking-parent-to-transaction.ts b/src/database/migrations/1749604239749-add-booking-parent-to-transaction.ts new file mode 100644 index 0000000..b9a7012 --- /dev/null +++ b/src/database/migrations/1749604239749-add-booking-parent-to-transaction.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddBookingParentToTransaction1749604239749 + implements MigrationInterface +{ + name = 'AddBookingParentToTransaction1749604239749'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "transactions" ADD "parent_id" uuid`); + + await queryRunner.query( + `ALTER TABLE "transactions" ADD CONSTRAINT "FK_413e95171729ba18cabce1c31e3" FOREIGN KEY ("parent_id") REFERENCES "transactions"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" DROP CONSTRAINT "FK_413e95171729ba18cabce1c31e3"`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "parent_id"`, + ); + } +} diff --git a/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts index 098b92b..b40d513 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/reschedule-verification.manager.ts @@ -57,7 +57,7 @@ export class RescheduleVerificationManager { phone: transaction.customer_phone, code: otp.toString(), }); - // whatsapp.bookingReschedule({ + // whatsapp.bookingRescheduleOTP({ // phone: transaction.customer_phone, // code: otp.toString(), // name: transaction.customer_name, @@ -76,17 +76,17 @@ export class RescheduleVerificationManager { async verifyOtp( booking_id: string, code: number, - ): Promise<{ success: boolean; message: string }> { + ): Promise { const verification = await this.rescheduleVerificationRepository.findOne({ where: { booking_id, code }, order: { created_at: 'DESC' }, }); if (!verification) { - return { + throw new UnprocessableEntityException({ success: false, - message: 'No verification code found for this booking.', - }; + message: 'Verification code not match', + }); } // Optionally, you can implement OTP expiration logic here @@ -95,12 +95,15 @@ export class RescheduleVerificationManager { // Increment tried count verification.tried = (verification.tried || 0) + 1; await this.rescheduleVerificationRepository.save(verification); - return { success: false, message: 'Invalid verification code.' }; + throw new UnprocessableEntityException({ + success: false, + message: 'Invalid verification code.', + }); } // Optionally, you can mark the verification as used or verified here - return { success: true, message: 'Verification successful.' }; + return verification; } async findDetailByBookingId(bookingId: string): Promise { diff --git a/src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts new file mode 100644 index 0000000..6d526bb --- /dev/null +++ b/src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts @@ -0,0 +1,116 @@ +import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { v4 as uuidv4 } from 'uuid'; +import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service'; +import { generateInvoiceCodeHelper } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/generate-invoice-code.helper'; +import * as moment from 'moment'; +import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model'; +import { RescheduleVerificationModel } from '../../../data/models/reschedule-verification.model'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; + +@Injectable() +export class RescheduleManager { + constructor(private serviceData: TransactionDataService) {} + + async reschedule(data: RescheduleVerificationModel) { + const transaction = await this.serviceData.getRepository().findOne({ + relations: ['children_transactions', 'items'], + where: { id: data.booking_id }, + }); + + const today = moment().startOf('day'); + const rescheduleDate = moment(data.reschedule_date, 'DD-MM-YYYY'); + const rescheduleDateStartOfDay = rescheduleDate.startOf('day'); + + //TODO: validate session period priority + + if (rescheduleDateStartOfDay.isSameOrBefore(today)) { + throw new UnprocessableEntityException( + 'Reschedule date must be in the future', + ); + } + + if (!transaction) { + throw new UnprocessableEntityException('Transaction not found'); + } + + if (transaction.status !== STATUS.SETTLED) { + throw new UnprocessableEntityException('Transaction is not settled'); + } + + if (transaction.children_transactions.length > 0) { + throw new UnprocessableEntityException('Transaction already rescheduled'); + } + + if (transaction.parent_id) { + throw new UnprocessableEntityException('Transaction is a reschedule'); + } + + const id = uuidv4(); + const invoiceCode = await generateInvoiceCodeHelper( + this.serviceData, + 'BOOK', + ); + + const items = this.makeItemZeroPrice(transaction.items); + const transactionData = this.makeTransactionZeroPrice(transaction); + + Object.assign(transactionData, { + parent_id: transaction.id, + id, + invoice_code: invoiceCode, + status: STATUS.SETTLED, + invoice_date: rescheduleDate.format('YYYY-MM-DD'), + created_at: moment().unix() * 1000, + updated_at: moment().unix() * 1000, + items, + }); + + await this.serviceData.getRepository().save(transactionData); + + const whatsapp = new WhatsappService(); + whatsapp.rescheduleCreated({ + id: transactionData.id, + name: transactionData.customer_name, + phone: transactionData.customer_phone, + time: moment(transactionData.invoice_date).unix() * 1000, + code: data.code.toString(), + }); + + return transactionData; + } + + private makeItemZeroPrice(items: TransactionItemModel[]) { + return items.map((item) => { + return { + ...item, + id: uuidv4(), + item_price: 0, + total_price: 0, + total_hpp: 0, + total_profit: 0, + total_profit_share: 0, + payment_total_dpp: 0, + payment_total_tax: 0, + total_net_price: 0, + }; + }); + } + + private makeTransactionZeroPrice(transaction: TransactionModel) { + return { + ...transaction, + payment_sub_total: 0, + payment_discount_total: 0, + payment_total: 0, + payment_total_pay: 0, + payment_total_share: 0, + payment_total_tax: 0, + payment_total_profit: 0, + payment_total_net_profit: 0, + payment_total_dpp: 0, + discount_percentage: 0, + }; + } +} diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 6e423d0..db38439 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -15,6 +15,7 @@ import { RescheduleVerificationOTP, } from './dto/reschedule.dto'; import { RescheduleVerificationManager } from '../domain/usecases/managers/reschedule-verification.manager'; +import { RescheduleManager } from '../domain/usecases/managers/reschedule.manager'; @ApiTags('Booking Order') @Controller('v1/booking') @@ -25,6 +26,7 @@ export class BookingOrderController { private serviceData: TransactionDataService, private midtransService: MidtransService, private rescheduleVerification: RescheduleVerificationManager, + private rescheduleManager: RescheduleManager, ) {} @Post() @@ -72,9 +74,16 @@ export class BookingOrderController { +data.code, ); - const transaction = await this.get(data.booking_id); + const reschedule = await this.rescheduleManager.reschedule(result); + const transaction = await this.get(reschedule.id); - return { ...result, transaction }; + return { + id: reschedule.id, + phone_number: result.phone_number, + name: result.name, + reschedule_date: result.reschedule_date, + transaction, + }; } @Get(':id') diff --git a/src/modules/booking-online/order/order.module.ts b/src/modules/booking-online/order/order.module.ts index 84c8626..d337988 100644 --- a/src/modules/booking-online/order/order.module.ts +++ b/src/modules/booking-online/order/order.module.ts @@ -13,6 +13,7 @@ import { MidtransModule } from 'src/modules/configuration/midtrans/midtrans.modu import { CqrsModule } from '@nestjs/cqrs'; import { RescheduleVerificationModel } from './data/models/reschedule-verification.model'; import { RescheduleVerificationManager } from './domain/usecases/managers/reschedule-verification.manager'; +import { RescheduleManager } from './domain/usecases/managers/reschedule.manager'; @Module({ imports: [ ConfigModule.forRoot(), @@ -26,6 +27,10 @@ import { RescheduleVerificationManager } from './domain/usecases/managers/resche CqrsModule, ], controllers: [ItemController, BookingOrderController], - providers: [CreateBookingManager, RescheduleVerificationManager], + providers: [ + CreateBookingManager, + RescheduleVerificationManager, + RescheduleManager, + ], }) export class BookingOrderModule {} diff --git a/src/modules/transaction/transaction/data/models/transaction.model.ts b/src/modules/transaction/transaction/data/models/transaction.model.ts index 83c4416..4e47acd 100644 --- a/src/modules/transaction/transaction/data/models/transaction.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction.model.ts @@ -275,6 +275,22 @@ export class TransactionModel }) refunds: RefundModel[]; + @Column('varchar', { name: 'parent_id', nullable: true }) + parent_id: string; + + @ManyToOne(() => TransactionModel, (model) => model.id, { + nullable: true, + }) + @JoinColumn({ name: 'parent_id' }) + parent_transaction: TransactionModel; + + @OneToMany(() => TransactionModel, (model) => model.parent_transaction, { + cascade: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + children_transactions: TransactionModel[]; + @Column('varchar', { name: 'otp_code', nullable: true }) otp_code: string; } diff --git a/src/modules/transaction/transaction/domain/entities/transaction.entity.ts b/src/modules/transaction/transaction/domain/entities/transaction.entity.ts index 8aa0b55..040c023 100644 --- a/src/modules/transaction/transaction/domain/entities/transaction.entity.ts +++ b/src/modules/transaction/transaction/domain/entities/transaction.entity.ts @@ -86,6 +86,8 @@ export interface TransactionEntity extends BaseStatusEntity { sending_qr_at: number; sending_qr_status: STATUS; + parent_id?: string; + calendar_id?: string; calendar_link?: string; diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index 3f95eae..4c51a4d 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -124,7 +124,6 @@ export class WhatsappService { } async bookingCreated(data: WhatsappBookingCreate) { - const ticketUrl = `${BOOKING_TICKET_URL}${data.id}`; const imageUrl = `${BOOKING_QR_URL}${data.id}`; const momentDate = moment(data.time); @@ -184,7 +183,82 @@ export class WhatsappService { parameters: [ { type: 'text', - text: ticketUrl, // replace with dynamic URL + text: data.id, // replace with dynamic URL + }, + ], + }, + ], + }, + }; + + const response = await this.sendMessage(payload); + if (response) + Logger.log( + `Notification register Booking for ${data.code} send to ${data.phone}`, + ); + } + + async rescheduleCreated(data: WhatsappBookingCreate) { + const imageUrl = `${BOOKING_QR_URL}${data.id}`; + + const momentDate = moment(data.time); + const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); + // const dayOfWeek = momentDate.day(); + // const dayOfMonth = momentDate.date(); + // const year = momentDate.year(); + // const month = momentDate.month() + 1; + // const hour = momentDate.hour(); + // const minute = momentDate.minute(); + + const payload = { + messaging_product: 'whatsapp', + to: phoneNumberOnly(data.phone), // recipient's phone number + type: 'template', + template: { + name: 'reschedule_created', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'header', + parameters: [ + { + type: 'image', + image: { + link: imageUrl, + }, + }, + ], + }, + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'customer', + text: data.name, // replace with name variable + }, + { + type: 'text', + parameter_name: 'booking_code', + text: data.code, // replace with queue_code variable + }, + { + type: 'text', + parameter_name: 'booking_date', + text: fallbackValue, + }, + ], + }, + { + type: 'button', + sub_type: 'url', + index: '0', + parameters: [ + { + type: 'text', + text: data.id, // replace with dynamic URL }, ], }, @@ -250,7 +324,7 @@ export class WhatsappService { ); } - async bookingReschedule(data: WhatsappBookingCreate) { + async bookingRescheduleOTP(data: WhatsappBookingCreate) { const momentDate = moment(data.time); const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); -- 2.40.1 From dc926d84e4bcff76ed58dbb6fe420b5ec327d590 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 10:35:06 +0700 Subject: [PATCH 46/83] refactor: streamline rescheduling logic and enhance transaction retrieval - Replaced direct transaction retrieval with a dedicated method for better clarity. - Consolidated validation checks for rescheduling into the controller. - Added booking date to the transaction data being created. - Improved error handling for various transaction states during rescheduling. --- .../usecases/managers/reschedule.manager.ts | 36 +++------------ .../order/infrastructure/order.controller.ts | 44 ++++++++++++++++++- .../data/services/transaction-data.service.ts | 7 +++ 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts index 6d526bb..cb67563 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/reschedule.manager.ts @@ -14,38 +14,11 @@ export class RescheduleManager { constructor(private serviceData: TransactionDataService) {} async reschedule(data: RescheduleVerificationModel) { - const transaction = await this.serviceData.getRepository().findOne({ - relations: ['children_transactions', 'items'], - where: { id: data.booking_id }, - }); + const transaction = await this.serviceData.getTransactionWithReschedule( + data.booking_id, + ); - const today = moment().startOf('day'); const rescheduleDate = moment(data.reschedule_date, 'DD-MM-YYYY'); - const rescheduleDateStartOfDay = rescheduleDate.startOf('day'); - - //TODO: validate session period priority - - if (rescheduleDateStartOfDay.isSameOrBefore(today)) { - throw new UnprocessableEntityException( - 'Reschedule date must be in the future', - ); - } - - if (!transaction) { - throw new UnprocessableEntityException('Transaction not found'); - } - - if (transaction.status !== STATUS.SETTLED) { - throw new UnprocessableEntityException('Transaction is not settled'); - } - - if (transaction.children_transactions.length > 0) { - throw new UnprocessableEntityException('Transaction already rescheduled'); - } - - if (transaction.parent_id) { - throw new UnprocessableEntityException('Transaction is a reschedule'); - } const id = uuidv4(); const invoiceCode = await generateInvoiceCodeHelper( @@ -62,6 +35,7 @@ export class RescheduleManager { invoice_code: invoiceCode, status: STATUS.SETTLED, invoice_date: rescheduleDate.format('YYYY-MM-DD'), + booking_date: rescheduleDate.format('YYYY-MM-DD'), created_at: moment().unix() * 1000, updated_at: moment().unix() * 1000, items, @@ -75,7 +49,7 @@ export class RescheduleManager { name: transactionData.customer_name, phone: transactionData.customer_phone, time: moment(transactionData.invoice_date).unix() * 1000, - code: data.code.toString(), + code: transactionData.invoice_code, }); return transactionData; diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index db38439..fcaebc9 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -1,4 +1,12 @@ -import { Body, Controller, Get, Param, Post, Res } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Res, + UnprocessableEntityException, +} from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Public } from 'src/core/guards'; import { TransactionDto } from './dto/booking-order.dto'; @@ -16,6 +24,8 @@ import { } from './dto/reschedule.dto'; import { RescheduleVerificationManager } from '../domain/usecases/managers/reschedule-verification.manager'; import { RescheduleManager } from '../domain/usecases/managers/reschedule.manager'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import * as moment from 'moment'; @ApiTags('Booking Order') @Controller('v1/booking') @@ -60,6 +70,38 @@ export class BookingOrderController { @Post('reschedule') async reschedule(@Body() data: RescheduleRequestDTO) { + const transaction = await this.serviceData.getTransactionWithReschedule( + data.booking_id, + ); + + const today = moment().startOf('day'); + const rescheduleDate = moment(data.reschedule_date, 'DD-MM-YYYY'); + const rescheduleDateStartOfDay = rescheduleDate.startOf('day'); + + //TODO: validate session period priority + + if (rescheduleDateStartOfDay.isSameOrBefore(today)) { + throw new UnprocessableEntityException( + 'Reschedule date must be in the future', + ); + } + + if (!transaction) { + throw new UnprocessableEntityException('Transaction not found'); + } + + if (transaction.status !== STATUS.SETTLED) { + throw new UnprocessableEntityException('Transaction is not settled'); + } + + if (transaction.children_transactions.length > 0) { + throw new UnprocessableEntityException('Transaction already rescheduled'); + } + + if (transaction.parent_id) { + throw new UnprocessableEntityException('Transaction is a reschedule'); + } + const result = await this.rescheduleVerification.saveVerification(data); const maskedPhoneNumber = result.phone_number.replace(/.(?=.{4})/g, '*'); result.phone_number = maskedPhoneNumber; diff --git a/src/modules/transaction/transaction/data/services/transaction-data.service.ts b/src/modules/transaction/transaction/data/services/transaction-data.service.ts index 40b8f6a..827be1c 100644 --- a/src/modules/transaction/transaction/data/services/transaction-data.service.ts +++ b/src/modules/transaction/transaction/data/services/transaction-data.service.ts @@ -13,4 +13,11 @@ export class TransactionDataService extends BaseDataService { ) { super(repo); } + + async getTransactionWithReschedule(booking_id: string) { + return this.repo.findOne({ + relations: ['children_transactions', 'items'], + where: { id: booking_id }, + }); + } } -- 2.40.1 From b476c92b7016cfc46b5e53b83872c9619f0fd47d Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 11:04:41 +0700 Subject: [PATCH 47/83] feat: add time group information to detail booking --- .../order/infrastructure/order.controller.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index fcaebc9..442ccd6 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -131,7 +131,7 @@ export class BookingOrderController { @Get(':id') async get(@Param('id') transactionId: string) { const data = await this.serviceData.getOneByOptions({ - relations: ['items'], + relations: ['items', 'items.item', 'items.item.time_group'], where: { id: transactionId }, }); @@ -146,6 +146,9 @@ export class BookingOrderController { } = data; const usageItems = items.map((item) => { + const itemData = item.item; + const timeGroupData = itemData.time_group; + const { id: groupId, name, start_time, end_time } = timeGroupData; const { id, item_id, @@ -167,6 +170,12 @@ export class BookingOrderController { total_net_price, qty, qty_remaining, + time_group: { + id: groupId, + name, + start_time, + end_time, + }, }; }); -- 2.40.1 From 9bbd37ba389b6f8156b0603159530577ca9a8e2d Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 13:03:48 +0700 Subject: [PATCH 48/83] refactor: update time group handling in booking order controller --- .../order/infrastructure/item.controller.ts | 2 +- .../order/infrastructure/order.controller.ts | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/modules/booking-online/order/infrastructure/item.controller.ts b/src/modules/booking-online/order/infrastructure/item.controller.ts index 061edcf..166818e 100644 --- a/src/modules/booking-online/order/infrastructure/item.controller.ts +++ b/src/modules/booking-online/order/infrastructure/item.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Query } from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiTags } from '@nestjs/swagger'; import { Public } from 'src/core/guards'; import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 442ccd6..5ebf5a5 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -145,10 +145,18 @@ export class BookingOrderController { items, } = data; + let timeGroup = null; + const usageItems = items.map((item) => { const itemData = item.item; const timeGroupData = itemData.time_group; const { id: groupId, name, start_time, end_time } = timeGroupData; + timeGroup = { + id: groupId, + name, + start_time, + end_time, + }; const { id, item_id, @@ -170,12 +178,6 @@ export class BookingOrderController { total_net_price, qty, qty_remaining, - time_group: { - id: groupId, - name, - start_time, - end_time, - }, }; }); @@ -194,6 +196,7 @@ export class BookingOrderController { status, id, items: usageItems, + time_group: timeGroup, }; } -- 2.40.1 From 7ff0040f9e6c1d23496a857fb9ed428c412e2caf Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 13:04:32 +0700 Subject: [PATCH 49/83] refactor: enhance time group data structure in booking order controller --- .../order/infrastructure/order.controller.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 5ebf5a5..c812924 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -150,12 +150,19 @@ export class BookingOrderController { const usageItems = items.map((item) => { const itemData = item.item; const timeGroupData = itemData.time_group; - const { id: groupId, name, start_time, end_time } = timeGroupData; + const { + id: groupId, + name, + start_time, + end_time, + max_usage_time, + } = timeGroupData; timeGroup = { id: groupId, name, start_time, end_time, + max_usage_time, }; const { id, -- 2.40.1 From 7c45a208660b49760c3c727a7cce0cb07800fdf6 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:56:43 +0700 Subject: [PATCH 50/83] feat: add feature basic auth request OTP --- .../data/services/otp-verification.service.ts | 81 ++++++++++++++++--- .../infrastructure/guards/otp-auth-guard.ts | 80 ++++++++++++++++++ .../otp-verification-data.controller.ts | 21 +++-- .../otp-verification.module.ts | 14 +++- src/services/whatsapp/whatsapp.service.ts | 16 ++++ 5 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts 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 148325e..e4d87ba 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 @@ -37,14 +37,69 @@ export class OtpVerificationService { return moment().valueOf(); // epoch millis verification time (now) } - async requestOTP(payload: OtpRequestEntity) { + 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 + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + + return { + name: 'general_flow', + language: { code: 'id' }, + components: [ + { + type: 'header', + parameters: [ + { + type: 'text', + parameter_name: 'header', + text: header, + }, + ], + }, + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'name', + text: username, + }, + { + type: 'text', + parameter_name: 'code', + text: otpCode, + }, + { + type: 'text', + parameter_name: 'type', + text: otpType, + }, + ], + }, + { + type: 'footer', + parameters: [ + { + type: 'text', + text: 'Kode berlaku selama 5 menit.', + }, + ], + }, + ], + }; + } + + async requestOTP(payload: OtpRequestEntity, req: any) { 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 userRequest = req?.user; const newOtp: OtpVerificationEntity = { otp_code: otpCode, @@ -56,13 +111,13 @@ export class OtpVerificationService { is_replaced: false, expired_at: expiredAt, - creator_id: creator.id, - creator_name: creator.name, + creator_id: userRequest?.id, + creator_name: userRequest?.name, created_at: dateNow, verified_at: null, - editor_id: creator.id, - editor_name: creator.name, + editor_id: userRequest?.id, + editor_name: userRequest?.name, updated_at: dateNow, }; @@ -95,9 +150,9 @@ export class OtpVerificationService { const notificationService = new WhatsappService(); verifiers.map((v) => { - notificationService.sendOtpNotification({ + notificationService.sendTemplateMessage({ phone: v.phone_number, - code: otpCode, + templateMsg: this.generateOTPMsgTemplate({ userRequest, newOtp }), }); }); @@ -108,7 +163,8 @@ export class OtpVerificationService { }; } - async verifyOTP(payload: OtpVerifyEntity) { + async verifyOTP(payload: OtpVerifyEntity, req: any) { + const userRequest = req?.user; const { otp_code, action_type, target_id, reference, source } = payload; const dateNow = this.generateTimestamp(); @@ -154,6 +210,9 @@ export class OtpVerificationService { otp.is_used = true; otp.verified_at = dateNow; + otp.editor_id = userRequest?.id; + otp.editor_name = userRequest?.name; + otp.updated_at = dateNow; // update otp to database await this.otpVerificationRepo.save(otp); diff --git a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts new file mode 100644 index 0000000..2312e74 --- /dev/null +++ b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts @@ -0,0 +1,80 @@ +// auth/otp-auth.guard.ts +import { + CanActivate, + ExecutionContext, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { validatePassword } from 'src/core/helpers/password/bcrypt.helpers'; +import { + CONNECTION_NAME, + STATUS, +} from 'src/core/strings/constants/base.constants'; +import { UserRole } from 'src/modules/user-related/user/constants'; +import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; +import { DataSource, Not } from 'typeorm'; + +@Injectable() +export class OtpAuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + + @InjectDataSource(CONNECTION_NAME.DEFAULT) + protected readonly dataSource: DataSource, + ) {} + + get userRepository() { + return this.dataSource.getRepository(UserModel); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const jwtAuth = request.headers['authorization']; + const basicAuth = request.headers['basic_authorization']; + + // 1. Cek OTP Auth (basic_authorization header) + if (basicAuth) { + try { + const decoded = Buffer.from(basicAuth, 'base64').toString('ascii'); + const [username, password] = decoded.split('|'); + + const userLogin = await this.userRepository.findOne({ + where: { + username: username, + status: STATUS.ACTIVE, + role: Not(UserRole.QUEUE_ADMIN), + }, + }); + + const valid = await validatePassword(password, userLogin?.password); + + if (userLogin && valid) { + request.user = userLogin; + return true; + } else { + throw new UnprocessableEntityException('Invalid OTP credentials'); + } + } catch (err) { + throw new UnprocessableEntityException('Invalid OTP encoding'); + } + } + + // 2. Cek JWT (Authorization: Bearer ) + if (jwtAuth && jwtAuth.startsWith('Bearer ')) { + const token = jwtAuth.split(' ')[1]; + try { + const payload = await this.jwtService.verifyAsync(token); + request.user = payload; + return true; + } catch (err) { + throw new UnprocessableEntityException('Invalid JWT token'); + } + } + + throw new UnprocessableEntityException( + 'No valid authentication method found', + ); + } +} 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 783f109..bd249ac 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 @@ -1,9 +1,18 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Req, + UseGuards, +} 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'; +import { OtpAuthGuard } from './guards/otp-auth-guard'; //TODO implementation auth @ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`) @@ -15,13 +24,15 @@ export class OtpVerificationController { ) {} @Post('request') - async request(@Body() body: OtpRequestDto) { - return await this.otpVerificationService.requestOTP(body); + @UseGuards(OtpAuthGuard) + async request(@Body() body: OtpRequestDto, @Req() req) { + return await this.otpVerificationService.requestOTP(body, req); } @Post('verify') - async verify(@Body() body: OtpVerifyDto) { - return await this.otpVerificationService.verifyOTP(body); + @UseGuards(OtpAuthGuard) + async verify(@Body() body: OtpVerifyDto, @Req() req) { + return await this.otpVerificationService.verifyOTP(body, req); } @Get(':ref_or_target_id') diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index 6e1f02d..1407b71 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -7,15 +7,27 @@ 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'; +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'; + @Module({ imports: [ ConfigModule.forRoot(), + TypeOrmModule.forFeature( [OtpVerificationModel, OtpVerifierModel], CONNECTION_NAME.DEFAULT, ), + + JwtModule.register({ + secret: JWT_SECRET, + signOptions: { expiresIn: JWT_EXPIRED }, + }), ], controllers: [OtpVerificationController], - providers: [OtpVerificationService], + providers: [OtpAuthGuard, OtpVerificationService], }) export class OtpVerificationModule {} diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index cad1b36..fb07a2a 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -243,6 +243,22 @@ export class WhatsappService { } } + async sendTemplateMessage(data: { phone: string; templateMsg: any }) { + const payload = { + messaging_product: 'whatsapp', + to: data.phone, + type: 'template', + template: data?.templateMsg, + }; + + const response = await this.sendMessage(payload); + if (response) { + Logger.log( + `OTP notification for template ${data.templateMsg} sent to ${data.phone}`, + ); + } + } + async queueProcess(data: WhatsappQueue) { const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`; const payload = { -- 2.40.1 From d8fa72ba20851447a563618ca9cafa283d58f465 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 15:41:07 +0700 Subject: [PATCH 51/83] feat: implement BookingItemManager for enhanced item booking functionality --- .../usecases/managers/booking-item.manager.ts | 66 +++++++++++++++++++ .../order/infrastructure/item.controller.ts | 4 +- .../booking-online/order/order.module.ts | 2 + .../item/domain/entities/item.entity.ts | 3 + .../usecases/managers/index-item.manager.ts | 2 +- 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts diff --git a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts new file mode 100644 index 0000000..b392b78 --- /dev/null +++ b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { RelationParam } from 'src/core/modules/domain/entities/base-filter.entity'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +import { ItemEntity } from 'src/modules/item-related/item/domain/entities/item.entity'; +import { IndexItemManager } from 'src/modules/item-related/item/domain/usecases/managers/index-item.manager'; +import { SelectQueryBuilder } from 'typeorm'; + +@Injectable() +export class BookingItemManager extends IndexItemManager { + get relations(): RelationParam { + return { + // relation only join (for query purpose) + joinRelations: [], + + // relation join and select (relasi yang ingin ditampilkan), + selectRelations: [ + 'item_category', + 'bundling_items', + 'tenant', + 'time_group', + 'item_rates', + ], + + // relation yang hanya ingin dihitung (akan return number) + countRelations: [], + }; + } + + get selects(): string[] { + const parent = super.selects; + return [ + ...parent, + 'item_rates.id', + 'item_rates.price', + 'item_rates.season_period_id', + ]; + } + + getResult(): PaginationResponse { + const result = super.getResult(); + const { data, total } = result; + const hasRates = (this.filterParam.season_period_ids?.length ?? 0) > 0; + const items = data.map((item) => { + const { item_rates, ...rest } = item; + const rate = item_rates?.[0]?.['price'] ?? rest.base_price; + return { + ...rest, + base_price: hasRates ? rate : rest.base_price, + }; + }); + return { total, data: items }; + } + + setQueryFilter( + queryBuilder: SelectQueryBuilder, + ): SelectQueryBuilder { + const query = super.setQueryFilter(queryBuilder); + + if (this.filterParam.season_period_ids) { + query.andWhere(`item_rates.season_period_id In (:...seasonIds)`, { + seasonIds: this.filterParam.season_period_ids, + }); + } + return query; + } +} diff --git a/src/modules/booking-online/order/infrastructure/item.controller.ts b/src/modules/booking-online/order/infrastructure/item.controller.ts index 166818e..5e15333 100644 --- a/src/modules/booking-online/order/infrastructure/item.controller.ts +++ b/src/modules/booking-online/order/infrastructure/item.controller.ts @@ -5,15 +5,15 @@ import { PaginationResponse } from 'src/core/response/domain/ok-response.interfa import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { ItemReadService } from 'src/modules/item-related/item/data/services/item-read.service'; import { ItemEntity } from 'src/modules/item-related/item/domain/entities/item.entity'; -import { IndexItemManager } from 'src/modules/item-related/item/domain/usecases/managers/index-item.manager'; import { FilterItemDto } from 'src/modules/item-related/item/infrastructure/dto/filter-item.dto'; +import { BookingItemManager } from '../domain/usecases/managers/booking-item.manager'; @ApiTags('Booking Item') @Controller('v1/booking-item') @Public(true) export class ItemController { constructor( - private indexManager: IndexItemManager, + private indexManager: BookingItemManager, private serviceData: ItemReadService, ) {} diff --git a/src/modules/booking-online/order/order.module.ts b/src/modules/booking-online/order/order.module.ts index d337988..b0aea9e 100644 --- a/src/modules/booking-online/order/order.module.ts +++ b/src/modules/booking-online/order/order.module.ts @@ -14,6 +14,7 @@ import { CqrsModule } from '@nestjs/cqrs'; import { RescheduleVerificationModel } from './data/models/reschedule-verification.model'; import { RescheduleVerificationManager } from './domain/usecases/managers/reschedule-verification.manager'; import { RescheduleManager } from './domain/usecases/managers/reschedule.manager'; +import { BookingItemManager } from './domain/usecases/managers/booking-item.manager'; @Module({ imports: [ ConfigModule.forRoot(), @@ -31,6 +32,7 @@ import { RescheduleManager } from './domain/usecases/managers/reschedule.manager CreateBookingManager, RescheduleVerificationManager, RescheduleManager, + BookingItemManager, ], }) export class BookingOrderModule {} diff --git a/src/modules/item-related/item/domain/entities/item.entity.ts b/src/modules/item-related/item/domain/entities/item.entity.ts index 8d4a0a7..e8a158c 100644 --- a/src/modules/item-related/item/domain/entities/item.entity.ts +++ b/src/modules/item-related/item/domain/entities/item.entity.ts @@ -1,6 +1,7 @@ import { BaseStatusEntity } from 'src/core/modules/domain/entities/base-status.entity'; import { ItemType } from 'src/modules/item-related/item-category/constants'; import { LimitType } from '../../constants'; +import { ItemRateEntity } from 'src/modules/item-related/item-rate/domain/entities/item-rate.entity'; export interface ItemEntity extends BaseStatusEntity { name: string; @@ -19,4 +20,6 @@ export interface ItemEntity extends BaseStatusEntity { show_to_booking: boolean; breakdown_bundling?: boolean; booking_description?: string; + + item_rates?: ItemRateEntity[] | any[]; } diff --git a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts index f32ac74..6277d2b 100644 --- a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts @@ -110,7 +110,7 @@ export class IndexItemManager extends BaseIndexManager { if (this.filterParam.time_group_ids?.length) { queryBuilder.andWhere( - `${this.tableName}.time_group_id In (:...timeGroupIds) OR ${this.tableName}.time_group_id Is Null`, + `(${this.tableName}.time_group_id In (:...timeGroupIds) OR ${this.tableName}.time_group_id Is Null)`, { timeGroupIds: this.filterParam.time_group_ids, }, -- 2.40.1 From f0e8fbddc9e3fdc939164ad6dfe1b0c710e8a9a6 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 15:46:43 +0700 Subject: [PATCH 52/83] feat: include parent transaction details in booking order retrieval --- .../order/infrastructure/order.controller.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index c812924..e5d24cb 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -131,11 +131,17 @@ export class BookingOrderController { @Get(':id') async get(@Param('id') transactionId: string) { const data = await this.serviceData.getOneByOptions({ - relations: ['items', 'items.item', 'items.item.time_group'], + relations: [ + 'items', + 'parent_transaction', + 'items.item', + 'items.item.time_group', + ], where: { id: transactionId }, }); const { + parent_id, customer_name, customer_phone, booking_date, @@ -143,6 +149,7 @@ export class BookingOrderController { status, id, items, + parent_transaction, } = data; let timeGroup = null; @@ -195,6 +202,20 @@ export class BookingOrderController { maskedCustomerPhone = '*'.repeat(customer_phone.length - 4) + last4; } + let parentTransaction = undefined; + if (parent_transaction) { + const { + id: parentId, + invoice_code: parentInvoiceCode, + invoice_date: parentInvoiceDate, + } = parent_transaction; + parentTransaction = { + id: parentId, + invoice_code: parentInvoiceCode, + invoice_date: parentInvoiceDate, + }; + } + return { customer_name, customer_phone: maskedCustomerPhone, @@ -202,8 +223,10 @@ export class BookingOrderController { invoice_code, status, id, + is_reschedule: !!parent_id, items: usageItems, time_group: timeGroup, + parent: parentTransaction, }; } -- 2.40.1 From 3a0efb00a93eda8123e5b78e8696efbf8bfe5c53 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:55:21 +0700 Subject: [PATCH 53/83] feat: update validation otp cancel reconciliation --- .../reconciliation/infrastructure/dto/cancel-top-dto.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts b/src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts index 042a863..63cc18f 100644 --- a/src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts +++ b/src/modules/transaction/reconciliation/infrastructure/dto/cancel-top-dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { IsString, ValidateIf } from 'class-validator'; export class OtpVerifyDto { @ApiProperty({ @@ -9,6 +9,6 @@ export class OtpVerifyDto { example: '2345', }) @IsString() - @IsNotEmpty() + @ValidateIf((body) => body.otp_code) otp_code: string; } -- 2.40.1 From 34d8882ec3e89c22e38fa53562a38cf376c4a59c Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 16:31:15 +0700 Subject: [PATCH 54/83] feat: make update price public --- .../item-related/item/infrastructure/item-data.controller.ts | 1 + 1 file changed, 1 insertion(+) 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 f416665..fefb105 100644 --- a/src/modules/item-related/item/infrastructure/item-data.controller.ts +++ b/src/modules/item-related/item/infrastructure/item-data.controller.ts @@ -29,6 +29,7 @@ export class ItemDataController { return await this.orchestrator.create(data); } + @Public(true) @Post('update-price') async updatePrice(@Body() body: UpdateItemPriceDto): Promise { return await this.orchestrator.updatePrice(body); -- 2.40.1 From e1d3ee7188970e35adfef8b16af922a232c5c8a5 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:52:18 +0700 Subject: [PATCH 55/83] feat: update validation otp cancel reconciliation --- .../otp-verification/infrastructure/guards/otp-auth-guard.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts index 2312e74..b63a673 100644 --- a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts +++ b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts @@ -34,6 +34,7 @@ export class OtpAuthGuard implements CanActivate { const jwtAuth = request.headers['authorization']; const basicAuth = request.headers['basic_authorization']; + console.log({ jwtAuth, basicAuth }); // 1. Cek OTP Auth (basic_authorization header) if (basicAuth) { try { -- 2.40.1 From 7dd29c2a702455fb6ca5e495828a5415ea364c34 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:15:09 +0700 Subject: [PATCH 56/83] feat: rename header key basic auth --- .../otp-verification/infrastructure/guards/otp-auth-guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts index b63a673..a8e8cb6 100644 --- a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts +++ b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts @@ -32,7 +32,7 @@ export class OtpAuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const jwtAuth = request.headers['authorization']; - const basicAuth = request.headers['basic_authorization']; + const basicAuth = request.headers['x-basic-authorization']; console.log({ jwtAuth, basicAuth }); // 1. Cek OTP Auth (basic_authorization header) -- 2.40.1 From 79c9139c3cfa0f3b44b30c199f7ae0cb99de9c56 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:28:21 +0700 Subject: [PATCH 57/83] feat: rename header key basic auth --- .../otp-verification/infrastructure/guards/otp-auth-guard.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts index a8e8cb6..1af5e2a 100644 --- a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts +++ b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts @@ -34,7 +34,6 @@ export class OtpAuthGuard implements CanActivate { const jwtAuth = request.headers['authorization']; const basicAuth = request.headers['x-basic-authorization']; - console.log({ jwtAuth, basicAuth }); // 1. Cek OTP Auth (basic_authorization header) if (basicAuth) { try { -- 2.40.1 From 3116acd5ab814669aff55ee695faf9e59e5448dc Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 19:02:10 +0700 Subject: [PATCH 58/83] feat: add image_url to booking item selection in BookingItemManager --- .../order/domain/usecases/managers/booking-item.manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts index b392b78..416635a 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts @@ -30,6 +30,7 @@ export class BookingItemManager extends IndexItemManager { const parent = super.selects; return [ ...parent, + `${this.tableName}.image_url`, 'item_rates.id', 'item_rates.price', 'item_rates.season_period_id', -- 2.40.1 From 56e475d61f196f879fe0836aeaa782390d41f140 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 11 Jun 2025 19:03:09 +0700 Subject: [PATCH 59/83] fix: ensure time_group data is only accessed if it exists in BookingOrderController --- .../order/infrastructure/order.controller.ts | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index e5d24cb..01edcae 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -156,21 +156,23 @@ export class BookingOrderController { const usageItems = items.map((item) => { const itemData = item.item; - const timeGroupData = itemData.time_group; - const { - id: groupId, - name, - start_time, - end_time, - max_usage_time, - } = timeGroupData; - timeGroup = { - id: groupId, - name, - start_time, - end_time, - max_usage_time, - }; + if (itemData.time_group) { + const timeGroupData = itemData.time_group; + const { + id: groupId, + name, + start_time, + end_time, + max_usage_time, + } = timeGroupData; + timeGroup = { + id: groupId, + name, + start_time, + end_time, + max_usage_time, + }; + } const { id, item_id, -- 2.40.1 From d6717c9c603d52156a000985091e409d661c48e4 Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 16 Jun 2025 09:26:09 +0700 Subject: [PATCH 60/83] feat: add resend notification endpoint in BookingOrderController to send WhatsApp notifications --- .../order/infrastructure/order.controller.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/modules/booking-online/order/infrastructure/order.controller.ts b/src/modules/booking-online/order/infrastructure/order.controller.ts index 01edcae..1139260 100644 --- a/src/modules/booking-online/order/infrastructure/order.controller.ts +++ b/src/modules/booking-online/order/infrastructure/order.controller.ts @@ -26,6 +26,7 @@ import { RescheduleVerificationManager } from '../domain/usecases/managers/resch import { RescheduleManager } from '../domain/usecases/managers/reschedule.manager'; import { STATUS } from 'src/core/strings/constants/base.constants'; import * as moment from 'moment'; +import { WhatsappService } from 'src/services/whatsapp/whatsapp.service'; @ApiTags('Booking Order') @Controller('v1/booking') @@ -243,4 +244,31 @@ export class BookingOrderController { const buffer = Buffer.from(base64Data, 'base64'); res.send(buffer); } + + @Post('resend-notification/:id') + async resendNotification(@Param('id') id: string) { + try { + const transaction = await this.serviceData.getOneByOptions({ + where: { id }, + }); + + const whatsappService = new WhatsappService(); + const formattedDate = moment(transaction.booking_date); + const payload = { + id: transaction.id, + phone: transaction.customer_phone, + code: transaction.invoice_code, + name: transaction.customer_name, + time: formattedDate.valueOf(), + }; + await whatsappService.bookingCreated(payload); + return { + message: 'Notification sent successfully', + }; + } catch (error) { + throw new UnprocessableEntityException({ + message: 'Failed to send notification', + }); + } + } } -- 2.40.1 From 47d45cb65c946c1781a94d7b7e8c9ce3526f1ebc Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 16 Jun 2025 11:27:12 +0700 Subject: [PATCH 61/83] feat: integrate CouchDB module across various services and add transaction summary endpoint --- .../google-calendar/google-calendar.module.ts | 3 +- src/modules/configuration/mail/mail.module.ts | 3 +- src/modules/queue/queue.module.ts | 3 +- .../reconciliation/reconciliation.module.ts | 4 +- .../transaction/refund/refund.module.ts | 3 +- .../data/services/transaction-data.service.ts | 12 +++++ .../data/services/transaction-read.service.ts | 48 +++++++++++++++++++ .../usecases/transaction-data.orchestrator.ts | 6 +++ .../usecases/transaction-read.orchestrator.ts | 23 ++++++++- .../transaction-data.controller.ts | 30 +++++++++++- .../transaction-read.controller.ts | 6 +++ .../transaction/transaction.module.ts | 7 +++ 12 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/modules/configuration/google-calendar/google-calendar.module.ts b/src/modules/configuration/google-calendar/google-calendar.module.ts index 24fde4b..75a10a2 100644 --- a/src/modules/configuration/google-calendar/google-calendar.module.ts +++ b/src/modules/configuration/google-calendar/google-calendar.module.ts @@ -10,7 +10,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model'; import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/models/transaction-tax.model'; - +import { CouchModule } from 'src/modules/configuration/couch/couch.module'; @Module({ imports: [ ConfigModule.forRoot(), @@ -19,6 +19,7 @@ import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/mo CONNECTION_NAME.DEFAULT, ), CqrsModule, + CouchModule, ], controllers: [GoogleCalendarController], providers: [ diff --git a/src/modules/configuration/mail/mail.module.ts b/src/modules/configuration/mail/mail.module.ts index 412b0c3..ab13a2e 100644 --- a/src/modules/configuration/mail/mail.module.ts +++ b/src/modules/configuration/mail/mail.module.ts @@ -10,7 +10,7 @@ import { TransactionDataService } from 'src/modules/transaction/transaction/data import { PaymentTransactionHandler } from './domain/handlers/payment-transaction.handler'; import { MailTemplateController } from './infrastructure/mail.controller'; import { PdfMakeManager } from '../export/domain/managers/pdf-make.manager'; - +import { CouchModule } from '../couch/couch.module'; @Module({ imports: [ ConfigModule.forRoot(), @@ -19,6 +19,7 @@ import { PdfMakeManager } from '../export/domain/managers/pdf-make.manager'; CONNECTION_NAME.DEFAULT, ), CqrsModule, + CouchModule, ], controllers: [MailTemplateController], providers: [ diff --git a/src/modules/queue/queue.module.ts b/src/modules/queue/queue.module.ts index b974dd3..172a800 100644 --- a/src/modules/queue/queue.module.ts +++ b/src/modules/queue/queue.module.ts @@ -39,7 +39,7 @@ import { ItemQueueModel } from '../item-related/item-queue/data/models/item-queu import { QueueTimeFormula } from './domain/usecases/formula/queue-time.formula'; import { QueueJobController } from './infrastructure/controllers/queue-job.controller'; import { GenerateQueueManager } from './domain/usecases/generate-queue.manager'; - +import { CouchModule } from 'src/modules/configuration/couch/couch.module'; @Module({ imports: [ ConfigModule.forRoot(), @@ -57,6 +57,7 @@ import { GenerateQueueManager } from './domain/usecases/generate-queue.manager'; CONNECTION_NAME.DEFAULT, ), CqrsModule, + CouchModule, ], controllers: [QueueController, QueueAdminController, QueueJobController], providers: [ diff --git a/src/modules/transaction/reconciliation/reconciliation.module.ts b/src/modules/transaction/reconciliation/reconciliation.module.ts index e47d163..17d27ef 100644 --- a/src/modules/transaction/reconciliation/reconciliation.module.ts +++ b/src/modules/transaction/reconciliation/reconciliation.module.ts @@ -19,13 +19,13 @@ import { BatchCancelReconciliationManager } from './domain/usecases/managers/bat import { BatchConfirmReconciliationManager } from './domain/usecases/managers/batch-confirm-reconciliation.manager'; import { RecapReconciliationManager } from './domain/usecases/managers/recap-reconciliation.manager'; import { RecapPosTransactionHandler } from './domain/usecases/handlers/recap-pos-transaction.handler'; -import { SalesPriceFormulaDataService } from '../sales-price-formula/data/services/sales-price-formula-data.service'; - +import { CouchModule } from 'src/modules/configuration/couch/couch.module'; @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forFeature([TransactionModel], CONNECTION_NAME.DEFAULT), CqrsModule, + CouchModule, ], controllers: [ReconciliationDataController, ReconciliationReadController], providers: [ diff --git a/src/modules/transaction/refund/refund.module.ts b/src/modules/transaction/refund/refund.module.ts index c45fc5a..922dd14 100644 --- a/src/modules/transaction/refund/refund.module.ts +++ b/src/modules/transaction/refund/refund.module.ts @@ -23,7 +23,7 @@ import { CancelRefundManager } from './domain/usecases/managers/cancel-refund.ma import { RefundItemModel } from './data/models/refund-item.model'; import { TransactionDataService } from '../transaction/data/services/transaction-data.service'; import { TransactionModel } from '../transaction/data/models/transaction.model'; - +import { CouchModule } from 'src/modules/configuration/couch/couch.module'; @Module({ imports: [ ConfigModule.forRoot(), @@ -32,6 +32,7 @@ import { TransactionModel } from '../transaction/data/models/transaction.model'; CONNECTION_NAME.DEFAULT, ), CqrsModule, + CouchModule, ], controllers: [RefundDataController, RefundReadController], providers: [ diff --git a/src/modules/transaction/transaction/data/services/transaction-data.service.ts b/src/modules/transaction/transaction/data/services/transaction-data.service.ts index 827be1c..70c8dc4 100644 --- a/src/modules/transaction/transaction/data/services/transaction-data.service.ts +++ b/src/modules/transaction/transaction/data/services/transaction-data.service.ts @@ -4,12 +4,14 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TransactionModel } from '../models/transaction.model'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; import { Repository } from 'typeorm'; +import { CouchService } from 'src/modules/configuration/couch/data/services/couch.service'; @Injectable() export class TransactionDataService extends BaseDataService { constructor( @InjectRepository(TransactionModel, CONNECTION_NAME.DEFAULT) private repo: Repository, + private couchService: CouchService, ) { super(repo); } @@ -20,4 +22,14 @@ export class TransactionDataService extends BaseDataService { where: { id: booking_id }, }); } + + async saveTransactionToCouch(transaction) { + const id = transaction.id ?? transaction._id; + const couchData = await this.couchService.getDoc(id, 'transaction'); + if (!couchData) { + await this.couchService.createDoc(transaction, 'transaction'); + } else { + await this.couchService.updateDoc(transaction, 'transaction'); + } + } } diff --git a/src/modules/transaction/transaction/data/services/transaction-read.service.ts b/src/modules/transaction/transaction/data/services/transaction-read.service.ts index 7efd6bf..47aec59 100644 --- a/src/modules/transaction/transaction/data/services/transaction-read.service.ts +++ b/src/modules/transaction/transaction/data/services/transaction-read.service.ts @@ -30,4 +30,52 @@ export class TransactionReadService extends BaseReadService { return transactions; } + + async getSummary(posId: string, startDate: string) { + const query = `select payment_type_counter, payment_type_method_name, sum(payment_total) payment_total, sum(payment_total_pay) payment_total_pay + from transactions t + where 1=1 + and t.creator_counter_no IN (${posId}) + and invoice_date = '${startDate}' + and status = 'settled' + group by payment_type_counter, payment_type_method_name;`; + const transactions = await this.repo.query(query); + + const qtyQuery = `select ti.item_name, sum(ti.qty) total_qty, count(ti.item_name), sum(ti.qty), string_agg(distinct ti.item_price::text, '') price, + sum(payment_total) payment_total, sum(payment_total_pay) payment_total_pay + from transactions t + inner join transaction_items ti on t.id = ti.transaction_id + where t.creator_counter_no IN (${posId}) + and invoice_date = '${startDate}' + and t.status = 'settled' + group by ti.item_name`; + const qtyTransactions = await this.repo.query(qtyQuery); + + return { payment: transactions, qty: qtyTransactions }; + } + + async getLastTransactionByPos( + posId: string, + ): Promise { + const transaction = await this.repo.findOne({ + select: [ + 'id', + 'created_at', + 'updated_at', + 'status', + 'invoice_code', + 'creator_counter_no', + 'invoice_date', + 'payment_total', + ], + where: { + creator_counter_no: parseInt(posId), + }, + order: { + created_at: 'DESC', + }, + }); + + return transaction; + } } diff --git a/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts b/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts index c717131..0a6e113 100644 --- a/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts +++ b/src/modules/transaction/transaction/domain/usecases/transaction-data.orchestrator.ts @@ -141,4 +141,10 @@ export class TransactionDataOrchestrator { await this.batchConfirmDataManager.execute(); return this.batchConfirmDataManager.getResult(); } + + async saveTransactionToCouch(transaction: any[]) { + for (const t of transaction) { + await this.serviceData.saveTransactionToCouch(t); + } + } } diff --git a/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts b/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts index cbc1912..b212bbd 100644 --- a/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts +++ b/src/modules/transaction/transaction/domain/usecases/transaction-read.orchestrator.ts @@ -7,6 +7,8 @@ import { BaseReadOrchestrator } from 'src/core/modules/domain/usecase/orchestrat import { DetailTransactionManager } from './managers/detail-transaction.manager'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { PriceCalculator } from './calculator/price.calculator'; +import { In } from 'typeorm'; +import * as moment from 'moment'; @Injectable() export class TransactionReadOrchestrator extends BaseReadOrchestrator { @@ -26,6 +28,16 @@ export class TransactionReadOrchestrator extends BaseReadOrchestrator { this.detailManager.setData(dataId); this.detailManager.setService(this.serviceData, TABLE_NAME.TRANSACTION); @@ -50,17 +62,24 @@ export class TransactionReadOrchestrator extends BaseReadOrchestrator { const transactions = await this.serviceData.getManyByOptions({ where: { - is_recap_transaction: false, + // is_recap_transaction: false, + id: In(this.ids), }, relations: ['items', 'items.bundling_items'], }); + console.log(transactions.length); + // return; + for (const transaction of transactions) { try { const price = await this.calculator.calculate(transaction); diff --git a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts index d9c7269..e51690d 100644 --- a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts +++ b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Delete, @@ -7,17 +8,19 @@ import { Post, Put, Res, + UseGuards, } from '@nestjs/common'; import { Response } from 'express'; import { TransactionDataOrchestrator } from '../domain/usecases/transaction-data.orchestrator'; import { TransactionDto } from './dto/transaction.dto'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiTags } from '@nestjs/swagger'; import { TransactionEntity } from '../domain/entities/transaction.entity'; import { BatchResult } from 'src/core/response/domain/ok-response.interface'; import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto'; import { Public } from 'src/core/guards'; import { DownloadPdfDto } from './dto/donwload-pdf.dto'; +import { OtpAuthGuard } from 'src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard'; @ApiTags(`${MODULE_NAME.TRANSACTION.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.TRANSACTION}`) @@ -91,4 +94,29 @@ export class TransactionDataController { async delete(@Param('id') dataId: string): Promise { return await this.orchestrator.delete(dataId); } + + @Post('save-to-couch') + @ApiBody({ + schema: { + type: 'array', + items: { + type: 'object', + }, + }, + }) + @Public(true) + @UseGuards(OtpAuthGuard) + async saveToCouch(@Body() body: any[]) { + try { + await this.orchestrator.saveTransactionToCouch(body); + return { + message: 'Success', + }; + } catch (error) { + throw new BadRequestException({ + message: error.message, + error: error.stack, + }); + } + } } diff --git a/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts b/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts index 8446857..2a4b499 100644 --- a/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts +++ b/src/modules/transaction/transaction/infrastructure/transaction-read.controller.ts @@ -28,6 +28,12 @@ export class TransactionReadController { return await this.orchestrator.detail(id); } + @Public(true) + @Get('summary/:posId') + async summary(@Param('posId') posId: string) { + return await this.orchestrator.summary(posId); + } + @Public(true) @Get('dummy/:id') async calculate(@Param('id') id: string): Promise { diff --git a/src/modules/transaction/transaction/transaction.module.ts b/src/modules/transaction/transaction/transaction.module.ts index af14bef..66f58d9 100644 --- a/src/modules/transaction/transaction/transaction.module.ts +++ b/src/modules/transaction/transaction/transaction.module.ts @@ -47,6 +47,9 @@ import { TransactionDemographyModel } from './data/models/transaction-demography import { PriceCalculator } from './domain/usecases/calculator/price.calculator'; import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; import { CouchModule } from 'src/modules/configuration/couch/couch.module'; +import { JWT_EXPIRED } from 'src/core/sessions/constants'; +import { JWT_SECRET } from 'src/core/sessions/constants'; +import { JwtModule } from '@nestjs/jwt'; @Module({ exports: [ @@ -56,6 +59,10 @@ import { CouchModule } from 'src/modules/configuration/couch/couch.module'; ], imports: [ ConfigModule.forRoot(), + JwtModule.register({ + secret: JWT_SECRET, + signOptions: { expiresIn: JWT_EXPIRED }, + }), TypeOrmModule.forFeature( [ TransactionModel, -- 2.40.1 From ec5229645f775b16b4b251b341172a065cb32834 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:48:56 +0700 Subject: [PATCH 62/83] feat(SPG-1234): add action_type at verifier --- .../strings/constants/module.constants.ts | 2 + ...ion_type_and_update_column_otp_verifier.ts | 55 +++++++++++++++++ .../data/models/otp-verifier.model.ts | 11 +++- .../data/services/otp-verification.service.ts | 43 +++++++++++--- .../data/services/otp-verifier.service.ts | 24 ++++++++ .../entities/otp-verification.entity.ts | 7 +++ .../dto/otp-verification.dto.ts | 59 ++++++++++++++++++- .../otp-verification-data.controller.ts | 26 ++++++-- .../otp-verification.module.ts | 10 +++- 9 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 src/database/migrations/1750045520332-add_enum_otp_action_type_and_update_column_otp_verifier.ts create mode 100644 src/modules/configuration/otp-verification/data/services/otp-verifier.service.ts 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..82903cb 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,11 @@ export enum OTP_ACTION_TYPE { CREATE_DISCOUNT = 'CREATE_DISCOUNT', CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', 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 { @@ -37,4 +42,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 {} -- 2.40.1 From af1ee2fbee392b3ff04cdd1e2643875aa9b48723 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:11:24 +0700 Subject: [PATCH 63/83] feat(SPG-1234): add action_type at verifier --- .../item-related/item/infrastructure/item-data.controller.ts | 4 ++++ .../infrastructure/season-period-data.controller.ts | 1 + .../user-related/user/infrastructure/user-data.controller.ts | 4 ++++ 3 files changed, 9 insertions(+) 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..8f87a94 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') + // TOD0 => simpan OTP update yang disikim dari request ini async active(@Param('id') dataId: string): Promise { return await this.orchestrator.active(dataId); } @Put('/batch-active') + // TOD0 => simpan OTP update yang disikim dari request ini async batchActive(@Body() body: BatchIdsDto): Promise { return await this.orchestrator.batchActive(body.ids); } @Patch(':id/confirm') + // TOD0 => 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') + // TOD0 => 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..e2b905a 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 + // TOD0 => 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..d0d3e39 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') + // TOD0 => simpan OTP update yang disikim dari request ini async active(@Param('id') dataId: string): Promise { return await this.orchestrator.active(dataId); } @Put('/batch-active') + // TOD0 => simpan OTP update yang disikim dari request ini async batchActive(@Body() body: BatchIdsDto): Promise { return await this.orchestrator.batchActive(body.ids); } @Patch(':id/confirm') + // TOD0 => simpan OTP update yang disikim dari request ini async confirm(@Param('id') dataId: string): Promise { return await this.orchestrator.confirm(dataId); } @Put('/batch-confirm') + // TOD0 => simpan OTP update yang disikim dari request ini async batchConfirm(@Body() body: BatchIdsDto): Promise { return await this.orchestrator.batchConfirm(body.ids); } -- 2.40.1 From ccc363f74f568f15d6b67c0f1b69e725e0129a05 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:15:20 +0700 Subject: [PATCH 64/83] feat(SPG-1234): add action_type at verifier --- .../domain/entities/otp-verification.entity.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 82903cb..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,10 +4,13 @@ export enum OTP_ACTION_TYPE { CREATE_DISCOUNT = 'CREATE_DISCOUNT', CANCEL_TRANSACTION = 'CANCEL_TRANSACTION', REJECT_RECONCILIATION = 'REJECT_RECONCILIATION', - ACTIVATE_ITEM = 'ACTIVATE_ITEM', + ACTIVATE_USER = 'ACTIVATE_USER', + + ACTIVATE_ITEM = 'ACTIVATE_ITEM', UPDATE_ITEM_PRICE = 'UPDATE_ITEM_PRICE', UPDATE_ITEM_DETAILS = 'UPDATE_ITEM_DETAILS', + CONFIRM_TRANSACTION = 'CONFIRM_TRANSACTION', } -- 2.40.1 From 1d9cdfe8e6c66338f0761f40f11c6d434664c224 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:20:47 +0700 Subject: [PATCH 65/83] feat(SPG-1234): add action_type at verifier --- .../item/infrastructure/item-data.controller.ts | 8 ++++---- .../infrastructure/season-period-data.controller.ts | 2 +- .../user/infrastructure/user-data.controller.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) 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 8f87a94..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,19 +41,19 @@ export class ItemDataController { } @Patch(':id/active') - // TOD0 => simpan OTP update yang disikim dari request ini + // 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') - // TOD0 => simpan OTP update yang disikim dari request ini + // 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') - // TOD0 => simpan OTP update yang disikim dari request ini + // TODO => simpan OTP update yang disikim dari request ini async confirm(@Param('id') dataId: string): Promise { return await this.orchestrator.confirm(dataId); } @@ -74,7 +74,7 @@ export class ItemDataController { } @Put(':id') - // TOD0 => simpan OTP update yang disikim dari request ini + // 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 e2b905a..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,7 +80,7 @@ export class SeasonPeriodDataController { } // pemisahan update data dengan update items dikarenakan payload (based on tampilan) berbeda - // TOD0 => simpan OTP update yang disikim dari request ini + // 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 d0d3e39..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,25 +36,25 @@ export class UserDataController { } @Patch(':id/active') - // TOD0 => simpan OTP update yang disikim dari request ini + // 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') - // TOD0 => simpan OTP update yang disikim dari request ini + // 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') - // TOD0 => simpan OTP update yang disikim dari request ini + // 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') - // TOD0 => simpan OTP update yang disikim dari request ini + // TODO => simpan OTP update yang disikim dari request ini async batchConfirm(@Body() body: BatchIdsDto): Promise { return await this.orchestrator.batchConfirm(body.ids); } -- 2.40.1 From 01fb35887591c45cefafb4c4b0f0a6b033bd2c8c Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 17 Jun 2025 14:26:40 +0700 Subject: [PATCH 66/83] feat: enhance transaction read service to include total sales calculation and item ID in quantity query --- .../data/services/transaction-read.service.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/modules/transaction/transaction/data/services/transaction-read.service.ts b/src/modules/transaction/transaction/data/services/transaction-read.service.ts index 47aec59..eefa87a 100644 --- a/src/modules/transaction/transaction/data/services/transaction-read.service.ts +++ b/src/modules/transaction/transaction/data/services/transaction-read.service.ts @@ -41,17 +41,29 @@ export class TransactionReadService extends BaseReadService { group by payment_type_counter, payment_type_method_name;`; const transactions = await this.repo.query(query); - const qtyQuery = `select ti.item_name, sum(ti.qty) total_qty, count(ti.item_name), sum(ti.qty), string_agg(distinct ti.item_price::text, '') price, + const qtyQuery = `select ti.item_id, ti.item_name, sum(ti.qty) total_qty, count(ti.item_name), sum(ti.qty), string_agg(distinct ti.item_price::text, '') price, sum(payment_total) payment_total, sum(payment_total_pay) payment_total_pay from transactions t inner join transaction_items ti on t.id = ti.transaction_id where t.creator_counter_no IN (${posId}) and invoice_date = '${startDate}' and t.status = 'settled' - group by ti.item_name`; + group by ti.item_name, ti.item_id`; const qtyTransactions = await this.repo.query(qtyQuery); - return { payment: transactions, qty: qtyTransactions }; + const totalSalesQuery = `select sum(payment_total) payment_total + from transactions t + where 1=1 + and t.creator_counter_no IN (${posId}) + and invoice_date = '${startDate}' + and status = 'settled'`; + const totalSales = await this.repo.query(totalSalesQuery); + + return { + payment: transactions, + qty: qtyTransactions, + totalSales: totalSales?.[0]?.payment_total ?? 0, + }; } async getLastTransactionByPos( -- 2.40.1 From afad02ba52b0facacbc7bc81c97cde5f00ed7ce0 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 17 Jun 2025 15:03:17 +0700 Subject: [PATCH 67/83] fix: temporarily disable OtpAuthGuard in saveToCouch method for transaction data controller --- .../transaction/infrastructure/transaction-data.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts index e51690d..97d3e30 100644 --- a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts +++ b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts @@ -105,7 +105,7 @@ export class TransactionDataController { }, }) @Public(true) - @UseGuards(OtpAuthGuard) + // @UseGuards(OtpAuthGuard) async saveToCouch(@Body() body: any[]) { try { await this.orchestrator.saveTransactionToCouch(body); -- 2.40.1 From a5da557dd942fdcbe3191282b0695ed56346d4fe Mon Sep 17 00:00:00 2001 From: shancheas Date: Thu, 19 Jun 2025 14:40:17 +0700 Subject: [PATCH 68/83] feat: implement booking expired notification in WhatsApp service --- .../midtrans-transaction-callback.handler.ts | 16 ++++++ src/services/whatsapp/whatsapp.service.ts | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts b/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts index 35f9214..c2cec81 100644 --- a/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts +++ b/src/modules/transaction/transaction/domain/usecases/handlers/midtrans-transaction-callback.handler.ts @@ -77,6 +77,22 @@ export class MidtransCallbackHandler await whatsappService.bookingCreated(payload); } + if ( + transaction.status === STATUS.EXPIRED && + transaction.type === TransactionType.ONLINE + ) { + const whatsappService = new WhatsappService(); + const formattedDate = moment(transaction.booking_date); + const payload = { + id: transaction.id, + phone: transaction.customer_phone, + code: transaction.invoice_code, + name: transaction.customer_name, + time: formattedDate.valueOf(), + }; + await whatsappService.bookingExpired(payload); + } + this.eventBus.publish( new TransactionChangeStatusEvent({ id: data_id, diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index 61785f9..85ec5e6 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -198,6 +198,57 @@ export class WhatsappService { ); } + async bookingExpired(data: WhatsappBookingCreate) { + const momentDate = moment(data.time); + const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); + // const dayOfWeek = momentDate.day(); + // const dayOfMonth = momentDate.date(); + // const year = momentDate.year(); + // const month = momentDate.month() + 1; + // const hour = momentDate.hour(); + // const minute = momentDate.minute(); + + const payload = { + messaging_product: 'whatsapp', + to: phoneNumberOnly(data.phone), // recipient's phone number + type: 'template', + template: { + name: 'booking_expired', + language: { + code: 'id', // language code + }, + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'customer', + text: data.name, // replace with name variable + }, + { + type: 'text', + parameter_name: 'code', + text: data.code, // replace with queue_code variable + }, + { + type: 'text', + parameter_name: 'booking_date', + text: fallbackValue, + }, + ], + }, + ], + }, + }; + + const response = await this.sendMessage(payload); + if (response) + Logger.log( + `Notification register Booking for ${data.code} send to ${data.phone}`, + ); + } + async rescheduleCreated(data: WhatsappBookingCreate) { const imageUrl = `${BOOKING_QR_URL}${data.id}`; -- 2.40.1 From a77e6b03812cfe5849da4135f5c89ad85612fb05 Mon Sep 17 00:00:00 2001 From: shancheas Date: Thu, 19 Jun 2025 14:49:09 +0700 Subject: [PATCH 69/83] feat: add usage_type field to items and item_queues with corresponding database migration --- .../1750319148269-add-usage-type-to-item.ts | 29 +++++++++++++++++++ .../item-related/item-queue/constants.ts | 5 ++++ .../data/models/item-queue.model.ts | 9 +++++- .../domain/entities/item-queue.entity.ts | 3 +- .../managers/detail-item-queue.manager.ts | 1 + .../managers/index-item-queue.manager.ts | 2 +- .../item/data/models/item.model.ts | 8 +++++ .../item/domain/entities/item.entity.ts | 3 +- .../usecases/managers/detail-item.manager.ts | 1 + .../usecases/managers/index-item.manager.ts | 1 + 10 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 src/database/migrations/1750319148269-add-usage-type-to-item.ts diff --git a/src/database/migrations/1750319148269-add-usage-type-to-item.ts b/src/database/migrations/1750319148269-add-usage-type-to-item.ts new file mode 100644 index 0000000..a9eac38 --- /dev/null +++ b/src/database/migrations/1750319148269-add-usage-type-to-item.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUsageTypeToItem1750319148269 implements MigrationInterface { + name = 'AddUsageTypeToItem1750319148269'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."item_queues_usage_type_enum" AS ENUM('one_time', 'multiple')`, + ); + await queryRunner.query( + `ALTER TABLE "item_queues" ADD "usage_type" "public"."item_queues_usage_type_enum" NOT NULL DEFAULT 'one_time'`, + ); + await queryRunner.query( + `CREATE TYPE "public"."items_usage_type_enum" AS ENUM('one_time', 'multiple')`, + ); + await queryRunner.query( + `ALTER TABLE "items" ADD "usage_type" "public"."items_usage_type_enum" NOT NULL DEFAULT 'one_time'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "usage_type"`); + await queryRunner.query(`DROP TYPE "public"."items_usage_type_enum"`); + await queryRunner.query( + `ALTER TABLE "item_queues" DROP COLUMN "usage_type"`, + ); + await queryRunner.query(`DROP TYPE "public"."item_queues_usage_type_enum"`); + } +} diff --git a/src/modules/item-related/item-queue/constants.ts b/src/modules/item-related/item-queue/constants.ts index 9b62bb1..44e6c0d 100644 --- a/src/modules/item-related/item-queue/constants.ts +++ b/src/modules/item-related/item-queue/constants.ts @@ -5,3 +5,8 @@ export enum ItemType { FREE_GIFT = 'free gift', OTHER = 'other', } + +export enum UsageType { + ONE_TIME = 'one_time', + MULTIPLE = 'multiple', +} diff --git a/src/modules/item-related/item-queue/data/models/item-queue.model.ts b/src/modules/item-related/item-queue/data/models/item-queue.model.ts index c298f6d..fe7317d 100644 --- a/src/modules/item-related/item-queue/data/models/item-queue.model.ts +++ b/src/modules/item-related/item-queue/data/models/item-queue.model.ts @@ -2,7 +2,7 @@ import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { ItemQueueEntity } from '../../domain/entities/item-queue.entity'; import { Column, Entity, OneToMany } from 'typeorm'; import { BaseStatusModel } from 'src/core/modules/data/model/base-status.model'; -import { ItemType } from '../../constants'; +import { ItemType, UsageType } from '../../constants'; import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; @Entity(TABLE_NAME.ITEM_QUEUE) @@ -29,6 +29,13 @@ export class ItemQueueModel }) item_type: ItemType; + @Column('enum', { + name: 'usage_type', + enum: UsageType, + default: UsageType.ONE_TIME, + }) + usage_type: UsageType; + @OneToMany(() => ItemModel, (model) => model.item_queue, { onUpdate: 'CASCADE', }) diff --git a/src/modules/item-related/item-queue/domain/entities/item-queue.entity.ts b/src/modules/item-related/item-queue/domain/entities/item-queue.entity.ts index 1d7c765..c40c0b3 100644 --- a/src/modules/item-related/item-queue/domain/entities/item-queue.entity.ts +++ b/src/modules/item-related/item-queue/domain/entities/item-queue.entity.ts @@ -1,5 +1,5 @@ import { BaseStatusEntity } from 'src/core/modules/domain/entities/base-status.entity'; -import { ItemType } from '../../constants'; +import { ItemType, UsageType } from '../../constants'; import { ItemEntity } from 'src/modules/item-related/item/domain/entities/item.entity'; export interface ItemQueueEntity extends BaseStatusEntity { @@ -11,4 +11,5 @@ export interface ItemQueueEntity extends BaseStatusEntity { items: ItemEntity[]; use_notification?: boolean; requiring_notification?: boolean; + usage_type?: UsageType; } diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts index 7c62c0c..461e0b2 100644 --- a/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts +++ b/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts @@ -40,6 +40,7 @@ export class DetailItemQueueManager extends BaseDetailManager { `${this.tableName}.call_preparation`, `${this.tableName}.use_notification`, `${this.tableName}.requiring_notification`, + `${this.tableName}.usage_type`, `items.id`, `items.created_at`, diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts index d3ba4d8..1da7e07 100644 --- a/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts +++ b/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts @@ -43,7 +43,7 @@ export class IndexItemQueueManager extends BaseIndexManager { `${this.tableName}.call_preparation`, `${this.tableName}.use_notification`, `${this.tableName}.requiring_notification`, - + `${this.tableName}.usage_type`, `items.id`, `items.created_at`, `items.status`, diff --git a/src/modules/item-related/item/data/models/item.model.ts b/src/modules/item-related/item/data/models/item.model.ts index ca7951e..72b584b 100644 --- a/src/modules/item-related/item/data/models/item.model.ts +++ b/src/modules/item-related/item/data/models/item.model.ts @@ -18,6 +18,7 @@ import { ItemRateModel } from 'src/modules/item-related/item-rate/data/models/it import { GateModel } from 'src/modules/web-information/gate/data/models/gate.model'; import { ItemQueueModel } from 'src/modules/item-related/item-queue/data/models/item-queue.model'; import { TimeGroupModel } from 'src/modules/item-related/time-group/data/models/time-group.model'; +import { UsageType } from 'src/modules/item-related/item-queue/constants'; @Entity(TABLE_NAME.ITEM) export class ItemModel @@ -43,6 +44,13 @@ export class ItemModel }) item_type: ItemType; + @Column('enum', { + name: 'usage_type', + enum: UsageType, + default: UsageType.ONE_TIME, + }) + usage_type: UsageType; + @Column('bigint', { name: 'hpp', nullable: true }) hpp: number; diff --git a/src/modules/item-related/item/domain/entities/item.entity.ts b/src/modules/item-related/item/domain/entities/item.entity.ts index e8a158c..b131162 100644 --- a/src/modules/item-related/item/domain/entities/item.entity.ts +++ b/src/modules/item-related/item/domain/entities/item.entity.ts @@ -2,7 +2,7 @@ import { BaseStatusEntity } from 'src/core/modules/domain/entities/base-status.e import { ItemType } from 'src/modules/item-related/item-category/constants'; import { LimitType } from '../../constants'; import { ItemRateEntity } from 'src/modules/item-related/item-rate/domain/entities/item-rate.entity'; - +import { UsageType } from 'src/modules/item-related/item-queue/constants'; export interface ItemEntity extends BaseStatusEntity { name: string; item_type: ItemType; @@ -20,6 +20,7 @@ export interface ItemEntity extends BaseStatusEntity { show_to_booking: boolean; breakdown_bundling?: boolean; booking_description?: string; + usage_type?: UsageType; item_rates?: ItemRateEntity[] | any[]; } diff --git a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts index 7aae478..0738850 100644 --- a/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/detail-item.manager.ts @@ -53,6 +53,7 @@ export class DetailItemManager extends BaseDetailManager { `${this.tableName}.total_price`, `${this.tableName}.base_price`, `${this.tableName}.use_queue`, + `${this.tableName}.usage_type`, `${this.tableName}.show_to_booking`, `${this.tableName}.breakdown_bundling`, `${this.tableName}.play_estimation`, diff --git a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts index 6277d2b..884828b 100644 --- a/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/index-item.manager.ts @@ -55,6 +55,7 @@ export class IndexItemManager extends BaseIndexManager { `${this.tableName}.play_estimation`, `${this.tableName}.show_to_booking`, `${this.tableName}.booking_description`, + `${this.tableName}.usage_type`, `item_category.id`, `item_category.name`, -- 2.40.1 From 09b0133bf47ab066b13366cca32d9a7a31d2e526 Mon Sep 17 00:00:00 2001 From: shancheas Date: Thu, 19 Jun 2025 15:34:12 +0700 Subject: [PATCH 70/83] fix: update booking item pricing logic to use current rate based on season period IDs --- .../usecases/managers/booking-item.manager.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts index 416635a..21577cb 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts @@ -42,8 +42,11 @@ export class BookingItemManager extends IndexItemManager { const { data, total } = result; const hasRates = (this.filterParam.season_period_ids?.length ?? 0) > 0; const items = data.map((item) => { + const currentRate = item.item_rates.find((rate) => + this.filterParam.season_period_ids.includes(rate.season_period_id), + ); const { item_rates, ...rest } = item; - const rate = item_rates?.[0]?.['price'] ?? rest.base_price; + const rate = currentRate?.['price'] ?? rest.base_price; return { ...rest, base_price: hasRates ? rate : rest.base_price, @@ -51,17 +54,4 @@ export class BookingItemManager extends IndexItemManager { }); return { total, data: items }; } - - setQueryFilter( - queryBuilder: SelectQueryBuilder, - ): SelectQueryBuilder { - const query = super.setQueryFilter(queryBuilder); - - if (this.filterParam.season_period_ids) { - query.andWhere(`item_rates.season_period_id In (:...seasonIds)`, { - seasonIds: this.filterParam.season_period_ids, - }); - } - return query; - } } -- 2.40.1 From de43f0f28bd013d6e85bafb5b4a98e8dc74cd598 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:55:01 +0700 Subject: [PATCH 71/83] feat(SPG-1236): setup otp checker guard --- src/app.module.ts | 3 + src/core/guards/domain/otp-checker.guard.ts | 57 +++++++++++++++++++ .../{otp-auth-guard.ts => otp-auth.guard.ts} | 0 .../otp-verification-data.controller.ts | 2 +- .../otp-verification.module.ts | 2 +- .../transaction-data.controller.ts | 2 +- 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/core/guards/domain/otp-checker.guard.ts rename src/modules/configuration/otp-verification/infrastructure/guards/{otp-auth-guard.ts => otp-auth.guard.ts} (100%) diff --git a/src/app.module.ts b/src/app.module.ts index 7a8461f..cef2349 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -106,6 +106,7 @@ import { OtpVerificationModule } from './modules/configuration/otp-verification/ 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 { RescheduleVerificationModel } from './modules/booking-online/order/data/models/reschedule-verification.model'; +import { OtpCheckerGuard } from './core/guards/domain/otp-checker.guard'; @Module({ imports: [ @@ -246,6 +247,8 @@ import { RescheduleVerificationModel } from './modules/booking-online/order/data providers: [ AuthService, PrivilegeService, + OtpCheckerGuard, + /** * By default all request from client will protect by JWT * if there is some endpoint/function that does'nt require authentication diff --git a/src/core/guards/domain/otp-checker.guard.ts b/src/core/guards/domain/otp-checker.guard.ts new file mode 100644 index 0000000..2ef4802 --- /dev/null +++ b/src/core/guards/domain/otp-checker.guard.ts @@ -0,0 +1,57 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { OtpVerificationModel } from 'src/modules/configuration/otp-verification/data/models/otp-verification.model'; +import { OtpVerificationEntity } from 'src/modules/configuration/otp-verification/domain/entities/otp-verification.entity'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class OtpCheckerGuard implements CanActivate { + constructor( + @InjectDataSource(CONNECTION_NAME.DEFAULT) + protected readonly dataSource: DataSource, + ) {} + + get otpRepository() { + return this.dataSource.getRepository(OtpVerificationModel); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const verificationCode = request.headers['x-verification-code']; + console.log({ verificationCode }); + + if (verificationCode) { + const decoded = Buffer.from(verificationCode, 'base64').toString('ascii'); + const [dataIdentity, otpCode] = decoded.split('|'); + + let otpData: OtpVerificationEntity; + + otpData = await this.otpRepository.findOne({ + where: { + otp_code: otpCode, + target_id: dataIdentity, + }, + }); + + if (!otpData) { + otpData = await this.otpRepository.findOne({ + where: { + otp_code: otpCode, + reference: dataIdentity, + }, + }); + } + + if (otpData && otpData?.verified_at) return true; + console.log({ dataIdentity, otpCode, otpData }); + } + + throw new UnprocessableEntityException('OTP not verified.'); + } +} diff --git a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth.guard.ts similarity index 100% rename from src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts rename to src/modules/configuration/otp-verification/infrastructure/guards/otp-auth.guard.ts 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 1d1124f..b441441 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 @@ -16,7 +16,7 @@ import { 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'; @ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`) diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index efdfba6..d4e933d 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -10,7 +10,7 @@ import { } 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'; +import { OtpAuthGuard } from './infrastructure/guards/otp-auth.guard'; import { JwtModule } from '@nestjs/jwt'; import { JWT_EXPIRED } from 'src/core/sessions/constants'; diff --git a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts index 97d3e30..d2b401d 100644 --- a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts +++ b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts @@ -20,7 +20,7 @@ import { BatchResult } from 'src/core/response/domain/ok-response.interface'; import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto'; import { Public } from 'src/core/guards'; import { DownloadPdfDto } from './dto/donwload-pdf.dto'; -import { OtpAuthGuard } from 'src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard'; +import { OtpAuthGuard } from 'src/modules/configuration/otp-verification/infrastructure/guards/otp-auth.guard'; @ApiTags(`${MODULE_NAME.TRANSACTION.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.TRANSACTION}`) -- 2.40.1 From 822cfe606a2dff1ae28008b7ee534d4623d2db93 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:55:19 +0700 Subject: [PATCH 72/83] feat(SPG-1236): implement otp checker guard on active and confirm user --- .../user/infrastructure/user-data.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 d3b6d64..34adc6b 100644 --- a/src/modules/user-related/user/infrastructure/user-data.controller.ts +++ b/src/modules/user-related/user/infrastructure/user-data.controller.ts @@ -6,6 +6,7 @@ import { Patch, Post, Put, + UseGuards, } from '@nestjs/common'; import { UserDataOrchestrator } from '../domain/usecases/user-data.orchestrator'; import { UserDto } from './dto/user.dto'; @@ -17,6 +18,7 @@ import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto' import { Public } from 'src/core/guards'; import { UpdateUserDto } from './dto/update-user.dto'; import { UpdatePasswordUserDto } from './dto/update-password-user.dto'; +import { OtpCheckerGuard } from 'src/core/guards/domain/otp-checker.guard'; @ApiTags(`${MODULE_NAME.USER.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.USER}`) @@ -36,25 +38,23 @@ export class UserDataController { } @Patch(':id/active') - // TODO => simpan OTP update yang disikim dari request ini + @UseGuards(OtpCheckerGuard) 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 + @UseGuards(OtpCheckerGuard) 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); } -- 2.40.1 From 8df836ff3e71774f468305850b5f9f67c82f1d05 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:24:20 +0700 Subject: [PATCH 73/83] feat(SPG-1236): implement otp checker guard on session period --- src/core/guards/domain/otp-checker.guard.ts | 2 +- .../item/infrastructure/item-data.controller.ts | 9 +++++---- .../infrastructure/season-period-data.controller.ts | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/core/guards/domain/otp-checker.guard.ts b/src/core/guards/domain/otp-checker.guard.ts index 2ef4802..00b1af2 100644 --- a/src/core/guards/domain/otp-checker.guard.ts +++ b/src/core/guards/domain/otp-checker.guard.ts @@ -48,8 +48,8 @@ export class OtpCheckerGuard implements CanActivate { }); } - if (otpData && otpData?.verified_at) return true; console.log({ dataIdentity, otpCode, otpData }); + if (otpData && otpData?.verified_at) return true; } throw new UnprocessableEntityException('OTP not verified.'); 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 01ed1dd..76e43fb 100644 --- a/src/modules/item-related/item/infrastructure/item-data.controller.ts +++ b/src/modules/item-related/item/infrastructure/item-data.controller.ts @@ -6,6 +6,7 @@ import { Patch, Post, Put, + UseGuards, } from '@nestjs/common'; import { ItemDataOrchestrator } from '../domain/usecases/item-data.orchestrator'; import { ItemDto } from './dto/item.dto'; @@ -16,6 +17,7 @@ import { BatchResult } from 'src/core/response/domain/ok-response.interface'; import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto'; import { Public } from 'src/core/guards'; import { UpdateItemPriceDto } from './dto/update-item-price.dto'; +import { OtpCheckerGuard } from 'src/core/guards/domain/otp-checker.guard'; @ApiTags(`${MODULE_NAME.ITEM.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.ITEM}`) @@ -41,19 +43,18 @@ export class ItemDataController { } @Patch(':id/active') - // TODO => simpan OTP update yang disikim dari request ini + @UseGuards(OtpCheckerGuard) 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 + @UseGuards(OtpCheckerGuard) async confirm(@Param('id') dataId: string): Promise { return await this.orchestrator.confirm(dataId); } @@ -74,7 +75,7 @@ export class ItemDataController { } @Put(':id') - // TODO => simpan OTP update yang disikim dari request ini + @UseGuards(OtpCheckerGuard) 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 20aa20a..dde6225 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 @@ -6,6 +6,7 @@ import { Patch, Post, Put, + UseGuards, } from '@nestjs/common'; import { SeasonPeriodDataOrchestrator } from '../domain/usecases/season-period-data.orchestrator'; import { SeasonPeriodDto } from './dto/season-period.dto'; @@ -18,6 +19,7 @@ import { Public } from 'src/core/guards'; import { UpdateSeasonPeriodDto } from './dto/update-season-period.dto'; import { UpdateSeasonPeriodItemDto } from './dto/update-season-period-item.dto'; import { UpdateSeasonPriceDto } from './dto/update-season-price.dto'; +import { OtpCheckerGuard } from 'src/core/guards/domain/otp-checker.guard'; @ApiTags(`${MODULE_NAME.SEASON_PERIOD.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.SEASON_PERIOD}`) @@ -27,11 +29,13 @@ export class SeasonPeriodDataController { constructor(private orchestrator: SeasonPeriodDataOrchestrator) {} @Post() + @UseGuards(OtpCheckerGuard) async create(@Body() data: SeasonPeriodDto): Promise { return await this.orchestrator.create(data); } @Post('/update-price') + @UseGuards(OtpCheckerGuard) async updatePrice(@Body() body: UpdateSeasonPriceDto): Promise { return await this.orchestrator.updatePrice(body); } -- 2.40.1 From 08a35dfdf4b5e8043e8dd069b889e58e7f294242 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:32:48 +0700 Subject: [PATCH 74/83] feat(SPG-1236): implement otp checker guard on booking transaction --- .../transaction/infrastructure/transaction-data.controller.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts index d2b401d..5b239ff 100644 --- a/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts +++ b/src/modules/transaction/transaction/infrastructure/transaction-data.controller.ts @@ -21,6 +21,7 @@ import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto' import { Public } from 'src/core/guards'; import { DownloadPdfDto } from './dto/donwload-pdf.dto'; import { OtpAuthGuard } from 'src/modules/configuration/otp-verification/infrastructure/guards/otp-auth.guard'; +import { OtpCheckerGuard } from 'src/core/guards/domain/otp-checker.guard'; @ApiTags(`${MODULE_NAME.TRANSACTION.split('-').join(' ')} - data`) @Controller(`v1/${MODULE_NAME.TRANSACTION}`) @@ -53,6 +54,7 @@ export class TransactionDataController { } @Patch(':id/confirm-data') + @UseGuards(OtpCheckerGuard) async confirmData(@Param('id') dataId: string): Promise { return await this.orchestrator.confirmData(dataId); } @@ -63,6 +65,7 @@ export class TransactionDataController { } @Patch(':id/confirm') + @UseGuards(OtpCheckerGuard) async confirm(@Param('id') dataId: string): Promise { return await this.orchestrator.confirm(dataId); } -- 2.40.1 From 42060384aa0ef844a8039e676237edd062a54556 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:04:07 +0700 Subject: [PATCH 75/83] feat(SPG-1236): implement otp checker guard on session period --- .../infrastructure/season-period-data.controller.ts | 1 + 1 file changed, 1 insertion(+) 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 dde6225..4b8a761 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 @@ -86,6 +86,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') + @UseGuards(OtpCheckerGuard) async updateItems( @Param('id') dataId: string, @Body() data: UpdateSeasonPeriodItemDto, -- 2.40.1 From 83f33774651a0ead94dd5645b934f5caade9ac9e Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:09:42 +0700 Subject: [PATCH 76/83] feat(SPG-1236): setup otp checker guard --- src/core/guards/domain/otp-checker.guard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/guards/domain/otp-checker.guard.ts b/src/core/guards/domain/otp-checker.guard.ts index 00b1af2..61e5146 100644 --- a/src/core/guards/domain/otp-checker.guard.ts +++ b/src/core/guards/domain/otp-checker.guard.ts @@ -48,7 +48,7 @@ export class OtpCheckerGuard implements CanActivate { }); } - console.log({ dataIdentity, otpCode, otpData }); + // console.log({ dataIdentity, otpCode, otpData }); if (otpData && otpData?.verified_at) return true; } -- 2.40.1 From 63bb55b04be44ac053f40d93135fe2e627fc0cf0 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:30:07 +0700 Subject: [PATCH 77/83] feat(SPG-1137): add time group at booking and refund detail --- .../domain/usecases/managers/detail-refund.manager.ts | 8 ++++++++ .../usecases/managers/detail-transaction.manager.ts | 5 +++++ .../managers/helpers/mapping-transaction.helper.ts | 3 +++ 3 files changed, 16 insertions(+) diff --git a/src/modules/transaction/refund/domain/usecases/managers/detail-refund.manager.ts b/src/modules/transaction/refund/domain/usecases/managers/detail-refund.manager.ts index 722f94b..b40b563 100644 --- a/src/modules/transaction/refund/domain/usecases/managers/detail-refund.manager.ts +++ b/src/modules/transaction/refund/domain/usecases/managers/detail-refund.manager.ts @@ -31,6 +31,10 @@ export class DetailRefundManager extends BaseDetailManager { 'items.bundling_items', 'items.refunds item_refunds', 'item_refunds.refund item_refunds_refund', + + 'transaction.items transaction_items', + 'transaction_items.item transaction_items_item', + 'transaction_items_item.time_group transaction_items_item_time_group', ], // relation yang hanya ingin dihitung (akan return number) @@ -65,6 +69,10 @@ export class DetailRefundManager extends BaseDetailManager { 'item_refunds', 'item_refunds_refund.id', 'item_refunds_refund.status', + + 'transaction_items', + 'transaction_items_item', + 'transaction_items_item_time_group', ]; } diff --git a/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts b/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts index b788110..6d229b6 100644 --- a/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts +++ b/src/modules/transaction/transaction/domain/usecases/managers/detail-transaction.manager.ts @@ -31,6 +31,9 @@ export class DetailTransactionManager extends BaseDetailManager item.refund.id == refundId); + const timeGroup = itemData?.item?.time_group; + return { item: { id: itemData.item_id, @@ -57,6 +59,7 @@ export function mappingTransaction(data, refundId?: string) { }, breakdown_bundling: itemData.breakdown_bundling, bundling_items: itemData.bundling_items, + time_group: timeGroup, }, id: itemData.id, refund: refund, -- 2.40.1 From 162bd0918fd7145c02fb1be5cc085d516af36f4f Mon Sep 17 00:00:00 2001 From: shancheas Date: Fri, 20 Jun 2025 14:48:45 +0700 Subject: [PATCH 78/83] fix: ensure safe access to season period IDs in booking item pricing logic --- .../order/domain/usecases/managers/booking-item.manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts index 21577cb..6ecbf3e 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts @@ -43,7 +43,7 @@ export class BookingItemManager extends IndexItemManager { const hasRates = (this.filterParam.season_period_ids?.length ?? 0) > 0; const items = data.map((item) => { const currentRate = item.item_rates.find((rate) => - this.filterParam.season_period_ids.includes(rate.season_period_id), + this.filterParam.season_period_ids?.includes(rate.season_period_id), ); const { item_rates, ...rest } = item; const rate = currentRate?.['price'] ?? rest.base_price; -- 2.40.1 From 5d3f9d7bff032b083ad3130fb83a773423528382 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 24 Jun 2025 11:51:43 +0700 Subject: [PATCH 79/83] fix(SPG-1254): ONLINE BOOKING wahana tenant tidak muncul pada catalog online booking --- .../booking-online/order/infrastructure/item.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/booking-online/order/infrastructure/item.controller.ts b/src/modules/booking-online/order/infrastructure/item.controller.ts index 5e15333..bb0741c 100644 --- a/src/modules/booking-online/order/infrastructure/item.controller.ts +++ b/src/modules/booking-online/order/infrastructure/item.controller.ts @@ -23,6 +23,7 @@ export class ItemController { ): Promise> { params.limit = 1000; params.show_to_booking = true; + params.all_item = true; this.indexManager.setFilterParam(params); this.indexManager.setService(this.serviceData, TABLE_NAME.ITEM); await this.indexManager.execute(); -- 2.40.1 From b96d24de1a6c253cad3b0aeeda5802fff9980414 Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 24 Jun 2025 12:44:00 +0700 Subject: [PATCH 80/83] feat: add query filter for active booking items in BookingItemManager --- .../domain/usecases/managers/booking-item.manager.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts index 6ecbf3e..050458b 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/booking-item.manager.ts @@ -54,4 +54,14 @@ export class BookingItemManager extends IndexItemManager { }); return { total, data: items }; } + + setQueryFilter( + queryBuilder: SelectQueryBuilder, + ): SelectQueryBuilder { + const query = super.setQueryFilter(queryBuilder); + + query.andWhere(`${this.tableName}.status = 'active'`); + + return query; + } } -- 2.40.1 From 23b3c31810d0770077e658aedaf2551264cac2f8 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 25 Jun 2025 14:52:41 +0700 Subject: [PATCH 81/83] refactor(SPG-1199): remove unique constraint on item name and update validation logic in item managers --- .../validation/validate-relation.helper.ts | 2 +- .../1750834308368-remove-item-name-unique.ts | 17 ++++++++++ .../item/data/models/item.model.ts | 2 +- .../usecases/managers/create-item.manager.ts | 31 +++++++++++++++++-- .../usecases/managers/update-item.manager.ts | 25 +++++++++++++-- 5 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 src/database/migrations/1750834308368-remove-item-name-unique.ts diff --git a/src/core/helpers/validation/validate-relation.helper.ts b/src/core/helpers/validation/validate-relation.helper.ts index 2468595..ae5b929 100644 --- a/src/core/helpers/validation/validate-relation.helper.ts +++ b/src/core/helpers/validation/validate-relation.helper.ts @@ -55,7 +55,7 @@ export class ValidateRelationHelper { const relationColumn = data[relation.relation]?.[`${relation.singleQuery[0]}`]; if ( - !!relationColumn && + // !!relationColumn && this.mappingValidator( relationColumn, relation.singleQuery[1], diff --git a/src/database/migrations/1750834308368-remove-item-name-unique.ts b/src/database/migrations/1750834308368-remove-item-name-unique.ts new file mode 100644 index 0000000..1c1b7c4 --- /dev/null +++ b/src/database/migrations/1750834308368-remove-item-name-unique.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveItemNameUnique1750834308368 implements MigrationInterface { + name = 'RemoveItemNameUnique1750834308368'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" DROP CONSTRAINT "UQ_213736582899b3599acaade2cd1"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" ADD CONSTRAINT "UQ_213736582899b3599acaade2cd1" UNIQUE ("name")`, + ); + } +} diff --git a/src/modules/item-related/item/data/models/item.model.ts b/src/modules/item-related/item/data/models/item.model.ts index 72b584b..b161c50 100644 --- a/src/modules/item-related/item/data/models/item.model.ts +++ b/src/modules/item-related/item/data/models/item.model.ts @@ -25,7 +25,7 @@ export class ItemModel extends BaseStatusModel implements ItemEntity { - @Column('varchar', { name: 'name', unique: true }) + @Column('varchar', { name: 'name' }) name: string; @Column('text', { name: 'booking_description', nullable: true }) diff --git a/src/modules/item-related/item/domain/usecases/managers/create-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/create-item.manager.ts index 2b9d9e0..db1c055 100644 --- a/src/modules/item-related/item/domain/usecases/managers/create-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/create-item.manager.ts @@ -8,6 +8,7 @@ import { ItemEntity } from '../../entities/item.entity'; import { ItemModel } from '../../../data/models/item.model'; import { BaseCreateManager } from 'src/core/modules/domain/usecase/managers/base-create.manager'; import { ItemCreatedEvent } from '../../entities/event/item-created.event'; +import { STATUS } from 'src/core/strings/constants/base.constants'; @Injectable() export class CreateItemManager extends BaseCreateManager { @@ -29,11 +30,37 @@ export class CreateItemManager extends BaseCreateManager { } get validateRelations(): validateRelations[] { - return []; + const timeGroupId = this.data.time_group_id ?? this.data.time_group?.id; + const relation = + this.data.bundling_items?.length > 0 + ? 'bundling_items' + : 'bundling_parents'; + return timeGroupId != null + ? [ + { + relation: relation, + singleQuery: ['time_group_id', '!=', timeGroupId], + message: `Gagal Update! Time group item dan bundling item tidak sama`, + }, + ] + : []; } get uniqueColumns(): columnUniques[] { - return [{ column: 'name' }]; + const timeGroupId = this.data.time_group_id ?? this.data.time_group?.id; + return timeGroupId != null + ? [ + { + column: 'name', + query: `(status = '${STATUS.ACTIVE}' AND (${this.tableName}.time_group_id Is Null OR ${this.tableName}.time_group_id = '${timeGroupId}'))`, + }, + ] + : [ + { + column: 'name', + query: `(status = '${STATUS.ACTIVE}')`, + }, + ]; } get eventTopics(): EventTopics[] { diff --git a/src/modules/item-related/item/domain/usecases/managers/update-item.manager.ts b/src/modules/item-related/item/domain/usecases/managers/update-item.manager.ts index 510da69..bfc9620 100644 --- a/src/modules/item-related/item/domain/usecases/managers/update-item.manager.ts +++ b/src/modules/item-related/item/domain/usecases/managers/update-item.manager.ts @@ -8,6 +8,7 @@ import { columnUniques, validateRelations, } from 'src/core/strings/constants/interface.constants'; +import { STATUS } from 'src/core/strings/constants/base.constants'; @Injectable() export class UpdateItemManager extends BaseUpdateManager { @@ -39,11 +40,31 @@ export class UpdateItemManager extends BaseUpdateManager { } get validateRelations(): validateRelations[] { - return []; + const timeGroupId = this.data.time_group_id ?? this.data.time_group?.id; + const relation = + this.data.bundling_items?.length > 0 + ? 'bundling_items' + : 'bundling_parents'; + + return timeGroupId != null + ? [ + { + relation: relation, + singleQuery: ['time_group_id', '!=', timeGroupId], + message: `Gagal Update! Time group item dan bundling item tidak sama`, + }, + ] + : []; } get uniqueColumns(): columnUniques[] { - return []; + const timeGroupId = this.data.time_group_id ?? this.data.time_group?.id; + return [ + { + column: 'name', + query: `(status = '${STATUS.ACTIVE}' AND (${this.tableName}.time_group_id Is Null OR ${this.tableName}.time_group_id = '${timeGroupId}'))`, + }, + ]; } get entityTarget(): any { -- 2.40.1 From 7be4c26ef26666651ef46b4db24bc159f29417cc Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 25 Jun 2025 15:19:59 +0700 Subject: [PATCH 82/83] fix(SPG-1270): Pada Invoice/ tagihan booking tambahkan kode booking nya dan informasi booking date dan jumlah yang harus dibayarkan --- .../managers/create-booking.manager.ts | 1 + src/services/whatsapp/whatsapp.service.ts | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts index 750006b..1af417e 100644 --- a/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts +++ b/src/modules/booking-online/order/domain/usecases/managers/create-booking.manager.ts @@ -57,6 +57,7 @@ export class CreateBookingManager extends CreateTransactionManager { time: this.data.booking_date, id: this.data.id, }, + this.data.payment_total, `snap/v4/redirection/${this.data.payment_midtrans_token}`, ); } diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index 85ec5e6..3d8209a 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -324,10 +324,23 @@ export class WhatsappService { ); } - async bookingRegister(data: WhatsappBookingCreate, paymentUrl: string) { + async bookingRegister( + data: WhatsappBookingCreate, + total: number, + paymentUrl: string, + ) { const momentDate = moment(data.time); const fallbackValue = momentDate.locale('id').format('dddd, DD MMMM YYYY'); + const formattedTotal = new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }) + .format(total) + .replace('IDR', 'Rp'); + const payload = { messaging_product: 'whatsapp', to: phoneNumberOnly(data.phone), // recipient's phone number @@ -351,6 +364,16 @@ export class WhatsappService { parameter_name: 'booking_date', text: fallbackValue, }, + { + type: 'text', + parameter_name: 'booking_code', + text: data.code, + }, + { + type: 'text', + parameter_name: 'total', + text: formattedTotal, + }, ], }, { -- 2.40.1 From 92b54635d009209b91b698e705228b6ce6e322d7 Mon Sep 17 00:00:00 2001 From: shancheas Date: Wed, 25 Jun 2025 15:48:59 +0700 Subject: [PATCH 83/83] fix(SPG-1262): ONLINE BOOKING - Redirect ke antrian jika tidak sama dengan today - seharusnya diarahkan ke halaman login jika invoice tidak sama dengan today --- .../queue/data/services/ticket.service.ts | 38 +++++++++---------- .../queue/domain/queue.orchestrator.ts | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/modules/queue/data/services/ticket.service.ts b/src/modules/queue/data/services/ticket.service.ts index c3d1f11..812d4e3 100644 --- a/src/modules/queue/data/services/ticket.service.ts +++ b/src/modules/queue/data/services/ticket.service.ts @@ -51,27 +51,27 @@ export class TicketDataService extends BaseDataService { ], }); - if (!order) { - const { customer_name, customer_phone } = - await this.transaction.findOneOrFail({ - where: { - id, - }, - }); + // if (!order) { + // const { customer_name, customer_phone } = + // await this.transaction.findOneOrFail({ + // where: { + // id, + // }, + // }); - const start = moment().startOf('day').valueOf(); - const end = moment().endOf('day').valueOf(); - const order = this.order.findOneOrFail({ - relations: ['tickets'], - where: { - customer: customer_name, - phone: customer_phone, - date: Between(start, end), - }, - }); + // const start = moment().startOf('day').valueOf(); + // const end = moment().endOf('day').valueOf(); + // const order = this.order.findOneOrFail({ + // relations: ['tickets'], + // where: { + // customer: customer_name, + // phone: customer_phone, + // date: Between(start, end), + // }, + // }); - return order; - } + // return order; + // } return order; } diff --git a/src/modules/queue/domain/queue.orchestrator.ts b/src/modules/queue/domain/queue.orchestrator.ts index 7ce8c81..39b40af 100644 --- a/src/modules/queue/domain/queue.orchestrator.ts +++ b/src/modules/queue/domain/queue.orchestrator.ts @@ -56,7 +56,7 @@ export class QueueOrchestrator { return order; } catch (error) { throw new UnauthorizedException({ - message: 'Invoice tidak ditemukan', + message: 'Invoice tidak ditemukan untuk tanggal hari ini', error: 'INVOICE_NOT_FOUND', }); } -- 2.40.1