From 14bad8cf4741afff91ac8378ed086299fad0f9ab Mon Sep 17 00:00:00 2001 From: pdax-liamnguyen Date: Sat, 24 Feb 2024 21:49:17 +0700 Subject: [PATCH] Features/lamnv/ahamove integration (#59) * feat: get ahamove token, estimate fee, webhook and ws api * fix: update migrations and update ahamove api * fix: update script migrations * fix: update on wheel api * update logic handling ahamove status * remove unused importing --------- Co-authored-by: lamnv Co-authored-by: NHT Co-authored-by: nfesta2023 <142601504+nfesta2023@users.noreply.github.com> --- .prettierrc | 3 +- README.md | 26 +-- package.json | 11 +- src/app.module.ts | 7 +- .../migrations/1708344487764-CreateTables.ts | 156 ++++++++++++++ .../1708357786332-TableAhamoveHook.ts | 13 ++ .../1708402970486-TableAhamoveOrder.ts | 15 ++ src/dependency/ahamove/ahamove.controller.ts | 34 +++ src/dependency/ahamove/ahamove.module.ts | 8 +- src/dependency/ahamove/ahamove.service.ts | 199 +++++++++++++++++- src/dependency/ahamove/dto/ahamove.dto.ts | 71 +++++++ src/dependency/ahamove/dto/ahamove.hook.ts | 63 ++++++ .../ahamove/mapper/ahamove.mapper.ts | 16 ++ .../ahamove/schema/ahamove.schema.ts | 57 +++++ src/entity/ahamove-order-hook.entity.ts | 128 +++++++++++ src/entity/ahamove-order.entity.ts | 60 ++++++ src/entity/order.entity.ts | 100 +++++++++ src/enum/index.ts | 11 + src/feature/order/order.controller.ts | 23 ++ src/feature/order/order.module.ts | 13 ++ src/feature/order/order.service.ts | 111 ++++++++++ src/migration.config.ts | 6 + src/ormconfig.ts | 76 +++++++ yarn.lock | 184 +++++++++++++++- 24 files changed, 1350 insertions(+), 41 deletions(-) create mode 100644 src/database/migrations/1708344487764-CreateTables.ts create mode 100644 src/database/migrations/1708357786332-TableAhamoveHook.ts create mode 100644 src/database/migrations/1708402970486-TableAhamoveOrder.ts create mode 100644 src/dependency/ahamove/ahamove.controller.ts create mode 100644 src/dependency/ahamove/dto/ahamove.dto.ts create mode 100644 src/dependency/ahamove/dto/ahamove.hook.ts create mode 100644 src/dependency/ahamove/mapper/ahamove.mapper.ts create mode 100644 src/dependency/ahamove/schema/ahamove.schema.ts create mode 100644 src/entity/ahamove-order-hook.entity.ts create mode 100644 src/entity/ahamove-order.entity.ts create mode 100644 src/entity/order.entity.ts create mode 100644 src/feature/order/order.controller.ts create mode 100644 src/feature/order/order.module.ts create mode 100644 src/feature/order/order.service.ts create mode 100644 src/migration.config.ts create mode 100644 src/ormconfig.ts diff --git a/.prettierrc b/.prettierrc index dcb7279..d6c339a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "printWidth": 200 } \ No newline at end of file diff --git a/README.md b/README.md index 8372941..1fca135 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,3 @@ -

- Nest Logo -

- -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Coverage -Discord -Backers on Open Collective -Sponsors on Open Collective - - Support us - -

- ## Description @@ -33,6 +10,9 @@ $ yarn install ``` ## Running the app +$ export BACKEND_ENV=dev +$ npm run typeorm migration:run -- -d ./src/migration.config.ts + ```bash # development diff --git a/package.json b/package.json index b0a4936..e782d62 100644 --- a/package.json +++ b/package.json @@ -17,21 +17,28 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", + "typeorm:sync": "npm run typeorm schema:sync" }, "dependencies": { + "@hapi/joi": "^17.1.1", "@nestjs/axios": "^3.0.1", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/microservices": "^10.2.10", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.3.3", "@nestjs/typeorm": "^10.0.1", + "@nestjs/websockets": "^10.3.3", "axios": "^1.6.2", "flagsmith-nodejs": "^3.2.0", + "joi": "^17.12.1", "mysql2": "^3.6.5", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", + "socket.io": "^4.7.4", "typeorm": "^0.3.17" }, "devDependencies": { @@ -74,4 +81,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 17e9a2d..0ff5106 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,8 @@ import { SearchModule } from './feature/search/search.module'; import { CommonModule } from './feature/common/common.module'; import { CartModule } from './feature/cart/cart.module'; import { RatingAndReviewModule } from './feature/rating-and-review/rating-and-review.module'; +import { AhamoveModule } from './dependency/ahamove/ahamove.module'; +import { OrderModule } from './feature/order/order.module'; @Module({ imports: [ @@ -30,10 +32,11 @@ import { RatingAndReviewModule } from './feature/rating-and-review/rating-and-re username: configService.get('database.username'), password: configService.get('database.password'), database: configService.get('database.name'), - entities: [], + entities: [__dirname + '/entity/*.entity{.ts,.js}'], synchronize: false, autoLoadEntities: true, }), + // useFactory: (configService: ConfigService) => ormConfig(), inject: [ConfigService], }), FoodModule, @@ -45,6 +48,8 @@ import { RatingAndReviewModule } from './feature/rating-and-review/rating-and-re CommonModule, CartModule, RatingAndReviewModule, + AhamoveModule, + OrderModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/database/migrations/1708344487764-CreateTables.ts b/src/database/migrations/1708344487764-CreateTables.ts new file mode 100644 index 0000000..1d6a438 --- /dev/null +++ b/src/database/migrations/1708344487764-CreateTables.ts @@ -0,0 +1,156 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateTables1708344487764 implements MigrationInterface { + name = 'CreateTables1708344487764'; + + public async up(queryRunner: QueryRunner): Promise { + // await queryRunner.query( + // `CREATE TABLE \`No_Adding_Ext\` (\`no_adding_id\` varchar(255) NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`description\` varchar(255) NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`no_adding_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Basic_Customization\` (\`menu_item_id\` int NOT NULL, \`no_adding_id\` varchar(45) NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`menu_item_id\`, \`no_adding_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Contact\` (\`contact_id\` int NOT NULL AUTO_INCREMENT, \`email\` varchar(255) NOT NULL, \`content\` longtext NOT NULL, \`is_contacted\` tinyint NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`contact_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Address\` (\`address_id\` int NOT NULL AUTO_INCREMENT, \`address_line\` varchar(255) NOT NULL, \`ward\` varchar(64) NOT NULL, \`district\` varchar(64) NOT NULL, \`city\` varchar(64) NOT NULL, \`country\` varchar(64) NOT NULL, \`latitude\` decimal(8,6) NOT NULL, \`longitude\` decimal(9,6) NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`address_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Media\` (\`media_id\` int NOT NULL AUTO_INCREMENT, \`type\` varchar(45) NOT NULL, \`name\` varchar(255) NOT NULL, \`description\` text NULL, \`url\` varchar(2048) NOT NULL, \`restaurant_id\` int NULL, \`menu_item_id\` int NULL, \`packaging_id\` int NULL, \`driver_rating_id\` int NULL, \`food_rating_id\` int NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`media_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Restaurant_Owner\` (\`restaurant_owner_id\` int NOT NULL AUTO_INCREMENT, \`phone_number\` varchar(25) NOT NULL, \`name\` varchar(255) NULL, \`email\` varchar(255) NULL, \`username\` varchar(255) NOT NULL, \`password\` varchar(255) NOT NULL, \`is_active\` tinyint NOT NULL DEFAULT '0', \`refresh_token\` varchar(255) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, \`profile_image\` int NULL, UNIQUE INDEX \`IDX_ede6dcc70f5cda98ab2b89b7fc\` (\`phone_number\`), UNIQUE INDEX \`REL_4d59745ca8bac26c65e3048b36\` (\`profile_image\`), PRIMARY KEY (\`restaurant_owner_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Restaurant_Ext\` (\`restaurant_id\` int NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`name\` varchar(255) NOT NULL, \`specialty\` varchar(255) NULL, \`introduction\` text NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`restaurant_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Unit_Ext\` (\`unit_id\` int NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`description\` varchar(255) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`unit_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Unit\` (\`unit_id\` int NOT NULL AUTO_INCREMENT, \`type\` varchar(45) NOT NULL, \`symbol\` varchar(32) NOT NULL, \`decimal_place\` varchar(2) NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`unit_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Restaurant\` (\`restaurant_id\` int NOT NULL AUTO_INCREMENT, \`phone_number\` varchar(25) NOT NULL, \`is_auto_confirmed\` tinyint NOT NULL DEFAULT '0', \`bank_name\` varchar(100) NULL, \`bin\` varchar(11) NULL, \`account_number\` varchar(50) NULL, \`account_owner_name\` varchar(255) NULL, \`shared_link\` varchar(2048) NULL, \`is_active\` tinyint NOT NULL DEFAULT '0', \`intro_video\` int NULL, \`rating\` decimal(3,2) NULL, \`review_total_count\` int NULL, \`top_food\` varchar(255) NULL, \`promotion\` varchar(255) NULL, \`unit\` int NULL, \`utc_time_zone\` decimal(2,0) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, \`address_id\` int NULL, \`restaurant_owner_id\` int NULL, \`logo\` int NULL, UNIQUE INDEX \`REL_a20ebf05e0223cc5cf786a1dca\` (\`address_id\`), UNIQUE INDEX \`REL_9d07d3fcb1152c4ac3f366a26f\` (\`logo\`), PRIMARY KEY (\`restaurant_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Menu_Item_Ext\` (\`menu_item_id\` int NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`name\` varchar(255) NOT NULL, \`short_name\` varchar(255) NOT NULL, \`description\` text NOT NULL, \`main_cooking_method\` varchar(64) NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`menu_item_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Food_Rating\` (\`food_rating_id\` int NOT NULL AUTO_INCREMENT, \`order_sku_id\` int NOT NULL, \`score\` tinyint NOT NULL, \`remarks\` text NULL, \`customer_id\` int NOT NULL, \`is_active\` tinyint NOT NULL DEFAULT '1', \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE INDEX \`REL_81ed649e8508964c64037eb130\` (\`order_sku_id\`), PRIMARY KEY (\`food_rating_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Order_SKU\` (\`order_sku_id\` int NOT NULL AUTO_INCREMENT, \`order_id\` int NOT NULL, \`sku_id\` int NOT NULL, \`qty_ordered\` int NOT NULL, \`price\` int NOT NULL, \`currency\` int NOT NULL, \`advanced_taste_customization\` varchar(255) NULL, \`basic_taste_customization\` varchar(255) NULL, \`notes\` text NULL, \`label_id\` int NULL, \`calorie_kcal\` decimal(10,2) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`order_sku_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Menu_Item_Attribute_Ext\` (\`attribute_id\` int NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`name\` varchar(255) NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`attribute_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Taste_Value_Ext\` (\`value_id\` varchar(255) NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`name\` varchar(255) NULL, \`description\` text NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`value_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Taste_Value\` (\`value_id\` varchar(255) NOT NULL, \`order\` int NULL, \`is_default_taste\` tinyint NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`value_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Menu_Item_Attribute_Value\` (\`value_id\` int NOT NULL AUTO_INCREMENT, \`attribute_id\` int NOT NULL, \`value\` int NULL, \`unit\` int NULL, \`taste_value\` varchar(45) NULL, \`note\` varchar(255) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`value_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Taste_Ext\` (\`taste_id\` varchar(255) NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`name\` varchar(255) NULL, \`description\` text NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`taste_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Menu_Item_Attribute\` (\`attribute_id\` int NOT NULL AUTO_INCREMENT, \`menu_item_id\` int NOT NULL, \`type_id\` varchar(45) NULL, \`taste_id\` varchar(45) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`attribute_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`SKU_Detail\` (\`sku_id\` int NOT NULL, \`attribute_id\` int NOT NULL, \`value_id\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`sku_id\`, \`attribute_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`SKU_Discount\` (\`sku_discount_id\` int NOT NULL AUTO_INCREMENT, \`sku_id\` int NOT NULL, \`discount_value\` int NOT NULL, \`discount_unit\` int NOT NULL, \`is_active\` tinyint NOT NULL DEFAULT '0', \`valid_from\` datetime NOT NULL, \`valid_until\` datetime NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`sku_discount_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`SKU\` (\`sku_id\` int NOT NULL AUTO_INCREMENT, \`menu_item_id\` int NOT NULL, \`sku\` varchar(32) NULL, \`price\` int NOT NULL, \`is_active\` tinyint NOT NULL DEFAULT '0', \`is_standard\` tinyint NOT NULL DEFAULT '0', \`calorie_kcal\` decimal(10,2) NULL, \`protein_g\` decimal(10,2) NULL, \`fat_g\` decimal(10,2) NULL, \`carbohydrate_g\` decimal(10,2) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`sku_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Packaging_Ext\` (\`packaging_id\` int NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`description\` varchar(255) NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`packaging_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Packaging\` (\`packaging_id\` int NOT NULL AUTO_INCREMENT, \`menu_item_id\` int NOT NULL, \`price\` int NOT NULL, \`currency\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`packaging_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Menu_Item\` (\`menu_item_id\` int NOT NULL AUTO_INCREMENT, \`restaurant_id\` int NULL, \`preparing_time_s\` int NULL, \`cooking_time_s\` int NULL, \`cutoff_time\` time NULL, \`quantity_available\` int NULL, \`is_active\` tinyint NOT NULL DEFAULT '0', \`is_vegetarian\` tinyint NOT NULL DEFAULT '0', \`cooking_schedule\` varchar(22) NOT NULL, \`res_category_id\` int NOT NULL, \`image\` int NOT NULL, \`rating\` decimal(3,2) NULL, \`ingredient_brief_vie\` varchar(100) NULL, \`ingredient_brief_eng\` varchar(100) NULL, \`promotion\` varchar(255) NULL, \`units_sold\` int NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`menu_item_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Recipe\` (\`ingredient_id\` int NOT NULL, \`menu_item_id\` int NOT NULL, \`quantity\` int NOT NULL, \`unit\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`ingredient_id\`, \`menu_item_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Ingredient\` (\`ingredient_id\` int NOT NULL AUTO_INCREMENT, \`vie_name\` varchar(255) NOT NULL, \`eng_name\` varchar(255) NULL, \`calorie_kcal\` int NULL, \`protein_g\` int NULL, \`fat_g\` int NULL, \`carbohydrate_g\` int NULL, \`ma_BTP2007\` varchar(10) NULL, \`food_id\` int NULL, \`img_object_id\` varchar(255) NULL, \`source\` varchar(255) NULL, \`image\` int NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`ingredient_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Manual_Open_Restaurant\` (\`manual_id\` int NOT NULL AUTO_INCREMENT, \`date\` date NULL, \`from_time\` datetime NULL, \`to_time\` datetime NULL, \`restaurant_id\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`manual_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Cart_Item\` (\`item_id\` int NOT NULL AUTO_INCREMENT, \`customer_id\` int NOT NULL, \`sku_id\` int NOT NULL, \`qty_ordered\` int NULL, \`advanced_taste_customization\` varchar(255) NULL, \`basic_taste_customization\` varchar(255) NULL, \`portion_customization\` varchar(255) NULL, \`advanced_taste_customization_obj\` text NULL, \`basic_taste_customization_obj\` text NULL, \`notes\` text NULL, \`restaurant_id\` int NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`item_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Operation_Hours\` (\`ops_hour_id\` int NOT NULL AUTO_INCREMENT, \`day_of_week\` int NOT NULL, \`from_time\` time NOT NULL, \`to_time\` time NOT NULL, \`restaurant_id\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`ops_hour_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Main_Side_Dish\` (\`main_dish_id\` int NOT NULL, \`side_dish_id\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`main_dish_id\`, \`side_dish_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Restaurant_Day_Off\` (\`day_off_id\` int NOT NULL AUTO_INCREMENT, \`date\` date NOT NULL, \`restaurant_id\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`day_off_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Sys_Category\` (\`sys_category_id\` int NOT NULL AUTO_INCREMENT, \`type\` varchar(45) NOT NULL, \`is_active\` tinyint NOT NULL DEFAULT '0', \`image\` int NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`sys_category_id\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Sys_Category_Ext\` (\`sys_category_id\` int NOT NULL, \`ISO_language_code\` varchar(255) NOT NULL, \`name\` varchar(255) NOT NULL, \`description\` text NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`sys_category_id\`, \`ISO_language_code\`)) ENGINE=InnoDB`, + // ); + // await queryRunner.query( + // `CREATE TABLE \`Sys_Category_Menu_Item\` (\`sys_category_id\` int NOT NULL, \`menu_item_id\` int NOT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`sys_category_id\`, \`menu_item_id\`)) ENGINE=InnoDB`, + // ); + } + + public async down(queryRunner: QueryRunner): Promise { + // await queryRunner.query(`DROP TABLE \`Sys_Category_Menu_Item\``); + // await queryRunner.query(`DROP TABLE \`Sys_Category_Ext\``); + // await queryRunner.query(`DROP TABLE \`Sys_Category\``); + // await queryRunner.query(`DROP TABLE \`Restaurant_Day_Off\``); + // await queryRunner.query(`DROP TABLE \`Main_Side_Dish\``); + // await queryRunner.query(`DROP TABLE \`Operation_Hours\``); + // await queryRunner.query(`DROP TABLE \`Cart_Item\``); + // await queryRunner.query(`DROP TABLE \`Manual_Open_Restaurant\``); + // await queryRunner.query(`DROP TABLE \`Ingredient\``); + // await queryRunner.query(`DROP TABLE \`Recipe\``); + // await queryRunner.query(`DROP TABLE \`Menu_Item\``); + // await queryRunner.query(`DROP TABLE \`Packaging\``); + // await queryRunner.query(`DROP TABLE \`Packaging_Ext\``); + // await queryRunner.query(`DROP TABLE \`SKU\``); + // await queryRunner.query(`DROP TABLE \`SKU_Discount\``); + // await queryRunner.query(`DROP TABLE \`SKU_Detail\``); + // await queryRunner.query(`DROP TABLE \`Menu_Item_Attribute\``); + // await queryRunner.query(`DROP TABLE \`Taste_Ext\``); + // await queryRunner.query(`DROP TABLE \`Menu_Item_Attribute_Value\``); + // await queryRunner.query(`DROP TABLE \`Taste_Value\``); + // await queryRunner.query(`DROP TABLE \`Taste_Value_Ext\``); + // await queryRunner.query(`DROP TABLE \`Menu_Item_Attribute_Ext\``); + // await queryRunner.query(`DROP TABLE \`Order_SKU\``); + // await queryRunner.query(`DROP INDEX \`REL_81ed649e8508964c64037eb130\` ON \`Food_Rating\``); + // await queryRunner.query(`DROP TABLE \`Food_Rating\``); + // await queryRunner.query(`DROP TABLE \`Menu_Item_Ext\``); + // await queryRunner.query(`DROP INDEX \`REL_9d07d3fcb1152c4ac3f366a26f\` ON \`Restaurant\``); + // await queryRunner.query(`DROP INDEX \`REL_a20ebf05e0223cc5cf786a1dca\` ON \`Restaurant\``); + // await queryRunner.query(`DROP TABLE \`Restaurant\``); + // await queryRunner.query(`DROP TABLE \`Unit\``); + // await queryRunner.query(`DROP TABLE \`Unit_Ext\``); + // await queryRunner.query(`DROP TABLE \`Restaurant_Ext\``); + // await queryRunner.query(`DROP INDEX \`REL_4d59745ca8bac26c65e3048b36\` ON \`Restaurant_Owner\``); + // await queryRunner.query(`DROP INDEX \`IDX_ede6dcc70f5cda98ab2b89b7fc\` ON \`Restaurant_Owner\``); + // await queryRunner.query(`DROP TABLE \`Restaurant_Owner\``); + // await queryRunner.query(`DROP TABLE \`Media\``); + // await queryRunner.query(`DROP TABLE \`Address\``); + // await queryRunner.query(`DROP TABLE \`Contact\``); + // await queryRunner.query(`DROP TABLE \`Basic_Customization\``); + // await queryRunner.query(`DROP TABLE \`No_Adding_Ext\``); + } +} diff --git a/src/database/migrations/1708357786332-TableAhamoveHook.ts b/src/database/migrations/1708357786332-TableAhamoveHook.ts new file mode 100644 index 0000000..bfad8d6 --- /dev/null +++ b/src/database/migrations/1708357786332-TableAhamoveHook.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class TableAhamoveHook1708357786332 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`Ahamove_Order_Hook\` (\`id\` int NOT NULL AUTO_INCREMENT, \`_id\` varchar(255) NOT NULL, \`accept_time\` int NOT NULL, \`board_time\` int NOT NULL, \`cancel_by_user\` tinyint NOT NULL DEFAULT 0, \`cancel_comment\` varchar(255) NOT NULL DEFAULT '', \`cancel_image_url\` varchar(255) NOT NULL DEFAULT '', \`cancel_time\` int NOT NULL, \`city_id\` varchar(255) NOT NULL, \`complete_time\` int NOT NULL, \`create_time\` int NOT NULL, \`currency\` varchar(255) NOT NULL, \`order_time\` int NOT NULL, \`partner\` varchar(255) NOT NULL, \`path\` json NOT NULL, \`payment_method\` varchar(255) NOT NULL, \`pickup_time\` int NOT NULL, \`service_id\` varchar(255) NOT NULL, \`status\` varchar(255) NOT NULL, \`sub_status\` varchar(255) NOT NULL, \`supplier_id\` varchar(255) NOT NULL, \`supplier_name\` varchar(255) NOT NULL, \`surcharge\` int NOT NULL, \`user_id\` varchar(255) NOT NULL, \`user_name\` varchar(255) NOT NULL, \`total_pay\` int NOT NULL, \`promo_code\` varchar(255) NOT NULL, \`stoppoint_price\` int NOT NULL, \`special_request_price\` int NOT NULL, \`vat\` int NOT NULL, \`distance_price\` int NOT NULL, \`voucher_discount\` int NOT NULL, \`subtotal_price\` int NOT NULL, \`total_price\` int NOT NULL, \`surge_rate\` int NOT NULL, \`api_key\` varchar(255) NOT NULL, \`shared_link\` varchar(255) NOT NULL, \`insurance_portal_url\` varchar(255) NOT NULL, \`app\` varchar(255) NOT NULL, \`store_id\` int NOT NULL, \`distance\` int NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE \`Ahamove_Order_Hook\``); + } +} diff --git a/src/database/migrations/1708402970486-TableAhamoveOrder.ts b/src/database/migrations/1708402970486-TableAhamoveOrder.ts new file mode 100644 index 0000000..81c9028 --- /dev/null +++ b/src/database/migrations/1708402970486-TableAhamoveOrder.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class TableAhamoveOrder1708402970486 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`Ahamove_Order\` (\`id\` varchar(36) NOT NULL, \`service_id\` varchar(255) NOT NULL, \`path\` json NOT NULL, \`requests\` json NOT NULL, \`payment_method\` varchar(255) NOT NULL, \`total_pay\` int NOT NULL, \`order_time\` int NOT NULL, \`promo_code\` varchar(255) NULL, \`remarks\` varchar(255) NOT NULL, \`admin_note\` varchar(255) NOT NULL, \`route_optimized\` tinyint NOT NULL, \`idle_until\` int NOT NULL, \`items\` json NOT NULL, \`package_detail\` json NOT NULL, \`group_service_id\` varchar(255) NULL, \`group_requests\` varchar(255) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, + ); + await queryRunner.query(`ALTER TABLE \`Ahamove_Order\` ADD \`order_id\` varchar(255) NULL`); + await queryRunner.query(`ALTER TABLE \`Ahamove_Order\` ADD \`response\` json NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE \`Ahamove_Order\``); + } +} diff --git a/src/dependency/ahamove/ahamove.controller.ts b/src/dependency/ahamove/ahamove.controller.ts new file mode 100644 index 0000000..825501f --- /dev/null +++ b/src/dependency/ahamove/ahamove.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Logger, Body, Post } from '@nestjs/common'; +// import { SseGateway } from './sse.gateway'; +import { Coordinate } from 'src/type'; +import { AhamoveService } from './ahamove.service'; +import { MessagePattern } from '@nestjs/microservices'; +import { PostAhaOrderRequest } from './dto/ahamove.dto'; +import { AhamoveOrderEntity } from 'src/entity/ahamove-order.entity'; + +@Controller('ahamove') +export class AhamoveController { + private readonly logger = new Logger(AhamoveController.name); + + constructor(private readonly ahamoveService: AhamoveService) {} + /** List of connected clients */ + @MessagePattern({ cmd: 'get_ahamove_order_by_id' }) + getAhamoveOrderByOrderId(orderId: string): Promise { + return this.ahamoveService.getAhamoveOrderByOrderId(orderId); + } + + @MessagePattern({ cmd: 'save_ahamove_order_tracking' }) + saveAhamoveTrackingWebhook(order: any): Promise { + return this.ahamoveService.saveAhamoveTrackingWebhook(order); + } + + @MessagePattern({ cmd: 'get_ahamove_estimate' }) + getEstimateFee(@Body() coordinates: Coordinate[]) { + return this.ahamoveService.estimatePrice(coordinates); + } + + @MessagePattern({ cmd: 'create_ahamove_order' }) + postAhamoveOrder(@Body() order: PostAhaOrderRequest) { + return this.ahamoveService.postAhamoveOrder(order); + } +} diff --git a/src/dependency/ahamove/ahamove.module.ts b/src/dependency/ahamove/ahamove.module.ts index eb75e55..91be752 100644 --- a/src/dependency/ahamove/ahamove.module.ts +++ b/src/dependency/ahamove/ahamove.module.ts @@ -2,10 +2,16 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { AhamoveService } from './ahamove.service'; import { ConfigModule } from '@nestjs/config'; +import { AhamoveController } from './ahamove.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AhamoveOrderEntity } from 'src/entity/ahamove-order.entity'; +import { AhamoveOrderHookEntity } from 'src/entity/ahamove-order-hook.entity'; +import { OrderModule } from 'src/feature/order/order.module'; @Module({ - imports: [HttpModule, ConfigModule], + imports: [HttpModule, ConfigModule, TypeOrmModule.forFeature([AhamoveOrderEntity, AhamoveOrderHookEntity]), OrderModule], providers: [AhamoveService], exports: [AhamoveService], + controllers: [AhamoveController], }) export class AhamoveModule {} diff --git a/src/dependency/ahamove/ahamove.service.ts b/src/dependency/ahamove/ahamove.service.ts index 3bc1ad5..5efbf48 100644 --- a/src/dependency/ahamove/ahamove.service.ts +++ b/src/dependency/ahamove/ahamove.service.ts @@ -1,23 +1,81 @@ import { HttpService } from '@nestjs/axios'; -import { Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger, OnModuleInit } from '@nestjs/common'; import { AxiosRequestConfig } from 'axios'; import { firstValueFrom, lastValueFrom } from 'rxjs'; import { Coordinate } from 'src/type'; import { FlagsmithService } from '../flagsmith/flagsmith.service'; import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; +import { AhamoveOrder, PostAhaOrderRequest } from './dto/ahamove.dto'; +import postAhaOrderRequestSchema, { coordinateListSchema } from './schema/ahamove.schema'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AhamoveOrderEntity } from 'src/entity/ahamove-order.entity'; +import { AhamoveMapper } from './mapper/ahamove.mapper'; +import { AhamoveOrderHookEntity } from 'src/entity/ahamove-order-hook.entity'; +import { OrderService } from 'src/feature/order/order.service'; @Injectable() -export class AhamoveService { +export class AhamoveService implements OnModuleInit { + AHA_MOVE_BASE_URL: string = ''; + AHA_MOVE_API_KEY: string = ''; + AHA_MOVE_MOBILE: string = ''; + AHA_MOVE_USERNAME: string = ''; + AHA_MOVE_TOKEN: string = ''; + AHA_MOVE_REFRESH_TOKEN: string = ''; + ON_WHEEL_SERVICE_NAME: string = 'VNM-PARTNER-2ALL'; + SGN_EXPRESS_SERVICE_NAME: string = 'SGN-EXPRESS'; + REQUEST_ID: string = 'SGN-EXPRESS-TRANSFER-COD'; + private readonly logger = new Logger(AhamoveService.name); + constructor( private readonly httpService: HttpService, @Inject('FLAGSMITH_SERVICE') private flagService: FlagsmithService, private configService: ConfigService, - ) {} - - async estimateTimeAndDistance( - startingPoint: Coordinate, - destination: Coordinate, + @InjectRepository(AhamoveOrderEntity) private ahamoveOrder: Repository, + @InjectRepository(AhamoveOrderHookEntity) private ahamoveOrderHook: Repository, + private readonly orderService: OrderService, ) { + this.AHA_MOVE_BASE_URL = configService.get('ahamove.baseUrl') || 'https://apistg.ahamove.com/api'; + this.AHA_MOVE_API_KEY = configService.get('ahamove.apiKey') || '7bbc5c69e7237f267e97f81237a717c387f13bdb'; + this.AHA_MOVE_USERNAME = configService.get('ahamove.username') || '2ALL Admin'; + this.AHA_MOVE_MOBILE = configService.get('ahamove.mobile') || '84905005248'; + } + + async onModuleInit() { + await this.getAhamoveAccessToken(); + } + + async getAhamoveAccessToken() { + let data = JSON.stringify({ + mobile: '84905005248', + name: '2ALL Admin', + api_key: '7bbc5c69e7237f267e97f81237a717c387f13bdb', + }); + + let config = { + method: 'post', + url: `${this.AHA_MOVE_BASE_URL}/v3/partner/account/register`, + headers: { + 'Content-Type': 'application/json', + }, + data: data, + }; + + axios + .request(config) + .then((response) => { + this.logger.log('getting token', response.data); + this.AHA_MOVE_TOKEN = response.data?.token; + this.AHA_MOVE_REFRESH_TOKEN = response.data?.refresh_token; + }) + .catch((error) => { + console.log(error); + this.logger.error('An error occurred ', JSON.stringify(error)); + }); + } + + async estimateTimeAndDistance(startingPoint: Coordinate, destination: Coordinate) { try { const data: any = JSON.stringify({ order_time: 0, @@ -51,9 +109,7 @@ export class AhamoveService { url: 'https://apistg.ahamove.com/api/v3/partner/order/estimate', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${this.configService.get( - 'ahamoveToken', - )}`, + Authorization: `Bearer ${this.AHA_MOVE_TOKEN}`, }, data: data, }; @@ -74,4 +130,127 @@ export class AhamoveService { }; } } + + async estimatePrice(coordinates: Coordinate[]) { + const { error, value } = await coordinateListSchema.validate(coordinates); + if (error) { + this.logger.warn('Bad coordinates: ' + coordinates); + throw new BadRequestException(error?.details.map((x) => x.message).join(', ')); + } + const startingPoint = coordinates[0]; + const destination = coordinates[1]; + const data: any = JSON.stringify({ + order_time: 0, + path: [ + { + lat: Number(startingPoint.lat), + lng: Number(startingPoint.long), + address: 'starting point', + }, + { + lat: Number(destination.lat), + lng: Number(destination.long), + address: 'destination', + }, + ], + services: [ + { + _id: 'VNM-PARTNER-2ALL', + }, + ], + payment_method: 'BALANCE', + requests: [ + { + _id: 'SGN-EXPRESS-TRANSFER-COD', + }, + ], + }); + const config: AxiosRequestConfig = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.AHA_MOVE_BASE_URL}/v3/partner/order/estimate`, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.AHA_MOVE_TOKEN}`, + }, + data: data, + }; + try { + const response = await axios.request(config); + return response.data; + } catch (error) { + this.logger.error('Error occurred while call to get estimate: ' + JSON.stringify(error)); + throw new InternalServerErrorException(error); + } + } + + async postAhamoveOrder(order: PostAhaOrderRequest) { + let serviceType = this.SGN_EXPRESS_SERVICE_NAME; + + if (order.serviceType === this.ON_WHEEL_SERVICE_NAME) { + serviceType = this.ON_WHEEL_SERVICE_NAME; + } + const orderRequest = await this.#buildAhamoveRequest(order, serviceType); + let config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.AHA_MOVE_BASE_URL}/v3/partner/order/create`, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.AHA_MOVE_TOKEN}`, + }, + data: JSON.stringify(orderRequest), + }; + try { + const { data } = await axios.request(config); + // update order id and response from ahamove + orderRequest.order_id = data.order_id; + orderRequest.response = data; + const result = await this.ahamoveOrder.save(AhamoveMapper.fromDTOtoEntity(orderRequest)); + this.logger.verbose('created ahamove order', JSON.stringify(result)); + return data; + } catch (error) { + this.logger.error('An error occurred while calling post ahamove order', JSON.stringify(error)); + throw new InternalServerErrorException('An error occurred'); + } + } + + async getAhamoveOrderByOrderId(orderId: string): Promise { + const result = await this.ahamoveOrder.findOne({ where: { order_id: orderId } }); + return result; + } + + async saveAhamoveTrackingWebhook(order) { + const result = await this.ahamoveOrderHook.save(order); + return result; + } + + async #buildAhamoveRequest(order: PostAhaOrderRequest, service_type: string): Promise { + try { + await postAhaOrderRequestSchema.validate(order); + } catch (error) { + this.logger.error('An error occurred ', JSON.stringify(error)); + throw new BadRequestException(error?.message); + } + const result = { + service_id: service_type, + path: [order.startingPoint, order.destination], + payment_method: order.paymentMethod || 'BALANCE', + total_pay: order.totalPay || 0, + order_time: order.orderTime || 0, + promo_code: order.promoCode || null, + remarks: order.remarks || '', + admin_note: order.adminNote || '', + route_optimized: false, + idle_until: 0, + items: order.items, + group_service_id: null, + group_requests: null, + package_detail: order.packageDetails, + } as unknown as AhamoveOrder; + if (service_type === this.SGN_EXPRESS_SERVICE_NAME) { + result.requests = [{ _id: this.REQUEST_ID }]; + } + return result; + } } diff --git a/src/dependency/ahamove/dto/ahamove.dto.ts b/src/dependency/ahamove/dto/ahamove.dto.ts new file mode 100644 index 0000000..aaeda8f --- /dev/null +++ b/src/dependency/ahamove/dto/ahamove.dto.ts @@ -0,0 +1,71 @@ +export interface AhamoveLocation { + address: string; + lat: number; + lng: number; + name: string; + mobile: string; + cod: number; + formatted_address: string; + short_address: string; + address_code: null | string; + remarks: string; + item_value: number; + require_pod?: boolean; // Optional property +} + +export interface AhaMoveRequest { + _id: string; +} + +export interface Item { + _id: string; + num: number; + name: string; + price: number; +} + +export interface PackageDetail { + weight: number; + length: number; + width: number; + height: number; + description: string; +} + +export interface AhamoveOrder { + service_id: string; + path: AhamoveLocation[]; + requests?: AhaMoveRequest[]; + payment_method: string; + total_pay: number; + order_time: number; + promo_code: null | string; + remarks: string; + admin_note: string; + route_optimized: boolean; + idle_until: number; + items: Item[]; + package_detail: PackageDetail[]; + group_service_id: null | string; + group_requests: null | string; + order_id?: string; + response: string; +} + +export interface PostAhaOrderRequest { + startingPoint: AhamoveLocation; + destination: AhamoveLocation; + paymentMethod: string; + totalPay: number; + orderTime: number; + promoCode: null | string; + remarks: string; + adminNote: string; + routeOptimized: boolean; + idleUntil: number; + items: Item[]; + packageDetails: PackageDetail[]; + groupServiceId: null | string; + groupRequests: null | string; + serviceType: null | string; +} diff --git a/src/dependency/ahamove/dto/ahamove.hook.ts b/src/dependency/ahamove/dto/ahamove.hook.ts new file mode 100644 index 0000000..d7a0fc1 --- /dev/null +++ b/src/dependency/ahamove/dto/ahamove.hook.ts @@ -0,0 +1,63 @@ +export interface PathLocation { + address: string; + cod: number; + por_info: string; + short_address: string | null; + formatted_address: string; + mobile: string; + status: string; + complete_lat: number; + complete_lng: number; + fail_lat: number; + fail_lng: number; + complete_time: number; + fail_time: number; + return_time: number; + pod_info: string; + fail_comment: string; + name: string; + remarks: string; +} + +interface Tracking { + _id: string; + accept_time: number; + board_time: number; + cancel_by_user: boolean; + cancel_comment: string; + cancel_image_url: string; + cancel_time: number; + city_id: string; + complete_time: number; + create_time: number; + currency: string; + order_time: number; + partner: string; + path: PathLocation[]; + payment_method: string; + pickup_time: number; + service_id: string; + status: string; + sub_status: string; + supplier_id: string; + supplier_name: string; + surcharge: number; + user_id: string; + user_name: string; + total_pay: number; + promo_code: string; + stoppoint_price: number; + special_request_price: number; + vat: number; + distance_price: number; + voucher_discount: number; + subtotal_price: number; + total_price: number; + surge_rate: number; + api_key: string; + shared_link: string; + insurance_portal_url: string; + app: string; + store_id: number; + distance: number; +} diff --git a/src/dependency/ahamove/mapper/ahamove.mapper.ts b/src/dependency/ahamove/mapper/ahamove.mapper.ts new file mode 100644 index 0000000..81abac7 --- /dev/null +++ b/src/dependency/ahamove/mapper/ahamove.mapper.ts @@ -0,0 +1,16 @@ +import { AhamoveOrderEntity } from 'src/entity/ahamove-order.entity'; +import { AhamoveOrder } from '../dto/ahamove.dto'; + +export class AhamoveMapper { + static fromDTOtoEntity(entityDTO: AhamoveOrder): AhamoveOrderEntity { + if (!entityDTO) { + return; + } + const entity = new AhamoveOrderEntity(); + const fields = Object.getOwnPropertyNames(entityDTO); + fields.forEach((field) => { + entity[field] = entityDTO[field]; + }); + return entity; + } +} diff --git a/src/dependency/ahamove/schema/ahamove.schema.ts b/src/dependency/ahamove/schema/ahamove.schema.ts new file mode 100644 index 0000000..d5292dc --- /dev/null +++ b/src/dependency/ahamove/schema/ahamove.schema.ts @@ -0,0 +1,57 @@ +import * as Joi from '@hapi/joi'; + +const locationSchema = Joi.object({ + address: Joi.string().optional(), + lat: Joi.number().required(), + lng: Joi.number().required(), + name: Joi.string().required(), + mobile: Joi.string().required(), + cod: Joi.number().required().min(0), + formattedAddress: Joi.string().optional(), + shortAddress: Joi.string().optional(), + addressCode: Joi.string().allow(null).optional(), + remarks: Joi.string().optional(), + itemValue: Joi.number().required(), + requirePod: Joi.boolean().optional(), +}); + +const itemSchema = Joi.object({ + _id: Joi.string().required(), + num: Joi.number().required(), + name: Joi.string().required(), + price: Joi.number().required(), +}); + +const packageDetailSchema = Joi.object({ + weight: Joi.number().optional(), + length: Joi.number().optional(), + width: Joi.number().optional(), + height: Joi.number().optional(), + description: Joi.string().optional(), +}); + +const postAhaOrderRequestSchema = Joi.object({ + startingPoint: locationSchema.required(), + destination: locationSchema.required(), + paymentMethod: Joi.string().required(), + totalPay: Joi.number().optional(), + orderTime: Joi.number().optional(), + promoCode: Joi.string().allow(null).optional(), + remarks: Joi.string().optional().allow(null), + adminNote: Joi.string().optional().allow(null), + routeOptimized: Joi.boolean().optional(), + idleUntil: Joi.number().optional(), + items: Joi.array().items(itemSchema).required(), + packageDetails: Joi.array().items(packageDetailSchema).required(), + groupServiceId: Joi.string().allow(null).optional(), + groupRequests: Joi.string().allow(null).optional(), + serviceType: Joi.string().required(), +}); + +const coordinateSchema = Joi.object({ + lat: Joi.number().required(), + long: Joi.number().required(), +}); + +export const coordinateListSchema = Joi.array().items(coordinateSchema).required(); +export default postAhaOrderRequestSchema; diff --git a/src/entity/ahamove-order-hook.entity.ts b/src/entity/ahamove-order-hook.entity.ts new file mode 100644 index 0000000..6b2f5a5 --- /dev/null +++ b/src/entity/ahamove-order-hook.entity.ts @@ -0,0 +1,128 @@ +import { PathLocation } from 'src/dependency/ahamove/dto/ahamove.hook'; +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('Ahamove_Order_Hook') +export class AhamoveOrderHookEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true }) + _id: string; + + @Column({ nullable: true }) + accept_time: number; + + @Column({ nullable: true }) + board_time: number; + + @Column({ default: false }) + cancel_by_user: boolean; + + @Column({ default: '' }) + cancel_comment: string; + + @Column({ default: '' }) + cancel_image_url: string; + + @Column({ nullable: true }) + cancel_time: number; + + @Column({ nullable: true }) + city_id: string; + + @Column({ nullable: true }) + complete_time: number; + + @Column({ nullable: true }) + create_time: number; + + @Column({ nullable: true }) + currency: string; + + @Column({ nullable: true }) + order_time: number; + + @Column({ nullable: true }) + partner: string; + + // Map the 'path' property as a JSON column + @Column('json') + path: PathLocation[]; + + @Column({ nullable: true }) + payment_method: string; + + @Column({ nullable: true }) + pickup_time: number; + + @Column({ nullable: true }) + service_id: string; + + @Column({ nullable: true }) + status: string; + + @Column({ nullable: true }) + sub_status: string; + + @Column({ nullable: true }) + supplier_id: string; + + @Column({ nullable: true }) + supplier_name: string; + + @Column({ nullable: true }) + surcharge: number; + + @Column({ nullable: true }) + user_id: string; + + @Column({ nullable: true }) + user_name: string; + + @Column({ nullable: true }) + total_pay: number; + + @Column({ nullable: true }) + promo_code: string; + + stoppoint_price: number; + + @Column({ nullable: true }) + special_request_price: number; + + @Column({ nullable: true }) + vat: number; + + @Column({ nullable: true }) + distance_price: number; + + @Column({ nullable: true }) + voucher_discount: number; + + @Column({ nullable: true }) + subtotal_price: number; + + @Column({ nullable: true }) + total_price: number; + + @Column({ nullable: true }) + surge_rate: number; + + @Column({ nullable: true }) + api_key: string; + + @Column({ nullable: true }) + shared_link: string; + + @Column({ nullable: true }) + insurance_portal_url: string; + + @Column({ nullable: true }) + app: string; + + @Column({ nullable: true }) + store_id: number; + + @Column({ nullable: true }) + distance: number; +} diff --git a/src/entity/ahamove-order.entity.ts b/src/entity/ahamove-order.entity.ts new file mode 100644 index 0000000..1fd7307 --- /dev/null +++ b/src/entity/ahamove-order.entity.ts @@ -0,0 +1,60 @@ +import { AhaMoveRequest, Item, PackageDetail } from 'src/dependency/ahamove/dto/ahamove.dto'; +import { PathLocation } from 'src/dependency/ahamove/dto/ahamove.hook'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm'; + +@Entity('Ahamove_Order') +export class AhamoveOrderEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: true }) + service_id: string; + + @Column({ nullable: true }) + order_id: string; + + @Column('json') + path: string; + + @Column('json', { nullable: true }) + requests: string; + + @Column() + payment_method: string; + + @Column() + total_pay: number; + + @Column() + order_time: number; + + @Column({ nullable: true }) + promo_code: string | null; + + @Column({ nullable: true }) + remarks: string; + + @Column({ nullable: true }) + admin_note: string; + + @Column() + route_optimized: boolean; + + @Column({ nullable: true }) + idle_until: number; + + @Column('json') + items: string; + + @Column('json') + response: string; + + @Column('json') + package_detail: string; + + @Column({ nullable: true }) + group_service_id: string | null; + + @Column({ nullable: true }) + group_requests: string | null; +} diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts new file mode 100644 index 0000000..afdfb98 --- /dev/null +++ b/src/entity/order.entity.ts @@ -0,0 +1,100 @@ +import { OrderStatus } from 'src/enum'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; + +@Entity({ name: 'Order' }) +export class Order { + @PrimaryGeneratedColumn() + order_id: number; + + @Column({ type: 'int', nullable: false }) + customer_id: number; + + @Column({ type: 'int', nullable: false }) + restaurant_id: number; + + @Column({ type: 'int', nullable: false }) + address_id: number; + + @Column({ type: 'int', nullable: true }) + driver_id: number; + + @Column({ type: 'int', nullable: false }) + order_total: number; + + @Column({ type: 'int', nullable: false }) + delivery_fee: number; + + @Column({ type: 'int', nullable: false }) + packaging_fee: number; + + @Column({ type: 'int', nullable: false }) + cutlery_fee: number; + + @Column({ type: 'int', nullable: false }) + app_fee: number; + + @Column({ type: 'int', nullable: false, default: 0 }) + coupon_value_from_platform: number; + + @Column({ type: 'int', nullable: false, default: 0 }) + coupon_value_from_restaurant: number; + + @Column({ type: 'int', nullable: true, default: null }) + coupon_id: number; + + @Column({ type: 'int', nullable: false }) + currency: number; + + @Column({ type: 'tinyint', nullable: false, default: '0' }) + is_preorder: boolean; + + @Column({ type: 'int', nullable: false }) + payment_method: number; + + @Column({ type: 'bigint', nullable: true }) + expected_arrival_time: number; + + @Column({ type: 'varchar', length: 255, nullable: true }) + delivery_order_id: string; + + //TODO Add other relations... + @Column({ + type: 'enum', + enum: OrderStatus, + default: OrderStatus.NEW, + }) + order_status_id: OrderStatus; + + @Column({ type: 'bigint', nullable: true }) + confirm_time: number; + + @Column({ type: 'bigint', nullable: true }) + processing_time: number; + + @Column({ type: 'bigint', nullable: true }) + driver_accept_time: number; + + @Column({ type: 'bigint', nullable: true }) + driver_cancel_time: number; + + @Column({ type: 'bigint', nullable: true }) + ready_time: number; + + @Column({ type: 'bigint', nullable: true }) + pickup_time: number; + + @Column({ type: 'bigint', nullable: true }) + completed_time: number; + + @Column({ type: 'bigint', nullable: true }) + fail_time: number; + + @Column({ type: 'bigint', nullable: true }) + return_time: number; + + @Column({ type: 'bigint', nullable: true }) + cancel_time: number; + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + created_at: Date; +} diff --git a/src/enum/index.ts b/src/enum/index.ts index 25f757d..d670fab 100644 --- a/src/enum/index.ts +++ b/src/enum/index.ts @@ -29,3 +29,14 @@ export enum FetchMode { Some = 'some', Full = 'full', } + +export enum OrderStatus { + CANCELLED = 'CANCELLED', + COMPLETED = 'COMPLETED', + DELIVERING = 'DELIVERING', + FAILED = 'FAILED', + IDLE = 'IDLE', + NEW = 'NEW', + PROCESSING = 'PROCESSING', + READY = 'READY', +} diff --git a/src/feature/order/order.controller.ts b/src/feature/order/order.controller.ts new file mode 100644 index 0000000..198c886 --- /dev/null +++ b/src/feature/order/order.controller.ts @@ -0,0 +1,23 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Order } from 'src/entity/order.entity'; +import { Repository } from 'typeorm'; +import { OrderService } from './order.service'; + +@Controller('order') +export class OrderController { + constructor( + @InjectRepository(Order) private orderRepo: Repository, + private readonly orderService: OrderService, + ) {} + @MessagePattern({ cmd: 'get_order_by_id' }) + async getOrderByOrderId(order_id) { + return this.orderRepo.findOne({ where: { order_id } }); + } + + @MessagePattern({ cmd: 'update_order_status_by_webhook' }) + async updateOrderStatusByWebhook({ delivery_order_id, webhookData }) { + return this.orderService.updateOrderStatusFromAhamoveWebhook(delivery_order_id, webhookData); + } +} diff --git a/src/feature/order/order.module.ts b/src/feature/order/order.module.ts new file mode 100644 index 0000000..f8eb72a --- /dev/null +++ b/src/feature/order/order.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { OrderService } from './order.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Order } from 'src/entity/order.entity'; +import { OrderController } from './order.controller'; + +@Module({ + providers: [OrderService], + imports: [TypeOrmModule.forFeature([Order])], + controllers: [OrderController], + exports: [OrderService], +}) +export class OrderModule {} diff --git a/src/feature/order/order.service.ts b/src/feature/order/order.service.ts new file mode 100644 index 0000000..4d71f13 --- /dev/null +++ b/src/feature/order/order.service.ts @@ -0,0 +1,111 @@ +import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Order } from 'src/entity/order.entity'; +import { OrderStatus } from 'src/enum'; +import { Repository } from 'typeorm'; + +@Injectable() +export class OrderService { + private readonly logger = new Logger(OrderService.name); + constructor(@InjectRepository(Order) private orderRepo: Repository) {} + + async updateOrderStatusFromAhamoveWebhook(orderId, webhookData): Promise { + try { + this.logger.log(`receiving data from webhook to ${orderId} with ${webhookData.status}`); + const currentOrder = await this.orderRepo.findOne({ where: { delivery_order_id: orderId } }); + if (currentOrder) { + const { status, cancel_time, sub_status, pickup_time, complete_time, fail_time, return_time, processing_time, accept_time } = webhookData; + const ahavemoveData = { status, accept_time, cancel_time, pickup_time, complete_time, fail_time, return_time, sub_status, processing_time, path_status: '' }; + ahavemoveData.path_status = webhookData?.path?.status; + const orderData = { + order_pickup_time: currentOrder.pickup_time, + is_preorder: currentOrder.is_preorder, + ready_time: currentOrder.ready_time, + }; + const updateData = this.getOrderStatusBaseOnAhahaStatus(orderData, ahavemoveData); + this.logger.log(`Updating data from webhook to ${orderId} with ${JSON.stringify(updateData)}`); + await this.orderRepo.update(currentOrder.order_id, { ...currentOrder, ...updateData }); + return { message: 'Order updated successfully' }; + } + return { message: 'Order not existed' }; + } catch (error) { + this.logger.error(`An error occurred while updating order: ${error.message}`); + throw new InternalServerErrorException(); + } + } + private getOrderStatusBaseOnAhahaStatus( + { order_pickup_time, is_preorder, ready_time }, + { status, sub_status, path_status, accept_time, cancel_time, pickup_time, complete_time, fail_time, return_time, processing_time }, + ) { + let data = {}; + switch (status) { + case 'ASSIGNING': + if (!is_preorder) { + data = { + order_status_id: OrderStatus.PROCESSING, + processing_time, + }; + } else if (is_preorder) { + data = {}; + } + break; + case 'ACCEPTED': + data = { + driver_accept_time: accept_time, + }; + break; + case 'CANCELLED': + data = { + driver_cancel_time: cancel_time, + }; + break; + // case 'CANCELLED': + // data = { + // cancel_time, + // }; + // break; + case 'IN PROCESS': + if (path_status === 'FAILED') { + data = {}; + } else { + if (pickup_time) { + data = { + order_status_id: OrderStatus.DELIVERING, + pickup_time: pickup_time, + ready_time: !ready_time ? pickup_time : ready_time, // if ready_time is null or equal = 0 + }; + } else if (!pickup_time) { + data = {}; + } + } + break; + case 'COMPLETED': + if (path_status === 'FAILED') { + data = { + order_status_id: OrderStatus.FAILED, + fail_time, + }; + } else if (path_status === 'RETURNED') { + data = { + return_time, + }; + } else if (sub_status === 'COMPLETED') { + data = { + order_status_id: OrderStatus.COMPLETED, + completed_time: complete_time, + }; + } else { + // data = { + // order_status_id: OrderStatus.COMPLETED, + // completed_time: complete_time, + // }; + data = {}; + } + break; + default: + this.logger.log('The value does not match any case'); + break; + } + return data; + } +} diff --git a/src/migration.config.ts b/src/migration.config.ts new file mode 100644 index 0000000..394d25d --- /dev/null +++ b/src/migration.config.ts @@ -0,0 +1,6 @@ +import { DataSource } from 'typeorm'; +import { ormConfig } from './ormconfig'; + +const datasource = new DataSource(ormConfig()); // config is one that is defined in datasource.config.ts file +datasource.initialize(); +export default datasource; diff --git a/src/ormconfig.ts b/src/ormconfig.ts new file mode 100644 index 0000000..0c11083 --- /dev/null +++ b/src/ormconfig.ts @@ -0,0 +1,76 @@ +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { DataSourceOptions } from 'typeorm'; + +function ormConfig(): DataSourceOptions { + console.log(__dirname + '/migrations/**/*{.ts,.js}'); + + const commonConf = { + SYNCRONIZE: true, + ENTITIES: [__dirname + '/entity/*.entity{.ts,.js}'], + MIGRATIONS: [__dirname + '/database/migrations/**/*{.ts,.js}'], + CLI: { + migrationsDir: 'src/migrations', + }, + MIGRATIONS_RUN: true, + }; + + let ormconfig: DataSourceOptions = { + name: 'default', + type: 'sqlite', + database: '../target/db/sqlite-dev-db.sql', + logging: true, + synchronize: true, + entities: commonConf.ENTITIES, + migrations: commonConf.MIGRATIONS, + migrationsRun: commonConf.MIGRATIONS_RUN, + }; + if (process.env.BACKEND_ENV === 'dev') { + ormconfig = { + name: 'default', + type: 'mysql', + database: 'new-2all-dev', + host: 'localhost', + port: 3308, + username: 'root', + password: 'P@ssW0rd', + logging: false, + synchronize: commonConf.SYNCRONIZE, + entities: commonConf.ENTITIES, + migrations: commonConf.MIGRATIONS, + migrationsRun: commonConf.MIGRATIONS_RUN, + }; + } + + if (process.env.BACKEND_ENV === 'stage') { + ormconfig = { + name: 'default', + type: 'mysql', + database: 'new-2all-dev', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + logging: false, + synchronize: commonConf.SYNCRONIZE, + entities: commonConf.ENTITIES, + migrations: commonConf.MIGRATIONS, + migrationsRun: commonConf.MIGRATIONS_RUN, + }; + } + + if (process.env.BACKEND_ENV === 'test') { + ormconfig = { + name: 'default', + type: 'sqlite', + database: ':memory:', + logging: true, + synchronize: true, + entities: commonConf.ENTITIES, + migrations: commonConf.MIGRATIONS, + migrationsRun: commonConf.MIGRATIONS_RUN, + }; + } + return ormconfig; +} + +export { ormConfig }; diff --git a/yarn.lock b/yarn.lock index 192b659..d7dbe2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -388,6 +388,46 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.54.0.tgz#4fab9a2ff7860082c304f750e94acd644cf984cf" integrity sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ== +"@hapi/address@^4.0.1": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d" + integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@hapi/formula@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128" + integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A== + +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/joi@^17.1.1": + version "17.1.1" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-17.1.1.tgz#9cc8d7e2c2213d1e46708c6260184b447c661350" + integrity sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg== + dependencies: + "@hapi/address" "^4.0.1" + "@hapi/formula" "^2.0.0" + "@hapi/hoek" "^9.0.0" + "@hapi/pinpoint" "^2.0.0" + "@hapi/topo" "^5.0.0" + +"@hapi/pinpoint@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.1.tgz#32077e715655fc00ab8df74b6b416114287d6513" + integrity sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q== + +"@hapi/topo@^5.0.0", "@hapi/topo@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -764,6 +804,14 @@ multer "1.4.4-lts.1" tslib "2.6.2" +"@nestjs/platform-socket.io@^10.3.3": + version "10.3.3" + resolved "https://registry.yarnpkg.com/@nestjs/platform-socket.io/-/platform-socket.io-10.3.3.tgz#eeb0e4d868bdb607de6648efd08e20753a264030" + integrity sha512-QqM9BMTdYPvXOqx3oWrv130HOtc2krPvfgqgDsPWkBLfR+TssrA5QDaTW8HSjEQAfmugvHwhEAAU4+yXRl6tKg== + dependencies: + socket.io "4.7.4" + tslib "2.6.2" + "@nestjs/schematics@^10.0.0", "@nestjs/schematics@^10.0.1": version "10.0.3" resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-10.0.3.tgz#0f48af0a20983ffecabcd8763213a3e53d43f270" @@ -789,6 +837,15 @@ dependencies: uuid "9.0.1" +"@nestjs/websockets@^10.3.3": + version "10.3.3" + resolved "https://registry.yarnpkg.com/@nestjs/websockets/-/websockets-10.3.3.tgz#fcb5420027c5968f042e4862bbf85fbe540a4a44" + integrity sha512-cR5cB0bLS87vd0iu7Nud/4x2EH1Vs0aIgwGWd0eH/5SAw0rrDNU81PiOde+rnMXETbxvSVfOZuLRyn7/WQtGUg== + dependencies: + iterare "1.2.1" + object-hash "3.0.0" + tslib "2.6.2" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -836,6 +893,23 @@ picocolors "^1.0.0" tslib "^2.6.0" +"@sideway/address@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" + integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -855,6 +929,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@sqltools/formatter@^1.2.5": version "1.2.5" resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12" @@ -928,11 +1007,23 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/cookiejar@*": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== +"@types/cors@^2.8.12": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + "@types/eslint-scope@^3.7.3": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -1035,6 +1126,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@>=10.0.0": + version "20.11.19" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" + integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== + dependencies: + undici-types "~5.26.4" + "@types/qs@*": version "6.9.10" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.10.tgz#0af26845b5067e1c9a622658a51f60a3934d51e8" @@ -1327,7 +1425,7 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -1582,6 +1680,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + big-integer@^1.6.44, big-integer@^1.6.51: version "1.6.52" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" @@ -1996,6 +2099,11 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" @@ -2006,7 +2114,7 @@ core-util-is@^1.0.3, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cors@2.8.5: +cors@2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -2070,7 +2178,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2240,6 +2348,27 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-parser@~5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" + integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== + +engine.io@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc" + integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -3635,6 +3764,17 @@ jest@^29.5.0: import-local "^3.0.2" jest-cli "^29.7.0" +joi@^17.12.1: + version "17.12.1" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.12.1.tgz#3347ecf4cd3301962d42191c021b165eef1f395b" + integrity sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ== + dependencies: + "@hapi/hoek" "^9.3.0" + "@hapi/topo" "^5.1.0" + "@sideway/address" "^4.1.5" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4108,6 +4248,11 @@ object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-hash@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.9.0: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" @@ -4830,6 +4975,34 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@4.7.4, socket.io@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.4.tgz#2401a2d7101e4bdc64da80b140d5d8b6a8c7738b" + integrity sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + sonic-boom@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.7.0.tgz#b4b7b8049a912986f4a92c51d4660b721b11f2f2" @@ -5500,6 +5673,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"