From 4cebb8e59b41f39252569ed898cdd2fba396ff23 Mon Sep 17 00:00:00 2001 From: nfesta2023 <142601504+nfesta2023@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:35:52 +0700 Subject: [PATCH] Fes 107 create the quick api to create menu item for testing (#105) * change db, create dtos and route * add logic to create menu item --------- Co-authored-by: NHT --- ...321852821-UpdateMenuItemCreationProcess.ts | 37 ++ src/dependency/momo/momo.service.ts | 1 - .../menu-item-attribute-ingredient.entity.ts | 18 + .../menu-item-attribute-value.entity.ts | 6 + src/entity/menu-item.entity.ts | 9 + src/entity/sku.entity.ts | 6 +- .../food/dto/create-menu-item-request.dto.ts | 80 ++++ .../food/dto/create-menu-item-response.dto.ts | 3 + src/feature/food/food.controller.ts | 50 +++ src/feature/food/food.service.ts | 418 +++++++++++++++++- src/feature/order/order.service.ts | 8 +- src/type/index.ts | 16 + 12 files changed, 641 insertions(+), 11 deletions(-) create mode 100644 src/database/migrations/1712321852821-UpdateMenuItemCreationProcess.ts create mode 100644 src/entity/menu-item-attribute-ingredient.entity.ts create mode 100644 src/feature/food/dto/create-menu-item-request.dto.ts create mode 100644 src/feature/food/dto/create-menu-item-response.dto.ts diff --git a/src/database/migrations/1712321852821-UpdateMenuItemCreationProcess.ts b/src/database/migrations/1712321852821-UpdateMenuItemCreationProcess.ts new file mode 100644 index 0000000..476448c --- /dev/null +++ b/src/database/migrations/1712321852821-UpdateMenuItemCreationProcess.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateMenuItemCreationProcess1712321852821 + implements MigrationInterface +{ + name = 'UpdateMenuItemCreationProcess1712321852821'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE Menu_Item + ADD COLUMN standard_price INT NULL AFTER units_sold; + `); + + await queryRunner.query(`ALTER TABLE Menu_Item + ADD COLUMN min_price INT NULL AFTER standard_price, + ADD COLUMN max_price INT NULL AFTER min_price; + `); + + await queryRunner.query(`ALTER TABLE Menu_Item_Attribute_Value + ADD COLUMN price_variance INT NULL AFTER note, + ADD COLUMN is_standard TINYINT NULL AFTER price_variance;`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE Menu_Item + DROP COLUMN standard_price; + `); + + await queryRunner.query(`ALTER TABLE Menu_Item + DROP COLUMN max_price, + DROP COLUMN min_price; + `); + + await queryRunner.query(`ALTER TABLE Menu_Item_Attribute_Value + DROP COLUMN is_standard, + DROP COLUMN price_variance;`); + } +} diff --git a/src/dependency/momo/momo.service.ts b/src/dependency/momo/momo.service.ts index f21a9d9..a3d8502 100644 --- a/src/dependency/momo/momo.service.ts +++ b/src/dependency/momo/momo.service.ts @@ -181,7 +181,6 @@ export class MomoService { throw new CustomRpcException(201, error.response?.data); } const momoOrderResult = response.data; - this.logger.debug('momoOrderResult: ', momoOrderResult); if (momoOrderResult.resultCode === 0) { // ABLE TO CALL API MOMO AND GET SUCESSFUL RESPONSE const momoResult = { diff --git a/src/entity/menu-item-attribute-ingredient.entity.ts b/src/entity/menu-item-attribute-ingredient.entity.ts new file mode 100644 index 0000000..3639597 --- /dev/null +++ b/src/entity/menu-item-attribute-ingredient.entity.ts @@ -0,0 +1,18 @@ +import { Entity, PrimaryColumn, CreateDateColumn } from 'typeorm'; + +@Entity('Menu_Item_Attribute_Ingredient') +export class MenuItemAttributeIngredient { + @PrimaryColumn() + public attribute_id: number; + + @PrimaryColumn() + public ingredient_id: number; + + @CreateDateColumn({ + type: 'datetime', + nullable: false, + unique: false, + default: () => 'CURRENT_TIMESTAMP', + }) + public created_at: Date; +} diff --git a/src/entity/menu-item-attribute-value.entity.ts b/src/entity/menu-item-attribute-value.entity.ts index 0deecc8..0702c04 100644 --- a/src/entity/menu-item-attribute-value.entity.ts +++ b/src/entity/menu-item-attribute-value.entity.ts @@ -30,6 +30,12 @@ export class MenuItemAttributeValue { @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) public note: string; + @Column({ type: 'int', nullable: true, unique: false }) + public price_variance: number; + + @Column({ type: 'tinyint', nullable: true, unique: false }) + public is_standard: number; + @CreateDateColumn({ type: 'datetime', nullable: false, diff --git a/src/entity/menu-item.entity.ts b/src/entity/menu-item.entity.ts index ca1dee4..d967f3b 100644 --- a/src/entity/menu-item.entity.ts +++ b/src/entity/menu-item.entity.ts @@ -95,6 +95,15 @@ export class MenuItem { @Column({ type: 'int', nullable: true, unique: false }) public units_sold: number; + @Column({ type: 'int', nullable: true, unique: false }) + public standard_price: number; + + @Column({ type: 'int', nullable: true, unique: false }) + public min_price: number; + + @Column({ type: 'int', nullable: true, unique: false }) + public max_price: number; + @CreateDateColumn({ type: 'datetime', nullable: false, diff --git a/src/entity/sku.entity.ts b/src/entity/sku.entity.ts index 70f12f0..4a0d4c2 100644 --- a/src/entity/sku.entity.ts +++ b/src/entity/sku.entity.ts @@ -57,7 +57,7 @@ export class SKU { nullable: true, unique: false, }) - public protein_g: number; + public protein_g: string; @Column({ type: 'decimal', @@ -66,7 +66,7 @@ export class SKU { nullable: true, unique: false, }) - public fat_g: number; + public fat_g: string; @Column({ type: 'decimal', @@ -75,7 +75,7 @@ export class SKU { nullable: true, unique: false, }) - public carbohydrate_g: number; + public carbohydrate_g: string; @CreateDateColumn({ type: 'datetime', diff --git a/src/feature/food/dto/create-menu-item-request.dto.ts b/src/feature/food/dto/create-menu-item-request.dto.ts new file mode 100644 index 0000000..d7c9171 --- /dev/null +++ b/src/feature/food/dto/create-menu-item-request.dto.ts @@ -0,0 +1,80 @@ +enum IsoLangCode { + VIE = 'vie', + ENG = 'eng', +} + +export interface RecipeItem { + ingredient_id: number; + + quantity: number; + + unit_id: number; +} + +interface PortionCustomizationValue { + value: number; + + unit_id: number; + + price_variance: number; //Can be negative + + is_standard: boolean; +} + +export interface PortionCustomizationItem { + name: string; + + corresponding_ingredients: number[]; + + value: PortionCustomizationValue[]; +} + +export interface PriceSetting { + standard: number; + min: number; + max: number; +} +export interface TasteCustomizationItem { + taste_id: string; + taste_values: string[]; +} + +export class CreateMenuItemRequest { + restaurant_id: number; //REQUIRED + + ISO_language_code: IsoLangCode; //REQUIRED + + name: string; //REQUIRED + + short_name: string; //REQUIRED + + description: string; //REQUIRED + + main_cooking_method: string; //REQUIRED + + preparing_time_minutes: number; //REQUIRED + + cooking_time_minutes: number; //REQUIRED + + is_vegetarian: boolean; //REQUIRED + + res_category_id: number; //REQUIRED + + image_url: string; //REQUIRED + + other_image_url: string[]; //OPTIONAL + + basic_customization: string[]; //REQUIRED + + recipe: RecipeItem[]; //REQUIRED + + portion_customization: PortionCustomizationItem[]; //REQUIRED + + price: PriceSetting; //REQUIRED + + taste_customization: TasteCustomizationItem[]; //REQUIRED + + packaging: number[]; //REQUIRED + + secret_key: string; //OPTIONAL +} diff --git a/src/feature/food/dto/create-menu-item-response.dto.ts b/src/feature/food/dto/create-menu-item-response.dto.ts new file mode 100644 index 0000000..02e7ed1 --- /dev/null +++ b/src/feature/food/dto/create-menu-item-response.dto.ts @@ -0,0 +1,3 @@ +export class CreateMenuItemResponse { + message: string; +} diff --git a/src/feature/food/food.controller.ts b/src/feature/food/food.controller.ts index 7c15b5f..7716df9 100644 --- a/src/feature/food/food.controller.ts +++ b/src/feature/food/food.controller.ts @@ -11,6 +11,8 @@ import { GetAvailableFoodByRestaurantResponse } from './dto/get-available-food-b import { GetSimilarFoodRequest } from './dto/get-similar-food-request.dto'; import { GetSimilarFoodResponse } from './dto/get-similar-food-response.dto'; import { CustomRpcExceptionFilter } from 'src/filters/custom-rpc-exception.filter'; +import { CreateMenuItemRequest } from './dto/create-menu-item-request.dto'; +import { CreateMenuItemResponse } from './dto/create-menu-item-response.dto'; @Controller() export class FoodController { @@ -106,4 +108,52 @@ export class FoodController { ); return await this.foodService.buildSimilarFoodResponse(similarMenuItemIds); } + + @MessagePattern({ cmd: 'create_menu_item_from_restaurant' }) + @UseFilters(new CustomRpcExceptionFilter()) + async createMenuItemFromRestaurant( + data: CreateMenuItemRequest, + ): Promise { + const { + restaurant_id, + ISO_language_code, + name, + short_name, + description, + main_cooking_method, + preparing_time_minutes, + cooking_time_minutes, + is_vegetarian, + res_category_id, + image_url, + other_image_url = [], + basic_customization, + recipe, + portion_customization, + price, + taste_customization, + packaging, + } = data; + await this.foodService.createMenuItemFromRestaurant( + restaurant_id, + ISO_language_code, + name, + short_name, + description, + main_cooking_method, + preparing_time_minutes, + cooking_time_minutes, + is_vegetarian, + res_category_id, + image_url, + other_image_url, + basic_customization, + recipe, + portion_customization, + price, + taste_customization, + packaging, + ); + return { message: 'create menu item successfullly' }; + } } diff --git a/src/feature/food/food.service.ts b/src/feature/food/food.service.ts index 11e8173..4f7ba78 100644 --- a/src/feature/food/food.service.ts +++ b/src/feature/food/food.service.ts @@ -13,6 +13,9 @@ import { Option, OptionValue, PackagingInfo, + PortionAttribute, + PortionAttributeValue, + PortionValue, PriceRange, RatingStatistic, Review, @@ -41,6 +44,19 @@ import { readFileSync } from 'fs'; import { MenuItemPackaging } from 'src/entity/menuitem-packaging.entity'; import { GetSimilarFoodResponse } from './dto/get-similar-food-response.dto'; import { CustomRpcException } from 'src/exceptions/custom-rpc.exception'; +import { MenuItemExt } from 'src/entity/menu-item-ext.entity'; +import { + RecipeItem, + PortionCustomizationItem, + PriceSetting, + TasteCustomizationItem, +} from './dto/create-menu-item-request.dto'; +import { BasicCustomization } from 'src/entity/basic-customization.entity'; +import { MenuItemAttributeExt } from 'src/entity/menu-item-attribute-ext.entity'; +import { MenuItemAttributeValue } from 'src/entity/menu-item-attribute-value.entity'; +import { MenuItemAttributeIngredient } from 'src/entity/menu-item-attribute-ingredient.entity'; +import { SkuDetail } from 'src/entity/sku-detail.entity'; +import { RpcException } from '@nestjs/microservices'; @Injectable() export class FoodService { @@ -530,9 +546,9 @@ export class FoodService { unit: priceUnit, is_standard: Boolean(rawSKU.is_standard), calorie_kcal: Number(rawSKU.calorie_kcal), - carb_g: rawSKU.carbohydrate_g, - protein_g: rawSKU.protein_g, - fat_g: rawSKU.fat_g, + carb_g: Number(rawSKU.carbohydrate_g), + protein_g: Number(rawSKU.protein_g), + fat_g: Number(rawSKU.fat_g), portion_customization: rawSKU.detail .filter((i) => i.attribute_obj.type_id == 'portion') .map((e) => { @@ -962,4 +978,400 @@ export class FoodService { return response; } + + async createMenuItemFromRestaurant( + restaurant_id: number, + + ISO_language_code, + + name: string, + + short_name: string, + + description: string, + + main_cooking_method: string, + + preparing_time_minutes: number, + + cooking_time_minutes: number, + + is_vegetarian: boolean, + + res_category_id: number, + + image_url: string, + + other_image_url: string[], + + basic_customization: string[], + + recipe: RecipeItem[], + + portion_customization: PortionCustomizationItem[], + + price: PriceSetting, + + taste_customization: TasteCustomizationItem[], + + packaging: number[], + ): Promise { + await this.entityManager.transaction(async (transactionalEntityManager) => { + //insert table Media + const menuItemImage = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(Media) + .values({ + type: 'image', + name: short_name, + url: image_url, + }) + .execute(); + const menuItemImageId = Number(menuItemImage.identifiers[0].media_id); + + //insert table Menu_Item + const menuItem = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItem) + .values({ + restaurant_id: restaurant_id, + is_active: 1, + is_vegetarian: Number(is_vegetarian), + cooking_schedule: '[]', + res_category_id: res_category_id, + preparing_time_s: preparing_time_minutes * 60, + cooking_time_s: cooking_time_minutes * 60, + image: menuItemImageId, + standard_price: price.standard, + min_price: price.min, + max_price: price.max, + }) + .execute(); + const menuItemId = Number(menuItem.identifiers[0].menu_item_id); + + //insert table Menu_Item_Ext + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemExt) + .values({ + menu_item_id: menuItemId, + ISO_language_code: ISO_language_code, + name: name, + short_name: short_name, + description: description, + main_cooking_method: main_cooking_method, + }) + .execute(); + + //insert table Media + for (let index = 0; index < other_image_url.length; index++) { + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(Media) + .values({ + type: 'image', + name: short_name + `_${index + 1}`, + url: other_image_url[index], + menu_item_id: menuItemId, + }) + .execute(); + } + + //insert table Basic_Customization + for (const basic of basic_customization) { + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(BasicCustomization) + .values({ + menu_item_id: menuItemId, + no_adding_id: basic, + }) + .execute(); + } + //insert table Recipe + for (const recipeItem of recipe) { + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(Recipe) + .values({ + ingredient_id: recipeItem.ingredient_id, + menu_item_id: menuItemId, + quantity: recipeItem.quantity, + unit: recipeItem.unit_id, + }) + .execute(); + } + + //insert Menu_Item_Attribute, Menu_Item_Attribute_Ext, Menu_Item_Attribute_Value + const portionAttributes: PortionAttribute[] = []; + for (const portionCustomizationItem of portion_customization) { + const portionAttribute: PortionAttribute = { + attribute_id: null, + values: [], + }; + + //insert Menu_Item_Attribute + const menuItemAttribute = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemAttribute) + .values({ + menu_item_id: menuItemId, + type_id: 'portion', + }) + .execute(); + const menuItemAttributeId = Number( + menuItemAttribute.identifiers[0].attribute_id, + ); + portionAttribute.attribute_id = menuItemAttributeId; + //insert Menu_Item_Attribute_Ext + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemAttributeExt) + .values({ + attribute_id: menuItemAttributeId, + ISO_language_code: ISO_language_code, + name: portionCustomizationItem.name, + }) + .execute(); + //Menu_Item_Attribute_Value + for (const val of portionCustomizationItem.value) { + const portionValue: PortionValue = { + value_id: null, + price_variance: val.price_variance, + is_standard: val.is_standard, + }; + const menuItemAttributeValue = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemAttributeValue) + .values({ + attribute_id: menuItemAttributeId, + value: val.value, + unit: val.unit_id, + price_variance: val.price_variance, + is_standard: Number(val.is_standard), + }) + .execute(); + const menuItemAttributeValueId = Number( + menuItemAttributeValue.identifiers[0].value_id, + ); + portionValue.value_id = menuItemAttributeValueId; + portionAttribute.values.push(portionValue); + } + + //insert table Menu_Item_Attribute_Ingredient + for (const ingredient of portionCustomizationItem.corresponding_ingredients) { + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemAttributeIngredient) + .values({ + attribute_id: menuItemAttributeId, + ingredient_id: ingredient, + }) + .execute(); + } + + portionAttributes.push(portionAttribute); + } + + // insert SKU, SKU_Detail + await this.createSKUs( + transactionalEntityManager, + portionAttributes, + menuItemId, + price.standard, + price.min, + price.max, + [], + ); + + // insert table Menu_Item_Attribute, Menu_Item_Attribute_Value + for (const tasteCustomizationItem of taste_customization) { + // insert table Menu_Item_Attribute + const menuItemAttribute = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemAttribute) + .values({ + menu_item_id: menuItemId, + type_id: 'taste', + taste_id: tasteCustomizationItem.taste_id, + }) + .execute(); + const menuItemAttributeId = Number( + menuItemAttribute.identifiers[0].attribute_id, + ); + for (const tasteValue of tasteCustomizationItem.taste_values) { + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemAttributeValue) + .values({ + attribute_id: menuItemAttributeId, + taste_value: tasteValue, + }) + .execute(); + } + } + + //insert table MenuItem_Packaging + for (let index = 0; index < packaging.length; index++) { + if (index === 0) { + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemPackaging) + .values({ + menu_item_id: menuItemId, + packaging_id: packaging[index], + is_default: 1, + }) + .execute(); + continue; + } + await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(MenuItemPackaging) + .values({ + menu_item_id: menuItemId, + packaging_id: packaging[index], + }) + .execute(); + } + }); + } + + async createSKUs( + entity_manager: EntityManager, + portion_attributes: PortionAttribute[], + menu_item_id: number, + standard_price: number, + min_price: number, + max_price: number, + // is_standard: number, + // price_variance: number, + previous_portion_attribute_values: PortionAttributeValue[], + ): Promise { + const CALORIE_KCAL = 366.0; + const PROTEIN_G = 4.0; + const FAT_G = 2.7; + const CARBONHYDRATE_G = 16.0; + if (portion_attributes.length === 1) { + for (const attributeValue of portion_attributes[0].values) { + //Calculate price for SKU + let price = 0; + let estimatedPrice = standard_price + attributeValue.price_variance; + // const estimatedPrice = standard_price + price_variance; + for (const previousPortionAttributeValue of previous_portion_attribute_values) { + estimatedPrice += previousPortionAttributeValue.price_variance; + } + if (estimatedPrice < min_price) { + price = min_price; + } else if (estimatedPrice > max_price) { + price = max_price; + } else { + price = estimatedPrice; + } + + //Check if SKU is standard + let is_standard = 1; + if (attributeValue.is_standard) { + for (const previousPortionAttributeValue of previous_portion_attribute_values) { + if (!previousPortionAttributeValue.is_standard) { + is_standard = 0; + break; + } + } + } else { + is_standard = 0; + } + + //Insert SKU + const sku = await entity_manager + .createQueryBuilder() + .insert() + .into(SKU) + .values({ + menu_item_id: menu_item_id, + price: price, + is_active: 1, + is_standard: is_standard, + calorie_kcal: ( + CALORIE_KCAL * + (estimatedPrice / standard_price) + ).toFixed(2), + protein_g: (PROTEIN_G * (estimatedPrice / standard_price)).toFixed( + 2, + ), + fat_g: (FAT_G * (estimatedPrice / standard_price)).toFixed(2), + carbohydrate_g: ( + CARBONHYDRATE_G * + (estimatedPrice / standard_price) + ).toFixed(2), + }) + .execute(); + const skuId = Number(sku.identifiers[0].sku_id); + + //Insert SKU Detail + await entity_manager + .createQueryBuilder() + .insert() + .into(SkuDetail) + .values({ + sku_id: skuId, + attribute_id: portion_attributes[0].attribute_id, + value_id: attributeValue.value_id, + }) + .execute(); + for (const previousPortionAttributeValue of previous_portion_attribute_values) { + await entity_manager + .createQueryBuilder() + .insert() + .into(SkuDetail) + .values({ + sku_id: skuId, + attribute_id: previousPortionAttributeValue.attribute_id, + value_id: previousPortionAttributeValue.value_id, + }) + .execute(); + } + } + } else if (portion_attributes.length > 1) { + for (const attributeValue of portion_attributes[0].values) { + //Add data to sku_details + const previousPortionAttributeValues = [ + ...previous_portion_attribute_values, + ]; + previousPortionAttributeValues.push({ + attribute_id: portion_attributes[0].attribute_id, + value_id: attributeValue.value_id, + price_variance: attributeValue.price_variance, + is_standard: attributeValue.is_standard, + }); + //Recursive call + await this.createSKUs( + entity_manager, + portion_attributes.slice(1), + menu_item_id, + standard_price, + min_price, + max_price, + previousPortionAttributeValues, + ); + } + } else { + this.logger.error('unknown error'); + throw new CustomRpcException(999, 'unknown error'); + } + } } diff --git a/src/feature/order/order.service.ts b/src/feature/order/order.service.ts index c452542..86bf982 100644 --- a/src/feature/order/order.service.ts +++ b/src/feature/order/order.service.ts @@ -304,7 +304,7 @@ export class OrderService { where: { order_id: order_id }, }); const isMomo = source?.isMomo; - this.logger.debug('canceling Order', JSON.stringify(currentOrder)); + // this.logger.debug('canceling Order', JSON.stringify(currentOrder)); if (!currentOrder) { this.logger.warn('The order status is not existed'); return; @@ -634,7 +634,7 @@ export class OrderService { )[0].data; const deliveryFee = deliveryEstimation.total_price; - this.logger.debug(deliveryFee); + // this.logger.debug(deliveryFee); if (deliveryFee != delivery_fee) { // throw new CustomRpcException(101, 'Delivery fee is not correct'); throw new CustomRpcException(101, { @@ -982,12 +982,12 @@ export class OrderService { const menuItemWithSkuIds = skus .filter((sku) => sku.menu_item_id == menuItem.menu_item_id) .map((i) => i.sku_id); - this.logger.debug(menuItemWithSkuIds); + // this.logger.debug(menuItemWithSkuIds); const orderingQuantity = items .filter((item) => menuItemWithSkuIds.includes(item.sku_id)) .map((i) => i.qty_ordered) .reduce((sum, quantity) => (sum += quantity), 0); - this.logger.debug(orderingQuantity); + // this.logger.debug(orderingQuantity); if (orderingQuantity > menuItem.quantity_available) { throw new CustomRpcException(108, { message: `Cannot order more than available quantity`, diff --git a/src/type/index.ts b/src/type/index.ts index 71b8f2f..cb019eb 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -252,3 +252,19 @@ interface OrderItemPackaging { description: TextByLang[]; price: number; } + +export interface PortionAttribute { + attribute_id: number; + values: PortionValue[]; +} +export interface PortionValue { + value_id: number; + price_variance: number; + is_standard: boolean; +} +export interface PortionAttributeValue { + attribute_id: number; + value_id: number; + price_variance: number; + is_standard: boolean; +}