diff --git a/.drone.yml b/.drone.yml index f9fa9bf..e2fe1f9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,29 +2,37 @@ kind: pipeline type: docker name: server steps: - - name: build - image: appleboy/drone-ssh + # - name: build + # image: appleboy/drone-ssh + # settings: + # host: + # - 172.10.10.10 + # username: eigen + # key: + # from_secret: DEVOPS_SSH_PRIVATE_OPEN + # port: 22 + # script: + # - cd /home/eigen/PROJECT/POS/POS.DEV/BE + # - sh build.sh + # when: + # ref: + # - refs/tags/devel_* + # - refs/tags/*-alpha.* + - name: build-testing + image: plugins/docker settings: - host: - - 172.10.10.10 - username: eigen - key: - from_secret: DEVOPS_SSH_PRIVATE_OPEN - port: 22 - script: - - cd /home/eigen/PROJECT/POS/POS.DEV/BE - - sh build.sh + registry: registry.eigen.co.id + repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME} + tags: ${DRONE_TAG} + custom_dns: 172.10.10.16 when: ref: - - refs/tags/devel_* - refs/tags/*-alpha.* - name: build-production image: plugins/docker settings: registry: registry.eigen.co.id repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME} - build_args: - - env_target=env.production tags: ${DRONE_TAG} custom_dns: 172.10.10.16 when: @@ -46,3 +54,56 @@ trigger: event: exclude: - promote +--- +kind: pipeline +type: docker +name: kustomize + +clone: + disable: true + +steps: + - name: kustomize-testing + image: registry.k8s.io/kustomize/kustomize:v5.0.0 + environment: + DEVOPS_SSH_PRIVATE: + from_secret: DEVOPS_SSH_PRIVATE + DEVOPS_SSH_PUBLIC: + from_secret: DEVOPS_SSH_PUBLIC + INFRASTRUCTURE_REPO: "k8s-kustomize-external" + DIRECTORY_NAME: "weplay-pos-testing" + commands: + - mkdir -p ~/.ssh && + - echo $DEVOPS_SSH_PRIVATE | base64 -d > ~/.ssh/id_rsa && + - echo $DEVOPS_SSH_PUBLIC | base64 -d > ~/.ssh/id_rsa.pub && + - ssh-keyscan -H -p 2222 git.eigen.co.id >> ~/.ssh/known_hosts && + - chmod 700 ~/.ssh/ && + - chmod 600 ~/.ssh/id_rsa && + - git clone ssh://git@git.eigen.co.id:2222/eigen/$INFRASTRUCTURE_REPO.git && + - cd $INFRASTRUCTURE_REPO/$DIRECTORY_NAME + - kustomize edit set image registry.eigen.co.id/eigen/$DRONE_REPO_NAME=registry.eigen.co.id/eigen/$DRONE_REPO_NAME:$DRONE_TAG && + - git add . && + - |- + git commit -m "feat: update $DRONE_REPO_NAME testing to $DRONE_TAG" && + - git push origin master + - name: send-message + image: harbor.eigen.co.id/docker.com/plugins/webhook + settings: + urls: https://mattermost.eigen.co.id/api/v4/posts + content_type: application/json + headers: + - Authorization=Bearer 5zubexudb38uuradfa36qy98ca + template: | + { + "channel_id": "s1ekqde1c3du5p35g6budnuotc", + "message": "ALERT: {{ repo.name }} gagal update dengan tag ${DRONE_TAG}" + } + when: + status: + - failure +trigger: + ref: + include: + - refs/tags/*-alpha.* +depends_on: + - server \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 9b354f4..0b4ded3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,11 +5,11 @@ COPY . . RUN yarn install RUN yarn build FROM node:18.17-alpine -ARG env_target +# ARG env_target WORKDIR /app -RUN echo ${env_target} -COPY env/$env_target /app/.env -COPY --from=builder /app/env/$env_target .env +# RUN echo ${env_target} +# COPY env/$env_target /app/.env +# COPY --from=builder /app/env/$env_target .env COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/assets ./assets diff --git a/src/app.module.ts b/src/app.module.ts index b8001b4..3015ab4 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -83,6 +83,16 @@ import { UserLoginModel } from './modules/user-related/user/data/models/user-log import { LogUserLoginModel } from './modules/configuration/log/data/models/log-user-login.model'; import { AuthService } from './core/guards/domain/services/auth.service'; import { ReportSummaryModule } from './modules/reports/report-summary/report-summary.module'; +import { QueueModule } from './modules/queue/queue.module'; +import { + QueueOrderModel, + QueueTicketModel, + QueueItemModel, + QueueModel, +} from './modules/queue/data/models/queue.model'; +import { ItemQueueModule } from './modules/item-related/item-queue/item-queue.module'; +import { ItemQueueModel } from './modules/item-related/item-queue/data/models/item-queue.model'; +import { QueueBucketModel } from './modules/queue/data/models/queue-bucket.model'; @Module({ imports: [ @@ -107,6 +117,7 @@ import { ReportSummaryModule } from './modules/reports/report-summary/report-sum ItemModel, ItemCategoryModel, ItemRateModel, + ItemQueueModel, LogModel, LogUserLoginModel, NewsModel, @@ -135,6 +146,13 @@ import { ReportSummaryModule } from './modules/reports/report-summary/report-sum // report ReportBookmarkModel, ExportReportHistoryModel, + + // Queue + QueueOrderModel, + QueueTicketModel, + QueueItemModel, + QueueModel, + QueueBucketModel, ], synchronize: false, }), @@ -160,6 +178,7 @@ import { ReportSummaryModule } from './modules/reports/report-summary/report-sum ItemCategoryModule, ItemModule, ItemRateModule, + ItemQueueModule, // transaction PaymentMethodModule, @@ -193,6 +212,8 @@ import { ReportSummaryModule } from './modules/reports/report-summary/report-sum SupersetModule, GateScanModule, + + QueueModule, ], controllers: [], providers: [ diff --git a/src/core/helpers/path/upload-store-path.helper.ts b/src/core/helpers/path/upload-store-path.helper.ts index 2fbb495..96ceed9 100644 --- a/src/core/helpers/path/upload-store-path.helper.ts +++ b/src/core/helpers/path/upload-store-path.helper.ts @@ -8,7 +8,9 @@ import { diskStorage } from 'multer'; const MB = 1024 * 1024; const fileFilter = (req, file, callback) => { - if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) { + if ( + file.mimetype.match(/\/(jpg|jpeg|png|flv|mp4|m3u8|ts|3gp|mov|avi|wmv)$/) + ) { callback(null, true); } else { callback( diff --git a/src/core/modules/domain/usecase/base.manager.ts b/src/core/modules/domain/usecase/base.manager.ts index 0afe79f..f47f3c2 100644 --- a/src/core/modules/domain/usecase/base.manager.ts +++ b/src/core/modules/domain/usecase/base.manager.ts @@ -32,7 +32,7 @@ export abstract class BaseManager { setUser() { try { - this.user = this.userProvider?.user; + this.user = this.userProvider?.user ?? BLANK_USER; } catch (error) { this.user = BLANK_USER; } diff --git a/src/core/modules/domain/usecase/managers/base-create.manager.ts b/src/core/modules/domain/usecase/managers/base-create.manager.ts index 79bded9..309be3b 100644 --- a/src/core/modules/domain/usecase/managers/base-create.manager.ts +++ b/src/core/modules/domain/usecase/managers/base-create.manager.ts @@ -71,7 +71,7 @@ export abstract class BaseCreateManager extends BaseManager { } async publishEvents() { - this.eventBus.publish( + this.eventBus?.publish( new RecordLog({ id: this.result['id'], old: null, diff --git a/src/core/modules/domain/usecase/managers/base-index.manager.ts b/src/core/modules/domain/usecase/managers/base-index.manager.ts index fb5ece5..357fc04 100644 --- a/src/core/modules/domain/usecase/managers/base-index.manager.ts +++ b/src/core/modules/domain/usecase/managers/base-index.manager.ts @@ -50,7 +50,7 @@ export abstract class BaseIndexManager extends BaseReadManager { // jika searching status terdapat dalam enum, maka dia mencari specific data // ? karena jika tidak, ketika dia search "active" maka "inactive" juga ikut - return `'${STATUS[statusData.toUpperCase()]}'` ?? `'%${statusData}%'`; + return `'${STATUS[statusData.toUpperCase()]}'`; }); const exist = specificFilter.find((item) => item.isStatus); diff --git a/src/core/modules/domain/usecase/managers/base-update-status.manager.ts b/src/core/modules/domain/usecase/managers/base-update-status.manager.ts index 64516db..e585577 100644 --- a/src/core/modules/domain/usecase/managers/base-update-status.manager.ts +++ b/src/core/modules/domain/usecase/managers/base-update-status.manager.ts @@ -1,6 +1,10 @@ import { ValidateRelationHelper } from 'src/core/helpers/validation/validate-relation.helper'; import { BaseManager } from '../base.manager'; -import { OPERATION, STATUS } from 'src/core/strings/constants/base.constants'; +import { + OPERATION, + QUEUE_STATUS, + STATUS, +} from 'src/core/strings/constants/base.constants'; import * as _ from 'lodash'; import { RecordLog } from 'src/modules/configuration/log/domain/entities/log.event'; @@ -8,12 +12,12 @@ export abstract class BaseUpdateStatusManager extends BaseManager { protected dataId: string; protected result: Entity; protected oldData: Entity; - protected dataStatus: STATUS; + protected dataStatus: STATUS | QUEUE_STATUS; protected relations = []; protected duplicateColumn: string[]; abstract get entityTarget(): any; - setData(id: string, status: STATUS): void { + setData(id: string, status: STATUS | QUEUE_STATUS): void { /** * // TODO: Handle case confirm multiple tabs; * Pola id yang dikirim dirubah menjadi data_id___updated_at diff --git a/src/core/modules/infrastructure/dto/base-filter.dto.ts b/src/core/modules/infrastructure/dto/base-filter.dto.ts index b149663..a33dec5 100644 --- a/src/core/modules/infrastructure/dto/base-filter.dto.ts +++ b/src/core/modules/infrastructure/dto/base-filter.dto.ts @@ -23,6 +23,7 @@ export class BaseFilterDto implements BaseFilterEntity { @IsNumber() limit = 10; + @ApiProperty({ type: String, required: false }) q: string; @ApiProperty({ type: ['string'], required: false }) diff --git a/src/core/sessions/domain/entities/user-sessions.interface.ts b/src/core/sessions/domain/entities/user-sessions.interface.ts index 848d44e..c59f85b 100644 --- a/src/core/sessions/domain/entities/user-sessions.interface.ts +++ b/src/core/sessions/domain/entities/user-sessions.interface.ts @@ -6,5 +6,6 @@ export interface UsersSession { name: string; role: UserRole; source?: AppSource; + item_id?: string; user_privilege_id: string; } diff --git a/src/core/strings/constants/base.constants.ts b/src/core/strings/constants/base.constants.ts index 0e1f7d4..466d2dc 100644 --- a/src/core/strings/constants/base.constants.ts +++ b/src/core/strings/constants/base.constants.ts @@ -15,6 +15,11 @@ export enum STATUS { WAITING = 'waiting', } +export enum QUEUE_STATUS { + DONE = 'done', + CALLED = 'called', +} + export enum ORDER_TYPE { ASC = 'ASC', DESC = 'DESC', diff --git a/src/core/strings/constants/module.constants.ts b/src/core/strings/constants/module.constants.ts index f78ebca..b05838e 100644 --- a/src/core/strings/constants/module.constants.ts +++ b/src/core/strings/constants/module.constants.ts @@ -4,6 +4,7 @@ export enum MODULE_NAME { GATE = 'gates', ITEM = 'items', ITEM_CATEGORY = 'item-categories', + ITEM_QUEUE = 'item-queues', ITEM_RATE = 'item-rates', NEWS = 'news', PAYMENT_METHOD = 'payment-methods', @@ -25,4 +26,6 @@ export enum MODULE_NAME { REPORT_BOOKMARK = 'report-bookmark', REPORT_EXPORT = 'report-export', REPORT_SUMMARY = 'report-summary', + + QUEUE = 'queue', } diff --git a/src/core/strings/constants/table.constants.ts b/src/core/strings/constants/table.constants.ts index 990c4cb..27eb096 100644 --- a/src/core/strings/constants/table.constants.ts +++ b/src/core/strings/constants/table.constants.ts @@ -4,6 +4,7 @@ export enum TABLE_NAME { FAQ = 'faqs', ITEM = 'items', ITEM_CATEGORY = 'item_categories', + ITEM_QUEUE = 'item_queues', ITEM_RATE = 'item_rates', GATE = 'gates', LOG = 'logs', @@ -35,4 +36,10 @@ export enum TABLE_NAME { REPORT_BOOKMARK = 'report_bookmark', EXPORT_REPORT_HISTORY = 'export_report_history', + + QUEUE = 'queues', + QUEUE_ORDER = 'queue_orders', + QUEUE_TICKET = 'queue_tickets', + QUEUE_ITEM = 'queue_items', + QUEUE_BUCKET = 'queue_bucket', } diff --git a/src/database/migrations/1729151429165-queue-table.ts b/src/database/migrations/1729151429165-queue-table.ts new file mode 100644 index 0000000..cb3b8e8 --- /dev/null +++ b/src/database/migrations/1729151429165-queue-table.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class QueueTable1729151429165 implements MigrationInterface { + name = 'QueueTable1729151429165'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "queue_orders" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "code" character varying NOT NULL, "customer" character varying, "phone" character varying, "date" bigint NOT NULL, CONSTRAINT "PK_b139e4cc9ca3e709c152f820d2e" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "queue_tickets" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "code" character varying NOT NULL, "customer" character varying, "phone" character varying, "date" bigint NOT NULL, "order_id" uuid, CONSTRAINT "PK_1b903aa90bcc04136caa6540c55" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "queue_items" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "qty" integer NOT NULL, "ticket_id" uuid, "item_id" uuid, CONSTRAINT "PK_2245e11ac3517494bacfe932773" PRIMARY KEY ("id"))`, + ); + + await queryRunner.query( + `ALTER TABLE "queue_tickets" ADD CONSTRAINT "FK_0e9823b8b7ca9523b3be73878e5" FOREIGN KEY ("order_id") REFERENCES "queue_orders"("id") ON DELETE SET NULL ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "queue_items" ADD CONSTRAINT "FK_25352739034765f6917757df74b" FOREIGN KEY ("ticket_id") REFERENCES "queue_tickets"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "queue_items" ADD CONSTRAINT "FK_ab15c053aeb4f739ebf533b61cd" FOREIGN KEY ("item_id") REFERENCES "items"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "queue_items" DROP CONSTRAINT "FK_ab15c053aeb4f739ebf533b61cd"`, + ); + await queryRunner.query( + `ALTER TABLE "queue_items" DROP CONSTRAINT "FK_25352739034765f6917757df74b"`, + ); + await queryRunner.query( + `ALTER TABLE "queue_tickets" DROP CONSTRAINT "FK_0e9823b8b7ca9523b3be73878e5"`, + ); + + await queryRunner.query(`DROP TABLE "queue_items"`); + await queryRunner.query(`DROP TABLE "queue_tickets"`); + await queryRunner.query(`DROP TABLE "queue_orders"`); + } +} diff --git a/src/database/migrations/1729248576381-video-url-to-json.ts b/src/database/migrations/1729248576381-video-url-to-json.ts new file mode 100644 index 0000000..4761e3c --- /dev/null +++ b/src/database/migrations/1729248576381-video-url-to-json.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class VideoUrlToJson1729248576381 implements MigrationInterface { + name = 'VideoUrlToJson1729248576381'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "video_url"`); + await queryRunner.query(`ALTER TABLE "items" ADD "video_url" json`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "video_url"`); + await queryRunner.query( + `ALTER TABLE "items" ADD "video_url" character varying`, + ); + await queryRunner.query(`DROP TABLE "item_queues"`); + } +} diff --git a/src/database/migrations/1729570177597-item-queues.ts b/src/database/migrations/1729570177597-item-queues.ts new file mode 100644 index 0000000..96acb18 --- /dev/null +++ b/src/database/migrations/1729570177597-item-queues.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ItemQueues1729570177597 implements MigrationInterface { + name = 'ItemQueues1729570177597'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."item_queues_status_enum" AS ENUM('active', 'cancel', 'confirmed', 'draft', 'expired', 'inactive', 'partial refund', 'pending', 'proses refund', 'refunded', 'rejected', 'settled', 'waiting')`, + ); + await queryRunner.query( + `CREATE TYPE "public"."item_queues_item_type_enum" AS ENUM('tiket masuk', 'wahana', 'bundling', 'free gift', 'other')`, + ); + await queryRunner.query( + `CREATE TABLE "item_queues" ("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"."item_queues_status_enum" NOT NULL DEFAULT 'draft', "name" character varying NOT NULL, "item_type" "public"."item_queues_item_type_enum" NOT NULL DEFAULT 'tiket masuk', CONSTRAINT "PK_e19adb0b99d995e8f10c189985f" PRIMARY KEY ("id"))`, + ); + await queryRunner.query(`ALTER TABLE "items" ADD "item_queue_id" uuid`); + + await queryRunner.query( + `ALTER TABLE "items" ADD CONSTRAINT "FK_2cbbeb03e176addcf60d65f7c9c" FOREIGN KEY ("item_queue_id") REFERENCES "item_queues"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" DROP CONSTRAINT "FK_2cbbeb03e176addcf60d65f7c9c"`, + ); + + await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "item_queue_id"`); + await queryRunner.query(`DROP TABLE "item_queues"`); + await queryRunner.query(`DROP TYPE "public"."item_queues_item_type_enum"`); + await queryRunner.query(`DROP TYPE "public"."item_queues_status_enum"`); + } +} diff --git a/src/database/migrations/1729582398827-item-queue-on-delete.ts b/src/database/migrations/1729582398827-item-queue-on-delete.ts new file mode 100644 index 0000000..09fe090 --- /dev/null +++ b/src/database/migrations/1729582398827-item-queue-on-delete.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ItemQueueOnDelete1729582398827 implements MigrationInterface { + name = 'ItemQueueOnDelete1729582398827'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" DROP CONSTRAINT "FK_2cbbeb03e176addcf60d65f7c9c"`, + ); + + await queryRunner.query( + `ALTER TABLE "items" ADD CONSTRAINT "FK_2cbbeb03e176addcf60d65f7c9c" FOREIGN KEY ("item_queue_id") REFERENCES "item_queues"("id") ON DELETE SET NULL ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "items" DROP CONSTRAINT "FK_2cbbeb03e176addcf60d65f7c9c"`, + ); + + await queryRunner.query( + `ALTER TABLE "items" ADD CONSTRAINT "FK_2cbbeb03e176addcf60d65f7c9c" FOREIGN KEY ("item_queue_id") REFERENCES "item_queues"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } +} diff --git a/src/database/migrations/1729653046392-add-transaction-id-to-queue-order.ts b/src/database/migrations/1729653046392-add-transaction-id-to-queue-order.ts new file mode 100644 index 0000000..c80ff83 --- /dev/null +++ b/src/database/migrations/1729653046392-add-transaction-id-to-queue-order.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTransactionIdToQueueOrder1729653046392 + implements MigrationInterface +{ + name = 'AddTransactionIdToQueueOrder1729653046392'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "queue_orders" ADD "transaction_id" character varying NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "queue_orders" DROP COLUMN "transaction_id"`, + ); + } +} diff --git a/src/database/migrations/1729756969674-add-queue-table.ts b/src/database/migrations/1729756969674-add-queue-table.ts new file mode 100644 index 0000000..6b25c0c --- /dev/null +++ b/src/database/migrations/1729756969674-add-queue-table.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddQueueTable1729756969674 implements MigrationInterface { + name = 'AddQueueTable1729756969674'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "queues" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "code" character varying NOT NULL, "status" character varying NOT NULL, "time" bigint NOT NULL, "call_time" bigint NOT NULL, "vip" boolean NOT NULL, "item_id" uuid NOT NULL, "qty" integer NOT NULL, CONSTRAINT "PK_d966f9eb39a9396658387071bb3" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "queues" ADD CONSTRAINT "FK_435954e9a0d9967f17e043d54b4" FOREIGN KEY ("item_id") REFERENCES "queue_items"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "queues" DROP CONSTRAINT "FK_435954e9a0d9967f17e043d54b4"`, + ); + + await queryRunner.query(`DROP TABLE "queues"`); + } +} diff --git a/src/database/migrations/1729838994129-add-queue-base-model.ts b/src/database/migrations/1729838994129-add-queue-base-model.ts new file mode 100644 index 0000000..5a85010 --- /dev/null +++ b/src/database/migrations/1729838994129-add-queue-base-model.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddQueueBaseModel1729838994129 implements MigrationInterface { + name = 'AddQueueBaseModel1729838994129'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "queues" ADD "creator_id" character varying(36)`, + ); + await queryRunner.query( + `ALTER TABLE "queues" ADD "creator_name" character varying(125)`, + ); + await queryRunner.query( + `ALTER TABLE "queues" ADD "editor_id" character varying(36)`, + ); + await queryRunner.query( + `ALTER TABLE "queues" ADD "editor_name" character varying(125)`, + ); + await queryRunner.query( + `ALTER TABLE "queues" ADD "created_at" bigint NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "queues" ADD "updated_at" bigint NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "queues" ALTER COLUMN "call_time" DROP NOT NULL`, + ); + + await queryRunner.query(`ALTER TABLE "queues" DROP COLUMN "updated_at"`); + await queryRunner.query(`ALTER TABLE "queues" DROP COLUMN "created_at"`); + await queryRunner.query(`ALTER TABLE "queues" DROP COLUMN "editor_name"`); + await queryRunner.query(`ALTER TABLE "queues" DROP COLUMN "editor_id"`); + await queryRunner.query(`ALTER TABLE "queues" DROP COLUMN "creator_name"`); + await queryRunner.query(`ALTER TABLE "queues" DROP COLUMN "creator_id"`); + } +} diff --git a/src/database/migrations/1730859187883-add-queue-bucket.ts b/src/database/migrations/1730859187883-add-queue-bucket.ts new file mode 100644 index 0000000..9e06f8d --- /dev/null +++ b/src/database/migrations/1730859187883-add-queue-bucket.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddQueueBucket1730859187883 implements MigrationInterface { + name = 'AddQueueBucket1730859187883'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "queue_bucket" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "queue_item_id" character varying NOT NULL, "date" bigint NOT NULL, "regular" integer NOT NULL, "vip" integer NOT NULL, CONSTRAINT "PK_cdd58b0d9e93e4be922da9d8bd6" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "queue_bucket"`); + } +} diff --git a/src/database/migrations/1731383726542-add-transaction-and-item-relation.ts b/src/database/migrations/1731383726542-add-transaction-and-item-relation.ts new file mode 100644 index 0000000..4f9e288 --- /dev/null +++ b/src/database/migrations/1731383726542-add-transaction-and-item-relation.ts @@ -0,0 +1,50 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTransactionAndItemRelation1731383726542 + implements MigrationInterface +{ + name = 'AddTransactionAndItemRelation1731383726542'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP COLUMN "item_id"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD "item_id" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "customer_category_id"`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" ADD "customer_category_id" uuid`, + ); + + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD CONSTRAINT "FK_edb934ab033f847e3f7ed4fc0fc" FOREIGN KEY ("item_id") REFERENCES "items"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" ADD CONSTRAINT "FK_08dc8138714894a66e94820766d" FOREIGN KEY ("customer_category_id") REFERENCES "vip_categories"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" DROP CONSTRAINT "FK_08dc8138714894a66e94820766d"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP CONSTRAINT "FK_edb934ab033f847e3f7ed4fc0fc"`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "customer_category_id"`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" ADD "customer_category_id" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" DROP COLUMN "item_id"`, + ); + await queryRunner.query( + `ALTER TABLE "transaction_items" ADD "item_id" character varying`, + ); + } +} diff --git a/src/database/migrations/1731498661938-add-item-queue-to-queue.ts b/src/database/migrations/1731498661938-add-item-queue-to-queue.ts new file mode 100644 index 0000000..a1151af --- /dev/null +++ b/src/database/migrations/1731498661938-add-item-queue-to-queue.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddItemQueueToQueue1731498661938 implements MigrationInterface { + name = 'AddItemQueueToQueue1731498661938'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "queues" ADD "item_queue_id" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "queues" DROP COLUMN "item_queue_id"`); + } +} diff --git a/src/database/migrations/1731570311609-add-information-to-item-queue.ts b/src/database/migrations/1731570311609-add-information-to-item-queue.ts new file mode 100644 index 0000000..ccc0993 --- /dev/null +++ b/src/database/migrations/1731570311609-add-information-to-item-queue.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddInformationToItemQueue1731570311609 + implements MigrationInterface +{ + name = 'AddInformationToItemQueue1731570311609'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "item_queues" ADD "information" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "item_queues" DROP COLUMN "information"`, + ); + } +} diff --git a/src/database/migrations/1733199330134-item-queue-add-time-and-peak-level.ts b/src/database/migrations/1733199330134-item-queue-add-time-and-peak-level.ts new file mode 100644 index 0000000..6a21374 --- /dev/null +++ b/src/database/migrations/1733199330134-item-queue-add-time-and-peak-level.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ItemQueueAddTimeAndPeakLevel1733199330134 + implements MigrationInterface +{ + name = 'ItemQueueAddTimeAndPeakLevel1733199330134'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "item_queues" ADD "max_peak_level" integer NOT NULL DEFAULT '100'`, + ); + await queryRunner.query( + `ALTER TABLE "item_queues" ADD "call_preparation" integer NOT NULL DEFAULT '5'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "item_queues" DROP COLUMN "call_preparation"`, + ); + await queryRunner.query( + `ALTER TABLE "item_queues" DROP COLUMN "max_peak_level"`, + ); + } +} diff --git a/src/modules/configuration/couch/domain/managers/booking.handler.ts b/src/modules/configuration/couch/domain/managers/booking.handler.ts index 200afed..4e48be0 100644 --- a/src/modules/configuration/couch/domain/managers/booking.handler.ts +++ b/src/modules/configuration/couch/domain/managers/booking.handler.ts @@ -97,9 +97,7 @@ export class ChangeStatusBookingHandler }, relations: ['items', 'items.bundling_items'], }); - console.log('change status', { dataID, couchData, booking }); mappingTransaction(booking); - console.log('after mapping'); if (!couchData) { console.log('save data to couch'); diff --git a/src/modules/gates/domain/entity/gate-request.entity.ts b/src/modules/gates/domain/entity/gate-request.entity.ts index 299e086..37b62c1 100644 --- a/src/modules/gates/domain/entity/gate-request.entity.ts +++ b/src/modules/gates/domain/entity/gate-request.entity.ts @@ -3,3 +3,9 @@ export interface GateScanEntity { type: string; uuid: string; } + +export interface GateLogEntity { + gate_id: string; + code: string; + error: any; +} diff --git a/src/modules/gates/infrastructure/dto/logs.dto.ts b/src/modules/gates/infrastructure/dto/logs.dto.ts new file mode 100644 index 0000000..011e757 --- /dev/null +++ b/src/modules/gates/infrastructure/dto/logs.dto.ts @@ -0,0 +1,26 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { GateLogEntity } from '../../domain/entity/gate-request.entity'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GateLogDto implements GateLogEntity { + @ApiProperty({ + type: String, + required: true, + }) + @IsNotEmpty() + @IsString() + gate_id: string; + + @ApiProperty({ + type: String, + required: true, + }) + @IsNotEmpty() + @IsString() + code: string; + + @ApiProperty({ + required: false, + }) + error: any; +} diff --git a/src/modules/gates/infrastructure/gate.controller.ts b/src/modules/gates/infrastructure/gate.controller.ts index 74fa973..4da847b 100644 --- a/src/modules/gates/infrastructure/gate.controller.ts +++ b/src/modules/gates/infrastructure/gate.controller.ts @@ -8,6 +8,7 @@ import { GateResponseEntity, } from '../domain/entity/gate-response.entity'; import { Gate } from 'src/core/response'; +import { GateLogDto } from './dto/logs.dto'; const masterGates = [ '319b6d3e-b661-4d19-8695-0dd6fb76465e', @@ -72,6 +73,13 @@ export class GateController { return responseValue; } + @Post('logs') + async logs(@Body() data: GateLogDto): Promise { + console.log(data); + + return { code: 1, message: 'success' }; + } + @Get(':id/master') async detail(@Param('id') id: string): Promise { if (id == '1') return { codes: masterGates }; diff --git a/src/modules/item-related/item-queue/constants.ts b/src/modules/item-related/item-queue/constants.ts new file mode 100644 index 0000000..9b62bb1 --- /dev/null +++ b/src/modules/item-related/item-queue/constants.ts @@ -0,0 +1,7 @@ +export enum ItemType { + TIKET_MASUK = 'tiket masuk', + WAHANA = 'wahana', + BUNDLING = 'bundling', + FREE_GIFT = 'free gift', + OTHER = 'other', +} 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 new file mode 100644 index 0000000..7f0be80 --- /dev/null +++ b/src/modules/item-related/item-queue/data/models/item-queue.model.ts @@ -0,0 +1,36 @@ +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 { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; + +@Entity(TABLE_NAME.ITEM_QUEUE) +export class ItemQueueModel + extends BaseStatusModel + implements ItemQueueEntity +{ + @Column('int', { default: 100 }) + max_peak_level: number; + + @Column('int', { default: 5 }) + call_preparation: number; + + @Column('varchar', { name: 'name' }) + name: string; + + @Column('varchar', { name: 'information', nullable: true }) + information: string; + + @Column('enum', { + name: 'item_type', + enum: ItemType, + default: ItemType.TIKET_MASUK, + }) + item_type: ItemType; + + @OneToMany(() => ItemModel, (model) => model.item_queue, { + onUpdate: 'CASCADE', + }) + items: ItemModel[]; +} diff --git a/src/modules/item-related/item-queue/data/services/item-queue-data.service.ts b/src/modules/item-related/item-queue/data/services/item-queue-data.service.ts new file mode 100644 index 0000000..7b67210 --- /dev/null +++ b/src/modules/item-related/item-queue/data/services/item-queue-data.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { BaseDataService } from 'src/core/modules/data/service/base-data.service'; +import { ItemQueueEntity } from '../../domain/entities/item-queue.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ItemQueueModel } from '../models/item-queue.model'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { Repository } from 'typeorm'; + +@Injectable() +export class ItemQueueDataService extends BaseDataService { + constructor( + @InjectRepository(ItemQueueModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(repo); + } +} diff --git a/src/modules/item-related/item-queue/data/services/item-queue-read.service.ts b/src/modules/item-related/item-queue/data/services/item-queue-read.service.ts new file mode 100644 index 0000000..40d3ab9 --- /dev/null +++ b/src/modules/item-related/item-queue/data/services/item-queue-read.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { ItemQueueEntity } from '../../domain/entities/item-queue.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ItemQueueModel } from '../models/item-queue.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 ItemQueueReadService extends BaseReadService { + constructor( + @InjectRepository(ItemQueueModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(repo); + } + + async list(): Promise { + return this.repo.find(); + } +} diff --git a/src/modules/item-related/item-queue/domain/entities/filter-item-queue.entity.ts b/src/modules/item-related/item-queue/domain/entities/filter-item-queue.entity.ts new file mode 100644 index 0000000..e01cdcc --- /dev/null +++ b/src/modules/item-related/item-queue/domain/entities/filter-item-queue.entity.ts @@ -0,0 +1,5 @@ +import { BaseFilterEntity } from 'src/core/modules/domain/entities/base-filter.entity'; + +export interface FilterItemQueueEntity extends BaseFilterEntity { + item_types: string[]; +} 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 new file mode 100644 index 0000000..5b89cfe --- /dev/null +++ b/src/modules/item-related/item-queue/domain/entities/item-queue.entity.ts @@ -0,0 +1,12 @@ +import { BaseStatusEntity } from 'src/core/modules/domain/entities/base-status.entity'; +import { ItemType } from '../../constants'; +import { ItemEntity } from 'src/modules/item-related/item/domain/entities/item.entity'; + +export interface ItemQueueEntity extends BaseStatusEntity { + name: string; + item_type: ItemType; + information?: string; + max_peak_level: number; + call_preparation: number; + items: ItemEntity[]; +} diff --git a/src/modules/item-related/item-queue/domain/usecases/item-queue-data.orchestrator.ts b/src/modules/item-related/item-queue/domain/usecases/item-queue-data.orchestrator.ts new file mode 100644 index 0000000..91dcd16 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/item-queue-data.orchestrator.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@nestjs/common'; +import { CreateItemQueueManager } from './managers/create-item-queue.manager'; +import { ItemQueueDataService } from '../../data/services/item-queue-data.service'; +import { ItemQueueEntity } from '../entities/item-queue.entity'; +import { DeleteItemQueueManager } from './managers/delete-item-queue.manager'; +import { UpdateItemQueueManager } from './managers/update-item-queue.manager'; +import { BaseDataTransactionOrchestrator } from 'src/core/modules/domain/usecase/orchestrators/base-data-transaction.orchestrator'; +import { ActiveItemQueueManager } from './managers/active-item-queue.manager'; +import { InactiveItemQueueManager } from './managers/inactive-item-queue.manager'; +import { ConfirmItemQueueManager } from './managers/confirm-item-queue.manager'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { BatchConfirmItemQueueManager } from './managers/batch-confirm-item-queue.manager'; +import { BatchInactiveItemQueueManager } from './managers/batch-inactive-item-queue.manager'; +import { BatchActiveItemQueueManager } from './managers/batch-active-item-queue.manager'; +import { BatchDeleteItemQueueManager } from './managers/batch-delete-item-queue.manager'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; + +@Injectable() +export class ItemQueueDataOrchestrator extends BaseDataTransactionOrchestrator { + constructor( + private createManager: CreateItemQueueManager, + private updateManager: UpdateItemQueueManager, + private deleteManager: DeleteItemQueueManager, + private activeManager: ActiveItemQueueManager, + private confirmManager: ConfirmItemQueueManager, + private inactiveManager: InactiveItemQueueManager, + private batchDeleteManager: BatchDeleteItemQueueManager, + private batchActiveManager: BatchActiveItemQueueManager, + private batchConfirmManager: BatchConfirmItemQueueManager, + private batchInactiveManager: BatchInactiveItemQueueManager, + private serviceData: ItemQueueDataService, + ) { + super(); + } + + async create(data): Promise { + data.items = data.item_ids.map((id) => { + return { id }; + }); + this.createManager.setData(data); + this.createManager.setService(this.serviceData, TABLE_NAME.ITEM_CATEGORY); + 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.ITEM_CATEGORY); + await this.updateManager.execute(); + return this.updateManager.getResult(); + } + + async delete(dataId): Promise { + this.deleteManager.setData(dataId); + this.deleteManager.setService(this.serviceData, TABLE_NAME.ITEM_CATEGORY); + await this.deleteManager.execute(); + return this.deleteManager.getResult(); + } + + async batchDelete(dataIds: string[]): Promise { + this.batchDeleteManager.setData(dataIds); + this.batchDeleteManager.setService( + this.serviceData, + TABLE_NAME.ITEM_CATEGORY, + ); + 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.ITEM_CATEGORY); + 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.ITEM_CATEGORY, + ); + 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.ITEM_CATEGORY); + 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.ITEM_CATEGORY, + ); + 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.ITEM_CATEGORY); + 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.ITEM_CATEGORY, + ); + await this.batchInactiveManager.execute(); + return this.batchInactiveManager.getResult(); + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/item-queue-read.orchestrator.ts b/src/modules/item-related/item-queue/domain/usecases/item-queue-read.orchestrator.ts new file mode 100644 index 0000000..4c533fc --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/item-queue-read.orchestrator.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { IndexItemQueueManager } from './managers/index-item-queue.manager'; +import { ItemQueueReadService } from '../../data/services/item-queue-read.service'; +import { ItemQueueEntity } from '../entities/item-queue.entity'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +import { BaseReadOrchestrator } from 'src/core/modules/domain/usecase/orchestrators/base-read.orchestrator'; +import { DetailItemQueueManager } from './managers/detail-item-queue.manager'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; + +@Injectable() +export class ItemQueueReadOrchestrator extends BaseReadOrchestrator { + constructor( + private indexManager: IndexItemQueueManager, + private detailManager: DetailItemQueueManager, + private serviceData: ItemQueueReadService, + ) { + super(); + } + + async index(params): Promise> { + this.indexManager.setFilterParam(params); + this.indexManager.setService(this.serviceData, TABLE_NAME.ITEM_QUEUE); + await this.indexManager.execute(); + return this.indexManager.getResult(); + } + + async list(): Promise { + const items = await this.serviceData.list(); + return items; + } + + async detail(dataId: string): Promise { + this.detailManager.setData(dataId); + this.detailManager.setService(this.serviceData, TABLE_NAME.ITEM_QUEUE); + await this.detailManager.execute(); + return this.detailManager.getResult(); + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/active-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/active-item-queue.manager.ts new file mode 100644 index 0000000..ae3baba --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/active-item-queue.manager.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; + +@Injectable() +export class ActiveItemQueueManager 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 ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/batch-active-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/batch-active-item-queue.manager.ts new file mode 100644 index 0000000..e70d534 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/batch-active-item-queue.manager.ts @@ -0,0 +1,40 @@ +import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchActiveItemQueueManager extends BaseBatchUpdateStatusManager { + validateData(data: ItemQueueEntity): Promise { + return; + } + + beforeProcess(): Promise { + return; + } + + afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/batch-confirm-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/batch-confirm-item-queue.manager.ts new file mode 100644 index 0000000..02ec3d9 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/batch-confirm-item-queue.manager.ts @@ -0,0 +1,40 @@ +import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchConfirmItemQueueManager extends BaseBatchUpdateStatusManager { + validateData(data: ItemQueueEntity): Promise { + return; + } + + beforeProcess(): Promise { + return; + } + + afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/batch-delete-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/batch-delete-item-queue.manager.ts new file mode 100644 index 0000000..a226ce6 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/batch-delete-item-queue.manager.ts @@ -0,0 +1,46 @@ +import { BaseBatchDeleteManager } from 'src/core/modules/domain/usecase/managers/base-batch-delete.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchDeleteItemQueueManager extends BaseBatchDeleteManager { + async beforeProcess(): Promise { + return; + } + + async validateData(data: ItemQueueEntity): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return [ + { + relation: 'items', + message: + 'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item', + }, + ]; + } + + get entityTarget(): any { + return ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/batch-inactive-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/batch-inactive-item-queue.manager.ts new file mode 100644 index 0000000..2d3c725 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/batch-inactive-item-queue.manager.ts @@ -0,0 +1,46 @@ +import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; +import { BatchResult } from 'src/core/response/domain/ok-response.interface'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BatchInactiveItemQueueManager extends BaseBatchUpdateStatusManager { + validateData(data: ItemQueueEntity): Promise { + return; + } + + beforeProcess(): Promise { + return; + } + + afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return [ + { + relation: 'items', + message: + 'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item', + }, + ]; + } + + get entityTarget(): any { + return ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } + + getResult(): BatchResult { + return this.result; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/confirm-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/confirm-item-queue.manager.ts new file mode 100644 index 0000000..d0ad2b8 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/confirm-item-queue.manager.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; + +@Injectable() +export class ConfirmItemQueueManager 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 ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/create-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/create-item-queue.manager.ts new file mode 100644 index 0000000..09c3c21 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/create-item-queue.manager.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { + EventTopics, + columnUniques, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; +import { BaseCreateManager } from 'src/core/modules/domain/usecase/managers/base-create.manager'; + +@Injectable() +export class CreateItemQueueManager extends BaseCreateManager { + async beforeProcess(): Promise { + Object.assign(this.data, { + item_type: this.data.item_type.toLowerCase(), + }); + 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 []; + } + + get entityTarget(): any { + return ItemQueueModel; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/delete-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/delete-item-queue.manager.ts new file mode 100644 index 0000000..360aa89 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/delete-item-queue.manager.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { BaseDeleteManager } from 'src/core/modules/domain/usecase/managers/base-delete.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; + +@Injectable() +export class DeleteItemQueueManager extends BaseDeleteManager { + getResult(): string { + return `Success`; + } + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } +} 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 new file mode 100644 index 0000000..67d7861 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/detail-item-queue.manager.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@nestjs/common'; +import { BaseDetailManager } from 'src/core/modules/domain/usecase/managers/base-detail.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { RelationParam } from 'src/core/modules/domain/entities/base-filter.entity'; + +@Injectable() +export class DetailItemQueueManager extends BaseDetailManager { + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get relations(): RelationParam { + return { + joinRelations: [], + selectRelations: ['items'], + countRelations: [], + }; + } + + get selects(): string[] { + return [ + `${this.tableName}.id`, + `${this.tableName}.status`, + `${this.tableName}.name`, + `${this.tableName}.information`, + `${this.tableName}.item_type`, + `${this.tableName}.created_at`, + `${this.tableName}.creator_name`, + `${this.tableName}.updated_at`, + `${this.tableName}.editor_name`, + `${this.tableName}.max_peak_level`, + `${this.tableName}.call_preparation`, + + `items.id`, + `items.created_at`, + `items.status`, + `items.item_type`, + `items.name`, + `items.hpp`, + `items.limit_type`, + `items.limit_value`, + `items.base_price`, + `items.share_profit`, + `items.play_estimation`, + `items.video_url`, + ]; + } + + get setFindProperties(): any { + return { + id: this.dataId, + }; + } + + getResult(): ItemQueueEntity { + const videos = this.result.items.map((item) => { + return item.video_url ?? []; + }); + this.result['videos'] = videos.flat(); + return this.result; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/inactive-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/inactive-item-queue.manager.ts new file mode 100644 index 0000000..106ab98 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/inactive-item-queue.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 { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; + +@Injectable() +export class InactiveItemQueueManager 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 tipe item karena sudah berelasi dengan item', + }, + ]; + } + + get entityTarget(): any { + return ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } +} 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 new file mode 100644 index 0000000..93c6eb8 --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/index-item-queue.manager.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { BaseIndexManager } from 'src/core/modules/domain/usecase/managers/base-index.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { SelectQueryBuilder } from 'typeorm'; +import { + Param, + RelationParam, +} from 'src/core/modules/domain/entities/base-filter.entity'; + +@Injectable() +export class IndexItemQueueManager extends BaseIndexManager { + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get relations(): RelationParam { + return { + joinRelations: [], + selectRelations: ['items'], + countRelations: [], + }; + } + + get selects(): string[] { + return [ + `${this.tableName}.id`, + `${this.tableName}.status`, + `${this.tableName}.name`, + `${this.tableName}.item_type`, + `${this.tableName}.created_at`, + `${this.tableName}.creator_name`, + `${this.tableName}.updated_at`, + `${this.tableName}.editor_name`, + `${this.tableName}.max_peak_level`, + `${this.tableName}.call_preparation`, + + `items.id`, + `items.created_at`, + `items.status`, + `items.item_type`, + `items.name`, + `items.hpp`, + `items.limit_type`, + `items.limit_value`, + `items.base_price`, + `items.share_profit`, + `items.play_estimation`, + ]; + } + + get specificFilter(): Param[] { + return [ + { + cols: `${this.tableName}.name`, + data: this.filterParam.names, + }, + { + cols: `${this.tableName}.item_type::text`, + data: this.filterParam.item_types, + }, + ]; + } + + setQueryFilter( + queryBuilder: SelectQueryBuilder, + ): SelectQueryBuilder { + return queryBuilder; + } +} diff --git a/src/modules/item-related/item-queue/domain/usecases/managers/update-item-queue.manager.ts b/src/modules/item-related/item-queue/domain/usecases/managers/update-item-queue.manager.ts new file mode 100644 index 0000000..2801c3b --- /dev/null +++ b/src/modules/item-related/item-queue/domain/usecases/managers/update-item-queue.manager.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateManager } from 'src/core/modules/domain/usecase/managers/base-update.manager'; +import { ItemQueueEntity } from '../../entities/item-queue.entity'; +import { ItemQueueModel } from '../../../data/models/item-queue.model'; +import { + EventTopics, + columnUniques, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; + +@Injectable() +export class UpdateItemQueueManager extends BaseUpdateManager { + async validateProcess(): Promise { + Object.assign(this.data, { + item_type: this.data.item_type.toLowerCase(), + }); + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + if (this.data.item_type != this.oldData.item_type) { + return [ + { + relation: 'items', + message: + 'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item', + }, + ]; + } else { + return []; + } + } + + get uniqueColumns(): columnUniques[] { + return [{ column: 'name' }]; + } + + get entityTarget(): any { + return ItemQueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } +} diff --git a/src/modules/item-related/item-queue/index.ts b/src/modules/item-related/item-queue/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/item-related/item-queue/infrastructure/dto/filter-item-queue.dto.ts b/src/modules/item-related/item-queue/infrastructure/dto/filter-item-queue.dto.ts new file mode 100644 index 0000000..ab43721 --- /dev/null +++ b/src/modules/item-related/item-queue/infrastructure/dto/filter-item-queue.dto.ts @@ -0,0 +1,15 @@ +import { BaseFilterDto } from 'src/core/modules/infrastructure/dto/base-filter.dto'; +import { FilterItemQueueEntity } from '../../domain/entities/filter-item-queue.entity'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; + +export class FilterItemQueueDto + extends BaseFilterDto + implements FilterItemQueueEntity +{ + @ApiProperty({ type: ['string'], required: false }) + @Transform((body) => { + return Array.isArray(body.value) ? body.value : [body.value]; + }) + item_types: string[]; +} diff --git a/src/modules/item-related/item-queue/infrastructure/dto/item-queue.dto.ts b/src/modules/item-related/item-queue/infrastructure/dto/item-queue.dto.ts new file mode 100644 index 0000000..43cb677 --- /dev/null +++ b/src/modules/item-related/item-queue/infrastructure/dto/item-queue.dto.ts @@ -0,0 +1,58 @@ +import { BaseStatusDto } from 'src/core/modules/infrastructure/dto/base-status.dto'; +import { ItemQueueEntity } from '../../domain/entities/item-queue.entity'; +import { IsArray, IsNumber, IsString } from 'class-validator'; +import { ItemType } from '../../constants'; +import { ApiProperty } from '@nestjs/swagger'; +import { ItemEntity } from 'src/modules/item-related/item/domain/entities/item.entity'; +import { Exclude, Transform } from 'class-transformer'; + +export class ItemQueueDto extends BaseStatusDto implements ItemQueueEntity { + @ApiProperty({ + name: 'max_peak_level', + required: false, + example: 'Menentukan level peak maksimal penuhnya wahana', + default: 100, + }) + @IsNumber() + max_peak_level: number; + + @ApiProperty({ + name: 'call_preparation', + required: false, + example: 'Waktu persiapan untuk memanggil antrian dalam menit', + default: 5, + }) + @IsNumber() + call_preparation: number; + + @Exclude() + items: ItemEntity[]; + + @ApiProperty({ name: 'name', required: true, example: 'Bundling w Entrance' }) + @IsString() + name: string; + + @ApiProperty({ + name: 'information', + required: false, + example: 'Running text untuk display antrian', + }) + @IsString() + information: string; + + @ApiProperty({ + type: 'string', + required: true, + description: `Select (${JSON.stringify(Object.values(ItemType))})`, + example: ItemType.BUNDLING, + }) + item_type: ItemType; + + @ApiProperty({ type: [String], required: true }) + @Transform((body) => { + return Array.isArray(body.value) ? body.value : [body.value]; + }) + @IsArray() + @IsString({ each: true }) + item_ids: string[]; +} diff --git a/src/modules/item-related/item-queue/infrastructure/item-queue-data.controller.ts b/src/modules/item-related/item-queue/infrastructure/item-queue-data.controller.ts new file mode 100644 index 0000000..2639a8f --- /dev/null +++ b/src/modules/item-related/item-queue/infrastructure/item-queue-data.controller.ts @@ -0,0 +1,78 @@ +import { + Body, + Controller, + Delete, + Param, + Patch, + Post, + Put, +} from '@nestjs/common'; +import { ItemQueueDataOrchestrator } from '../domain/usecases/item-queue-data.orchestrator'; +import { ItemQueueDto } from './dto/item-queue.dto'; +import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ItemQueueEntity } from '../domain/entities/item-queue.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.ITEM_QUEUE.split('-').join(' ')} - data`) +@Controller(`v1/${MODULE_NAME.ITEM_QUEUE}`) +@Public(false) +@ApiBearerAuth('JWT') +export class ItemQueueDataController { + constructor(private orchestrator: ItemQueueDataOrchestrator) {} + + @Post() + async create(@Body() data: ItemQueueDto): 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: ItemQueueDto, + ): 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/item-queue/infrastructure/item-queue-read.controller.ts b/src/modules/item-related/item-queue/infrastructure/item-queue-read.controller.ts new file mode 100644 index 0000000..b3a78b6 --- /dev/null +++ b/src/modules/item-related/item-queue/infrastructure/item-queue-read.controller.ts @@ -0,0 +1,49 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { FilterItemQueueDto } from './dto/filter-item-queue.dto'; +import { Pagination } from 'src/core/response'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +import { ItemQueueEntity } from '../domain/entities/item-queue.entity'; +import { ItemQueueReadOrchestrator } from '../domain/usecases/item-queue-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.ITEM_QUEUE.split('-').join(' ')} - read`) +@Controller(`v1/${MODULE_NAME.ITEM_QUEUE}`) +@Public(false) +@ApiBearerAuth('JWT') +export class ItemQueueReadController { + constructor(private orchestrator: ItemQueueReadOrchestrator) {} + + @Get() + @Pagination() + async index( + @Query() params: FilterItemQueueDto, + ): Promise> { + return await this.orchestrator.index(params); + } + + @Get('list') + @Public(true) + async list() { + const list = await this.orchestrator.list(); + return list.map(({ id, name, item_type }) => { + return { + id, + name, + item_type, + }; + }); + } + + @Get(':id') + async detail(@Param('id') id: string): Promise { + return await this.orchestrator.detail(id); + } + + @Get('display/:id') + @Public(true) + async detailPublic(@Param('id') id: string): Promise { + return await this.orchestrator.detail(id); + } +} diff --git a/src/modules/item-related/item-queue/item-queue.module.ts b/src/modules/item-related/item-queue/item-queue.module.ts new file mode 100644 index 0000000..c009f9f --- /dev/null +++ b/src/modules/item-related/item-queue/item-queue.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 { ItemQueueDataService } from './data/services/item-queue-data.service'; +import { ItemQueueReadService } from './data/services/item-queue-read.service'; +import { ItemQueueReadController } from './infrastructure/item-queue-read.controller'; +import { ItemQueueReadOrchestrator } from './domain/usecases/item-queue-read.orchestrator'; +import { ItemQueueDataController } from './infrastructure/item-queue-data.controller'; +import { ItemQueueDataOrchestrator } from './domain/usecases/item-queue-data.orchestrator'; +import { CreateItemQueueManager } from './domain/usecases/managers/create-item-queue.manager'; +import { CqrsModule } from '@nestjs/cqrs'; +import { IndexItemQueueManager } from './domain/usecases/managers/index-item-queue.manager'; +import { DeleteItemQueueManager } from './domain/usecases/managers/delete-item-queue.manager'; +import { UpdateItemQueueManager } from './domain/usecases/managers/update-item-queue.manager'; +import { ActiveItemQueueManager } from './domain/usecases/managers/active-item-queue.manager'; +import { ConfirmItemQueueManager } from './domain/usecases/managers/confirm-item-queue.manager'; +import { InactiveItemQueueManager } from './domain/usecases/managers/inactive-item-queue.manager'; +import { DetailItemQueueManager } from './domain/usecases/managers/detail-item-queue.manager'; +import { BatchDeleteItemQueueManager } from './domain/usecases/managers/batch-delete-item-queue.manager'; +import { BatchActiveItemQueueManager } from './domain/usecases/managers/batch-active-item-queue.manager'; +import { BatchConfirmItemQueueManager } from './domain/usecases/managers/batch-confirm-item-queue.manager'; +import { BatchInactiveItemQueueManager } from './domain/usecases/managers/batch-inactive-item-queue.manager'; +import { ItemQueueModel } from './data/models/item-queue.model'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forFeature([ItemQueueModel], CONNECTION_NAME.DEFAULT), + CqrsModule, + ], + controllers: [ItemQueueDataController, ItemQueueReadController], + providers: [ + IndexItemQueueManager, + DetailItemQueueManager, + CreateItemQueueManager, + DeleteItemQueueManager, + UpdateItemQueueManager, + ActiveItemQueueManager, + ConfirmItemQueueManager, + InactiveItemQueueManager, + BatchDeleteItemQueueManager, + BatchActiveItemQueueManager, + BatchConfirmItemQueueManager, + BatchInactiveItemQueueManager, + + ItemQueueDataService, + ItemQueueReadService, + + ItemQueueDataOrchestrator, + ItemQueueReadOrchestrator, + ], +}) +export class ItemQueueModule {} 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 7506733..90634a6 100644 --- a/src/modules/item-related/item/data/models/item.model.ts +++ b/src/modules/item-related/item/data/models/item.model.ts @@ -16,6 +16,7 @@ import { ItemCategoryModel } from 'src/modules/item-related/item-category/data/m 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'; @Entity(TABLE_NAME.ITEM) export class ItemModel @@ -28,8 +29,8 @@ export class ItemModel @Column('varchar', { name: 'image_url', nullable: true }) image_url: string; - @Column('varchar', { nullable: true }) - video_url: string; + @Column('json', { nullable: true }) + video_url: string[]; @Column('enum', { name: 'item_type', @@ -85,6 +86,16 @@ export class ItemModel @JoinColumn({ name: 'item_category_id' }) item_category: ItemCategoryModel; + @ManyToOne(() => ItemQueueModel, (model) => model.items, { + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }) + @JoinColumn({ name: 'item_queue_id' }) + item_queue: ItemQueueModel; + + @Column('varchar', { name: 'item_queue_id', nullable: true }) + item_queue_id: string; + // relation ke tenant // ? karena item bisajadi merupakan item dari tenant @Column('varchar', { name: 'tenant_id', nullable: true }) 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 7ade51e..995b113 100644 --- a/src/modules/item-related/item/domain/entities/item.entity.ts +++ b/src/modules/item-related/item/domain/entities/item.entity.ts @@ -6,7 +6,7 @@ export interface ItemEntity extends BaseStatusEntity { name: string; item_type: ItemType; image_url: string; - video_url?: string; + video_url?: string[]; hpp: number; sales_margin: number; 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 4da5d1a..3c51840 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 @@ -84,6 +84,12 @@ export class IndexItemManager extends BaseIndexManager { setQueryFilter( queryBuilder: SelectQueryBuilder, ): SelectQueryBuilder { + if (this.filterParam.q) { + queryBuilder.andWhere( + `${this.tableName}.name ILIKE '%${this.filterParam.q}%'`, + ); + } + if (this.filterParam.tenant_ids?.length) { queryBuilder.andWhere(`${this.tableName}.tenant_id In (:...tenantIds)`, { tenantIds: this.filterParam.tenant_ids, 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 c6f739e..c1add0f 100644 --- a/src/modules/item-related/item/infrastructure/dto/item.dto.ts +++ b/src/modules/item-related/item/infrastructure/dto/item.dto.ts @@ -12,6 +12,7 @@ import { ValidateIf, } from 'class-validator'; import { ItemCategoryEntity } from 'src/modules/item-related/item-category/domain/entities/item-category.entity'; +import { Transform } from 'class-transformer'; export class ItemDto extends BaseStatusDto implements ItemEntity { @ApiProperty({ @@ -32,13 +33,16 @@ export class ItemDto extends BaseStatusDto implements ItemEntity { image_url: string; @ApiProperty({ - type: String, + isArray: true, required: false, - example: '...', }) - @IsString() + // @IsString() @ValidateIf((body) => body.video_url) - video_url: string; + @Transform(({ value }) => { + if (!value) return []; + return Array.isArray(value) ? value : [value]; + }) + video_url: string[]; @ApiProperty({ type: 'string', diff --git a/src/modules/item-related/item/infrastructure/item-read.controller.ts b/src/modules/item-related/item/infrastructure/item-read.controller.ts index 91c7ee5..ae27dfb 100644 --- a/src/modules/item-related/item/infrastructure/item-read.controller.ts +++ b/src/modules/item-related/item/infrastructure/item-read.controller.ts @@ -41,18 +41,18 @@ export class ItemReadController { } } -@ApiTags(`Item Queue - Read`) -@Controller(`v1/item-queue`) -@Public(true) -export class ItemReadQueueController { - constructor(private orchestrator: ItemReadOrchestrator) {} +// @ApiTags(`Item Queue - Read`) +// @Controller(`v1/item-queue`) +// @Public(true) +// export class ItemReadQueueController { +// constructor(private orchestrator: ItemReadOrchestrator) {} - @Get() - @Pagination() - @ExcludePrivilege() - async indexQueue( - @Query() params: FilterItemDto, - ): Promise> { - return await this.orchestrator.indexQueue(params); - } -} +// @Get() +// @Pagination() +// @ExcludePrivilege() +// async indexQueue( +// @Query() params: FilterItemDto, +// ): Promise> { +// return await this.orchestrator.indexQueue(params); +// } +// } diff --git a/src/modules/item-related/item/item.module.ts b/src/modules/item-related/item/item.module.ts index 76e0fe6..8e8c26a 100644 --- a/src/modules/item-related/item/item.module.ts +++ b/src/modules/item-related/item/item.module.ts @@ -6,7 +6,7 @@ import { ItemDataService } from './data/services/item-data.service'; import { ItemReadService } from './data/services/item-read.service'; import { ItemReadController, - ItemReadQueueController, + // ItemReadQueueController, } from './infrastructure/item-read.controller'; import { ItemReadOrchestrator } from './domain/usecases/item-read.orchestrator'; import { ItemDataController } from './infrastructure/item-data.controller'; @@ -44,7 +44,7 @@ import { IndexItemQueueManager } from './domain/usecases/managers/index-queue-it controllers: [ ItemDataController, ItemReadController, - ItemReadQueueController, + // ItemReadQueueController, ], providers: [ IndexItemManager, diff --git a/src/modules/queue/data/models/queue-bucket.model.ts b/src/modules/queue/data/models/queue-bucket.model.ts new file mode 100644 index 0000000..51f2e3a --- /dev/null +++ b/src/modules/queue/data/models/queue-bucket.model.ts @@ -0,0 +1,21 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { QueueBucket } from '../../domain/entities/queue-bucket.entity'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; + +@Entity(TABLE_NAME.QUEUE_BUCKET) +export class QueueBucketModel implements QueueBucket { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column('varchar') + queue_item_id: string; + + @Column({ type: 'bigint', nullable: false }) + date: number; + + @Column('int') + regular: number; + + @Column('int') + vip: number; +} diff --git a/src/modules/queue/data/models/queue.model.ts b/src/modules/queue/data/models/queue.model.ts new file mode 100644 index 0000000..eff707f --- /dev/null +++ b/src/modules/queue/data/models/queue.model.ts @@ -0,0 +1,142 @@ +import { BaseCoreModel } from 'src/core/modules/data/model/base-core.model'; +import { QueueOrder } from '../../domain/entities/order.entity'; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { QueueTicket } from '../../domain/entities/ticket.entity'; +import { QueueItem } from '../../domain/entities/queue-item.entity'; + +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; +import { Queue } from '../../domain/entities/queue.entity'; +import { BaseModel } from 'src/core/modules/data/model/base.model'; + +@Entity(TABLE_NAME.QUEUE_ORDER) +export class QueueOrderModel + extends BaseCoreModel + implements QueueOrder +{ + @Column('varchar') + code: string; + + @Column('varchar', { nullable: true }) + customer: string; + + @Column('varchar', { nullable: true }) + phone: string; + + @Column('varchar', { nullable: false }) + transaction_id: string; + + @OneToMany(() => QueueTicketModel, (model) => model.order, { + cascade: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + tickets: QueueTicket[]; + + @Column({ type: 'bigint' }) + date: number; +} + +@Entity(TABLE_NAME.QUEUE_TICKET) +export class QueueTicketModel + extends BaseCoreModel + implements QueueTicket +{ + @Column('varchar') + code: string; + + @Column('varchar', { nullable: true }) + customer: string; + + @Column('varchar', { nullable: true }) + phone: string; + + @ManyToOne(() => QueueOrderModel, (model) => model.tickets, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + @JoinColumn({ name: 'order_id' }) + order: QueueOrder; + + @Column('varchar') + order_id: string; + + @Column({ type: 'bigint' }) + date: number; + + @OneToMany(() => QueueItemModel, (model) => model.ticket, { + cascade: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + items: QueueItemModel[]; +} + +@Entity(TABLE_NAME.QUEUE_ITEM) +export class QueueItemModel + extends BaseCoreModel + implements QueueItem +{ + @ManyToOne(() => QueueTicketModel, (model) => model.items, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + @JoinColumn({ name: 'ticket_id' }) + ticket: QueueTicket; + + @Column('varchar') + ticket_id: string; + + @OneToMany(() => QueueModel, (model) => model.item, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + queue: QueueModel[]; + + @Column('varchar') + item_id: string; + + @ManyToOne(() => ItemModel) + @JoinColumn({ name: 'item_id' }) + item: ItemModel; + + @Column('int') + qty: number; +} + +@Entity(TABLE_NAME.QUEUE) +export class QueueModel extends BaseModel implements Queue { + @Column('varchar') + code: string; + + @Column('varchar') + status: string; + + @Column({ type: 'bigint' }) + time: number; + + @Column({ type: 'bigint' }) + call_time: number; + + @Column({ type: 'boolean' }) + vip: boolean; + + @ManyToOne(() => QueueItemModel, (model) => model.queue, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }) + @JoinColumn({ name: 'item_id' }) + item: QueueItemModel; + + @Column('varchar') + item_id: string; + + @Column('varchar', { nullable: true }) + item_queue_id: string; + + @Column('int') + qty: number; + + average = 0; + peak_level = 100; +} diff --git a/src/modules/queue/data/services/queue-bucket.ts b/src/modules/queue/data/services/queue-bucket.ts new file mode 100644 index 0000000..4e5bacb --- /dev/null +++ b/src/modules/queue/data/services/queue-bucket.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; + +import { InjectRepository } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { Between, Repository } from 'typeorm'; +import { BaseReadService } from 'src/core/modules/data/service/base-read.service'; +import { QueueBucketModel } from '../models/queue-bucket.model'; +import * as moment from 'moment'; +import { QueueItemModel } from '../models/queue.model'; + +@Injectable() +export class QueueBucketReadService extends BaseReadService { + constructor( + @InjectRepository(QueueBucketModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + + @InjectRepository(QueueItemModel, CONNECTION_NAME.DEFAULT) + private item: Repository, + ) { + super(repo); + } + + async getQueue(item_id: string, vip = false): Promise { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + + const queue = await this.repo.findOne({ + where: { + queue_item_id: item_id, + date: Between(start, end), + }, + }); + + if (!queue) { + const regularNumber = vip ? 0 : 1; + const vipNumber = vip ? 1 : 0; + await this.repo.save({ + queue_item_id: item_id, + date: start, + regular: regularNumber, + vip: vipNumber, + }); + return Promise.resolve(1); + } else { + const field = vip ? 'vip' : 'regular'; + const data = await this.repo + .createQueryBuilder('bucket') + .update(QueueBucketModel) + .set({ + [field]: () => `${field} + 1`, + }) + .where('id = :key', { + key: queue.id, + }) + .returning(field) + .updateEntity(true) + .execute(); + + return data.raw[0]?.[field] ?? 1; + } + } +} diff --git a/src/modules/queue/data/services/queue.service.ts b/src/modules/queue/data/services/queue.service.ts new file mode 100644 index 0000000..bb03a82 --- /dev/null +++ b/src/modules/queue/data/services/queue.service.ts @@ -0,0 +1,242 @@ +import { Injectable } from '@nestjs/common'; + +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { Between, DataSource, In, Not, Repository } from 'typeorm'; +import { + QueueItemModel, + QueueModel, + QueueOrderModel, +} from '../models/queue.model'; +import { BaseReadService } from 'src/core/modules/data/service/base-read.service'; +import { BaseDataService } from 'src/core/modules/data/service/base-data.service'; +import { ItemQueueModel } from 'src/modules/item-related/item-queue/data/models/item-queue.model'; +import * as moment from 'moment'; +import * as math from 'mathjs'; +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; + +@Injectable() +export class QueueDataService extends BaseReadService { + constructor( + @InjectRepository(QueueModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + + @InjectRepository(ItemQueueModel, CONNECTION_NAME.DEFAULT) + private itemQueueRepo: Repository, + ) { + super(repo); + } + + async waitingQueue(item_queue_id: string) { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + return this.repo.find({ + where: { + time: Between(start, end), + item_queue_id, + status: 'waiting', + }, + order: { + time: 'ASC', + }, + }); + } + + async doneQueue(item_queue_id: string) { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + return this.repo.find({ + where: { + time: Between(start, end), + item_queue_id, + status: In(['done', 'called']), + }, + order: { + time: 'ASC', + }, + }); + } + + async exclude(item_queue_id: string[]) { + const queues = await this.itemQueueRepo.find({ + relations: ['items'], + where: { + id: Not(In(item_queue_id)), + }, + }); + + return queues.filter((q) => q.items.length > 0); + } + + async allQueue() { + const queues = await this.itemQueueRepo.find({ + relations: ['items'], + }); + + return queues.filter((q) => q.items.length > 0); + } + + async lastQueue(item_queue_id: string) { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + return this.repo.findOne({ + where: { + time: Between(start, end), + item_queue_id, + status: 'called', + }, + order: { + call_time: 'DESC', + }, + }); + } + + /** + * @deprecated + * Change to QueueTimeFormula (queue-time.formula.ts) + * @param item_queue_id + * @returns + */ + async queueTimes(item_queue_id: string): Promise { + const queueTimes = {}; + let now = moment().valueOf(); + const itemQueue = await this.itemQueueRepo.findOne({ + relations: ['items'], + where: { + id: item_queue_id, + }, + }); + + const times = itemQueue.items.map((item) => item.play_estimation ?? 0); + const average = math.mean(times) * 60 * 1000; // change average minute to milliseconds + const queues = await this.repo.find({ + where: { + item_queue_id, + status: 'waiting', + }, + }); + + queueTimes[queues[0].id] = now; // first queue will be now + + for (let i = 1; i < queues.length; i++) { + const queue = queues[i]; + // duration will be total qty multiple by average + const duration = queue.qty * average; + + // time to call will be now + duration + const time = now + duration; + queueTimes[queue.id] = time; + + // update now to last call time + now = time; + } + + return queueTimes; + } + + async queueItems(item_queue_id: string[]): Promise { + return this.repo.find({ + relations: ['item', 'item.item', 'item.item.item_queue'], + where: { + item: { item: { item_queue: { id: In(item_queue_id) } } }, + }, + order: { + time: 'DESC', + }, + }); + } +} + +@Injectable() +export class QueueService extends BaseDataService { + constructor( + @InjectRepository(QueueModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + + @InjectRepository(QueueItemModel, CONNECTION_NAME.DEFAULT) + private item: Repository, + + @InjectRepository(ItemModel, CONNECTION_NAME.DEFAULT) + private itemMaster: Repository, + + @InjectDataSource(CONNECTION_NAME.DEFAULT) + private dataSource: DataSource, + ) { + super(repo); + } + + async queues(ids: string[]) { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + const playEstimations = {}; + const queuePeakLevel = {}; + + for (const id of ids) { + const est = await this.itemAverageTimeEstimation(id); + playEstimations[id] = est.average; + queuePeakLevel[id] = est.peakLevel; + } + + const queues = await this.repo.find({ + where: { + item_queue_id: In(ids), + time: Between(start, end), + }, + order: { + time: 'ASC', + }, + }); + + queues.forEach((queue) => { + queue.average = playEstimations[queue.item_queue_id]; + queue.peak_level = queuePeakLevel[queue.item_queue_id]; + }); + + return queues; + } + + async itemAverageTimeEstimation(item_queue_id: string) { + const items = await this.itemMaster.find({ + relations: ['item_queue'], + where: { + item_queue_id, + }, + }); + + const times = items.map((item) => item.play_estimation ?? 0); + const average = times.length > 0 ? math.mean(times) * 60 * 1000 : 0; // change average minute to milliseconds + const peakLevel = items[0]?.item_queue?.max_peak_level ?? 100; + return { average, peakLevel }; + } + + async getTicketItems(ticket_id: string, item_id: string) { + return this.item.findOneOrFail({ + relations: ['item'], + where: [ + { + ticket_id, + item_id, + }, + { + ticket_id, + item: { item_queue_id: item_id }, + }, + ], + }); + } + + async updateItemQty(item_id: string, qty: number): Promise { + const query = `UPDATE queue_items SET qty = qty - ${qty} WHERE id = '${item_id}'`; + this.dataSource.query(query); + } +} + +@Injectable() +export class QueueOrderService extends BaseDataService { + constructor( + @InjectRepository(QueueOrderModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + ) { + super(repo); + } +} diff --git a/src/modules/queue/data/services/ticket.service.ts b/src/modules/queue/data/services/ticket.service.ts new file mode 100644 index 0000000..ed78fc9 --- /dev/null +++ b/src/modules/queue/data/services/ticket.service.ts @@ -0,0 +1,276 @@ +import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { BaseDataService } from 'src/core/modules/data/service/base-data.service'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { Between, In, IsNull, Like, Not, Repository } from 'typeorm'; +import { QueueTicket } from '../../domain/entities/ticket.entity'; +import { + QueueItemModel, + QueueOrderModel, + QueueTicketModel, +} from '../models/queue.model'; +import { QueueOrder } from '../../domain/entities/order.entity'; +import * as moment from 'moment'; + +@Injectable() +export class TicketDataService extends BaseDataService { + constructor( + @InjectRepository(QueueTicketModel, CONNECTION_NAME.DEFAULT) + private repo: Repository, + + @InjectRepository(QueueOrderModel, CONNECTION_NAME.DEFAULT) + private order: Repository, + + @InjectRepository(QueueItemModel, CONNECTION_NAME.DEFAULT) + private item: Repository, + ) { + super(repo); + } + + async createQueueOrder(order: QueueOrder): Promise { + return await this.order.save(order); + } + + async updateQueueTicket(ticket: QueueTicket): Promise { + return await this.repo.save(ticket); + } + + async loginQueue(id: string): Promise { + return this.order.findOneOrFail({ + relations: ['tickets'], + where: [ + { transaction_id: id }, + { code: id, transaction_id: Not(IsNull()) }, + ], + }); + } + + async ticketByCode(code: string): Promise { + return this.repo.find({ + where: { + code: Like(`${code}%`), + }, + }); + } + + async ticketByUser(user: string, phone: string): Promise { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + + return this.repo.findOne({ + relations: ['items'], + where: { + customer: user, + phone: phone, + date: Between(start, end), + }, + }); + } + + async loginPhone(user: string, phone: string): Promise { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + + return this.order.findOneOrFail({ + relations: ['tickets'], + where: { + customer: user, + phone: `+${phone}`, + date: Between(start, end), + }, + }); + } + + async orders(order_id: string): Promise { + const order = await this.order.findOneOrFail({ + where: { + id: order_id, + }, + }); + + if (order.transaction_id != null) { + return this.order.find({ + where: { + code: order.code, + }, + }); + } + + return [order]; + } + + async transactions(transaction_id: string): Promise { + const order = await this.order.findOneOrFail({ + where: { + transaction_id, + }, + }); + + if (order.transaction_id != null) { + return this.order.find({ + where: { + code: order.code, + }, + }); + } + + return [order]; + } + + async deleteQueue(transaction_id: string): Promise { + try { + const transactions = await this.transactions(transaction_id); + await this.order.remove(transactions); + } catch (error) { + console.log('transaction not found'); + } + } + + async orderIds(order_id: string): Promise { + const orders = await this.orders(order_id); + + return orders.map((order) => order.id); + } + + async orderItems( + order_id: string, + item_ids: string[], + ): Promise { + const order = await this.orderIds(order_id); + try { + const ticket = await this.order.findOneOrFail({ + relations: ['tickets', 'tickets.items'], + where: { + tickets: { + order_id: In(order), + items: { + id: In(item_ids), + }, + }, + }, + }); + + return ticket; + } catch (error) { + throw new UnprocessableEntityException('Ticket tidak dapat ditemukan'); + } + } + + async queuePosTickets(order_id: string): Promise { + return this.repo.find({ + relations: [ + 'items', + 'items.queue', + 'items.item', + 'items.item.item_queue', + ], + where: { + order_id, + }, + }); + } + + async queueTickets(order_id: string): Promise { + const order = await this.orderIds(order_id); + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + return this.repo.find({ + relations: [ + 'items', + 'items.queue', + 'items.item', + 'items.item.item_queue', + ], + where: { + order_id: In(order), + date: Between(start, end), + }, + }); + } + + async queueUniqueTickets(order_id: string): Promise { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + return this.repo.find({ + relations: [ + 'items', + 'items.queue', + 'items.item', + 'items.item.item_queue', + ], + where: { + order_id: order_id, + date: Between(start, end), + }, + }); + } + + async queueTicketItems( + order_id: string, + ticket_id: string, + ): Promise { + const order = await this.orderIds(order_id); + return this.repo.find({ + relations: [ + 'items', + 'items.queue', + 'items.item', + 'items.item.item_queue', + ], + where: { + order_id: In(order), + id: ticket_id, + }, + }); + } + + async queueTicketActive( + order_id: string, + ticket_id: string, + ): Promise { + // const order = await this.orderIds(order_id); + return this.repo.find({ + relations: ['items', 'items.queue'], + where: { + // order_id: In(order), + id: ticket_id, + // items: { + // queue: { + // status: In(['waiting']), + // }, + // }, + }, + }); + } + + async queueItemTickets( + order_id: string, + item_id: string, + ): Promise { + const order = await this.orderIds(order_id); + return this.repo.find({ + relations: [ + 'items', + 'items.queue', + 'items.item', + 'items.item.item_queue', + ], + where: { + order_id: In(order), + items: [{ item_id }, { item: { item_queue: { id: item_id } } }], + }, + }); + } + + async queueItems(order_id: string): Promise { + const order = await this.orderIds(order_id); + return this.item.find({ + relations: ['queue', 'ticket'], + where: { + ticket: { + order_id: In(order), + }, + }, + }); + } +} diff --git a/src/modules/queue/domain/entities/filter.entity.ts b/src/modules/queue/domain/entities/filter.entity.ts new file mode 100644 index 0000000..3bd24f5 --- /dev/null +++ b/src/modules/queue/domain/entities/filter.entity.ts @@ -0,0 +1,4 @@ +export interface FilterQueueEntity { + vip: boolean; + status: string[]; +} diff --git a/src/modules/queue/domain/entities/order.entity.ts b/src/modules/queue/domain/entities/order.entity.ts new file mode 100644 index 0000000..32f843b --- /dev/null +++ b/src/modules/queue/domain/entities/order.entity.ts @@ -0,0 +1,11 @@ +import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity'; +import { QueueTicket } from './ticket.entity'; + +export interface QueueOrder extends BaseCoreEntity { + code: string; + customer: string; + phone: string; + date: number; + transaction_id: string; + tickets: QueueTicket[]; +} diff --git a/src/modules/queue/domain/entities/queue-bucket.entity.ts b/src/modules/queue/domain/entities/queue-bucket.entity.ts new file mode 100644 index 0000000..8cda117 --- /dev/null +++ b/src/modules/queue/domain/entities/queue-bucket.entity.ts @@ -0,0 +1,7 @@ +export interface QueueBucket { + id: string; + queue_item_id: string; + date: number; + regular: number; + vip: number; +} diff --git a/src/modules/queue/domain/entities/queue-item.entity.ts b/src/modules/queue/domain/entities/queue-item.entity.ts new file mode 100644 index 0000000..36b434d --- /dev/null +++ b/src/modules/queue/domain/entities/queue-item.entity.ts @@ -0,0 +1,21 @@ +import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity'; +import { ItemEntity } from 'src/modules/item-related/item/domain/entities/item.entity'; +import { QueueTicket } from './ticket.entity'; +import { QueueModel } from '../../data/models/queue.model'; + +export interface QueueItem extends BaseCoreEntity { + ticket?: QueueTicket; + item?: ItemEntity; + item_id: string; + qty: number; +} + +export interface MergedItemQueue extends BaseCoreEntity { + id: string; + queue_item_id: string; + item_id: string; + title: string; + image_url: string; + qty: number; + queues: QueueModel[]; +} diff --git a/src/modules/queue/domain/entities/queue.entity.ts b/src/modules/queue/domain/entities/queue.entity.ts new file mode 100644 index 0000000..fea1945 --- /dev/null +++ b/src/modules/queue/domain/entities/queue.entity.ts @@ -0,0 +1,12 @@ +import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity'; +import { QueueItem } from './queue-item.entity'; + +export interface Queue extends BaseCoreEntity { + code: string; + qty: number; + status: string; + time: number; + vip: boolean; + call_time: number; + item: QueueItem; +} diff --git a/src/modules/queue/domain/entities/ticket.entity.ts b/src/modules/queue/domain/entities/ticket.entity.ts new file mode 100644 index 0000000..e5f60d5 --- /dev/null +++ b/src/modules/queue/domain/entities/ticket.entity.ts @@ -0,0 +1,14 @@ +import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity'; +import { QueueOrder } from './order.entity'; +import { QueueItem } from './queue-item.entity'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface QueueTicket extends BaseCoreEntity { + code: string; + customer: string; + phone: string; + date: number; + order?: QueueOrder; + order_id?: string; + items: QueueItem[]; +} diff --git a/src/modules/queue/domain/helpers/time.helper.ts b/src/modules/queue/domain/helpers/time.helper.ts new file mode 100644 index 0000000..88f094e --- /dev/null +++ b/src/modules/queue/domain/helpers/time.helper.ts @@ -0,0 +1,11 @@ +import * as moment from 'moment'; + +export function toTime(timestamp: number): string { + const date = moment.unix(timestamp / 1000).add(7, 'hours'); + + const hours = date.hours(); + const minutes = date.minutes(); + return `${hours < 10 ? '0' : ''}${hours}:${ + minutes < 10 ? '0' : '' + }${minutes}`; +} diff --git a/src/modules/queue/domain/queue-admin.orchestrator.ts b/src/modules/queue/domain/queue-admin.orchestrator.ts new file mode 100644 index 0000000..30d6b4d --- /dev/null +++ b/src/modules/queue/domain/queue-admin.orchestrator.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { QueueDataService, QueueService } from '../data/services/queue.service'; +import { IndexQueueManager } from './usecases/index-queue.manager'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +import { Queue } from './entities/queue.entity'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { + CallQueueManager, + DoneQueueManager, +} from './usecases/call-queue.manager'; +import { + ORDER_TYPE, + QUEUE_STATUS, +} from 'src/core/strings/constants/base.constants'; + +@Injectable() +export class QueueAdminOrchestrator { + constructor( + private readonly dataService: QueueDataService, + private readonly service: QueueService, + private indexManager: IndexQueueManager, + private callManager: CallQueueManager, + private doneManager: DoneQueueManager, + ) {} + + async index(params): Promise> { + this.indexManager.setFilterParam({ order_type: ORDER_TYPE.ASC, ...params }); + this.indexManager.setService(this.dataService, TABLE_NAME.QUEUE); + await this.indexManager.execute(); + return this.indexManager.getResult(); + } + + async call(dataId): Promise { + this.callManager.setData(dataId, QUEUE_STATUS.CALLED); + this.callManager.setService(this.service, TABLE_NAME.QUEUE); + await this.callManager.execute(); + return this.callManager.getResult(); + } + + async done(dataId): Promise { + this.doneManager.setData(dataId, QUEUE_STATUS.DONE); + this.doneManager.setService(this.service, TABLE_NAME.QUEUE); + await this.doneManager.execute(); + return this.doneManager.getResult(); + } +} diff --git a/src/modules/queue/domain/queue.orchestrator.ts b/src/modules/queue/domain/queue.orchestrator.ts new file mode 100644 index 0000000..112af5b --- /dev/null +++ b/src/modules/queue/domain/queue.orchestrator.ts @@ -0,0 +1,232 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { TicketDataService } from '../data/services/ticket.service'; +import { QueueOrder } from './entities/order.entity'; +import { Queue } from './entities/queue.entity'; +import { + QueueDataService, + QueueOrderService, + QueueService, +} from '../data/services/queue.service'; +import { RegisterQueueManager } from './usecases/register-queue.manager'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { RegisterQueueDto } from '../infrastructure/controllers/dto/register-queue.dto'; +import { CustomerQueueSummaryManager } from './usecases/queue/customer-queue-summary.manager'; +import { CustomerQueueDetailManager } from './usecases/queue/customer-queue-detail.manager'; +import { CustomerQueueItemManager } from './usecases/queue/customer-queue-item.manager'; +import { CustomerQueueItemListManager } from './usecases/queue/customer-queue-item-list.manager'; +import { CustomerQueueListManager } from './usecases/queue/customer-queue-list.manager'; +import { SplitQueueDto } from '../infrastructure/controllers/dto/split-queue.dto'; +import { SplitQueueManager } from './usecases/split-queue.manager'; +import { LoginQueueDto } from '../infrastructure/controllers/dto/login-queue.dto'; +import * as moment from 'moment'; +import { CustomerQueueTicketSummaryManager } from './usecases/queue/customer-queue-ticket.manager'; +import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service'; +import { CustomerQueuePosItemManager } from './usecases/queue/customer-queue-pos-item.manager'; +import { QueueOrderModel, QueueTicketModel } from '../data/models/queue.model'; +import { CustomerQueueRecommendManager } from './usecases/queue/customer-queue-recommend.manager'; + +@Injectable() +export class QueueOrchestrator { + constructor( + public readonly dataService: TicketDataService, + public readonly transactionService: TransactionDataService, + private readonly queueService: QueueService, + private readonly queueOrderService: QueueOrderService, + private readonly registerQueueManager: RegisterQueueManager, + private readonly splitQueueManager: SplitQueueManager, + private readonly queueDataService: QueueDataService, + ) {} + + async loginCustomer(id: string): Promise { + try { + const order = await this.dataService.loginQueue(id); + const code = order.tickets[0].code ?? order.code; + const tickets = await this.dataService.ticketByCode(code); + order.tickets = tickets; + return order; + } catch (error) { + throw new UnauthorizedException({ + message: 'Invoice tidak ditemukan', + error: 'INVOICE_NOT_FOUND', + }); + } + } + + async loginPhone(login: LoginQueueDto): Promise { + const { name, phone } = login; + const now = moment().format('DD/MM/YYYY'); + try { + const order = await this.dataService.loginPhone(name, phone); + const code = order.tickets[0].code ?? order.code; + const tickets = await this.dataService.ticketByCode(code); + order.tickets = tickets; + return order; + } catch (error) { + throw new UnauthorizedException({ + message: `Antrian atas nama ${name} dan nomor telepon ${phone} pada tanggal ${now} tidak ditemukan`, + error: 'INVOICE_NOT_FOUND', + }); + } + } + + async create(data: RegisterQueueDto): Promise { + const queue = await this.queueService.getTicketItems( + data.ticket_id, + data.item_id, + ); + const queueRequest: any = { + qty: data.qty, + item_id: queue.id, + }; + this.registerQueueManager.setData(queueRequest); + this.registerQueueManager.setService(this.queueService, TABLE_NAME.QUEUE); + await this.registerQueueManager.execute(); + return this.registerQueueManager.getResult(); + } + + async split(data: SplitQueueDto): Promise { + const queueIds = data.items.map((i) => i.queue_item_id); + const queue = await this.dataService.orderItems(data.order_id, queueIds); + + const existTicket = await this.dataService.ticketByUser( + data.name, + data.phone, + ); + + if (existTicket) { + const itemTickets = this.itemsFromOrder(queue); + + const items = data.items.map((item) => { + const item_id = itemTickets[item.queue_item_id]; + return { + item_queue_id: item_id, + item_id: item_id, + qty: item.qty, + }; + }); + existTicket.items.push(...items); + await this.dataService.updateQueueTicket(existTicket); + + data.items.forEach((item) => { + this.queueService.updateItemQty(item.queue_item_id, item.qty); + }); + + return queue; + } + + this.splitQueueManager.setRequestData(data); + this.splitQueueManager.setData(queue); + this.splitQueueManager.setService( + this.queueOrderService, + TABLE_NAME.QUEUE_ORDER, + ); + await this.splitQueueManager.execute(); + return this.splitQueueManager.getResult(); + } + + itemsFromOrder(order: QueueOrderModel) { + const itemTickets = {}; + order.tickets.forEach((ticket) => { + ticket.items.forEach((item) => { + itemTickets[item.id] = item.item_id; + }); + }); + + return itemTickets; + } + + async queuePOSTickets(order_id: string): Promise { + const tickets = await this.dataService.queuePosTickets(order_id); + const manager = new CustomerQueuePosItemManager(tickets); + return manager.data; + } + + async queueTickets(order_id: string): Promise { + const tickets = await this.dataService.queueTickets(order_id); + const manager = new CustomerQueueSummaryManager(tickets); + return manager.data; + } + + async queueTicketDetail(order_id: string, ticket_id: string): Promise { + const tickets = await this.dataService.queueTicketItems( + order_id, + ticket_id, + ); + + const queueItemIds = this.getQueueItemFromTickets(tickets); + const queues = await this.queueService.queues(queueItemIds); + const manager = new CustomerQueueDetailManager(tickets); + manager.currentQueues(queues); + return manager.data; + } + + async queueTicketSummary(order_id: string, ticket_id: string): Promise { + const tickets = await this.dataService.queueTicketActive( + order_id, + ticket_id, + ); + + const manager = new CustomerQueueTicketSummaryManager(tickets); + return manager.data; + } + + async queueItemDetail(order_id: string, item_id: string): Promise { + const tickets = await this.dataService.queueItemTickets(order_id, item_id); + const manager = new CustomerQueueItemManager(tickets); + return manager.data; + } + + async queueItemRecommendation(order_id: string): Promise { + const tickets = await this.dataService.queueTickets(order_id); + const queueItemIds = this.getQueueItemFromTickets(tickets); + const recommendItems = await this.queueDataService.exclude(queueItemIds); + const recommendIds = recommendItems.map((item) => item.id); + const queues = await this.queueService.queues(recommendIds); + const manager = new CustomerQueueRecommendManager(tickets); + manager.currentQueues(queues); + return manager.recommend(recommendItems); + } + + async queueItemSummary(): Promise { + const recommendItems = await this.queueDataService.allQueue(); + const recommendIds = recommendItems.map((item) => item.id); + const queues = await this.queueService.queues(recommendIds); + const manager = new CustomerQueueRecommendManager(null); + manager.currentQueues(queues); + return manager.recommend(recommendItems); + } + + async queueItems(order_id: string): Promise { + const tickets = await this.dataService.queueTickets(order_id); + const queueItemIds = this.getQueueItemFromTickets(tickets); + const queues = await this.queueService.queues(queueItemIds); + const manager = new CustomerQueueItemListManager(tickets); + manager.currentQueues(queues); + return manager.data; + } + + async queueUniqueItems(order_id: string): Promise { + const tickets = await this.dataService.queueUniqueTickets(order_id); + const queueItemIds = this.getQueueItemFromTickets(tickets); + const queues = await this.queueService.queues(queueItemIds); + const manager = new CustomerQueueItemListManager(tickets); + manager.currentQueues(queues); + return manager.data; + } + + async queueOrderItems(order_id: string): Promise { + const tickets = await this.dataService.queueTickets(order_id); + const manager = new CustomerQueueListManager(tickets); + return manager.data; + } + + getQueueItemFromTickets(tickets: QueueTicketModel[]) { + const queueItemIds = tickets.map((ticket) => { + return ticket.items.map((item) => { + return item.item.item_queue_id; + }); + }); + + return queueItemIds.flat(); + } +} diff --git a/src/modules/queue/domain/usecases/call-queue.manager.ts b/src/modules/queue/domain/usecases/call-queue.manager.ts new file mode 100644 index 0000000..edce656 --- /dev/null +++ b/src/modules/queue/domain/usecases/call-queue.manager.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager'; +import { + EventTopics, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { Queue } from '../entities/queue.entity'; +import { QueueModel } from '../../data/models/queue.model'; + +@Injectable() +export class CallQueueManager extends BaseUpdateStatusManager { + getResult(): string { + return `Success call Queue ${this.result.code}`; + } + + async validateProcess(): Promise { + return; + } + + async beforeProcess(): Promise { + this.data.call_time = new Date().getTime(); + return; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get entityTarget(): any { + return QueueModel; + } + + get eventTopics(): EventTopics[] { + return []; + } +} + +@Injectable() +export class DoneQueueManager extends CallQueueManager { + async beforeProcess(): Promise { + return; + } +} diff --git a/src/modules/queue/domain/usecases/formula/queue-condition.formula.ts b/src/modules/queue/domain/usecases/formula/queue-condition.formula.ts new file mode 100644 index 0000000..c3599c6 --- /dev/null +++ b/src/modules/queue/domain/usecases/formula/queue-condition.formula.ts @@ -0,0 +1,84 @@ +import { QueueModel } from 'src/modules/queue/data/models/queue.model'; +import { toTime } from '../../helpers/time.helper'; +import * as math from 'mathjs'; +import { QueueTimeFormula } from './queue-time.formula'; +import * as moment from 'moment'; + +export class QueueCondition { + private ticketItems = {}; + constructor(readonly items: QueueModel[]) { + items.forEach((item) => { + const item_id = item.item_queue_id; + const currentItem = this.ticketItems[item_id]; + this.ticketItems[item_id] = currentItem ? [...currentItem, item] : [item]; + }); + } + + condition(item_id: string) { + const queues: QueueModel[] = this.ticketItems[item_id] ?? []; + const playEstimation = queues[0]?.average ?? 0; + const peakLevel = queues[0]?.peak_level ?? 100; + const [time, last] = this.queueTime(queues, playEstimation); + const nearest = time ? toTime(time) : 0; + const lastTime = last ? toTime(last + playEstimation) : 0; + + const queuePeople = this.queuePeople(queues, peakLevel); + + return { + available: queuePeople == 0, + average: this.averageTime(queues), + nearest: nearest, + crowded_level: queuePeople, + available_time: lastTime, + }; + } + + queuePeople(queues: QueueModel[], peakLevel): number { + const queue = this.activeQueue(queues); + const level = peakLevel / 100; + const queuePeople = queue.reduce((acc, q) => { + return acc + q.qty; + }, 0); + return queuePeople * level; + } + + activeQueue(queues: QueueModel[]) { + return queues.filter((q) => q.status == 'waiting'); + } + + queueTime(queues: QueueModel[], average = 0): number[] { + const calledQueue = queues.filter((q) => + ['called', 'done'].includes(q.status), + ); + const lastCalledQueue = calledQueue[calledQueue.length - 1]; + const activeQueues = this.activeQueue(queues); + + const queueTimes = QueueTimeFormula.queueTime( + activeQueues, + lastCalledQueue, + average, + ); + + const queueEstimation = Object.values(queueTimes); + + const first = queueEstimation[0] ?? moment().valueOf(); + const last = queueEstimation[queueEstimation.length - 1] ?? 0; + return [first, last]; + } + + averageTime(queues: QueueModel[]) { + if (queues.length == 0) return 0; + const calledQueue = queues.filter((q) => + ['called', 'done'].includes(q.status), + ); + + if (calledQueue.length < 1) return 0; + + const times = calledQueue.map((queue) => { + return (queue.call_time - queue.time) / 1000; + }); + + const avg = Math.ceil(math.mean(times)); + return Math.max(0, avg); + } +} diff --git a/src/modules/queue/domain/usecases/formula/queue-time.formula.ts b/src/modules/queue/domain/usecases/formula/queue-time.formula.ts new file mode 100644 index 0000000..3cb72b3 --- /dev/null +++ b/src/modules/queue/domain/usecases/formula/queue-time.formula.ts @@ -0,0 +1,73 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { ItemQueueModel } from 'src/modules/item-related/item-queue/data/models/item-queue.model'; +import { QueueDataService } from 'src/modules/queue/data/services/queue.service'; +import { Repository } from 'typeorm'; +import * as moment from 'moment'; +import * as math from 'mathjs'; +import { QueueModel } from 'src/modules/queue/data/models/queue.model'; + +export class QueueTimeFormula { + constructor( + private readonly queueDataService: QueueDataService, + @InjectRepository(ItemQueueModel, CONNECTION_NAME.DEFAULT) + private itemQueueRepo: Repository, + ) {} + + public average = 0; + + async items(item_queue_id: string) { + const queues = await this.queueDataService.waitingQueue(item_queue_id); + if (queues.length == 0) { + return {}; + } + + const itemQueue = await this.itemQueueRepo.findOne({ + relations: ['items'], + where: { + id: item_queue_id, + }, + }); + + const times = itemQueue.items.map((item) => item.play_estimation ?? 0); + const average = times.length > 0 ? math.mean(times) * 60 * 1000 : 0; // change average minute to milliseconds + this.average = average; + + const calledQueue = await this.queueDataService.lastQueue(item_queue_id); + + return QueueTimeFormula.queueTime(queues, calledQueue, average); + } + + static queueTime( + queues: QueueModel[], + lastQueue: QueueModel, + average: number, + ) { + const queueTimes = {}; + if (queues.length < 1) return queueTimes; + const timeNow = moment().valueOf(); + const lastCall = lastQueue?.call_time ?? timeNow - average; + + const callTime = +lastCall + average; + const currentQueueCallTime = timeNow > callTime ? timeNow : callTime; + + queueTimes[queues[0].id] = currentQueueCallTime; + let now = currentQueueCallTime; + + for (let i = 1; i < queues.length; i++) { + const before = queues[i - 1]; + // duration will be total qty multiple by average from last queue + const duration = before.qty * average; + + const queue = queues[i]; + // time to call will be now + duration + const time = now + duration; + queueTimes[queue.id] = time; + + // update now to last call time + now = time; + } + + return queueTimes; + } +} diff --git a/src/modules/queue/domain/usecases/index-queue.manager.ts b/src/modules/queue/domain/usecases/index-queue.manager.ts new file mode 100644 index 0000000..a606a2b --- /dev/null +++ b/src/modules/queue/domain/usecases/index-queue.manager.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@nestjs/common'; +import { BaseIndexManager } from 'src/core/modules/domain/usecase/managers/base-index.manager'; +import { SelectQueryBuilder } from 'typeorm'; +import { + Param, + RelationParam, +} from 'src/core/modules/domain/entities/base-filter.entity'; +import { Queue } from '../entities/queue.entity'; +import * as moment from 'moment'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +import { QueueTimeFormula } from './formula/queue-time.formula'; + +@Injectable() +export class IndexQueueManager extends BaseIndexManager { + constructor(private readonly queueTimeFormula: QueueTimeFormula) { + super(); + } + + private queueTimes = {}; + async prepareData(): Promise { + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + this.queueTimes = await this.queueTimeFormula.items( + this.filterParam.queue_id, + ); + return; + } + + get relations(): RelationParam { + return { + joinRelations: [], + selectRelations: ['item', 'item.ticket'], + countRelations: [], + }; + } + + getResult(): PaginationResponse { + const result = this.result; + + return { + total: result.total, + data: result.data.map((queue) => { + return { + ...queue, + estimation: this.queueTimes[queue.id], + }; + }), + }; + } + + get selects(): string[] { + return [ + `${this.tableName}.id`, + `${this.tableName}.code`, + `${this.tableName}.status`, + `${this.tableName}.time`, + `${this.tableName}.call_time`, + `${this.tableName}.vip`, + `${this.tableName}.item`, + `${this.tableName}.qty`, + `${this.tableName}.created_at`, + + `item.id`, + `ticket.customer`, + `ticket.phone`, + ]; + } + + get specificFilter(): Param[] { + return [ + { + cols: `${this.tableName}.status`, + data: this.filterParam.status, + }, + ]; + } + + setQueryFilter( + queryBuilder: SelectQueryBuilder, + ): SelectQueryBuilder { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + + console.log({ start, end, item_queue_id: this.filterParam.queue_id }); + + queryBuilder.andWhere(`${this.tableName}.time BETWEEN :start AND :end`, { + start, + end, + }); + + queryBuilder.andWhere(`${this.tableName}.item_queue_id = :item_queue_id`, { + item_queue_id: this.filterParam.queue_id, + }); + + if (this.filterParam.vip != null) { + queryBuilder.andWhere(`${this.tableName}.vip = :vip`, { + vip: this.filterParam.vip, + }); + } + return queryBuilder; + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-detail.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-detail.manager.ts new file mode 100644 index 0000000..c656de8 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-detail.manager.ts @@ -0,0 +1,48 @@ +import { QueueModel } from 'src/modules/queue/data/models/queue.model'; +import { CustomerQueueManager } from './customer-queue.manager'; +import { QueueCondition } from '../formula/queue-condition.formula'; + +export class CustomerQueueDetailManager extends CustomerQueueManager { + private queues: QueueModel[] = []; + currentQueues(queues: QueueModel[]) { + this.queues = queues; + } + get data() { + const queueCondition = new QueueCondition(this.queues); + return this.tickets.map((ticket) => { + const queueItems = this.mergeQueueItems(ticket); + return { + id: ticket.id, + code: ticket.code, + customer: ticket.customer, + phone: ticket.phone, + date: ticket.date, + summary: this.summaryTicket(ticket), + items: queueItems.map((item) => { + return { + id: item.id, + item_queue_id: item.queue_item_id, + title: item.title, + image_url: item.image_url, + summary: { + total_tickets: item.qty, + total_used: this.totalUsedItems(item.queues), + total_queue: this.totalQueueItems(item.queues), + }, + queue: item.queues + .sort((a, b) => b.time - a.time) + .map((q) => { + return { + code: q.code, + qty: q.qty, + time: this.toTime(q.time), + status: q.status, + }; + }), + queue_condition: queueCondition.condition(item.queue_item_id), + }; + }), + }; + }); + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-item-list.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-item-list.manager.ts new file mode 100644 index 0000000..23431a6 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-item-list.manager.ts @@ -0,0 +1,62 @@ +import { + QueueItemModel, + QueueModel, +} from 'src/modules/queue/data/models/queue.model'; +import { CustomerQueueManager } from './customer-queue.manager'; +import { QueueCondition } from '../formula/queue-condition.formula'; + +export class CustomerQueueItemListManager extends CustomerQueueManager { + protected queues: QueueModel[] = []; + currentQueues(queues: QueueModel[]) { + this.queues = queues; + } + get data() { + const tickets = this.tickets; + const ticketCount = {}; + const queueCondition = new QueueCondition(this.queues); + + const ticketItems = {}; + tickets.forEach((ticket) => { + const itemsInTicket = []; + ticket.items.forEach((item) => { + const item_id = item.item.item_queue?.id ?? item.item.id; + const currentItem = ticketItems[item_id]; + ticketItems[item_id] = currentItem ? [...currentItem, item] : [item]; + itemsInTicket.push(item_id); + }); + + const uniqueTicket = Array.from(new Set(itemsInTicket)); + + uniqueTicket.forEach((item_id) => { + ticketCount[item_id] = ticketCount[item_id] + ? ticketCount[item_id] + 1 + : 1; + }); + }); + + return Object.values(ticketItems).map((items) => { + const item = items[0]; + const item_qty = items.reduce((acc, item) => acc + item.qty, 0); + const queue_qty = this.queueItemQty(items); + const queueItem = item.item.item_queue ?? item.item; + return { + id: queueItem.id, + queue_item_id: item.id, + title: queueItem.name, + image_url: item.item.image_url, + qty: item_qty - queue_qty, + ticket: ticketCount[queueItem.id], + + queue_condition: queueCondition.condition(queueItem.id), + ...queueCondition.condition(queueItem.id), + }; + }); + } + + protected queueItemQty(queues: QueueItemModel[]) { + return queues.reduce( + (acc, item) => acc + item.queue.reduce((acc, q) => acc + q.qty, 0), + 0, + ); + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-item.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-item.manager.ts new file mode 100644 index 0000000..68e9682 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-item.manager.ts @@ -0,0 +1,30 @@ +import { CustomerQueueManager } from './customer-queue.manager'; + +export class CustomerQueueItemManager extends CustomerQueueManager { + get data() { + return this.tickets.map((ticket) => { + const queueItems = this.mergeQueueItems(ticket); + const item = ticket.items[0]; + + return { + id: ticket.id, + code: ticket.code, + customer: ticket.customer, + phone: ticket.phone, + date: ticket.date, + image_url: item.item.image_url, + summary: this.summaryTicket(ticket), + queue: queueItems[0].queues + .sort((a, b) => b.time - a.time) + .map((q) => { + return { + code: q.code, + qty: q.qty, + time: this.toTime(q.time), + status: q.status, + }; + }), + }; + }); + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-list.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-list.manager.ts new file mode 100644 index 0000000..88e36c5 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-list.manager.ts @@ -0,0 +1,80 @@ +import { + QueueModel, + QueueTicketModel, +} from 'src/modules/queue/data/models/queue.model'; +import { CustomerQueueManager } from './customer-queue.manager'; + +export class CustomerQueueListManager extends CustomerQueueManager { + private ticketToQueues(tickets: QueueTicketModel[]): QueueModel[] { + const ticket = tickets.map((ticket) => { + const queue = ticket.items.map((item) => { + return item.queue; + }); + + return queue.flat(); + }); + + return ticket.flat().sort((a, b) => b.time - a.time); + } + + get data() { + const tickets = this.tickets; + const ticketItems = {}; + + tickets.forEach((ticket) => { + ticket.items.forEach((item) => { + const item_id = item.item.item_queue?.id ?? item.item.id; + const currentItem = ticketItems[item_id]; + const ticketQueue = { ...ticket, items: [item] }; + + ticketItems[item_id] = currentItem + ? [...currentItem, ticketQueue] + : [ticketQueue]; + }); + }); + + return Object.keys(ticketItems) + .map((item_id) => { + const tickets: QueueTicketModel[] = ticketItems[item_id]; + const item = tickets[0].items[0]; + const uniqueTicket = tickets.filter( + (obj1, i, arr) => arr.findIndex((obj2) => obj2.id === obj1.id) === i, + ); + let totalQueue = 0; + + const currentTickets = uniqueTicket.map((ticket) => { + const currentTicket = tickets.filter((t) => t.id === ticket.id); + const queues = this.ticketToQueues(currentTicket); + const currentQueues = queues.map((q) => { + return { + code: q.code, + qty: q.qty, + time: this.toTime(q.time), + status: q.status, + }; + }); + totalQueue += currentQueues.length; + return currentQueues.length > 0 + ? { + code: ticket.code, + customer: ticket.customer, + phone: ticket.phone, + id: ticket.id, + queue: currentQueues, + } + : null; + }); + + return currentTickets.filter(Boolean).length > 0 + ? { + id: item.item_id, + title: item.item.item_queue?.name ?? item.item.name, + image_url: item.item.image_url, + qty: totalQueue, + items: currentTickets.filter(Boolean), + } + : null; + }) + .filter(Boolean); + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-pos-item.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-pos-item.manager.ts new file mode 100644 index 0000000..663f68d --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-pos-item.manager.ts @@ -0,0 +1,30 @@ +import { CustomerQueueManager } from './customer-queue.manager'; + +export class CustomerQueuePosItemManager extends CustomerQueueManager { + get data() { + const ticket = this.tickets[0]; + const queueItems = this.mergeQueueItems(ticket); + return { + id: ticket.id, + code: ticket.code, + customer: ticket.customer, + phone: ticket.phone, + date: ticket.date, + summary: this.summaryTicket(ticket), + items: queueItems.map((item) => { + return { + id: item.id, + item_queue_id: item.item_id, + title: item.title, + image_url: item.image_url, + summary: { + total_tickets: item.qty, + total_used: this.totalUsedItems(item.queues), + total_queue: this.totalQueueItems(item.queues), + remaining: item.qty - this.totalQueueItems(item.queues), + }, + }; + }), + }; + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-recommend.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-recommend.manager.ts new file mode 100644 index 0000000..f1273a9 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-recommend.manager.ts @@ -0,0 +1,21 @@ +import { QueueCondition } from '../formula/queue-condition.formula'; +import { CustomerQueueItemListManager } from './customer-queue-item-list.manager'; +import { ItemQueueModel } from 'src/modules/item-related/item-queue/data/models/item-queue.model'; + +export class CustomerQueueRecommendManager extends CustomerQueueItemListManager { + recommend(items: ItemQueueModel[]) { + const queueCondition = new QueueCondition(this.queues); + + return items.map((queueItem) => { + const item = queueItem.items[0]; + return { + id: item.id, + queue_item_id: queueItem.id, + title: queueItem.name, + image_url: item.image_url, + + queue_condition: queueCondition.condition(queueItem.id), + }; + }); + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-summary.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-summary.manager.ts new file mode 100644 index 0000000..342baf3 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-summary.manager.ts @@ -0,0 +1,16 @@ +import { CustomerQueueManager } from './customer-queue.manager'; + +export class CustomerQueueSummaryManager extends CustomerQueueManager { + get data() { + return this.tickets.map((ticket) => { + return { + id: ticket.id, + code: ticket.code, + customer: ticket.customer, + phone: ticket.phone, + date: ticket.date, + summary: this.summaryTicket(ticket), + }; + }); + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-ticket.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-ticket.manager.ts new file mode 100644 index 0000000..d38c691 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue-ticket.manager.ts @@ -0,0 +1,18 @@ +import { CustomerQueueManager } from './customer-queue.manager'; + +export class CustomerQueueTicketSummaryManager extends CustomerQueueManager { + get data() { + const tickets = this.tickets.map((ticket) => { + return { + id: ticket.id, + code: ticket.code, + customer: ticket.customer, + phone: ticket.phone, + date: ticket.date, + summary: this.summaryTicket(ticket), + }; + }); + + return tickets[0]; + } +} diff --git a/src/modules/queue/domain/usecases/queue/customer-queue.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue.manager.ts new file mode 100644 index 0000000..0219db7 --- /dev/null +++ b/src/modules/queue/domain/usecases/queue/customer-queue.manager.ts @@ -0,0 +1,118 @@ +import { + QueueItemModel, + QueueModel, + QueueTicketModel, +} from '../../../data/models/queue.model'; +import { MergedItemQueue } from '../../entities/queue-item.entity'; +import { toTime } from '../../helpers/time.helper'; + +export class CustomerQueueManager { + constructor(protected readonly tickets: QueueTicketModel[]) {} + get data(): any { + return this.tickets; + } + + toTime(timestamp: number): string { + return toTime(timestamp); + } + + totalActivities(ticket: QueueTicketModel): number { + const ticketItems = {}; + ticket.items.forEach((item) => { + const item_id = + item.item?.item_queue?.id ?? item.item?.id ?? item.item_id; + const currentItem = ticketItems[item_id]; + ticketItems[item_id] = currentItem ? [...currentItem, item] : [item]; + }); + return Object.keys(ticketItems).length; + } + + totalTickets(ticket: QueueTicketModel): number { + return ticket.items.reduce((acc, item) => acc + item.qty, 0); + } + + totalUsedItems(queues: QueueModel[]): number { + return queues + .filter((q) => ['done', 'called'].includes(q.status)) + .reduce((acc, item) => acc + item.qty, 0); + } + + totalUsedTickets(ticket: QueueTicketModel): number { + const tickets = ticket.items.map((item) => { + return this.totalUsedItems(item.queue); + }); + + const reducer = (accumulator, currentValue) => accumulator + currentValue; + return tickets.reduce(reducer, 0); + } + + totalQueueItems(queues: QueueModel[]): number { + return queues + .filter((q) => ['waiting'].includes(q.status)) + .reduce((acc, item) => acc + item.qty, 0); + } + + totalQueueTickets(ticket: QueueTicketModel): number { + const tickets = ticket.items.map((item) => { + return this.totalQueueItems(item.queue); + }); + + const reducer = (accumulator, currentValue) => accumulator + currentValue; + return tickets.reduce(reducer, 0); + } + + totalQueueItemTickets(ticket: QueueTicketModel): number { + const tickets = ticket.items + .map((item) => { + return this.totalQueueItems(item.queue); + }) + .filter((item) => item > 0); + + return tickets.length; + } + + summaryTicket(ticket: QueueTicketModel): any { + const total_tickets = this.totalTickets(ticket); + const total_used = this.totalUsedTickets(ticket); + const total_queue = this.totalQueueTickets(ticket); + const total_queue_item = this.totalQueueItemTickets(ticket); + return { + total_activities: this.totalActivities(ticket), + total_tickets, + total_used, + total_queue, + total_queue_item, + remaining: total_tickets - total_queue, + }; + } + + mergeQueueItems(ticket: QueueTicketModel): MergedItemQueue[] { + const ticketItems = {}; + ticket.items.forEach((item) => { + const item_id = item.item.item_queue?.id ?? item.item.id; + const currentItem = ticketItems[item_id]; + ticketItems[item_id] = currentItem ? [...currentItem, item] : [item]; + }); + + return Object.values(ticketItems).map((items) => { + const item = items[0]; + const queues: QueueModel[] = []; + + items.forEach((item) => { + queues.push(...item.queue); + }); + + const item_qty = items.reduce((acc, item) => acc + item.qty, 0); + const queueItem = item.item.item_queue ?? item.item; + return { + id: item.item_id, + queue_item_id: queueItem.id, + item_id: item.id, + title: queueItem.name, + image_url: item.item.image_url, + qty: item_qty, + queues, + }; + }); + } +} diff --git a/src/modules/queue/domain/usecases/register-queue.manager.ts b/src/modules/queue/domain/usecases/register-queue.manager.ts new file mode 100644 index 0000000..90ec7d6 --- /dev/null +++ b/src/modules/queue/domain/usecases/register-queue.manager.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { + EventTopics, + columnUniques, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { BaseCreateManager } from 'src/core/modules/domain/usecase/managers/base-create.manager'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { QueueItemModel, QueueModel } from '../../data/models/queue.model'; +import { padCode } from 'src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper'; +import { QueueBucketReadService } from '../../data/services/queue-bucket'; +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; +import { QueueTimeFormula } from './formula/queue-time.formula'; +import * as moment from 'moment'; + +@Injectable() +export class RegisterQueueManager extends BaseCreateManager { + constructor( + private readonly bucketService: QueueBucketReadService, + private readonly queueTimeFormula: QueueTimeFormula, + ) { + super(); + } + + async averageTime(): Promise { + const item = await this.getItemMaster(); + return item.play_estimation; + } + + async queueTime(queue_id: string): Promise { + const queueTimes = await this.queueTimeFormula.items(queue_id); + const queues = Object.values(queueTimes); + + const first = queues[0]; + const last = queues[queues.length - 1] ?? moment().valueOf(); + const average = this.queueTimeFormula.average; + return [first, last + average]; + } + + async beforeProcess(): Promise { + const vip = this.data.vip ?? false; + const item = await this.getItemMaster(); + const [, end] = await this.queueTime(item.item_queue_id); + + const queueNumber = await this.bucketService.getQueue( + item.item_queue_id, + vip, + ); + const prefix = vip ? 'B' : 'A'; + const code = `${prefix}${padCode(queueNumber)}`; + + Object.assign(this.data, { + status: STATUS.WAITING, + time: end, + item_queue_id: item.item_queue_id, + vip, + code, + }); + return; + } + + async getItemMaster(): Promise { + const queueItem: QueueItemModel = await this.dataService.item.findOne({ + relations: ['item'], + where: { + id: this.data.item_id, + }, + }); + + return queueItem.item; + } + + async afterProcess(): Promise { + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get uniqueColumns(): columnUniques[] { + return []; + } + + get eventTopics(): EventTopics[] { + return []; + } + + get entityTarget(): any { + return QueueModel; + } +} diff --git a/src/modules/queue/domain/usecases/split-queue.manager.ts b/src/modules/queue/domain/usecases/split-queue.manager.ts new file mode 100644 index 0000000..6b67bd9 --- /dev/null +++ b/src/modules/queue/domain/usecases/split-queue.manager.ts @@ -0,0 +1,114 @@ +import { + HttpStatus, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; +import { + EventTopics, + columnUniques, + validateRelations, +} from 'src/core/strings/constants/interface.constants'; +import { BaseCreateManager } from 'src/core/modules/domain/usecase/managers/base-create.manager'; + +import { QueueOrderModel } from '../../data/models/queue.model'; +import { SplitQueueDto } from '../../infrastructure/controllers/dto/split-queue.dto'; +import { QueueService } from '../../data/services/queue.service'; +import { TicketDataService } from '../../data/services/ticket.service'; + +@Injectable() +export class SplitQueueManager extends BaseCreateManager { + private dto: SplitQueueDto; + constructor( + private readonly queueService: QueueService, + private readonly ticketService: TicketDataService, + ) { + super(); + } + + setRequestData(dto: SplitQueueDto): void { + this.dto = dto; + } + + async validateProcess(): Promise { + await super.validateProcess(); + const existTickets = await this.ticketService.ticketByUser( + this.dto.name, + this.dto.phone, + ); + if (existTickets) { + throw new UnprocessableEntityException({ + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + code: 20002, + message: + 'Antrian dengan nama dan nomor telepon sudah terdaftar, silahkan gunakan nomor telepon atau nama lain', + error: 'QUEUE_ALREADY_EXIST', + }); + } + } + + async prepareData(): Promise { + const existTickets = await this.ticketService.ticketByCode(this.data.code); + const { tickets, ...order } = this.data; + const ticket = tickets[0]; + const items = tickets + .map((ticket) => { + return ticket.items; + }) + .flat(); + const postfix = existTickets.length; + this.data = { + code: order.code, + customer: this.dto.name, + phone: this.dto.phone, + date: order.date, + updated_at: new Date().getTime(), + tickets: [ + { + code: `${order.code}-${postfix}`, + customer: this.dto.name, + phone: this.dto.phone, + date: ticket.date, + order_id: order.id, + items: items.map((item) => { + const itemQty = this.dto.items.find( + (i) => i.queue_item_id === item.id, + ); + return { + item_id: item.item_id, + qty: itemQty.qty, + }; + }), + }, + ], + }; + super.prepareData(); + return; + } + + async beforeProcess(): Promise { + return; + } + + async afterProcess(): Promise { + this.dto.items.forEach((item) => { + this.queueService.updateItemQty(item.queue_item_id, item.qty); + }); + return; + } + + get validateRelations(): validateRelations[] { + return []; + } + + get uniqueColumns(): columnUniques[] { + return []; + } + + get eventTopics(): EventTopics[] { + return []; + } + + get entityTarget(): any { + return QueueOrderModel; + } +} diff --git a/src/modules/queue/index.ts b/src/modules/queue/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/queue/infrastructure/controllers/dto/login-queue.dto.ts b/src/modules/queue/infrastructure/controllers/dto/login-queue.dto.ts new file mode 100644 index 0000000..6a0678d --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/dto/login-queue.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, Matches } from 'class-validator'; + +export class LoginQueueDto { + @ApiProperty({ name: 'name', required: true }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ + name: 'phone', + required: true, + description: 'Phone Number Must Start With 62, no + or 0', + example: '628123456789', + }) + @IsString() + @IsNotEmpty() + @Matches(/^628\d+$/i, { message: 'Nomor Telepon tidak valid' }) + phone: string; +} diff --git a/src/modules/queue/infrastructure/controllers/dto/login-receipt.dto.ts b/src/modules/queue/infrastructure/controllers/dto/login-receipt.dto.ts new file mode 100644 index 0000000..298fc1f --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/dto/login-receipt.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class LoginReceiptDto { + @ApiProperty({ name: 'id', required: true }) + @IsString() + @IsNotEmpty() + id: string; +} diff --git a/src/modules/queue/infrastructure/controllers/dto/queue.filter.ts b/src/modules/queue/infrastructure/controllers/dto/queue.filter.ts new file mode 100644 index 0000000..6f33aac --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/dto/queue.filter.ts @@ -0,0 +1,41 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsEnum, ValidateIf } from 'class-validator'; +import { Transform } from 'class-transformer'; +import { FilterQueueEntity } from 'src/modules/queue/domain/entities/filter.entity'; +import { ORDER_TYPE } from 'src/core/strings/constants/base.constants'; + +export class QueueDto implements FilterQueueEntity { + @ApiProperty({ + isArray: true, + required: false, + }) + // @IsString() + @ValidateIf((body) => body.status) + @Transform(({ value }) => { + if (!value) return []; + return Array.isArray(value) ? value : [value]; + }) + status: string[]; + + @ApiProperty({ type: Boolean, required: false }) + @Transform((body) => body.value == 'true') + @IsBoolean() + @ValidateIf((body) => body.vip) + vip: boolean; + + @ApiProperty({ type: String, required: false }) + order_by: string; + + @ApiProperty({ + type: 'string', + required: false, + description: `Select ("${ORDER_TYPE.ASC}", "${ORDER_TYPE.DESC}")`, + }) + @ValidateIf((body) => body.order_type) + @IsEnum(ORDER_TYPE, { + message: `order_type must be a valid enum ${JSON.stringify( + Object.values(ORDER_TYPE), + )}`, + }) + order_type: ORDER_TYPE; +} diff --git a/src/modules/queue/infrastructure/controllers/dto/register-queue.dto.ts b/src/modules/queue/infrastructure/controllers/dto/register-queue.dto.ts new file mode 100644 index 0000000..8141b71 --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/dto/register-queue.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; + +export class RegisterQueueDto { + @ApiProperty({ name: 'item_id', required: true }) + @IsString() + @IsNotEmpty() + item_id: string; + + @ApiProperty({ name: 'ticket_id', required: true }) + @IsString() + @IsNotEmpty() + ticket_id: string; + + @ApiProperty({ + type: Number, + required: true, + example: 1, + }) + @IsNumber() + @Min(1) + @IsNotEmpty() + qty: number; +} diff --git a/src/modules/queue/infrastructure/controllers/dto/split-queue.dto.ts b/src/modules/queue/infrastructure/controllers/dto/split-queue.dto.ts new file mode 100644 index 0000000..efdfecb --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/dto/split-queue.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsArray, + IsNotEmpty, + IsNumber, + IsString, + Matches, + Min, + ValidateNested, +} from 'class-validator'; + +export class ItemSplitQueueDto { + @ApiProperty({ name: 'queue_item_id', required: true }) + @IsString() + @IsNotEmpty() + queue_item_id: string; + @ApiProperty({ + type: Number, + required: true, + example: 1, + }) + @IsNumber() + @Min(1) + @IsNotEmpty() + qty: number; +} + +export class SplitQueueDto { + @ApiProperty({ name: 'order_id', required: true }) + @IsString() + @IsNotEmpty() + order_id: string; + + @ApiProperty({ name: 'name', required: true }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ + name: 'phone', + required: true, + description: 'Phone Number Must Start With 62, no + or 0', + example: '+628123456789', + }) + @IsString() + @IsNotEmpty() + @Matches(/^\+628\d+$/i, { message: 'Nomor Telepon tidak valid' }) + phone: string; + + @ApiProperty({ + type: [ItemSplitQueueDto], + required: false, + example: [ + { + queue_item_id: 'string', + qty: 1, + }, + ], + }) + @ValidateNested({ each: true }) + @IsArray() + items: ItemSplitQueueDto[]; +} diff --git a/src/modules/queue/infrastructure/controllers/queue-admin.controller.ts b/src/modules/queue/infrastructure/controllers/queue-admin.controller.ts new file mode 100644 index 0000000..9c08352 --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/queue-admin.controller.ts @@ -0,0 +1,65 @@ +import { + Controller, + Get, + Param, + Post, + Query, + UnauthorizedException, +} from '@nestjs/common'; + +import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; + +import { QueueAdminOrchestrator } from '../../domain/queue-admin.orchestrator'; +import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; +import { Queue } from '../../domain/entities/queue.entity'; +import { QueueDto } from './dto/queue.filter'; +import { Pagination } from 'src/core/response'; +import { ExcludePrivilege, Public } from 'src/core/guards'; +import { UserProvider } from 'src/core/sessions'; + +@ApiTags(`Queue Admin`) +@Controller(`v1/${MODULE_NAME.QUEUE}-admin`) +@ApiBearerAuth('JWT') +@Public(false) +@ExcludePrivilege() +export class QueueAdminController { + constructor( + private orchestrator: QueueAdminOrchestrator, + private readonly userProvider: UserProvider, + ) {} + + @Get('queues') + @Pagination() + async index(@Query() params: QueueDto): Promise> { + const queue_id = this.userProvider.user.item_id; + if (!queue_id) { + throw new UnauthorizedException({ + error: 'USER_IS_NOT_ADMIN', + message: "Login user isn't admin, please login in admin account", + code: 20001, + }); + } + return await this.orchestrator.index({ ...params, queue_id }); + } + + @Get('queues/:id') + @Public(true) + @Pagination() + async indexPublic( + @Param('id') id: string, + @Query() params: QueueDto, + ): Promise> { + return await this.orchestrator.index({ ...params, queue_id: id }); + } + + @Post('queues/:id/call') + async call(@Param('id') id: string) { + return await this.orchestrator.call(id); + } + + @Post('queues/:id/done') + async done(@Param('id') id: string) { + return await this.orchestrator.done(id); + } +} diff --git a/src/modules/queue/infrastructure/controllers/queue.controller.ts b/src/modules/queue/infrastructure/controllers/queue.controller.ts new file mode 100644 index 0000000..e7a12b7 --- /dev/null +++ b/src/modules/queue/infrastructure/controllers/queue.controller.ts @@ -0,0 +1,106 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; + +import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; + +import { Public } from 'src/core/guards'; +import { QueueOrchestrator } from '../../domain/queue.orchestrator'; +import { QueueOrder } from '../../domain/entities/order.entity'; +import { Queue } from '../../domain/entities/queue.entity'; +import { RegisterQueueDto } from './dto/register-queue.dto'; +import { SplitQueueDto } from './dto/split-queue.dto'; +import { LoginQueueDto } from './dto/login-queue.dto'; +import { LoginReceiptDto } from './dto/login-receipt.dto'; + +@ApiTags(`Queue`) +@Controller(`v1/${MODULE_NAME.QUEUE}`) +@Public(true) +@ApiBearerAuth('JWT') +export class QueueController { + constructor(private orchestrator: QueueOrchestrator) {} + + @Post('register') + async registerQueue(@Body() data: RegisterQueueDto): Promise { + return await this.orchestrator.create(data); + } + + @Post('split') + async splitQueue(@Body() data: SplitQueueDto): Promise { + return await this.orchestrator.split(data); + } + + @Post('login') + async loginQueue(@Body() data: LoginQueueDto): Promise { + return await this.orchestrator.loginPhone(data); + } + + @Post('login/receipt') + async loginReceipt(@Body() { id }: LoginReceiptDto): Promise { + console.log(id); + return await this.orchestrator.loginCustomer(id); + } + + @Get('login/:id') + async loginCustomer(@Param('id') id: string): Promise { + return await this.orchestrator.loginCustomer(id); + } + + @Get(':id/tickets') + async queueTickets(@Param('id') id: string): Promise { + return await this.orchestrator.queueTickets(id); + } + + @Get(':id/tickets/:ticket_id/items') + async queueTicketItems( + @Param('id') id: string, + @Param('ticket_id') ticket_id: string, + ): Promise { + return await this.orchestrator.queueTicketDetail(id, ticket_id); + } + + @Get(':id/tickets/:ticket_id/summary') + async queueTicketActive( + @Param('id') id: string, + @Param('ticket_id') ticket_id: string, + ): Promise { + return await this.orchestrator.queueTicketSummary(id, ticket_id); + } + + @Get(':id/recommendations') + async queueItemRecommends(@Param('id') id: string): Promise { + return await this.orchestrator.queueItemRecommendation(id); + } + + @Get(':id/queue-summary') + async queueItemSummary(): Promise { + return await this.orchestrator.queueItemSummary(); + } + + @Get(':id/items') + async queueItems(@Param('id') id: string): Promise { + return await this.orchestrator.queueItems(id); + } + + @Get(':id/unique-items') + async queueUniqueItems(@Param('id') id: string): Promise { + return await this.orchestrator.queueUniqueItems(id); + } + + @Get(':id/pos-items') + async queuePosItems(@Param('id') id: string): Promise { + return await this.orchestrator.queuePOSTickets(id); + } + + @Get(':id/queue') + async queueOrderItems(@Param('id') id: string): Promise { + return await this.orchestrator.queueOrderItems(id); + } + + @Get(':id/items/:item_id') + async queueItemTickets( + @Param('id') id: string, + @Param('item_id') item_id: string, + ): Promise { + return await this.orchestrator.queueItemDetail(id, item_id); + } +} diff --git a/src/modules/queue/infrastructure/handlers/cancel-transaction.handler.ts b/src/modules/queue/infrastructure/handlers/cancel-transaction.handler.ts new file mode 100644 index 0000000..84505cb --- /dev/null +++ b/src/modules/queue/infrastructure/handlers/cancel-transaction.handler.ts @@ -0,0 +1,21 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { TransactionChangeStatusEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event'; +import { TicketDataService } from '../../data/services/ticket.service'; + +@EventsHandler(TransactionChangeStatusEvent) +export class QueueTransactionCancelHandler + implements IEventHandler +{ + constructor(private queueService: TicketDataService) {} + + async handle(event: TransactionChangeStatusEvent) { + const process_data = event.data.data; + + /** + * If data still in process (not settled) then don't create the queue order + */ + if (!['cancel', 'rejected'].includes(process_data?.status)) return; + + this.queueService.deleteQueue(event.data.id); + } +} diff --git a/src/modules/queue/infrastructure/handlers/transaction.handler.ts b/src/modules/queue/infrastructure/handlers/transaction.handler.ts new file mode 100644 index 0000000..b3e5b42 --- /dev/null +++ b/src/modules/queue/infrastructure/handlers/transaction.handler.ts @@ -0,0 +1,176 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service'; +import { + TransactionChangeStatusEvent, + TransactionCreateQueueEvent, +} from 'src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event'; +import { TicketDataService } from '../../data/services/ticket.service'; +import { QueueOrder } from '../../domain/entities/order.entity'; +import { QueueTicket } from '../../domain/entities/ticket.entity'; +import { QueueItem } from '../../domain/entities/queue-item.entity'; +import * as moment from 'moment'; +import { TransactionUserType } from 'src/modules/transaction/transaction/constants'; +import { RegisterQueueManager } from '../../domain/usecases/register-queue.manager'; +import { RegisterQueueDto } from '../controllers/dto/register-queue.dto'; +import { QueueService } from '../../data/services/queue.service'; +import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; +import { QueueBucketReadService } from '../../data/services/queue-bucket'; +import { QueueTimeFormula } from '../../domain/usecases/formula/queue-time.formula'; +import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model'; + +@EventsHandler(TransactionChangeStatusEvent, TransactionCreateQueueEvent) +export class QueueTransactionHandler + implements IEventHandler +{ + constructor( + private readonly dataService: TransactionDataService, + private readonly ticketService: TicketDataService, + private readonly queueService: QueueService, + private readonly bucketService: QueueBucketReadService, + private readonly queueTimeFormula: QueueTimeFormula, + ) {} + + async handle(event: TransactionChangeStatusEvent) { + const process_data = event.data.data; + + /** + * If data still in process (not settled) then don't create the queue order + */ + if (process_data?.status != 'settled') return; + + const transaction = await this.dataService.getOneByOptions({ + where: { + id: event.data.id, + }, + relations: [ + 'customer_category', + 'items', + 'items.item', + 'items.item.bundling_items', + ], + }); + + const date = transaction.booking_date ?? transaction.invoice_date; + const queue_date = moment(date, 'YYYY-MM-DD').unix(); + + const { id, customer_name, customer_phone, invoice_code } = transaction; + + const customerOrder = { + code: invoice_code, + customer: customer_name, + phone: customer_phone, + date: queue_date * 1000, + transaction_id: id, + }; + + const items = this.generateItems(transaction.items); + + const existTicket = await this.ticketService.ticketByUser( + customer_name, + customer_phone, + ); + + const insertTickets: QueueTicket[] = []; + if (customer_name && customer_phone && existTicket) { + existTicket.items.push(...items); + await this.ticketService.updateQueueTicket(existTicket); + + existTicket.items = items; + insertTickets.push(existTicket); + } else { + const ticket: QueueTicket = { ...customerOrder, items }; + const order: QueueOrder = { ...customerOrder, tickets: [ticket] }; + + const queueOrder = await this.ticketService.createQueueOrder(order); + insertTickets.push(...queueOrder.tickets); + } + + if ( + transaction.customer_category?.has_vip_pass || + transaction.customer_type === TransactionUserType.VIP + ) { + insertTickets.forEach((ticket) => { + const ticket_id = ticket.id; + const items = {}; + ticket.items.forEach((item) => { + const item_id = item['item_queue_id']; + const currentItem = items[item_id]; + + if (currentItem) { + currentItem.qty += item.qty; + } + items[item_id] = currentItem + ? currentItem + : { + item_id, + ticket_id, + qty: item.qty, + }; + }); + + Object.values(items).forEach((payload: any) => { + this.create(payload); + }); + }); + } + } + + generateItems(items: TransactionItemModel[]): QueueItem[] { + const transactionItems = []; + + items.forEach((item) => { + if (item.item.use_queue) { + transactionItems.push({ + item_queue_id: item.item.item_queue_id, + item_id: item.item_id, + qty: item.qty, + }); + } + + if (item.item.bundling_items.length > 0) { + item.item.bundling_items.forEach((bundling_item) => { + if (bundling_item.use_queue) { + transactionItems.push({ + item_queue_id: bundling_item.item_queue_id, + item_id: bundling_item.id, + qty: item.qty, + }); + } + }); + } + }); + + return this.mergeQueueItems(transactionItems); + } + + mergeQueueItems(arr) { + const result = {}; + arr.forEach((item) => { + if (result[item.item_id]) { + result[item.item_id].qty += item.qty; + } else { + result[item.item_id] = { ...item }; + } + }); + return Object.values(result); + } + + async create(data: RegisterQueueDto): Promise { + const queue = await this.queueService.getTicketItems( + data.ticket_id, + data.item_id, + ); + const queueRequest: any = { + qty: data.qty, + item_id: queue.id, + vip: true, + }; + const registerQueueManager = new RegisterQueueManager( + this.bucketService, + this.queueTimeFormula, + ); + registerQueueManager.setData(queueRequest); + registerQueueManager.setService(this.queueService, TABLE_NAME.QUEUE); + await registerQueueManager.execute(); + } +} diff --git a/src/modules/queue/queue.module.ts b/src/modules/queue/queue.module.ts new file mode 100644 index 0000000..a117956 --- /dev/null +++ b/src/modules/queue/queue.module.ts @@ -0,0 +1,83 @@ +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 { CqrsModule } from '@nestjs/cqrs'; + +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; +import { + QueueItemModel, + QueueModel, + QueueOrderModel, + QueueTicketModel, +} from './data/models/queue.model'; +import { QueueController } from './infrastructure/controllers/queue.controller'; +import { QueueOrchestrator } from './domain/queue.orchestrator'; +import { QueueTransactionHandler } from './infrastructure/handlers/transaction.handler'; +import { TransactionDataService } from '../transaction/transaction/data/services/transaction-data.service'; +import { TransactionModel } from '../transaction/transaction/data/models/transaction.model'; +import { TicketDataService } from './data/services/ticket.service'; +import { + QueueDataService, + QueueOrderService, + QueueService, +} from './data/services/queue.service'; +import { QueueAdminController } from './infrastructure/controllers/queue-admin.controller'; +import { QueueAdminOrchestrator } from './domain/queue-admin.orchestrator'; +import { IndexQueueManager } from './domain/usecases/index-queue.manager'; +import { + CallQueueManager, + DoneQueueManager, +} from './domain/usecases/call-queue.manager'; +import { RegisterQueueManager } from './domain/usecases/register-queue.manager'; +import { QueueBucketModel } from './data/models/queue-bucket.model'; +import { QueueBucketReadService } from './data/services/queue-bucket'; +import { SplitQueueManager } from './domain/usecases/split-queue.manager'; +import { QueueTransactionCancelHandler } from './infrastructure/handlers/cancel-transaction.handler'; +import { ItemQueueModel } from '../item-related/item-queue/data/models/item-queue.model'; +import { QueueTimeFormula } from './domain/usecases/formula/queue-time.formula'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forFeature( + [ + QueueOrderModel, + QueueTicketModel, + QueueItemModel, + QueueModel, + QueueBucketModel, + ItemModel, + ItemQueueModel, + TransactionModel, + ], + CONNECTION_NAME.DEFAULT, + ), + CqrsModule, + ], + controllers: [QueueController, QueueAdminController], + providers: [ + QueueOrchestrator, + QueueAdminOrchestrator, + + QueueTransactionHandler, + QueueTransactionCancelHandler, + + TransactionDataService, + TicketDataService, + QueueDataService, + QueueService, + QueueBucketReadService, + QueueOrderService, + + IndexQueueManager, + CallQueueManager, + DoneQueueManager, + RegisterQueueManager, + SplitQueueManager, + + QueueTimeFormula, + ], +}) +export class QueueModule {} diff --git a/src/modules/transaction/transaction/data/models/transaction-item.model.ts b/src/modules/transaction/transaction/data/models/transaction-item.model.ts index 034bb47..8248388 100644 --- a/src/modules/transaction/transaction/data/models/transaction-item.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction-item.model.ts @@ -8,6 +8,7 @@ import { import { TransactionModel } from './transaction.model'; import { RefundItemModel } from 'src/modules/transaction/refund/data/models/refund-item.model'; import { TransactionTaxEntity } from '../../domain/entities/transaction-tax.entity'; +import { ItemModel } from 'src/modules/item-related/item/data/models/item.model'; @Entity(TABLE_NAME.TRANSACTION_ITEM) export class TransactionItemModel @@ -126,6 +127,10 @@ export class TransactionItemModel onUpdate: 'CASCADE', }) item_taxes: TransactionItemTaxModel[]; + + @ManyToOne(() => ItemModel) + @JoinColumn({ name: 'item_id' }) + item: ItemModel; } @Entity(TABLE_NAME.TRANSACTION_ITEM_BREAKDOWN) diff --git a/src/modules/transaction/transaction/data/models/transaction.model.ts b/src/modules/transaction/transaction/data/models/transaction.model.ts index 97d312a..053503c 100644 --- a/src/modules/transaction/transaction/data/models/transaction.model.ts +++ b/src/modules/transaction/transaction/data/models/transaction.model.ts @@ -1,18 +1,25 @@ import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { TransactionEntity } from '../../domain/entities/transaction.entity'; -import { Column, Entity, OneToMany, OneToOne } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, +} from 'typeorm'; import { BaseStatusModel } from 'src/core/modules/data/model/base-status.model'; import { TransactionType, TransactionUserType, TransactionPaymentType, } from '../../constants'; -import { TransactionItemEntity } from '../../domain/entities/transaction-item.entity'; import { TransactionItemModel } from './transaction-item.model'; import { TransactionTaxModel } from './transaction-tax.model'; import { STATUS } from 'src/core/strings/constants/base.constants'; import { RefundModel } from 'src/modules/transaction/refund/data/models/refund.model'; import { TransactionDemographyModel } from './transaction-demography.model'; +import { VipCategoryModel } from 'src/modules/transaction/vip-category/data/models/vip-category.model'; @Entity(TABLE_NAME.TRANSACTION) export class TransactionModel @@ -52,6 +59,10 @@ export class TransactionModel @Column('varchar', { name: 'season_period_type_name', nullable: true }) season_period_type_name: string; + @ManyToOne(() => VipCategoryModel) + @JoinColumn({ name: 'customer_category_id' }) + customer_category: VipCategoryModel; + // customer info @Column('enum', { name: 'customer_type', @@ -239,7 +250,7 @@ export class TransactionModel onDelete: 'CASCADE', onUpdate: 'CASCADE', }) - items: TransactionItemEntity[]; + items: TransactionItemModel[]; // relations to tax data @OneToMany(() => TransactionTaxModel, (model) => model.transaction, { 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 f13c3b7..40b8f6a 100644 --- a/src/modules/transaction/transaction/data/services/transaction-data.service.ts +++ b/src/modules/transaction/transaction/data/services/transaction-data.service.ts @@ -1,13 +1,12 @@ import { Injectable } from '@nestjs/common'; import { BaseDataService } from 'src/core/modules/data/service/base-data.service'; -import { TransactionEntity } from '../../domain/entities/transaction.entity'; 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'; @Injectable() -export class TransactionDataService extends BaseDataService { +export class TransactionDataService extends BaseDataService { constructor( @InjectRepository(TransactionModel, CONNECTION_NAME.DEFAULT) private repo: Repository, diff --git a/src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event.ts b/src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event.ts index 53ffa35..a2bbab5 100644 --- a/src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event.ts +++ b/src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event.ts @@ -3,3 +3,7 @@ import { IEvent } from 'src/core/strings/constants/interface.constants'; export class TransactionChangeStatusEvent { constructor(public readonly data: IEvent) {} } + +export class TransactionCreateQueueEvent { + constructor(public readonly data: IEvent) {} +} diff --git a/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts b/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts index 72b8ce8..1148789 100644 --- a/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts +++ b/src/modules/transaction/transaction/domain/usecases/calculator/price.calculator.ts @@ -133,7 +133,7 @@ export class PriceCalculator { calledVariable = []; } catch (error) { calledVariable.push(valueFor); - console.log(error); + console.log(error.message); } values[valueFor] = result; diff --git a/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts b/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts index 1207198..fce39cc 100644 --- a/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts +++ b/src/modules/transaction/transaction/domain/usecases/handlers/pos-transaction.handler.ts @@ -14,7 +14,10 @@ import { TransactionModel } from '../../../data/models/transaction.model'; import { mappingRevertTransaction } from '../managers/helpers/mapping-transaction.helper'; import { apm } from '../../../../../../core/apm'; import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; -import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event'; +import { + TransactionChangeStatusEvent, + TransactionCreateQueueEvent, +} from '../../entities/event/transaction-change-status.event'; import { PriceCalculator } from '../calculator/price.calculator'; @EventsHandler(ChangeDocEvent) @@ -120,6 +123,20 @@ export class PosTransactionHandler implements IEventHandler { }), ); } + + if (data.status == STATUS.SETTLED) { + this.eventBus.publish( + new TransactionCreateQueueEvent({ + id: data.id, + old: event.data.data, + data: data, + user: BLANK_USER, + description: '', + module: TABLE_NAME.TRANSACTION, + op: OPERATION.UPDATE, + }), + ); + } apmTransactions.result = 'Success'; } } catch (error) { diff --git a/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts b/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts index 4c65f72..8295e13 100644 --- a/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts +++ b/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts @@ -22,3 +22,7 @@ export function generateCodeDate() { return `${month}${year}`; } + +export function padCode(number: number, pad = 4): string { + return String(number).padStart(pad, '0'); +}