From 672f28a180bddd4ea9f63ab8210e0e72119d8769 Mon Sep 17 00:00:00 2001 From: nfesta2023 <142601504+nfesta2023@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:15:39 +0700 Subject: [PATCH] Fes 83 create order (#79) * create order & update order logic * get driver infor and adjust table to get share_link * create ahamove order * create ahamove order * build other data for order detail response --------- Co-authored-by: NHT --- src/config/configuration.ts | 2 + .../1710669092619-UpdateAhamoveOrder.ts | 17 + src/dependency/ahamove/ahamove.module.ts | 1 - src/dependency/ahamove/ahamove.service.ts | 1 + src/dependency/ahamove/dto/ahamove.dto.ts | 1 + src/entity/ahamove-order.entity.ts | 18 +- src/entity/customer.entity.ts | 63 ++ src/entity/driver-status-log.entity.ts | 11 +- src/entity/driver.entity.ts | 25 +- src/entity/ingredient.entity.ts | 40 +- src/entity/invoice-history-status.entity.ts | 27 +- src/entity/invoice-status-ext.entity.ts | 40 + src/entity/invoice.entity.ts | 41 + src/entity/order-sku.entity.ts | 57 +- src/entity/order-status-ext.entity.ts | 36 + src/entity/order-status-log.entity.ts | 25 +- src/entity/order.entity.ts | 75 +- src/entity/packaging-ext.entity.ts | 9 +- src/entity/restaurant.entity.ts | 15 +- src/entity/sku.entity.ts | 2 +- src/enum/index.ts | 15 + src/feature/cart/cart.service.ts | 607 ++++++------ src/feature/common/common.service.ts | 330 ++++++- src/feature/food/food.service.ts | 2 +- .../order/dto/create-order-request.dto.ts | 43 + .../order/dto/create-order-response.dto.ts | 3 + .../order/dto/order-detail-response.dto.ts | 94 ++ src/feature/order/order.controller.ts | 42 +- src/feature/order/order.service.ts | 896 +++++++++++++++++- src/type/index.ts | 19 + 30 files changed, 2168 insertions(+), 389 deletions(-) create mode 100644 src/database/migrations/1710669092619-UpdateAhamoveOrder.ts create mode 100644 src/entity/customer.entity.ts create mode 100644 src/entity/invoice-status-ext.entity.ts create mode 100644 src/entity/order-status-ext.entity.ts create mode 100644 src/feature/order/dto/create-order-request.dto.ts create mode 100644 src/feature/order/dto/create-order-response.dto.ts create mode 100644 src/feature/order/dto/order-detail-response.dto.ts diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 59d9644..b0a3f53 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -21,4 +21,6 @@ export default () => ({ featureFlag: process.env.FEATURE_FLAG || '', ahamoveToken: process.env.AHAMOVE_TOKEN, planningDay: 7, // the restaurant will plan new cooking schedule every Saturday (last until the end of the day) + timeStepInTimSlotConverterM: 15, //minutes + deliverBufferTime: 5, //minutes }); diff --git a/src/database/migrations/1710669092619-UpdateAhamoveOrder.ts b/src/database/migrations/1710669092619-UpdateAhamoveOrder.ts new file mode 100644 index 0000000..17cabd7 --- /dev/null +++ b/src/database/migrations/1710669092619-UpdateAhamoveOrder.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateAhamoveOrder1710669092619 implements MigrationInterface { + name = 'UpdateAhamoveOrder1710669092619'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`Ahamove_Order\` ADD COLUMN \`shared_link\` VARCHAR(2048) NOT NULL AFTER \`response\``, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`Ahamove_Order\` DROP COLUMN \`shared_link\` `, + ); + } +} diff --git a/src/dependency/ahamove/ahamove.module.ts b/src/dependency/ahamove/ahamove.module.ts index f939353..1a8f66a 100644 --- a/src/dependency/ahamove/ahamove.module.ts +++ b/src/dependency/ahamove/ahamove.module.ts @@ -7,7 +7,6 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AhamoveOrderEntity } from 'src/entity/ahamove-order.entity'; import { AhamoveOrderHookEntity } from 'src/entity/ahamove-order-hook.entity'; import { InvoiceStatusHistoryModule } from 'src/feature/invoice-status-history/invoice-status-history.module'; -import { OrderModule } from 'src/feature/order/order.module'; @Module({ imports: [ diff --git a/src/dependency/ahamove/ahamove.service.ts b/src/dependency/ahamove/ahamove.service.ts index 751c960..dee3355 100644 --- a/src/dependency/ahamove/ahamove.service.ts +++ b/src/dependency/ahamove/ahamove.service.ts @@ -226,6 +226,7 @@ export class AhamoveService implements OnModuleInit { const { data } = await axios.request(config); // update order id and response from ahamove orderRequest.order_id = data.order_id; + orderRequest.shared_link = data.shared_link; orderRequest.response = data; const result = await this.ahamoveOrder.save( AhamoveMapper.fromDTOtoEntity(orderRequest), diff --git a/src/dependency/ahamove/dto/ahamove.dto.ts b/src/dependency/ahamove/dto/ahamove.dto.ts index aaeda8f..cf1ec11 100644 --- a/src/dependency/ahamove/dto/ahamove.dto.ts +++ b/src/dependency/ahamove/dto/ahamove.dto.ts @@ -50,6 +50,7 @@ export interface AhamoveOrder { group_requests: null | string; order_id?: string; response: string; + shared_link?: string; } export interface PostAhaOrderRequest { diff --git a/src/entity/ahamove-order.entity.ts b/src/entity/ahamove-order.entity.ts index 1fd7307..5b86b39 100644 --- a/src/entity/ahamove-order.entity.ts +++ b/src/entity/ahamove-order.entity.ts @@ -1,6 +1,17 @@ -import { AhaMoveRequest, Item, PackageDetail } from 'src/dependency/ahamove/dto/ahamove.dto'; +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'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + OneToMany, + ManyToOne, + JoinColumn, +} from 'typeorm'; @Entity('Ahamove_Order') export class AhamoveOrderEntity { @@ -57,4 +68,7 @@ export class AhamoveOrderEntity { @Column({ nullable: true }) group_requests: string | null; + + @Column({ type: 'varchar', length: 2048, nullable: true }) + shared_link: string; } diff --git a/src/entity/customer.entity.ts b/src/entity/customer.entity.ts new file mode 100644 index 0000000..0a26123 --- /dev/null +++ b/src/entity/customer.entity.ts @@ -0,0 +1,63 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Media } from './media.entity'; + +@Entity('Customer') +export class Customer { + @PrimaryGeneratedColumn() + public customer_id: number; + + @Column({ type: 'varchar', length: 25, nullable: false, unique: true }) + public phone_number: string; + + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) + public name: string; + + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) + public email: string; + + @Column({ type: 'date', nullable: true, unique: false }) + public birthday: Date; + + @Column({ type: 'char', length: 1, nullable: true, unique: false }) + public sex: string; + + @OneToOne(() => Media) + @JoinColumn({ + name: 'profile_image', + referencedColumnName: 'media_id', + }) + public profile_image: Media; + + @Column({ + type: 'tinyint', + nullable: false, + unique: false, + default: 0, + }) + public is_active: number; + + // @OneToOne(() => HealthInfo) + // @JoinColumn({ + // name: 'health_info_id', + // referencedColumnName: 'health_info_id', + // }) + // public health_info: HealthInfo; + + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) + public refresh_token: string; + + @CreateDateColumn({ + type: 'datetime', + nullable: false, + unique: false, + default: () => 'CURRENT_TIMESTAMP', + }) + public created_at: Date; +} diff --git a/src/entity/driver-status-log.entity.ts b/src/entity/driver-status-log.entity.ts index 62e615e..5ea5e81 100644 --- a/src/entity/driver-status-log.entity.ts +++ b/src/entity/driver-status-log.entity.ts @@ -13,23 +13,24 @@ export class DriverStatusLog { @PrimaryGeneratedColumn('uuid') log_id: string; - @Column() + @Column({ type: 'int', nullable: false, unique: false }) order_id: number; - @Column({ nullable: true }) + @Column({ type: 'int', nullable: true, unique: false }) driver_id: number | null; - @Column('text', { nullable: true }) + @Column('text', { nullable: true, unique: false }) note: string | null; @Column('bigint') logged_at: number; + //RELATIONSHIP @ManyToOne(() => Order) - @JoinColumn({ name: 'order_id' }) + @JoinColumn({ name: 'order_id', referencedColumnName: 'order_id' }) order: Order; @ManyToOne(() => Driver, { nullable: true }) - @JoinColumn({ name: 'driver_id' }) + @JoinColumn({ name: 'driver_id', referencedColumnName: 'driver_id' }) driver: Driver | null; } diff --git a/src/entity/driver.entity.ts b/src/entity/driver.entity.ts index 9318d73..ee5ec96 100644 --- a/src/entity/driver.entity.ts +++ b/src/entity/driver.entity.ts @@ -3,26 +3,29 @@ import { PrimaryGeneratedColumn, Column, CreateDateColumn, + ManyToOne, + JoinColumn, } from 'typeorm'; +import { Media } from './media.entity'; @Entity('Driver') export class Driver { @PrimaryGeneratedColumn() driver_id: number; - @Column({ length: 255 }) + @Column({ type: 'varchar', length: 255, nullable: false, unique: false }) name: string; - @Column({ length: 25, nullable: true }) + @Column({ type: 'varchar', length: 25, nullable: true, unique: false }) phone_number: string | null; - @Column({ length: 255, nullable: true }) + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) email: string | null; - @Column({ length: 255, nullable: true }) + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) vehicle: string | null; - @Column({ length: 48, nullable: true }) + @Column({ type: 'varchar', length: 48, nullable: true, unique: false }) license_plates: string | null; @Column({ @@ -32,10 +35,10 @@ export class Driver { }) type: 'AHAMOVE' | 'ONWHEEL' | null; - @Column({ length: 255, nullable: true }) + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) reference_id: string; - @Column({ type: 'int', nullable: true }) + @Column({ type: 'int', nullable: true, unique: false }) profile_image: number; @CreateDateColumn({ @@ -45,4 +48,12 @@ export class Driver { default: () => 'CURRENT_TIMESTAMP', }) public created_at: Date; + + //RELATIONSHIP + @ManyToOne(() => Media) + @JoinColumn({ + name: 'profile_image', + referencedColumnName: 'media_id', + }) + profile_image_obj: Media; } diff --git a/src/entity/ingredient.entity.ts b/src/entity/ingredient.entity.ts index d474804..cec70a2 100644 --- a/src/entity/ingredient.entity.ts +++ b/src/entity/ingredient.entity.ts @@ -18,17 +18,41 @@ export class Ingredient { @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) public eng_name: string; - @Column({ type: 'int', nullable: true, unique: false }) - public calorie_kcal: number; + @Column({ + type: 'decimal', + precision: 10, + scale: 2, + nullable: true, + unique: false, + }) + public calorie_kcal: string; - @Column({ type: 'int', nullable: true, unique: false }) - public protein_g: number; + @Column({ + type: 'decimal', + precision: 10, + scale: 2, + nullable: true, + unique: false, + }) + public protein_g: string; - @Column({ type: 'int', nullable: true, unique: false }) - public fat_g: number; + @Column({ + type: 'decimal', + precision: 10, + scale: 2, + nullable: true, + unique: false, + }) + public fat_g: string; - @Column({ type: 'int', nullable: true, unique: false }) - public carbohydrate_g: number; + @Column({ + type: 'decimal', + precision: 10, + scale: 2, + nullable: true, + unique: false, + }) + public carbohydrate_g: string; @Column({ type: 'varchar', length: 10, nullable: true, unique: false }) public ma_BTP2007: string; diff --git a/src/entity/invoice-history-status.entity.ts b/src/entity/invoice-history-status.entity.ts index eec6363..08fa3fc 100644 --- a/src/entity/invoice-history-status.entity.ts +++ b/src/entity/invoice-history-status.entity.ts @@ -1,23 +1,23 @@ import { Entity, - PrimaryColumn, Column, + PrimaryGeneratedColumn, ManyToOne, JoinColumn, - CreateDateColumn, + OneToMany, } from 'typeorm'; import { Invoice } from './invoice.entity'; +import { InvoiceStatusExt } from './invoice-status-ext.entity'; @Entity('Invoice_Status_History') export class InvoiceStatusHistory { - @PrimaryColumn({ - type: 'varchar', - length: 36, - }) + @PrimaryGeneratedColumn('uuid') status_history_id: string; @Column({ type: 'int', + nullable: false, + unique: false, }) invoice_id: number; @@ -25,15 +25,28 @@ export class InvoiceStatusHistory { type: 'varchar', length: 64, nullable: true, + unique: false, }) status_id: string | null; @Column({ type: 'text', nullable: true, + unique: false, }) note: string | null; - @Column({ type: 'bigint', nullable: false }) + @Column({ type: 'bigint', nullable: false, unique: false }) created_at: number; + + //RELATIONSHIP + @ManyToOne(() => Invoice, (invoice) => invoice.history_status_obj) + @JoinColumn({ + name: 'invoice_id', + referencedColumnName: 'invoice_id', + }) + public invoice_obj: Invoice; + + @OneToMany(() => InvoiceStatusExt, (ext) => ext.invoice_status_history) + public invoice_status_ext: InvoiceStatusExt[]; } diff --git a/src/entity/invoice-status-ext.entity.ts b/src/entity/invoice-status-ext.entity.ts new file mode 100644 index 0000000..5ef5232 --- /dev/null +++ b/src/entity/invoice-status-ext.entity.ts @@ -0,0 +1,40 @@ +import { + Column, + Entity, + PrimaryColumn, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { InvoiceStatusHistory } from './invoice-history-status.entity'; + +@Entity('Invoice_Status_Ext') +export class InvoiceStatusExt { + @PrimaryColumn() + public status_id: string; + + @PrimaryColumn() + public ISO_language_code: string; + + @Column({ type: 'varchar', length: 64, nullable: false, unique: false }) + public name: string; + + @CreateDateColumn({ + type: 'datetime', + nullable: false, + unique: false, + default: () => 'CURRENT_TIMESTAMP', + }) + public created_at: Date; + + //RELATIONSHIP + @ManyToOne( + () => InvoiceStatusHistory, + (history) => history.invoice_status_ext, + ) + @JoinColumn({ + name: 'status_id', + referencedColumnName: 'status_id', + }) + public invoice_status_history: InvoiceStatusHistory; +} diff --git a/src/entity/invoice.entity.ts b/src/entity/invoice.entity.ts index 063ad78..19e26e6 100644 --- a/src/entity/invoice.entity.ts +++ b/src/entity/invoice.entity.ts @@ -3,7 +3,14 @@ import { PrimaryGeneratedColumn, Column, CreateDateColumn, + OneToOne, + JoinColumn, + OneToMany, + ManyToOne, } from 'typeorm'; +import { Order } from './order.entity'; +import { InvoiceStatusHistory } from './invoice-history-status.entity'; +import { PaymentOption } from './payment-option.entity'; @Entity('Invoice') export class Invoice { @@ -12,38 +19,51 @@ export class Invoice { @Column({ type: 'int', + nullable: false, + unique: false, }) payment_method: number; @Column({ type: 'int', + nullable: false, + unique: false, }) total_amount: number; @Column({ type: 'int', + nullable: false, + unique: false, }) tax_amount: number; @Column({ type: 'int', default: 0, + nullable: false, + unique: false, }) discount_amount: number; @Column({ type: 'text', nullable: true, + unique: false, }) description: string | null; @Column({ type: 'int', + nullable: false, + unique: false, }) order_id: number; @Column({ type: 'int', + nullable: false, + unique: false, }) currency: number; @@ -51,12 +71,33 @@ export class Invoice { type: 'varchar', length: 255, nullable: true, + unique: false, }) payment_order_id: string | null; @CreateDateColumn({ type: 'datetime', + nullable: false, + unique: false, default: () => 'CURRENT_TIMESTAMP', }) created_at: Date; + + //RELATIONSHIP + @OneToOne(() => Order, (order) => order.invoice_obj) + @JoinColumn({ + name: 'order_id', + referencedColumnName: 'order_id', + }) + public order_obj: Order; + + @OneToMany(() => InvoiceStatusHistory, (history) => history.invoice_obj) + public history_status_obj: InvoiceStatusHistory[]; + + @ManyToOne(() => PaymentOption) + @JoinColumn({ + name: 'payment_method', + referencedColumnName: 'option_id', + }) + public payment_option_obj: PaymentOption; } diff --git a/src/entity/order-sku.entity.ts b/src/entity/order-sku.entity.ts index a96a352..0013e58 100644 --- a/src/entity/order-sku.entity.ts +++ b/src/entity/order-sku.entity.ts @@ -1,7 +1,16 @@ -import { Entity, CreateDateColumn, PrimaryGeneratedColumn, Column, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; -import { Unit } from './unit.entity'; +import { + Entity, + CreateDateColumn, + PrimaryGeneratedColumn, + Column, + JoinColumn, + ManyToOne, + OneToOne, +} from 'typeorm'; import { SKU } from './sku.entity'; import { FoodRating } from './food-rating.entity'; +import { Packaging } from './packaging.entity'; +import { Order } from './order.entity'; @Entity('Order_SKU') export class OrderSKU { @@ -20,8 +29,8 @@ export class OrderSKU { @Column({ type: 'int', nullable: false, unique: false }) public price: number; - @Column({ type: 'int', nullable: false, unique: false }) - public currency: number; + // @Column({ type: 'int', nullable: false, unique: false }) + // public currency: number; @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) public advanced_taste_customization: string; @@ -29,9 +38,21 @@ export class OrderSKU { @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) public basic_taste_customization: string; + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) + public portion_customization: string; + + @Column({ type: 'text', nullable: true, unique: false }) + public advanced_taste_customization_obj: string; + + @Column({ type: 'text', nullable: true, unique: false }) + public basic_taste_customization_obj: string; + @Column({ type: 'text', nullable: true, unique: false }) public notes: string; + @Column({ type: 'int', nullable: true, unique: false }) + public packaging_id: number; + @Column({ type: 'int', nullable: true, unique: false }) public label_id: number; @@ -42,7 +63,7 @@ export class OrderSKU { nullable: true, unique: false, }) - public calorie_kcal: number; + public calorie_kcal: string; @CreateDateColumn({ type: 'datetime', @@ -54,12 +75,12 @@ export class OrderSKU { //RELATIONSHIP - @ManyToOne(() => Unit) - @JoinColumn({ - name: 'currency', - referencedColumnName: 'unit_id', - }) - public currency_obj: Unit; + // @ManyToOne(() => Unit) + // @JoinColumn({ + // name: 'currency', + // referencedColumnName: 'unit_id', + // }) + // public currency_obj: Unit; @ManyToOne(() => SKU, (sku) => sku.order_sku_obj) @JoinColumn({ @@ -70,4 +91,18 @@ export class OrderSKU { @OneToOne(() => FoodRating, (foodRating) => foodRating.order_sku_obj) public food_rating_obj: FoodRating; + + @ManyToOne(() => Packaging) + @JoinColumn({ + name: 'packaging_id', + referencedColumnName: 'packaging_id', + }) + packaging_obj: Packaging; + + @ManyToOne(() => Order, (order) => order.items) + @JoinColumn({ + name: 'order_id', + referencedColumnName: 'order_id', + }) + public order_obj: Order; } diff --git a/src/entity/order-status-ext.entity.ts b/src/entity/order-status-ext.entity.ts new file mode 100644 index 0000000..3dfb0df --- /dev/null +++ b/src/entity/order-status-ext.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { OrderStatusLog } from './order-status-log.entity'; + +@Entity('Order_Status_Ext') +export class OrderStatusExt { + @PrimaryColumn() + public order_status_id: string; + @PrimaryColumn() + public ISO_language_code: string; + + @Column({ type: 'text', nullable: false, unique: false }) + public description: string; + + @CreateDateColumn({ + type: 'datetime', + nullable: false, + unique: false, + default: () => 'CURRENT_TIMESTAMP', + }) + public created_at: Date; + + //Relationship + @ManyToOne(() => OrderStatusLog, (log) => log.order_status_ext) + @JoinColumn({ + name: 'order_status_id', + referencedColumnName: 'order_status_id', + }) + public order_status_log_obj: OrderStatusLog; +} diff --git a/src/entity/order-status-log.entity.ts b/src/entity/order-status-log.entity.ts index a24fd9b..1fa94d7 100644 --- a/src/entity/order-status-log.entity.ts +++ b/src/entity/order-status-log.entity.ts @@ -1,20 +1,37 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + OneToMany, +} from 'typeorm'; import { OrderStatus } from 'src/enum'; +import { Order } from './order.entity'; +import { OrderStatusExt } from './order-status-ext.entity'; @Entity('Order_Status_Log') export class OrderStatusLog { @PrimaryGeneratedColumn('uuid') log_id: string; - @Column({ type: 'int' }) + @Column({ type: 'int', nullable: false, unique: false }) order_id: number; - @Column({ type: 'varchar', length: 64 }) + @Column({ type: 'varchar', length: 64, nullable: false, unique: false }) order_status_id: OrderStatus; - @Column('text', { nullable: true }) + @Column('text', { nullable: true, unique: false }) note: string | null; @Column('bigint') logged_at: number; + + //RELATIONSHIP + @ManyToOne(() => Order, (order) => order.order_status_log) + @JoinColumn({ name: 'order_id', referencedColumnName: 'order_id' }) + public order_obj: Order; + + @OneToMany(() => OrderStatusExt, (ext) => ext.order_status_log_obj) + public order_status_ext: OrderStatusExt[]; } diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts index ba41172..275e3bf 100644 --- a/src/entity/order.entity.ts +++ b/src/entity/order.entity.ts @@ -1,61 +1,94 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + OneToMany, + OneToOne, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { OrderStatusLog } from './order-status-log.entity'; +import { Invoice } from './invoice.entity'; +import { OrderSKU } from './order-sku.entity'; +import { Address } from './address.entity'; @Entity({ name: 'Order' }) export class Order { @PrimaryGeneratedColumn() order_id: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) customer_id: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) restaurant_id: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) address_id: number; - // @Column({ type: 'int', nullable: true }) - // driver_id: number; - - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) order_total: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) delivery_fee: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) packaging_fee: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) cutlery_fee: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) app_fee: number; - @Column({ type: 'int', nullable: false, default: 0 }) + @Column({ type: 'int', nullable: false, default: 0, unique: false }) coupon_value_from_platform: number; - @Column({ type: 'int', nullable: false, default: 0 }) + @Column({ type: 'int', nullable: false, default: 0, unique: false }) coupon_value_from_restaurant: number; - @Column({ type: 'int', nullable: true, default: null }) + @Column({ type: 'int', nullable: true, default: null, unique: false }) coupon_id: number; - @Column({ type: 'int', nullable: false }) + @Column({ type: 'int', nullable: false, unique: false }) currency: number; - @Column({ type: 'tinyint', nullable: false, default: '0' }) + @Column({ type: 'tinyint', nullable: false, default: '0', unique: false }) is_preorder: boolean; - @Column({ type: 'bigint', nullable: true }) + @Column({ type: 'bigint', nullable: true, unique: false }) expected_arrival_time: number; - @Column({ type: 'varchar', length: 255, nullable: true }) + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) delivery_order_id: string; - @Column({ type: 'varchar', length: 255, nullable: true }) + @Column({ type: 'varchar', length: 255, nullable: true, unique: false }) driver_note: string; - @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + @CreateDateColumn({ + type: 'datetime', + nullable: false, + unique: false, + default: () => 'CURRENT_TIMESTAMP', + }) created_at: Date; + + //RELATIONSHIP + + @OneToMany(() => OrderStatusLog, (status) => status.order_obj) + public order_status_log: OrderStatusLog[]; + + @OneToOne(() => Invoice, (invoice) => invoice.order_obj) + public invoice_obj: Invoice; + + @OneToMany(() => OrderSKU, (item) => item.order_obj) + public items: OrderSKU[]; + + @ManyToOne(() => Address) + @JoinColumn({ + name: 'address_id', + referencedColumnName: 'address_id', + }) + public address_obj: Address; } diff --git a/src/entity/packaging-ext.entity.ts b/src/entity/packaging-ext.entity.ts index 0a7f552..6520534 100644 --- a/src/entity/packaging-ext.entity.ts +++ b/src/entity/packaging-ext.entity.ts @@ -1,4 +1,11 @@ -import { Entity, CreateDateColumn, PrimaryColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { + Entity, + CreateDateColumn, + PrimaryColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { Packaging } from './packaging.entity'; @Entity('Packaging_Ext') diff --git a/src/entity/restaurant.entity.ts b/src/entity/restaurant.entity.ts index 82cdaa8..b0bfd57 100644 --- a/src/entity/restaurant.entity.ts +++ b/src/entity/restaurant.entity.ts @@ -20,12 +20,8 @@ export class Restaurant { @PrimaryGeneratedColumn() public restaurant_id: number; - @OneToOne(() => Address, { eager: true }) - @JoinColumn({ - name: 'address_id', - referencedColumnName: 'address_id', - }) - public address: Address; + @Column({ type: 'int', nullable: true, unique: false }) + public address_id: number; @Column({ type: 'varchar', length: 25, nullable: false, unique: false }) public phone_number: string; @@ -145,4 +141,11 @@ export class Restaurant { referencedColumnName: 'unit_id', }) public unit_obj: Unit; + + @OneToOne(() => Address, { eager: true }) + @JoinColumn({ + name: 'address_id', + referencedColumnName: 'address_id', + }) + public address: Address; } diff --git a/src/entity/sku.entity.ts b/src/entity/sku.entity.ts index e961e5b..70f12f0 100644 --- a/src/entity/sku.entity.ts +++ b/src/entity/sku.entity.ts @@ -48,7 +48,7 @@ export class SKU { nullable: true, unique: false, }) - public calorie_kcal: number; + public calorie_kcal: string; @Column({ type: 'decimal', diff --git a/src/enum/index.ts b/src/enum/index.ts index 3da39b5..4deeb89 100644 --- a/src/enum/index.ts +++ b/src/enum/index.ts @@ -72,3 +72,18 @@ export enum CalculationType { PERCENTAGE = 'percentage', FIXED = 'fixed', } + +export enum OrderMilestones { + CANCELLED = 'cancelled', + COMPLETED = 'completed', + PICKED_UP = 'picked_up', + FAILED = 'failed', + CONFIRMED = 'confirmed', + CREATED = 'created', + START_TO_PROCESS = 'started_to_process', +} + +export enum PaymentList { + MOMO = 'momo', + COD = 'COD', +} diff --git a/src/feature/cart/cart.service.ts b/src/feature/cart/cart.service.ts index e34d512..4f9ea1a 100644 --- a/src/feature/cart/cart.service.ts +++ b/src/feature/cart/cart.service.ts @@ -883,309 +883,320 @@ export class CartService { ): Promise { const timeSlots = []; - const menuItems = await this.commonService.getMenuItemByIds(menu_item_ids); - - //Check if menu_item_ids do exist - if (menuItems.length != menu_item_ids.length) { - throw new HttpException('Some of menu items do not exist', 400); - } - - //Check if menu_item_ids belong to the same restaurant - const restaurantId = menuItems[0].restaurant_id; - if (menuItems.find((i) => i.restaurant_id != restaurantId)) { - throw new HttpException( - 'Some of menu items do not belong to the same restaurant', - 400, - ); - } + // const menuItems = await this.commonService.getMenuItemByIds(menu_item_ids); + + // //Check if menu_item_ids do exist + // if (menuItems.length != menu_item_ids.length) { + // throw new HttpException('Some of menu items do not exist', 400); + // } + + // //Check if menu_item_ids belong to the same restaurant + // const restaurantId = menuItems[0].restaurant_id; + // if (menuItems.find((i) => i.restaurant_id != restaurantId)) { + // throw new HttpException( + // 'Some of menu items do not belong to the same restaurant', + // 400, + // ); + // } + + // //get the delivery time + // const delivery_time_s = ( + // await this.commonService.estimateTimeAndDistanceForRestaurant( + // restaurantId, + // long, + // lat, + // ) + // ).duration_s; + // if (!delivery_time_s) { + // throw new HttpException( + // 'There some error with the delivery estimation', + // 500, + // ); + // } + + // const restaurantUtcTimeZone = + // await this.commonService.getUtcTimeZone(restaurantId); + // const timeZoneOffset = restaurantUtcTimeZone * 60 * 60 * 1000; // Offset in milliseconds for EST + + // const localTodayId = new Date(now + timeZoneOffset).getUTCDay() + 1; // 1->7: Sunday -> Saturday + + // //Step1: Find the schedule in which all of the menu items are available + // const overlapSchedule: DayShift[] = []; + // for (let index = localTodayId - 1; index < DAY_ID.length; index++) { + // const localTodayId = DAY_ID[index]; + // overlapSchedule.push({ + // day_id: localTodayId, + // day_name: DAY_NAME[index], + // from: Shift.MorningFrom, + // to: Shift.MorningTo, + // is_available: true, + // }); + // overlapSchedule.push({ + // day_id: localTodayId, + // day_name: DAY_NAME[index], + // from: Shift.AfternoonFrom, + // to: Shift.AfternoonTo, + // is_available: true, + // }); + // overlapSchedule.push({ + // day_id: localTodayId, + // day_name: DAY_NAME[index], + // from: Shift.NightFrom, + // to: Shift.NightTo, + // is_available: true, + // }); + // } + // for (const menuItem of menuItems) { + // //Get the cooking schedule of menu_item_ids + // const menuItemSchedule: DayShift[] = JSON.parse( + // menuItem.cooking_schedule, + // ); + // for (const dayShift of menuItemSchedule) { + // const index = overlapSchedule.findIndex( + // (i) => i.day_id == dayShift.day_id && i.from == dayShift.from, + // ); + // if (index == -1) { + // //cannot find the same day shift in overlapSchedule + // continue; + // } + // if (dayShift.is_available == false) { + // overlapSchedule[index].is_available = false; + // } + // } + // } + + // //build datesOfThisWeek => only for performance purpose + // const datesOfThisWeek: ThisDate[] = []; + // for (let index = localTodayId - 1; index < DAY_ID.length; index++) { + // const localTodayId = DAY_ID[index]; + // datesOfThisWeek.push( + // this.commonService.getThisDate(now, localTodayId, timeZoneOffset), + // ); + // } + + // // Convert the schedule to TimeSlot format (1) + // const menuItemAvailableTimeRanges: TimeRange[] = []; + // for (const dayShift of overlapSchedule) { + // if (dayShift.is_available == false) { + // continue; + // } + // const thisDate = datesOfThisWeek.find((i) => i.dayId == dayShift.day_id); + // let from: number = 0; + // let to: number = 0; + // switch (dayShift.from) { + // case Shift.MorningFrom: + // from = + // new Date(thisDate.date).setUTCHours(6, 0, 0, 0) - timeZoneOffset; + // to = from + 8 * 60 * 60 * 1000 - 1000; + // break; + // case Shift.AfternoonFrom: + // from = + // new Date(thisDate.date).setUTCHours(14, 0, 0, 0) - timeZoneOffset; + // to = from + 8 * 60 * 60 * 1000 - 1000; + // break; + // case Shift.NightFrom: + // from = + // new Date(thisDate.date).setUTCHours(22, 0, 0, 0) - timeZoneOffset; + // to = from + 8 * 60 * 60 * 1000 - 1000; + // break; + + // default: + // throw new HttpException( + // 'Unknown error with dayShift of function getAvailableDeliveryTimeFromEndPoint', + // 500, + // ); + // } + // menuItemAvailableTimeRanges.push({ + // from: from, + // to: to, + // }); + // } + + // // Step 2: Get time ranges in which the restaurant is available + // // Get operation data of the restaurant + // const fromTomorrowOpsHours = ( + // await this.commonService.getRestaurantOperationHours(restaurantId) + // ).filter((i) => i.day_of_week > localTodayId); + + // //Get the day off data from the table Restaurant_Day_Off with restaurant_id + // let dayOffs = await this.commonService.getAvailableRestaurantDayOff( + // restaurantId, + // now, + // ); + // //ONLY KEEP THE DAY OFF FOR THIS WEEK + // if (dayOffs.length > 0) { + // const thisSaturday = this.commonService.getThisDate( + // now, + // 7, + // timeZoneOffset, + // ); + // dayOffs = dayOffs.filter((i) => i.date <= new Date(thisSaturday.date)); + // } + // // filter the operation data above with the day off data + // dayOffs.forEach((i) => { + // const index = fromTomorrowOpsHours.findIndex( + // (j) => j.day_of_week == i.date.getUTCDay() + 1, + // ); + // if (index != -1) { + // fromTomorrowOpsHours.splice(index, 1); + // } + // }); + + // //convert fromTomorrowOpsHours to time ranges + // const fromTomorrowOperationTimeRanges: TimeRange[] = []; + // for (const opsHour of fromTomorrowOpsHours) { + // const [fromHours, fromMinutes, fromSeconds] = opsHour.from_time + // .split(':') + // .map((i) => parseInt(i)); + // const [toHours, toMinutes, toSeconds] = opsHour.to_time + // .split(':') + // .map((i) => parseInt(i)); + // const thisDate = datesOfThisWeek.find( + // (i) => i.dayId == opsHour.day_of_week, + // ); + + // fromTomorrowOperationTimeRanges.push({ + // from: + // new Date(thisDate.date).setUTCHours( + // fromHours, + // fromMinutes, + // fromSeconds, + // ) - timeZoneOffset, + // to: + // new Date(thisDate.date).setUTCHours(toHours, toMinutes, toSeconds) - + // timeZoneOffset, + // }); + // } + + // //Get todayOperationTimeRange + // const todayOperationTimeRange = await this.commonService.getTodayOpsTime( + // restaurantId, + // now, + // ); - //get the delivery time - const delivery_time_s = ( - await this.commonService.estimateTimeAndDistanceForRestaurant( - restaurantId, + // //Build restaurantAvailabeTimeRanges + // const restaurantAvailabeTimeRanges: TimeRange[] = []; + // restaurantAvailabeTimeRanges.push(...fromTomorrowOperationTimeRanges); + // if (todayOperationTimeRange) { + // restaurantAvailabeTimeRanges.push(todayOperationTimeRange); + // } + + // //find the overlap time ranges between menuItemAvailableTimeRanges and restaurantAvailabeTimeRanges + // const foodAvailabeTimeRanges: TimeRange[] = []; + // for (const menuItemAvailableTimeRange of menuItemAvailableTimeRanges) { + // for (const restaurantAvailabeTimeRange of restaurantAvailabeTimeRanges) { + // const overlapTimeRange = this.commonService.getOverlappingTimeRange( + // menuItemAvailableTimeRange, + // restaurantAvailabeTimeRange, + // ); + // if (overlapTimeRange) { + // foodAvailabeTimeRanges.push(overlapTimeRange); + // } + // } + // } + + // // //get the longest prepraring time for all the menu items + // // const listOfPreparingTime = menuItems.map((i) => i.preparing_time_s); + // // const longestPreparingTime = Math.max(...listOfPreparingTime); + + // //buil the AvailableDeliveryTime + // const availableDeliveryTime: TimeRange[] = []; + + // if (having_advanced_customization == false) { + // //THIS IS A NORMAL ORDER + // foodAvailabeTimeRanges.forEach((foodTimeRange) => { + // let from = 0; + // if (foodTimeRange.from < now) { + // if (foodTimeRange.to < now) { + // return; + // } else if (foodTimeRange.to >= now) { + // from = now; + // } + // } else if (foodTimeRange.from >= now) { + // from = foodTimeRange.from; + // } + // const timeRange: TimeRange = { + // from: from + (delivery_time_s + buffer_s) * 1000, + // to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + // }; + // availableDeliveryTime.push(timeRange); + // }); + // } else if (having_advanced_customization == true) { + // //THIS IS A PREORDER + + // //get cutoff time in timestamp format (milliseconds) + // const cutoffTimePoint = await this.commonService.getCutoffTimePoint( + // now, + // restaurantId, + // ); + + // if (cutoffTimePoint >= now) { + // // foodAvailabeTimeRanges.forEach((foodTimeRange) => { + // // const timeRange: TimeRange = { + // // from: foodTimeRange.from + (delivery_time_s + buffer_s) * 1000, + // // to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + // // }; + // // availableDeliveryTime.push(timeRange); + // // }); + // foodAvailabeTimeRanges.forEach((foodTimeRange) => { + // let from = 0; + // if (foodTimeRange.from < now) { + // if (foodTimeRange.to < now) { + // return; + // } else if (foodTimeRange.to >= now) { + // from = now; + // } + // } else if (foodTimeRange.from >= now) { + // from = foodTimeRange.from; + // } + // const timeRange: TimeRange = { + // from: from + (delivery_time_s + buffer_s) * 1000, + // to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + // }; + // availableDeliveryTime.push(timeRange); + // }); + // } else if (cutoffTimePoint < now) { + // const localToday = new Date(now + timeZoneOffset); + // localToday.setUTCHours(23, 59, 59, 999); + // const tomorrowBegining = localToday.getTime() + 1 - timeZoneOffset; + // const startTimeForAvailableDelivery = + // tomorrowBegining + + // Math.floor((now - cutoffTimePoint) / 86400000) * 86400000; + // console.log( + // 'startTimeForAvailableDelivery', + // startTimeForAvailableDelivery, + // ); + // //filter time range after the start time for delivery available + // foodAvailabeTimeRanges.forEach((foodTimeRange) => { + // let from = 0; + // if (foodTimeRange.from < startTimeForAvailableDelivery) { + // if (foodTimeRange.to < startTimeForAvailableDelivery) { + // return; + // } else if (foodTimeRange.to >= startTimeForAvailableDelivery) { + // from = startTimeForAvailableDelivery; + // } + // } else if (foodTimeRange.from >= startTimeForAvailableDelivery) { + // from = foodTimeRange.from; + // } + // const timeRange: TimeRange = { + // from: from + (delivery_time_s + buffer_s) * 1000, + // to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + // }; + // availableDeliveryTime.push(timeRange); + // }); + // } + // } + + const availableDeliveryTime: TimeRange[] = + await this.commonService.getAvailableDeliveryTime( + menu_item_ids, + now, long, lat, - ) - ).duration_s; - if (!delivery_time_s) { - throw new HttpException( - 'There some error with the delivery estimation', - 500, - ); - } - - const restaurantUtcTimeZone = - await this.commonService.getUtcTimeZone(restaurantId); - const timeZoneOffset = restaurantUtcTimeZone * 60 * 60 * 1000; // Offset in milliseconds for EST - - const localTodayId = new Date(now + timeZoneOffset).getUTCDay() + 1; // 1->7: Sunday -> Saturday - - //Step1: Find the schedule in which all of the menu items are available - const overlapSchedule: DayShift[] = []; - for (let index = localTodayId - 1; index < DAY_ID.length; index++) { - const localTodayId = DAY_ID[index]; - overlapSchedule.push({ - day_id: localTodayId, - day_name: DAY_NAME[index], - from: Shift.MorningFrom, - to: Shift.MorningTo, - is_available: true, - }); - overlapSchedule.push({ - day_id: localTodayId, - day_name: DAY_NAME[index], - from: Shift.AfternoonFrom, - to: Shift.AfternoonTo, - is_available: true, - }); - overlapSchedule.push({ - day_id: localTodayId, - day_name: DAY_NAME[index], - from: Shift.NightFrom, - to: Shift.NightTo, - is_available: true, - }); - } - for (const menuItem of menuItems) { - //Get the cooking schedule of menu_item_ids - const menuItemSchedule: DayShift[] = JSON.parse( - menuItem.cooking_schedule, - ); - for (const dayShift of menuItemSchedule) { - const index = overlapSchedule.findIndex( - (i) => i.day_id == dayShift.day_id && i.from == dayShift.from, - ); - if (index == -1) { - //cannot find the same day shift in overlapSchedule - continue; - } - if (dayShift.is_available == false) { - overlapSchedule[index].is_available = false; - } - } - } - - //build datesOfThisWeek => only for performance purpose - const datesOfThisWeek: ThisDate[] = []; - for (let index = localTodayId - 1; index < DAY_ID.length; index++) { - const localTodayId = DAY_ID[index]; - datesOfThisWeek.push( - this.commonService.getThisDate(now, localTodayId, timeZoneOffset), - ); - } - - // Convert the schedule to TimeSlot format (1) - const menuItemAvailableTimeRanges: TimeRange[] = []; - for (const dayShift of overlapSchedule) { - if (dayShift.is_available == false) { - continue; - } - const thisDate = datesOfThisWeek.find((i) => i.dayId == dayShift.day_id); - let from: number = 0; - let to: number = 0; - switch (dayShift.from) { - case Shift.MorningFrom: - from = - new Date(thisDate.date).setUTCHours(6, 0, 0, 0) - timeZoneOffset; - to = from + 8 * 60 * 60 * 1000 - 1000; - break; - case Shift.AfternoonFrom: - from = - new Date(thisDate.date).setUTCHours(14, 0, 0, 0) - timeZoneOffset; - to = from + 8 * 60 * 60 * 1000 - 1000; - break; - case Shift.NightFrom: - from = - new Date(thisDate.date).setUTCHours(22, 0, 0, 0) - timeZoneOffset; - to = from + 8 * 60 * 60 * 1000 - 1000; - break; - - default: - throw new HttpException( - 'Unknown error with dayShift of function getAvailableDeliveryTimeFromEndPoint', - 500, - ); - } - menuItemAvailableTimeRanges.push({ - from: from, - to: to, - }); - } - - // Step 2: Get time ranges in which the restaurant is available - // Get operation data of the restaurant - const fromTomorrowOpsHours = ( - await this.commonService.getRestaurantOperationHours(restaurantId) - ).filter((i) => i.day_of_week > localTodayId); - - //Get the day off data from the table Restaurant_Day_Off with restaurant_id - let dayOffs = await this.commonService.getAvailableRestaurantDayOff( - restaurantId, - now, - ); - //ONLY KEEP THE DAY OFF FOR THIS WEEK - if (dayOffs.length > 0) { - const thisSaturday = this.commonService.getThisDate( - now, - 7, - timeZoneOffset, - ); - dayOffs = dayOffs.filter((i) => i.date <= new Date(thisSaturday.date)); - } - // filter the operation data above with the day off data - dayOffs.forEach((i) => { - const index = fromTomorrowOpsHours.findIndex( - (j) => j.day_of_week == i.date.getUTCDay() + 1, - ); - if (index != -1) { - fromTomorrowOpsHours.splice(index, 1); - } - }); - - //convert fromTomorrowOpsHours to time ranges - const fromTomorrowOperationTimeRanges: TimeRange[] = []; - for (const opsHour of fromTomorrowOpsHours) { - const [fromHours, fromMinutes, fromSeconds] = opsHour.from_time - .split(':') - .map((i) => parseInt(i)); - const [toHours, toMinutes, toSeconds] = opsHour.to_time - .split(':') - .map((i) => parseInt(i)); - const thisDate = datesOfThisWeek.find( - (i) => i.dayId == opsHour.day_of_week, - ); - - fromTomorrowOperationTimeRanges.push({ - from: - new Date(thisDate.date).setUTCHours( - fromHours, - fromMinutes, - fromSeconds, - ) - timeZoneOffset, - to: - new Date(thisDate.date).setUTCHours(toHours, toMinutes, toSeconds) - - timeZoneOffset, - }); - } - - //Get todayOperationTimeRange - const todayOperationTimeRange = await this.commonService.getTodayOpsTime( - restaurantId, - now, - ); - - //Build restaurantAvailabeTimeRanges - const restaurantAvailabeTimeRanges: TimeRange[] = []; - restaurantAvailabeTimeRanges.push(...fromTomorrowOperationTimeRanges); - if (todayOperationTimeRange) { - restaurantAvailabeTimeRanges.push(todayOperationTimeRange); - } - - //find the overlap time ranges between menuItemAvailableTimeRanges and restaurantAvailabeTimeRanges - const foodAvailabeTimeRanges: TimeRange[] = []; - for (const menuItemAvailableTimeRange of menuItemAvailableTimeRanges) { - for (const restaurantAvailabeTimeRange of restaurantAvailabeTimeRanges) { - const overlapTimeRange = this.commonService.getOverlappingTimeRange( - menuItemAvailableTimeRange, - restaurantAvailabeTimeRange, - ); - if (overlapTimeRange) { - foodAvailabeTimeRanges.push(overlapTimeRange); - } - } - } - - // //get the longest prepraring time for all the menu items - // const listOfPreparingTime = menuItems.map((i) => i.preparing_time_s); - // const longestPreparingTime = Math.max(...listOfPreparingTime); - - //buil the AvailableDeliveryTime - const availableDeliveryTime: TimeRange[] = []; - - if (having_advanced_customization == false) { - //THIS IS A NORMAL ORDER - foodAvailabeTimeRanges.forEach((foodTimeRange) => { - let from = 0; - if (foodTimeRange.from < now) { - if (foodTimeRange.to < now) { - return; - } else if (foodTimeRange.to >= now) { - from = now; - } - } else if (foodTimeRange.from >= now) { - from = foodTimeRange.from; - } - const timeRange: TimeRange = { - from: from + (delivery_time_s + buffer_s) * 1000, - to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, - }; - availableDeliveryTime.push(timeRange); - }); - } else if (having_advanced_customization == true) { - //THIS IS A PREORDER - - //get cutoff time in timestamp format (milliseconds) - const cutoffTimePoint = await this.commonService.getCutoffTimePoint( - now, - restaurantId, + having_advanced_customization, ); - if (cutoffTimePoint >= now) { - // foodAvailabeTimeRanges.forEach((foodTimeRange) => { - // const timeRange: TimeRange = { - // from: foodTimeRange.from + (delivery_time_s + buffer_s) * 1000, - // to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, - // }; - // availableDeliveryTime.push(timeRange); - // }); - foodAvailabeTimeRanges.forEach((foodTimeRange) => { - let from = 0; - if (foodTimeRange.from < now) { - if (foodTimeRange.to < now) { - return; - } else if (foodTimeRange.to >= now) { - from = now; - } - } else if (foodTimeRange.from >= now) { - from = foodTimeRange.from; - } - const timeRange: TimeRange = { - from: from + (delivery_time_s + buffer_s) * 1000, - to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, - }; - availableDeliveryTime.push(timeRange); - }); - } else if (cutoffTimePoint < now) { - const localToday = new Date(now + timeZoneOffset); - localToday.setUTCHours(23, 59, 59, 999); - const tomorrowBegining = localToday.getTime() + 1 - timeZoneOffset; - const startTimeForAvailableDelivery = - tomorrowBegining + - Math.floor((now - cutoffTimePoint) / 86400000) * 86400000; - console.log( - 'startTimeForAvailableDelivery', - startTimeForAvailableDelivery, - ); - //filter time range after the start time for delivery available - foodAvailabeTimeRanges.forEach((foodTimeRange) => { - let from = 0; - if (foodTimeRange.from < startTimeForAvailableDelivery) { - if (foodTimeRange.to < startTimeForAvailableDelivery) { - return; - } else if (foodTimeRange.to >= startTimeForAvailableDelivery) { - from = startTimeForAvailableDelivery; - } - } else if (foodTimeRange.from >= startTimeForAvailableDelivery) { - from = foodTimeRange.from; - } - const timeRange: TimeRange = { - from: from + (delivery_time_s + buffer_s) * 1000, - to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, - }; - availableDeliveryTime.push(timeRange); - }); - } - } + console.log('availableDeliveryTime', availableDeliveryTime); //convert time ranges to time slots for (const timeRange of availableDeliveryTime) { diff --git a/src/feature/common/common.service.ts b/src/feature/common/common.service.ts index 7693079..27f2261 100644 --- a/src/feature/common/common.service.ts +++ b/src/feature/common/common.service.ts @@ -3,6 +3,7 @@ import { AdditionalInfoForSKU, BasicTasteSelection, Coordinate, + DayShift, DeliveryInfo, DeliveryRestaurant, OptionSelection, @@ -19,7 +20,7 @@ import { FlagsmithService } from 'src/dependency/flagsmith/flagsmith.service'; import { RestaurantExt } from 'src/entity/restaurant-ext.entity'; import { InjectEntityManager } from '@nestjs/typeorm'; import { EntityManager, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; -import { DAY_NAME, TRUE } from 'src/constant'; +import { DAY_ID, DAY_NAME, TRUE } from 'src/constant'; import { FoodRating } from 'src/entity/food-rating.entity'; import { SkuDiscount } from 'src/entity/sku-discount.entity'; import { SKU } from 'src/entity/sku.entity'; @@ -32,7 +33,7 @@ import { NoAddingExt } from 'src/entity/no-adding-ext.entity'; import { SkuDetail } from 'src/entity/sku-detail.entity'; import { BasicCustomization } from 'src/entity/basic-customization.entity'; import { Restaurant } from 'src/entity/restaurant.entity'; -import { DayId } from 'src/enum'; +import { DayId, Shift } from 'src/enum'; import { OperationHours } from 'src/entity/operation-hours.entity'; import { RestaurantDayOff } from 'src/entity/restaurant-day-off.entity'; import { ManualOpenRestaurant } from 'src/entity/manual-open-restaurant.entity'; @@ -181,7 +182,7 @@ export class CommonService { name: foodNameByLang, restaurant_name: restaurantNameByLang, restaurant_id: menuItem.restaurant_id, - calorie_kcal: menuItem.skus[0].calorie_kcal, + calorie_kcal: Number(menuItem.skus[0].calorie_kcal), rating: menuItem.rating, distance_km: correspondingRestaurant?.distance_km || null, delivery_time_s: correspondingRestaurant?.delivery_time_s || null, @@ -534,7 +535,9 @@ export class CommonService { time_range: TimeRange, // time_zone_offset_in_milliseconds: number, utc_offset: number = 7, - time_step_m = 15, //in minutes + time_step_m: number = this.configService.get( + 'timeStepInTimSlotConverterM', + ), //in minutes mode = 0, //ceiling; 1: floor ): TimeSlot[] { const { from, to } = time_range; @@ -952,4 +955,323 @@ export class CommonService { const overlap = arr1.filter((item) => arr2.includes(item)); return overlap; } //end of findOverlapItemOfTwoArrays + + async getAvailableDeliveryTime( + menu_item_ids: number[], + now: number, + long: number, + lat: number, + // utc_offset: number, + having_advanced_customization: boolean, + buffer_s = this.configService.get('deliverBufferTime') * 60, // 5 mins + ): Promise { + const menuItems = await this.getMenuItemByIds(menu_item_ids); + + //Check if menu_item_ids do exist + if (menuItems.length != menu_item_ids.length) { + throw new HttpException('Some of menu items do not exist', 400); + } + + //Check if menu_item_ids belong to the same restaurant + const restaurantId = menuItems[0].restaurant_id; + if (menuItems.find((i) => i.restaurant_id != restaurantId)) { + throw new HttpException( + 'Some of menu items do not belong to the same restaurant', + 400, + ); + } + + //get the delivery time + const delivery_time_s = ( + await this.estimateTimeAndDistanceForRestaurant(restaurantId, long, lat) + ).duration_s; + if (!delivery_time_s && delivery_time_s != 0) { + throw new HttpException( + 'There some error with the delivery estimation', + 500, + ); + } + + const restaurantUtcTimeZone = await this.getUtcTimeZone(restaurantId); + const timeZoneOffset = restaurantUtcTimeZone * 60 * 60 * 1000; // Offset in milliseconds for EST + + const localTodayId = new Date(now + timeZoneOffset).getUTCDay() + 1; // 1->7: Sunday -> Saturday + + //Step1: Find the schedule in which all of the menu items are available + const overlapSchedule: DayShift[] = []; + for (let index = localTodayId - 1; index < DAY_ID.length; index++) { + const localTodayId = DAY_ID[index]; + overlapSchedule.push({ + day_id: localTodayId, + day_name: DAY_NAME[index], + from: Shift.MorningFrom, + to: Shift.MorningTo, + is_available: true, + }); + overlapSchedule.push({ + day_id: localTodayId, + day_name: DAY_NAME[index], + from: Shift.AfternoonFrom, + to: Shift.AfternoonTo, + is_available: true, + }); + overlapSchedule.push({ + day_id: localTodayId, + day_name: DAY_NAME[index], + from: Shift.NightFrom, + to: Shift.NightTo, + is_available: true, + }); + } + for (const menuItem of menuItems) { + //Get the cooking schedule of menu_item_ids + const menuItemSchedule: DayShift[] = JSON.parse( + menuItem.cooking_schedule, + ); + for (const dayShift of menuItemSchedule) { + const index = overlapSchedule.findIndex( + (i) => i.day_id == dayShift.day_id && i.from == dayShift.from, + ); + if (index == -1) { + //cannot find the same day shift in overlapSchedule + continue; + } + if (dayShift.is_available == false) { + overlapSchedule[index].is_available = false; + } + } + } + + //build datesOfThisWeek => only for performance purpose + const datesOfThisWeek: ThisDate[] = []; + for (let index = localTodayId - 1; index < DAY_ID.length; index++) { + const localTodayId = DAY_ID[index]; + datesOfThisWeek.push(this.getThisDate(now, localTodayId, timeZoneOffset)); + } + + // Convert the schedule to TimeSlot format (1) + const menuItemAvailableTimeRanges: TimeRange[] = []; + for (const dayShift of overlapSchedule) { + if (dayShift.is_available == false) { + continue; + } + const thisDate = datesOfThisWeek.find((i) => i.dayId == dayShift.day_id); + let from: number = 0; + let to: number = 0; + switch (dayShift.from) { + case Shift.MorningFrom: + from = + new Date(thisDate.date).setUTCHours(6, 0, 0, 0) - timeZoneOffset; + to = from + 8 * 60 * 60 * 1000 - 1000; + break; + case Shift.AfternoonFrom: + from = + new Date(thisDate.date).setUTCHours(14, 0, 0, 0) - timeZoneOffset; + to = from + 8 * 60 * 60 * 1000 - 1000; + break; + case Shift.NightFrom: + from = + new Date(thisDate.date).setUTCHours(22, 0, 0, 0) - timeZoneOffset; + to = from + 8 * 60 * 60 * 1000 - 1000; + break; + + default: + throw new HttpException('Unknown error with dayShift', 500); + } + menuItemAvailableTimeRanges.push({ + from: from, + to: to, + }); + } + + // Step 2: Get time ranges in which the restaurant is available + // Get operation data of the restaurant + const fromTomorrowOpsHours = ( + await this.getRestaurantOperationHours(restaurantId) + ).filter((i) => i.day_of_week > localTodayId); + + //Get the day off data from the table Restaurant_Day_Off with restaurant_id + let dayOffs = await this.getAvailableRestaurantDayOff(restaurantId, now); + //ONLY KEEP THE DAY OFF FOR THIS WEEK + if (dayOffs.length > 0) { + const thisSaturday = this.getThisDate(now, 7, timeZoneOffset); + dayOffs = dayOffs.filter((i) => i.date <= new Date(thisSaturday.date)); + } + // filter the operation data above with the day off data + dayOffs.forEach((i) => { + const index = fromTomorrowOpsHours.findIndex( + (j) => j.day_of_week == i.date.getUTCDay() + 1, + ); + if (index != -1) { + fromTomorrowOpsHours.splice(index, 1); + } + }); + + //convert fromTomorrowOpsHours to time ranges + const fromTomorrowOperationTimeRanges: TimeRange[] = []; + for (const opsHour of fromTomorrowOpsHours) { + const [fromHours, fromMinutes, fromSeconds] = opsHour.from_time + .split(':') + .map((i) => parseInt(i)); + const [toHours, toMinutes, toSeconds] = opsHour.to_time + .split(':') + .map((i) => parseInt(i)); + const thisDate = datesOfThisWeek.find( + (i) => i.dayId == opsHour.day_of_week, + ); + + fromTomorrowOperationTimeRanges.push({ + from: + new Date(thisDate.date).setUTCHours( + fromHours, + fromMinutes, + fromSeconds, + ) - timeZoneOffset, + to: + new Date(thisDate.date).setUTCHours(toHours, toMinutes, toSeconds) - + timeZoneOffset, + }); + } + + //Get todayOperationTimeRange + const todayOperationTimeRange = await this.getTodayOpsTime( + restaurantId, + now, + ); + + //Build restaurantAvailabeTimeRanges + const restaurantAvailabeTimeRanges: TimeRange[] = []; + restaurantAvailabeTimeRanges.push(...fromTomorrowOperationTimeRanges); + if (todayOperationTimeRange) { + restaurantAvailabeTimeRanges.push(todayOperationTimeRange); + } + + //find the overlap time ranges between menuItemAvailableTimeRanges and restaurantAvailabeTimeRanges + const foodAvailabeTimeRanges: TimeRange[] = []; + for (const menuItemAvailableTimeRange of menuItemAvailableTimeRanges) { + for (const restaurantAvailabeTimeRange of restaurantAvailabeTimeRanges) { + const overlapTimeRange = this.getOverlappingTimeRange( + menuItemAvailableTimeRange, + restaurantAvailabeTimeRange, + ); + if (overlapTimeRange) { + foodAvailabeTimeRanges.push(overlapTimeRange); + } + } + } + + // //get the longest prepraring time for all the menu items + // const listOfPreparingTime = menuItems.map((i) => i.preparing_time_s); + // const longestPreparingTime = Math.max(...listOfPreparingTime); + + //buil the AvailableDeliveryTime + const availableDeliveryTime: TimeRange[] = []; + + if (having_advanced_customization == false) { + //THIS IS A NORMAL ORDER + foodAvailabeTimeRanges.forEach((foodTimeRange) => { + let from = 0; + if (foodTimeRange.from < now) { + if (foodTimeRange.to < now) { + return; + } else if (foodTimeRange.to >= now) { + from = now; + } + } else if (foodTimeRange.from >= now) { + from = foodTimeRange.from; + } + const timeRange: TimeRange = { + from: from + (delivery_time_s + buffer_s) * 1000, + to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + }; + availableDeliveryTime.push(timeRange); + }); + } else if (having_advanced_customization == true) { + //THIS IS A PREORDER + + //get cutoff time in timestamp format (milliseconds) + const cutoffTimePoint = await this.getCutoffTimePoint(now, restaurantId); + + if (cutoffTimePoint >= now) { + // foodAvailabeTimeRanges.forEach((foodTimeRange) => { + // const timeRange: TimeRange = { + // from: foodTimeRange.from + (delivery_time_s + buffer_s) * 1000, + // to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + // }; + // availableDeliveryTime.push(timeRange); + // }); + foodAvailabeTimeRanges.forEach((foodTimeRange) => { + let from = 0; + if (foodTimeRange.from < now) { + if (foodTimeRange.to < now) { + return; + } else if (foodTimeRange.to >= now) { + from = now; + } + } else if (foodTimeRange.from >= now) { + from = foodTimeRange.from; + } + const timeRange: TimeRange = { + from: from + (delivery_time_s + buffer_s) * 1000, + to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + }; + availableDeliveryTime.push(timeRange); + }); + } else if (cutoffTimePoint < now) { + const localToday = new Date(now + timeZoneOffset); + localToday.setUTCHours(23, 59, 59, 999); + const tomorrowBegining = localToday.getTime() + 1 - timeZoneOffset; + const startTimeForAvailableDelivery = + tomorrowBegining + + Math.floor((now - cutoffTimePoint) / 86400000) * 86400000; + //filter time range after the start time for delivery available + foodAvailabeTimeRanges.forEach((foodTimeRange) => { + let from = 0; + if (foodTimeRange.from < startTimeForAvailableDelivery) { + if (foodTimeRange.to < startTimeForAvailableDelivery) { + return; + } else if (foodTimeRange.to >= startTimeForAvailableDelivery) { + from = startTimeForAvailableDelivery; + } + } else if (foodTimeRange.from >= startTimeForAvailableDelivery) { + from = foodTimeRange.from; + } + const timeRange: TimeRange = { + from: from + (delivery_time_s + buffer_s) * 1000, + to: foodTimeRange.to + (delivery_time_s + buffer_s) * 1000, + }; + availableDeliveryTime.push(timeRange); + }); + } + } + + // //convert time ranges to time slots + // for (const timeRange of availableDeliveryTime) { + // const convertData = this.convertTimeRangeToTimeSlot( + // timeRange, + // utc_offset, + // ); + // convertData.forEach((i) => timeSlots.push(i)); + // } + return availableDeliveryTime; + } // end of getAvailableDeliveryTime + + isToday( + checking_date_in_milliseconds: number, + utc_time_zone: number, + ): boolean { + const timeZoneOffsetInMilliseconds = utc_time_zone * 60 * 60 * 1000; + const today = new Date(); + const localToday = new Date(today.getTime() + utc_time_zone * 60 * 1000); + const localCheckingDate = new Date( + checking_date_in_milliseconds + timeZoneOffsetInMilliseconds, + ); + + // Compare year, month, and day to check if they are the same + return ( + localToday.getUTCFullYear() === localCheckingDate.getUTCFullYear() && + localToday.getUTCMonth() === localCheckingDate.getUTCMonth() && + localToday.getUTCDate() === localCheckingDate.getUTCDate() + ); + } } diff --git a/src/feature/food/food.service.ts b/src/feature/food/food.service.ts index 9e70f11..6252693 100644 --- a/src/feature/food/food.service.ts +++ b/src/feature/food/food.service.ts @@ -525,7 +525,7 @@ export class FoodService { await this.commonService.getAvailableDiscountPrice(rawSKU), unit: priceUnit, is_standard: Boolean(rawSKU.is_standard), - calorie_kcal: rawSKU.calorie_kcal, + calorie_kcal: Number(rawSKU.calorie_kcal), carb_g: rawSKU.carbohydrate_g, protein_g: rawSKU.protein_g, fat_g: rawSKU.fat_g, diff --git a/src/feature/order/dto/create-order-request.dto.ts b/src/feature/order/dto/create-order-request.dto.ts new file mode 100644 index 0000000..1ede5ef --- /dev/null +++ b/src/feature/order/dto/create-order-request.dto.ts @@ -0,0 +1,43 @@ +interface Address { + address_line: string; + ward: string; + district: string; + city: string; + country: string; + latitude: number; + longitude: number; +} + +interface OrderItemRequest { + sku_id: number; + qty_ordered: number; + advanced_taste_customization_obj: OptionSelection[]; + basic_taste_customization_obj: BasicTasteSelection[]; + notes: string; + packaging_id: number; +} + +interface OptionSelection { + option_id: string; + value_id: string; +} +interface BasicTasteSelection { + no_adding_id: string; +} + +export class CreateOrderRequest { + customer_id: number; + restaurant_id: number; + address: Address; + order_total: number; + delivery_fee: number; + packaging_fee: number; + cutlery_fee: number; + app_fee: number; + coupon_value: number; + coupon_code: string; + payment_method_id: number; + expected_arrival_time: number; + driver_note: string; + order_items: OrderItemRequest[]; +} diff --git a/src/feature/order/dto/create-order-response.dto.ts b/src/feature/order/dto/create-order-response.dto.ts new file mode 100644 index 0000000..35c64a6 --- /dev/null +++ b/src/feature/order/dto/create-order-response.dto.ts @@ -0,0 +1,3 @@ +import { OrderDetailResponse } from './order-detail-response.dto'; + +export class CreateOrderResponse extends OrderDetailResponse {} diff --git a/src/feature/order/dto/order-detail-response.dto.ts b/src/feature/order/dto/order-detail-response.dto.ts new file mode 100644 index 0000000..489b769 --- /dev/null +++ b/src/feature/order/dto/order-detail-response.dto.ts @@ -0,0 +1,94 @@ +export class OrderDetailResponse { + order_id: number; + customer_id: number; + restaurant: RestaurantBasicInfo; + address: Address; + driver_note: string; + driver: Driver; + order_total: number; + delivery_fee: number; + packaging_fee: number; + cutlery_fee: number; + app_fee: number; + coupon_value: number; + coupon_id: number; + invoice_id: number; + payment_method: Payment; + payment_status_history: PaymentStatusHistory[]; + is_preorder: boolean; + expected_arrival_time: number; + order_items: OrderItemResponse[]; + order_status_log: OrderStatusLog[]; + tracking_url: string; +} +interface Driver { + driver_id: number; + name: string; + phone_number: string; + vehicle: string; + license_plates: string; + profile_image: string; +} +interface Payment { + id: number; + name: string; +} +interface PaymentStatusHistory { + status_id: string; + name: TextByLang[]; + note: string; + created_at: number; +} +interface TextByLang { + ISO_language_code: string; + text: string; +} +interface OrderItemResponse { + item_name: TextByLang[]; + item_img: string; + order_id: number; + sku_id: number; + qty_ordered: number; + price: number; + advanced_taste_customization_obj: OptionSelection[]; + basic_taste_customization_obj: BasicTasteSelection[]; + advanced_taste_customization: string; + basic_taste_customization: string; + portion_customization: string; + notes: string; + calorie_kcal: string; + packaging_info: OrderItemPackaging; +} +interface OrderItemPackaging { + packaging_id: number; + name: TextByLang[]; + description: TextByLang[]; + price: number; +} +interface OptionSelection { + option_id: string; + value_id: string; +} +interface BasicTasteSelection { + no_adding_id: string; +} +interface OrderStatusLog { + status: string; + description: TextByLang[]; + logged_at: number; + milestone?: string; +} +interface RestaurantBasicInfo { + restaurant_id: number; + restaurant_name: TextByLang[]; + restaurant_logo_img: string; +} +interface Address { + address_line: string; + ward: string; + district: string; + city: string; + country: string; + latitude: number; + longitude: number; +} diff --git a/src/feature/order/order.controller.ts b/src/feature/order/order.controller.ts index f5c2c50..7796c27 100644 --- a/src/feature/order/order.controller.ts +++ b/src/feature/order/order.controller.ts @@ -11,13 +11,16 @@ import { GetPaymentMethodResponse } from './dto/get-payment-method-response.dto' import { GetCutleryFeeRequest } from './dto/get-cutlery-fee-request.dto'; import { GetCutleryFeeResponse } from './dto/get-cutlery-fee-response.dto'; import { CommonService } from '../common/common.service'; -import { MoneyType } from 'src/type'; +import { CouponValue, MoneyType } from 'src/type'; import { GetCouponInfoRequest } from './dto/get-coupon-info-request.dto'; import { GetCouponInfoResponse } from './dto/get-coupon-info-response.dto'; import { Coupon } from 'src/entity/coupon.entity'; import { CustomRpcException } from 'src/exceptions/custom-rpc.exception'; import { ApplyPromotionCodeRequest } from './dto/apply-promotion-code-request.dto'; import { ApplyPromotionCodeResponse } from './dto/apply-promotion-code-response.dto'; +import { CreateOrderRequest } from './dto/create-order-request.dto'; +import { CreateOrderResponse } from './dto/create-order-response.dto'; +import { OrderDetailResponse } from './dto/order-detail-response.dto'; @Controller('order') export class OrderController { @@ -45,10 +48,14 @@ export class OrderController { data: GetApplicationFeeRequest, ): Promise { const { items_total, exchange_rate } = data; - return await this.orderService.getApplicationFeeFromEndPoint( + const applicationFee = await this.orderService.getApplicationFee( items_total, exchange_rate, ); + + return { + application_fee: applicationFee, + }; } @MessagePattern({ cmd: 'get_payment_method' }) @@ -183,14 +190,16 @@ export class OrderController { throw new CustomRpcException(4, 'Coupon code is invalid'); } - const discountAmount: number = this.orderService.calculateDiscountAmount( + const couponValue: CouponValue = this.orderService.calculateDiscountAmount( validCoupon, items, ); const unit = await this.orderService.getUnitById(restaurant.unit); - result.discount_amount = discountAmount; + result.discount_amount = + couponValue.coupon_value_from_platform + + couponValue.coupon_value_from_restaurant; result.currency = unit.symbol; result.coupon_code = coupon_code; result.restaurant_id = restaurant_id; @@ -198,4 +207,29 @@ export class OrderController { return result; } + + @MessagePattern({ cmd: 'create_order' }) + @UseFilters(new CustomRpcExceptionFilter()) + async createOrder(data: CreateOrderRequest): Promise { + return await this.orderService.createOrder(data); + } + + @MessagePattern({ cmd: 'get_order_detail' }) + @UseFilters(new CustomRpcExceptionFilter()) + async getOrderDetail( + data: OrderDetailResponse, + ): Promise { + const { order_id, customer_id } = data; + const order = await this.orderService.getOrderDetail(order_id); + + if (!order) { + throw new CustomRpcException(2, 'Order cannot be found'); + } + + if (order.customer_id && customer_id != order.customer_id) { + throw new CustomRpcException(3, "Cannot get other customer's order"); + } + + return order; + } } diff --git a/src/feature/order/order.service.ts b/src/feature/order/order.service.ts index 309b3dd..355f92a 100644 --- a/src/feature/order/order.service.ts +++ b/src/feature/order/order.service.ts @@ -10,7 +10,10 @@ import { CalculationType, CouponFilterType, CouponType, + InvoiceHistoryStatusEnum, + OrderMilestones, OrderStatus, + PaymentList, } from 'src/enum'; import { EntityManager, Repository } from 'typeorm'; import { GetApplicationFeeResponse } from './dto/get-application-fee-response.dto'; @@ -20,11 +23,31 @@ import { OrderStatusLog } from 'src/entity/order-status-log.entity'; import { DriverStatusLog } from 'src/entity/driver-status-log.entity'; import { Driver } from 'src/entity/driver.entity'; import { UrgentActionNeeded } from 'src/entity/urgent-action-needed.entity'; -import { CouponAppliedItem, MoneyType } from 'src/type'; +import { + CouponAppliedItem, + CouponValue, + MoneyType, + OrderItemRequest, + TextByLang, +} from 'src/type'; import { Restaurant } from 'src/entity/restaurant.entity'; import { CustomRpcException } from 'src/exceptions/custom-rpc.exception'; import { Coupon } from 'src/entity/coupon.entity'; import { Unit } from 'src/entity/unit.entity'; +import { CreateOrderRequest } from './dto/create-order-request.dto'; +import { CreateOrderResponse } from './dto/create-order-response.dto'; +import { CommonService } from '../common/common.service'; +import { Address } from 'src/entity/address.entity'; +import { OrderSKU } from 'src/entity/order-sku.entity'; +import { SKU } from 'src/entity/sku.entity'; +import { MenuItem } from 'src/entity/menu-item.entity'; +import { ConfigService } from '@nestjs/config'; +import { Invoice } from 'src/entity/invoice.entity'; +import { OrderDetailResponse } from './dto/order-detail-response.dto'; +import { PostAhaOrderRequest } from 'src/dependency/ahamove/dto/ahamove.dto'; +import { Customer } from 'src/entity/customer.entity'; +import { RestaurantExt } from 'src/entity/restaurant-ext.entity'; +import { Packaging } from 'src/entity/packaging.entity'; @Injectable() export class OrderService { @@ -43,6 +66,8 @@ export class OrderService { private urgentActionNeededRepo: Repository, private ahamoveService: AhamoveService, @InjectEntityManager() private entityManager: EntityManager, + private readonly commonService: CommonService, + private readonly configService: ConfigService, ) {} async updateOrderStatusFromAhamoveWebhook( @@ -270,10 +295,10 @@ export class OrderService { } } - async getApplicationFeeFromEndPoint( + async getApplicationFee( items_total: number, exchange_rate: number, //Exchange rate to VND - ): Promise { + ): Promise { const FEE_RATE = 0.03; const MAXIMUM_FEE = 75000; //VND const MINIMUM_FEE = 1000; //VND @@ -291,9 +316,7 @@ export class OrderService { applicationFee = preApplicationFee / exchange_rate; } - return { - application_fee: applicationFee, - }; + return applicationFee; } async getPaymentOptions(): Promise { @@ -439,7 +462,10 @@ export class OrderService { return validCoupon; } - calculateDiscountAmount(coupon: Coupon, items: CouponAppliedItem[]): number { + calculateDiscountAmount( + coupon: Coupon, + items: CouponAppliedItem[], + ): CouponValue { let discountAmount: number = 0; //calculate the amount base to apply promotion code let amount_base: number = 0; @@ -464,7 +490,13 @@ export class OrderService { discountAmount = coupon.discount_value; } - return discountAmount; + const couponValueFromPlatform = + (discountAmount * coupon.platform_sponsor_ratio_percentage) / 100; + + return { + coupon_value_from_platform: couponValueFromPlatform, + coupon_value_from_restaurant: discountAmount - couponValueFromPlatform, + }; } async getUnitById(unit_id: number): Promise { @@ -473,4 +505,852 @@ export class OrderService { .where('unit.unit_id = :unit_id', { unit_id }) .getOne(); } + + async createOrder(data: CreateOrderRequest): Promise { + const { + customer_id, + restaurant_id, + address, + order_total, + delivery_fee, + cutlery_fee, + packaging_fee, + app_fee, + coupon_value, + coupon_code, + payment_method_id, + expected_arrival_time, + driver_note, + order_items, + } = data; + + //validate restaurant exist + const restaurant = + await this.commonService.getRestaurantById(restaurant_id); + if (!restaurant) { + throw new CustomRpcException(100, 'Restaurant doesnot exist'); + } + + //validate customer exist + const customer = await this.entityManager + .createQueryBuilder(Customer, 'customer') + .where('customer.customer_id = :customer_id', { + customer_id, + }) + .getOne(); + if (!customer) { + throw new CustomRpcException(115, 'Customer is not found'); + } + + const skuList = [...new Set(order_items.map((i) => i.sku_id))]; + //Validate SKU list belongs to the restaurant + if (order_items.length > 0) { + const isValidSkuList = + await this.commonService.validateSkuListBelongsToRestaurant( + restaurant_id, + skuList, + ); + if (!isValidSkuList) { + throw new CustomRpcException( + 3, + 'item list does not belong to the resturant', + ); + } + } + + //Build OrderSKU data + const orderItems = await this.buildOrderSKUData(order_items); + this.logger.debug(orderItems); + + //calculate delivery fee + const restaurantAddress = await this.entityManager + .createQueryBuilder(Address, 'address') + .where('address.address_id = :address_id', { + address_id: restaurant.address_id, + }) + .getOne(); + const deliveryEstimation = ( + await this.ahamoveService.estimatePrice([ + { + lat: restaurantAddress.latitude, + long: restaurantAddress.longitude, + }, + { + lat: address.latitude, + long: address.longitude, + }, + ]) + )[0].data; + const deliveryFee = deliveryEstimation.total_price; + + this.logger.debug(deliveryFee); + if (deliveryFee != delivery_fee) { + console.log(deliveryFee); + // throw new CustomRpcException(101, 'Delivery fee is not correct'); + throw new CustomRpcException(101, { + message: 'Delivery fee is not correct', + delivery_fee: deliveryFee, + }); + } + + //calculate cutlery_fee + const orderQuantitySum = order_items + .map((i) => i.qty_ordered) + .reduce((sum, current_quan) => (sum += current_quan), 0); + const cutleryFee = ( + await this.getCutleryFee(restaurant_id, orderQuantitySum) + ).amount; + if (cutleryFee != cutlery_fee) { + // throw new CustomRpcException(102, 'Cutlery fee is not correct'); + throw new CustomRpcException(102, { + message: 'Cutlery fee is not correct', + cutlery_fee: cutleryFee, + }); + } + + //calculate app_fee + const appFee = await this.getApplicationFee(orderQuantitySum, 1); + this.logger.log(appFee); + if (appFee != app_fee) { + // throw new CustomRpcException(103, 'App fee is not correct'); + throw new CustomRpcException(103, { + message: 'App fee is not correct', + app_fee: appFee, + }); + } + + //get coupon info + const validCoupon = await this.validateApplyingCouponCode( + coupon_code, + restaurant_id, + skuList, + ); + if (!validCoupon) { + throw new CustomRpcException(104, 'Coupon code is invalid'); + } + const couponAppliedItems: CouponAppliedItem[] = orderItems.map((i) => { + return { + sku_id: i.sku_id, + qty_ordered: i.qty_ordered, + price_after_discount: i.price, + packaging_price: i.packaging_obj.price, + }; + }); + + const couponValue: CouponValue = this.calculateDiscountAmount( + validCoupon, + couponAppliedItems, + ); + if ( + couponValue.coupon_value_from_platform + + couponValue.coupon_value_from_restaurant != + coupon_value + ) { + throw new CustomRpcException(109, { + message: 'Coupon value is incorrect', + coupon_value: + couponValue.coupon_value_from_platform + + couponValue.coupon_value_from_restaurant, + }); + } + + let orderSubTotal = 0; + let packagingFee = 0; + for (const item of orderItems) { + orderSubTotal += item.price * item.qty_ordered; + packagingFee += item.packaging_obj.price * item.qty_ordered; + } + + //validate with packaging fee + if (packagingFee != packaging_fee) { + throw new CustomRpcException(110, { + message: 'Packaging fee is incorrect', + packaging_fee: packagingFee, + }); + } + + //validate order total + const orderTotal = + orderSubTotal + + deliveryFee + + packagingFee + + cutleryFee + + appFee - + couponValue.coupon_value_from_platform - + couponValue.coupon_value_from_restaurant; + if (orderTotal != order_total) { + throw new CustomRpcException(111, { + message: 'Order total is incorrect', + order_total: orderTotal, + }); + } + + //get payment info + const paymentMethod = await this.getPaymentMethodById(payment_method_id); + if (!paymentMethod) { + throw new CustomRpcException(113, 'Payment method is invalid'); + } + + //check expected_arrival_time is acceptable + const skus = await this.entityManager + .createQueryBuilder(SKU, 'sku') + .where('sku.sku_id IN (:...skuList)', { skuList }) + .getMany(); + + //Get list Menu Item info from SKU + const menuItemIds = [...new Set(skus.map((i) => i.menu_item_id))]; + let havingAdvancedCustomization: boolean = false; + for (const item of order_items) { + if ( + item.advanced_taste_customization_obj && + item.advanced_taste_customization_obj.length > 0 + ) { + havingAdvancedCustomization = true; + break; + } + } + const availableDeliveryTime = + await this.commonService.getAvailableDeliveryTime( + menuItemIds, + Date.now(), + address.longitude, + address.latitude, + havingAdvancedCustomization, + ); + const timeStepInMiliseconds = + this.configService.get('timeStepInTimSlotConverterM') * 60 * 1000; + let isValidExpectedArrivalTime = false; + for (const timeRange of availableDeliveryTime) { + if ( + timeRange.from - timeStepInMiliseconds <= expected_arrival_time && + timeRange.to + timeStepInMiliseconds >= expected_arrival_time + ) { + isValidExpectedArrivalTime = true; + break; + } + } + if (!isValidExpectedArrivalTime) { + throw new CustomRpcException(112, { + message: 'Invalid expected arrival time', + expected_arrival_time_ranges: availableDeliveryTime, + }); + } + + //set is_preorder or not + let isPreorder = false; + const isToday = this.commonService.isToday( + expected_arrival_time, + restaurant.utc_time_zone, + ); + if (!isToday) { + isPreorder = true; + } else if (isToday && havingAdvancedCustomization) { + isPreorder = true; + } + + //Create the request to delivery service + let deliveryOrderId = null; + if (isPreorder == false && paymentMethod.name == PaymentList.COD) { + const restaurantAddressString = restaurantAddress.address_line + ? `${restaurantAddress.address_line}, ${restaurantAddress.ward}, ${restaurantAddress.city}, ${restaurantAddress.country}` + : `${restaurantAddress.ward}, ${restaurantAddress.city}, ${restaurantAddress.country}`; + const customerAddressString = address.address_line + ? `${address.address_line}, ${address.ward}, ${address.city}, ${address.country}` + : `${address.ward}, ${address.city}, ${address.country}`; + const restaurantExt = await this.entityManager + .createQueryBuilder(RestaurantExt, 'ext') + .where('ext.restaurant_id = :restaurant_id', { restaurant_id }) + .andWhere('ext.ISO_language_code = :lang', { lang: 'vie' }) + .getOne(); + const deliveryTime = deliveryEstimation.duration * 1000; + const deliverBufferTime = + this.configService.get('deliverBufferTime') * 60 * 1000; + const orderTime = + (expected_arrival_time - deliveryTime - deliverBufferTime) / 1000; + const averageOtherFee = (orderTotal - orderSubTotal) / orderQuantitySum; + const skuWIthMenuItems = await this.entityManager + .createQueryBuilder(SKU, 'sku') + .leftJoinAndSelect('sku.menu_item', 'menuItem') + .leftJoinAndSelect('menuItem.menuItemExt', 'ext') + .where('sku.sku_id IN (:...skuList)', { skuList }) + .getMany(); + const deliveryItems = []; + orderItems.forEach((i) => { + const sku = skuWIthMenuItems.find( + (skuWIthMenuItem) => skuWIthMenuItem.sku_id == i.sku_id, + ); + const name = sku.menu_item.menuItemExt.find( + (ext) => ext.ISO_language_code == 'vie', + ).short_name; + deliveryItems.push({ + _id: i.sku_id.toString(), + num: i.qty_ordered, + name: `${name} - ${i.portion_customization} - ${i.advanced_taste_customization} - ${i.basic_taste_customization}`, + price: i.price + averageOtherFee, + }); + }); + const ahamoveOrderRequest: PostAhaOrderRequest = { + startingPoint: { + address: restaurantAddressString, + lat: Number(restaurantAddress.latitude), + lng: Number(restaurantAddress.longitude), + name: restaurantExt.name, + mobile: restaurant.phone_number, + cod: 0, + formatted_address: restaurantAddressString, + short_address: restaurantAddressString, + address_code: null, + remarks: 'KHÔNG ỨNG TIỀN', + item_value: 0, + // require_pod?: boolean; // Optional property + }, + destination: { + address: customerAddressString, + lat: Number(address.latitude), + lng: Number(address.longitude), + name: customer.name, + mobile: customer.phone_number, + cod: orderTotal, + formatted_address: customerAddressString, + short_address: customerAddressString, + address_code: null, + remarks: 'KHÔNG ỨNG TIỀN', + item_value: 0, + require_pod: true, + }, + paymentMethod: 'BALANCE', + totalPay: 0, + orderTime: orderTime, + promoCode: null, + remarks: driver_note, + adminNote: '', + routeOptimized: false, + idleUntil: orderTime, + items: deliveryItems, + packageDetails: [], + groupServiceId: null, + groupRequests: null, + serviceType: null, + }; + try { + deliveryOrderId = ( + await this.ahamoveService.postAhamoveOrder(ahamoveOrderRequest) + ).order_id; + } catch (error) { + throw new CustomRpcException( + 114, + 'There are some errors to request the delivery service', + ); + } + } + + //insert database (with transaction) + let newOrderId: number; + await this.entityManager.transaction(async (transactionalEntityManager) => { + // insert data into table Address + const newAddress = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(Address) + .values({ + address_line: address.address_line, + ward: address.ward, + district: address.district, + city: address.city, + country: address.country, + latitude: address.latitude, + longitude: address.longitude, + }) + .execute(); + this.logger.log(newAddress.identifiers); + const newAddressId = Number(newAddress.identifiers[0].address_id); + + // insert data into table Order + const newOrder = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(Order) + .values({ + customer_id: customer_id, + restaurant_id: restaurant.restaurant_id, + address_id: newAddressId, + order_total: orderTotal, + delivery_fee: deliveryFee, + packaging_fee: packagingFee, + cutlery_fee: cutleryFee, + app_fee: appFee, + coupon_value_from_platform: couponValue.coupon_value_from_platform, + coupon_value_from_restaurant: + couponValue.coupon_value_from_restaurant, + coupon_id: validCoupon.coupon_id, + currency: restaurant.unit, + is_preorder: isPreorder, + expected_arrival_time: expected_arrival_time, + delivery_order_id: deliveryOrderId, + driver_note: driver_note, + }) + .execute(); + this.logger.log(newOrder.identifiers); + newOrderId = Number(newOrder.identifiers[0].order_id); + + // insert data into table Order_SKU + orderItems.forEach((item) => { + item.order_id = Number(newOrderId); + }); + const newOrderSkuItems = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(OrderSKU) + .values(orderItems) + .execute(); + this.logger.log(newOrderSkuItems.identifiers); + + // insert data into table Order_Status_Log + // - status NEW + const newOrderStatus = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(OrderStatusLog) + .values({ + order_id: newOrderId, + order_status_id: OrderStatus.NEW, + }) + .execute(); + this.logger.log(newOrderStatus.identifiers); + + // insert data into table Invoice + const newInvoice = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(Invoice) + .values({ + payment_method: payment_method_id, + total_amount: orderTotal, + tax_amount: 0, + discount_amount: + couponValue.coupon_value_from_platform + + couponValue.coupon_value_from_restaurant, + description: '', + order_id: newOrderId, + currency: restaurant.unit, + }) + .execute(); + this.logger.log(newInvoice.identifiers); + const newInvoiceId = Number(newInvoice.identifiers[0].invoice_id); + + // insert data into table Invoice_Status_History + // - status STARTED + const newInvoiceStatus = await transactionalEntityManager + .createQueryBuilder() + .insert() + .into(InvoiceStatusHistory) + .values({ + invoice_id: newInvoiceId, + status_id: InvoiceHistoryStatusEnum.STARTED, + note: '', + }) + .execute(); + this.logger.log(newInvoiceStatus.identifiers); + this.logger.log(newInvoiceStatus.generatedMaps); + }); + + return await this.getOrderDetail(newOrderId); + } + + async buildOrderSKUData(items: OrderItemRequest[]): Promise { + if (!items || items.length == 0) { + return []; + } + const orderItems: OrderSKU[] = []; + + //Get list SKU info + const skuIds = [...new Set(items.map((i) => i.sku_id))]; + const skus = await this.entityManager + .createQueryBuilder(SKU, 'sku') + .where('sku.sku_id IN (:...skuIds)', { skuIds }) + .getMany(); + // this.logger.log(skus); + + //Get list Menu Item info from SKU + const menuItemIds = [...new Set(skus.map((i) => i.menu_item_id))]; + const menuItems = await this.entityManager + .createQueryBuilder(MenuItem, 'menuItem') + .leftJoinAndSelect('menuItem.menuItemPackaging_obj', 'menuItemPackaging') + .leftJoinAndSelect('menuItemPackaging.packaging_obj', 'packaging') + .where('menuItem.menu_item_id IN (:...menuItemIds)', { menuItemIds }) + .getMany(); + + //Cannot order more than available quantity + for (const menuItem of menuItems) { + const menuItemWithSkuIds = skus + .filter((sku) => sku.menu_item_id == menuItem.menu_item_id) + .map((i) => i.sku_id); + 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); + if (orderingQuantity > menuItem.quantity_available) { + throw new CustomRpcException(108, { + message: `Cannot order more than available quantity`, + menu_item_id: menuItem.menu_item_id, + quantity_available: menuItem.quantity_available, + }); + } + } + + for (const item of items) { + const sku = skus.find((i) => i.sku_id == item.sku_id); + const menuItem = menuItems.find( + (i) => i.menu_item_id == sku.menu_item_id, + ); + + //check package info is valid + const packaging = menuItem.menuItemPackaging_obj.find( + (i) => i.packaging_id == item.packaging_id, + )?.packaging_obj; + if (!packaging) { + throw new CustomRpcException(105, { + message: `Packaging id ${item.packaging_id} is not valid for sku_id ${item.sku_id}`, + sku_id: item.sku_id, + packaging_id: item.packaging_id, + }); + } + + // Check if the advanced_taste_customization_obj is all available to this SKU + const advancedTasteCustomizationValidation = + item.advanced_taste_customization_obj.length > 0 + ? await this.commonService.validateAdvacedTasteCustomizationObjWithMenuItem( + item.advanced_taste_customization_obj, + sku.menu_item_id, + ) + : { isValid: true, message: '' }; + if (!advancedTasteCustomizationValidation.isValid) { + throw new CustomRpcException(106, { + message: advancedTasteCustomizationValidation.message, + sku_id: item.sku_id, + }); + } + + // Check if the basic_taste_customization_obj is all available to this SKU + const basicTasteCustomizationValidation = + item.basic_taste_customization_obj.length > 0 + ? await this.commonService.validateBasicTasteCustomizationObjWithMenuItem( + item.basic_taste_customization_obj, + sku.menu_item_id, + ) + : { isValid: true, message: '' }; + if (!basicTasteCustomizationValidation.isValid) { + throw new CustomRpcException(107, { + message: basicTasteCustomizationValidation.message, + sku_id: item.sku_id, + }); + } + + //Buil output + const orderSku = new OrderSKU(); + orderSku.sku_id = item.sku_id; + orderSku.qty_ordered = item.qty_ordered; + orderSku.price = await this.commonService.getAvailableDiscountPrice(sku); + orderSku.advanced_taste_customization = + item.advanced_taste_customization_obj.length > 0 + ? await this.commonService.interpretAdvanceTaseCustomization( + item.advanced_taste_customization_obj, + ) + : ''; + orderSku.basic_taste_customization = + item.basic_taste_customization_obj.length > 0 + ? await this.commonService.interpretBasicTaseCustomization( + item.basic_taste_customization_obj, + ) + : ''; + orderSku.portion_customization = + await this.commonService.interpretPortionCustomization(item.sku_id); + orderSku.advanced_taste_customization_obj = + item.advanced_taste_customization_obj.length > 0 + ? JSON.stringify(item.advanced_taste_customization_obj) + : ''; + orderSku.basic_taste_customization_obj = + item.basic_taste_customization_obj.length > 0 + ? JSON.stringify(item.basic_taste_customization_obj) + : ''; + orderSku.notes = item.notes; + orderSku.packaging_id = item.packaging_id; + orderSku.calorie_kcal = sku.calorie_kcal; + orderSku.packaging_obj = packaging; + + orderItems.push(orderSku); + } + + return orderItems; + } + + async getPaymentMethodById( + payment_method_id: number, + ): Promise { + return await this.entityManager + .createQueryBuilder(PaymentOption, 'payment') + .where('payment.option_id = :payment_method_id', { + payment_method_id, + }) + .getOne(); + } + + async getOrderDetail(order_id): Promise { + console.log(order_id); + if (!order_id) { + return undefined; + } + const order = await this.entityManager + .createQueryBuilder(Order, 'order') + .leftJoinAndSelect('order.order_status_log', 'orderStatusLog') + .leftJoinAndSelect('orderStatusLog.order_status_ext', 'orderStatusExt') + .leftJoinAndSelect('order.invoice_obj', 'invoice') + .leftJoinAndSelect('invoice.payment_option_obj', 'payment') + .leftJoinAndSelect('invoice.history_status_obj', 'invoiceHistory') + .leftJoinAndSelect( + 'invoiceHistory.invoice_status_ext', + 'invoiceStatusExt', + ) + .leftJoinAndSelect('order.items', 'item') + .leftJoinAndSelect('order.address_obj', 'address') + .where('order.order_id = :order_id', { order_id }) + .getOne(); + + if (!order) { + return undefined; + } + console.log(order); + + //Get restaurant info + const skuId = order.items[0].sku_id; + const sku = await this.entityManager + .createQueryBuilder(SKU, 'sku') + .leftJoinAndSelect('sku.menu_item', 'menuItem') + .where('sku.sku_id = :skuId', { skuId }) + .getOne(); + const restaurant = await this.commonService.getRestaurantBasicInfo( + sku.menu_item.restaurant_id, + ); + + //Get driver info + const driver = ( + await this.entityManager + .createQueryBuilder(DriverStatusLog, 'driverLog') + .leftJoinAndSelect('driverLog.driver', 'driver') + .leftJoinAndSelect('driver.profile_image_obj', 'profileImg') + .where('driverLog.order_id = :order_id', { order_id }) + .orderBy('driverLog.logged_at', 'DESC') + .getOne() + )?.driver; + + //Get tracking link + const shareLink = order.delivery_order_id + ? ( + await this.ahamoveService.getAhamoveOrderByOrderId( + order.delivery_order_id, + ) + )?.shared_link + : undefined; + + //build PaymentStatusHistory List + const paymentStatusHistory = []; + for (const invoiceHistory of order.invoice_obj.history_status_obj) { + const nameByLang: TextByLang[] = []; + invoiceHistory.invoice_status_ext.forEach((status) => { + nameByLang.push({ + ISO_language_code: status.ISO_language_code, + text: status.name, + }); + }); + paymentStatusHistory.push({ + status_id: invoiceHistory.status_id, + name: nameByLang, + note: invoiceHistory.note, + created_at: invoiceHistory.created_at, + }); + } + + // Build OrderItemResponse + const orderItemResponse = []; + const skuIds = [...new Set(order.items.map((i) => i.sku_id))]; + const skus = await this.entityManager + .createQueryBuilder(SKU, 'sku') + .leftJoinAndSelect('sku.menu_item', 'menuItem') + .leftJoinAndSelect('menuItem.menuItemExt', 'menuItemExt') + .leftJoinAndSelect('menuItem.image_obj', 'image') + .where('sku.sku_id IN (:...skuIds)', { skuIds }) + .getMany(); + const packagingIds = [...new Set(order.items.map((i) => i.packaging_id))]; + const packages = await this.entityManager + .createQueryBuilder(Packaging, 'packaging') + .leftJoinAndSelect('packaging.packaging_ext_obj', 'ext') + .where('packaging.packaging_id IN (:...packagingIds)', { + packagingIds, + }) + .getMany(); + + for (const orderItem of order.items) { + const sku = skus.find((i) => i.sku_id); + const itemNameByLang: TextByLang[] = sku.menu_item.menuItemExt.map( + (i) => { + return { + ISO_language_code: i.ISO_language_code, + text: i.name, + }; + }, + ); + const packaging = packages.find( + (i) => i.packaging_id == orderItem.packaging_id, + ); + const packagingName: TextByLang[] = []; + const packagingDesc: TextByLang[] = []; + packaging.packaging_ext_obj.forEach((ext) => { + packagingName.push({ + ISO_language_code: ext.ISO_language_code, + text: ext.name, + }); + packagingDesc.push({ + ISO_language_code: ext.ISO_language_code, + text: ext.description, + }); + }); + const orderItemPackaging = { + packaging_id: packaging.packaging_id, + name: packagingName, + description: packagingDesc, + price: packaging.price, + }; + orderItemResponse.push({ + item_name: itemNameByLang, + item_img: sku.menu_item.image_obj.url, + order_id: orderItem.order_id, + sku_id: orderItem.sku_id, + qty_ordered: orderItem.qty_ordered, + price: orderItem.price, + advanced_taste_customization_obj: + orderItem.advanced_taste_customization_obj + ? JSON.parse(orderItem.advanced_taste_customization_obj) + : [], + basic_taste_customization_obj: orderItem.basic_taste_customization_obj + ? JSON.parse(orderItem.basic_taste_customization_obj) + : [], + advanced_taste_customization: orderItem.advanced_taste_customization, + basic_taste_customization: orderItem.basic_taste_customization, + portion_customization: orderItem.portion_customization, + notes: orderItem.notes, + calorie_kcal: orderItem.calorie_kcal, + packaging_info: orderItemPackaging, + }); + } + + //buil OrderStatusLog + const orderStatusLog = []; + order.order_status_log.forEach((log) => { + let milestone = null; + switch (log.order_status_id) { + case OrderStatus.NEW: { + milestone = OrderMilestones.CREATED; + break; + } + + case OrderStatus.IDLE: { + milestone = OrderMilestones.CONFIRMED; + break; + } + + case OrderStatus.PROCESSING: { + milestone = OrderMilestones.START_TO_PROCESS; + break; + } + + case OrderStatus.DELIVERING: { + milestone = OrderMilestones.PICKED_UP; + break; + } + + case OrderStatus.COMPLETED: { + milestone = OrderMilestones.COMPLETED; + break; + } + + case OrderStatus.FAILED: { + milestone = OrderMilestones.FAILED; + break; + } + + case OrderStatus.CANCELLED: { + milestone = OrderMilestones.CANCELLED; + break; + } + + default: + //do nothing + break; + } + orderStatusLog.push({ + status: log.order_status_id, + description: log.order_status_ext.map((i) => { + return { + ISO_language_code: i.ISO_language_code, + text: i.description, + }; + }), + logged_at: log.logged_at, + milestone: milestone, + }); + }); + + const orderDetail: OrderDetailResponse = { + order_id: order.order_id, + customer_id: order.customer_id, + restaurant: { + restaurant_id: restaurant.id, + restaurant_name: restaurant.name, + restaurant_logo_img: restaurant.logo_url, + }, + address: { + address_line: order.address_obj.address_line, + ward: order.address_obj.ward, + district: order.address_obj.district, + city: order.address_obj.city, + country: order.address_obj.country, + latitude: order.address_obj.latitude, + longitude: order.address_obj.longitude, + }, + driver_note: order.driver_note, + driver: !driver + ? null + : { + driver_id: driver.driver_id, + name: driver.name, + phone_number: driver.phone_number, + vehicle: driver.vehicle, + license_plates: driver.license_plates, + profile_image: driver.profile_image_obj?.url, + }, + order_total: order.order_total, + delivery_fee: order.delivery_fee, + packaging_fee: order.packaging_fee, + cutlery_fee: order.cutlery_fee, + app_fee: order.app_fee, + coupon_value: + order.coupon_value_from_platform + order.coupon_value_from_restaurant, + coupon_id: order.order_id, + invoice_id: order.invoice_obj.invoice_id, + payment_method: { + id: order.invoice_obj.payment_option_obj.option_id, + name: order.invoice_obj.payment_option_obj.name, + }, + payment_status_history: paymentStatusHistory, + is_preorder: Boolean(order.is_preorder), + expected_arrival_time: order.expected_arrival_time, + order_items: orderItemResponse, + order_status_log: orderStatusLog, + tracking_url: shareLink, + }; + + return orderDetail; + } } diff --git a/src/type/index.ts b/src/type/index.ts index 4e1e403..5f37e02 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -208,3 +208,22 @@ export interface CouponAppliedItem { price_after_discount: number; packaging_price: number; } + +export interface CouponValue { + coupon_value_from_platform: number; + coupon_value_from_restaurant: number; +} + +export interface OrderItemRequest { + sku_id: number; + qty_ordered: number; + advanced_taste_customization_obj: OptionSelection[]; + basic_taste_customization_obj: BasicTasteSelection[]; + notes: string; + packaging_id: number; +} + +export interface OrderDetailRequest { + order_id: number; + customer_id: number; +}