diff --git a/src/dto/food-detail.dto.ts b/src/dto/food-detail.dto.ts index 20370c5..1a5c643 100644 --- a/src/dto/food-detail.dto.ts +++ b/src/dto/food-detail.dto.ts @@ -11,13 +11,14 @@ export class FoodDetailDTO { review_number: number; promotion: string; packaging_info: PackagingInfo[]; - cutoff_time: string; + cutoff_time_m: number; ingredients: Ingredient[]; description: TextByLang[]; portion_customization: Option[]; taste_customization: Option[]; other_customizaton: BasicCustomization[]; reviews: Review[]; + is_advanced_customizable: boolean; } class Ingredient { diff --git a/src/dto/food.dto.ts b/src/dto/food.dto.ts index fd72e74..c8df2ea 100644 --- a/src/dto/food.dto.ts +++ b/src/dto/food.dto.ts @@ -18,11 +18,11 @@ export class FoodDTO { price: number; price_after_discount: number; promotion: string; - cutoff_time: string; preparing_time_s: number; cooking_time_s: number; quantity_available: number; is_vegetarian: boolean; cooking_schedule: string; units_sold: number; + is_advanced_customizable: boolean; } diff --git a/src/dto/restaurant-detail.dto.ts b/src/dto/restaurant-detail.dto.ts index 8b9024e..58a576d 100644 --- a/src/dto/restaurant-detail.dto.ts +++ b/src/dto/restaurant-detail.dto.ts @@ -14,10 +14,11 @@ export class RestaurantDetailDTO { specialty: TextByLang[]; introduction: TextByLang[]; review_total_count: number; - cutoff_time: string[]; + cutoff_time_m: number; having_vegeterian_food: boolean; unit: string; menu: FoodDTO[]; distance_km: number; delivery_time_s: number; + is_advanced_customizable: boolean; } diff --git a/src/dto/restaurant.dto.ts b/src/dto/restaurant.dto.ts index 97ef658..20f7e8c 100644 --- a/src/dto/restaurant.dto.ts +++ b/src/dto/restaurant.dto.ts @@ -11,9 +11,9 @@ export class RestaurantDTO { specialty: TextByLang[]; top_food: string; promotion: string; - cutoff_time: string[]; having_vegeterian_food: boolean; max_price: number; min_price: number; unit: string; + is_advanced_customizable: boolean; } diff --git a/src/entity/manual-cutoff-time.entity.ts b/src/entity/manual-cutoff-time.entity.ts new file mode 100644 index 0000000..005ce57 --- /dev/null +++ b/src/entity/manual-cutoff-time.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('Manual_Cutoff_Time') +export class ManualCutoffTime { + @PrimaryGeneratedColumn() + public manual_id: number; + + @Column({ type: 'date', nullable: false, unique: false }) + public date: Date; + + @Column({ type: 'int', nullable: false, unique: false }) + public cutoff_time_m: number; + + @Column({ type: 'int', nullable: false, unique: false }) + public restaurant_id: number; + + @Column({ type: 'bigint', nullable: false, unique: false }) + public logged_at: number; +} diff --git a/src/entity/menu-item-attribute-value.entity.ts b/src/entity/menu-item-attribute-value.entity.ts index 33cd40b..0deecc8 100644 --- a/src/entity/menu-item-attribute-value.entity.ts +++ b/src/entity/menu-item-attribute-value.entity.ts @@ -5,11 +5,9 @@ import { Column, ManyToOne, JoinColumn, - OneToMany, } from 'typeorm'; import { MenuItemAttribute } from './menu-item-attribute.entity'; import { Unit } from './unit.entity'; -import { TasteValueExt } from './taste-value-ext.entity'; import { TasteValue } from './taste-value.entity'; @Entity('Menu_Item_Attribute_Value') diff --git a/src/entity/menu-item-attribute.entity.ts b/src/entity/menu-item-attribute.entity.ts index 0f76971..3b51647 100644 --- a/src/entity/menu-item-attribute.entity.ts +++ b/src/entity/menu-item-attribute.entity.ts @@ -4,10 +4,13 @@ import { PrimaryGeneratedColumn, Column, OneToMany, + ManyToOne, + JoinColumn, } from 'typeorm'; import { MenuItemAttributeExt } from './menu-item-attribute-ext.entity'; import { MenuItemAttributeValue } from './menu-item-attribute-value.entity'; import { TasteExt } from './taste-ext.entity'; +import { MenuItem } from './menu-item.entity'; @Entity('Menu_Item_Attribute') export class MenuItemAttribute { @@ -40,4 +43,11 @@ export class MenuItemAttribute { @OneToMany(() => TasteExt, (tasteExt) => tasteExt.taste) public taste_ext: TasteExt[]; + + @ManyToOne(() => MenuItem, (menuItem) => menuItem.attribute_obj) + @JoinColumn({ + name: 'menu_item_id', + referencedColumnName: 'menu_item_id', + }) + public menu_item_obj: MenuItem; } diff --git a/src/entity/menu-item.entity.ts b/src/entity/menu-item.entity.ts index 3eaf1e9..a1f7b4a 100644 --- a/src/entity/menu-item.entity.ts +++ b/src/entity/menu-item.entity.ts @@ -13,6 +13,7 @@ import { SKU } from './sku.entity'; import { Media } from './media.entity'; import { Recipe } from './recipe.entity'; import { MenuItemPackaging } from './menuitem-packaging.entity'; +import { MenuItemAttribute } from './menu-item-attribute.entity'; @Entity('Menu_Item') export class MenuItem { @@ -28,8 +29,8 @@ export class MenuItem { @Column({ type: 'int', nullable: true, unique: false }) public cooking_time_s: number; - @Column({ type: 'time', nullable: true, unique: false }) - public cutoff_time: string; + // @Column({ type: 'time', nullable: true, unique: false }) + // public cutoff_time: string; @Column({ type: 'int', nullable: true, unique: false }) public quantity_available: number; @@ -113,12 +114,12 @@ export class MenuItem { @OneToMany(() => Recipe, (recipe) => recipe.menu_item) public recipe: Promise; - // @OneToMany(() => Packaging, (packaging) => packaging.menu_item_obj) - // public packaging_obj: Packaging[]; - @OneToMany( () => MenuItemPackaging, (menuItemPackaging) => menuItemPackaging.menu_item_obj, ) public menuItemPackaging_obj: MenuItemPackaging[]; + + @OneToMany(() => MenuItemAttribute, (att) => att.menu_item_obj) + public attribute_obj: MenuItemAttribute[]; } diff --git a/src/entity/restaurant.entity.ts b/src/entity/restaurant.entity.ts index 0cbaa8d..3a533f6 100644 --- a/src/entity/restaurant.entity.ts +++ b/src/entity/restaurant.entity.ts @@ -108,6 +108,9 @@ export class Restaurant { }) public utc_time_zone: number; + @Column({ type: 'int', nullable: false, unique: false, default: 0 }) + public cutoff_time_m: number; + @CreateDateColumn({ type: 'datetime', nullable: false, diff --git a/src/fake_data/hot-food.json b/src/fake_data/hot-food.json index 14276d7..a2342ea 100644 --- a/src/fake_data/hot-food.json +++ b/src/fake_data/hot-food.json @@ -32,13 +32,13 @@ "price": 95000, "price_after_discount": 65000, "promotion": "Ưu đãi đến 50k", - "cutoff_time": "09:00:00", "preparing_time_s": 600, "cooking_time_s": 1200, "quantity_available": 5, "is_vegetarian": false, "cooking_schedule": "[{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":true},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":true},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":true},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false}]", - "units_sold": 0 + "units_sold": 0, + "is_advanced_customizable": false }, { "id": 1, @@ -73,13 +73,13 @@ "price": 95000, "price_after_discount": 65000, "promotion": "Ưu đãi đến 50k", - "cutoff_time": "09:00:00", "preparing_time_s": 600, "cooking_time_s": 1200, "quantity_available": 5, "is_vegetarian": false, "cooking_schedule": "[{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":true},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":true},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":true},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false}]", - "units_sold": 0 + "units_sold": 0, + "is_advanced_customizable": true }, { "id": 1, @@ -114,12 +114,12 @@ "price": 95000, "price_after_discount": 65000, "promotion": "Ưu đãi đến 50k", - "cutoff_time": "09:00:00", "preparing_time_s": 600, "cooking_time_s": 1200, "quantity_available": 5, "is_vegetarian": false, "cooking_schedule": "[{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":1,\"dayName\":\"Sun\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":true},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":2,\"dayName\":\"Mon\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":3,\"dayName\":\"Tue\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":true},{\"dayId\":4,\"dayName\":\"Wed\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":5,\"dayName\":\"Thu\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":true},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":6,\"dayName\":\"Fri\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"06:00:00\",\"to\":\"13:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"14:00:00\",\"to\":\"21:59:59\",\"isAvailable\":false},{\"dayId\":7,\"dayName\":\"Sat\",\"from\":\"22:00:00\",\"to\":\"05:59:59\",\"isAvailable\":false}]", - "units_sold": 0 + "units_sold": 0, + "is_advanced_customizable": true } ] diff --git a/src/feature/cart/cart.controller.ts b/src/feature/cart/cart.controller.ts index 9a1622f..ced1ff0 100644 --- a/src/feature/cart/cart.controller.ts +++ b/src/feature/cart/cart.controller.ts @@ -309,7 +309,14 @@ export class CartController { data: GetAvailableDeliveryTimeRequest, ): Promise { const res = new GetAvailableDeliveryTimeResponse(200, ''); - const { menu_item_ids, now, long, lat, utc_offset } = data; + const { + menu_item_ids, + now, + long, + lat, + utc_offset, + having_advanced_customization, + } = data; try { const timeSlots: TimeSlot[] = @@ -319,6 +326,7 @@ export class CartController { long, lat, utc_offset, + having_advanced_customization, ); if (timeSlots.length > 0) { res.statusCode = 200; diff --git a/src/feature/cart/cart.service.ts b/src/feature/cart/cart.service.ts index 5e9c8ff..4065036 100644 --- a/src/feature/cart/cart.service.ts +++ b/src/feature/cart/cart.service.ts @@ -876,6 +876,7 @@ export class CartService { long: number, lat: number, utc_offset: number, + having_advanced_customization: boolean, buffer_s = 5 * 60, // 5 mins ): Promise { const timeSlots = []; @@ -917,7 +918,7 @@ export class CartService { const localTodayId = new Date(now + timeZoneOffset).getUTCDay() + 1; // 1->7: Sunday -> Saturday - //Find the schedule in which all of the menu items are available + //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]; @@ -927,7 +928,6 @@ export class CartService { from: Shift.MorningFrom, to: Shift.MorningTo, is_available: true, - waiting_time_s: 0, }); overlapSchedule.push({ day_id: localTodayId, @@ -935,7 +935,6 @@ export class CartService { from: Shift.AfternoonFrom, to: Shift.AfternoonTo, is_available: true, - waiting_time_s: 0, }); overlapSchedule.push({ day_id: localTodayId, @@ -943,10 +942,10 @@ export class CartService { from: Shift.NightFrom, to: Shift.NightTo, is_available: true, - waiting_time_s: 0, }); } for (const menuItem of menuItems) { + //Get the cooking schedule of menu_item_ids const menuItemSchedule: DayShift[] = JSON.parse( menuItem.cooking_schedule, ); @@ -960,19 +959,6 @@ export class CartService { } if (dayShift.is_available == false) { overlapSchedule[index].is_available = false; - } else if ( - dayShift.is_available == true && - overlapSchedule[index].is_available == true - ) { - if (dayShift.cutoff_time) { - const waiting_time_s = - this.commonService.convertTimeToSeconds(dayShift.cutoff_time) + - menuItem.cooking_time_s - - this.commonService.convertTimeToSeconds(dayShift.from); - if (waiting_time_s > overlapSchedule[index].waiting_time_s) { - overlapSchedule[index].waiting_time_s = waiting_time_s; - } - } } } } @@ -1019,11 +1005,12 @@ export class CartService { ); } menuItemAvailableTimeRanges.push({ - from: from + dayShift.waiting_time_s * 1000, + 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) @@ -1034,7 +1021,7 @@ export class CartService { restaurantId, now, ); - //Only keep the day off for this week + //ONLY KEEP THE DAY OFF FOR THIS WEEK if (dayOffs.length > 0) { const thisSaturday = this.commonService.getThisDate( now, @@ -1079,6 +1066,7 @@ export class CartService { }); } + //Get todayOperationTimeRange const todayOperationTimeRange = await this.commonService.getTodayOpsTime( restaurantId, now, @@ -1105,32 +1093,97 @@ export class CartService { } } - //get the longest prepraring time for all the menu items - const listOfPreparingTime = menuItems.map((i) => i.preparing_time_s); - const longestPreparingTime = Math.max(...listOfPreparingTime); + // //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 - //adjust the time ranges with - // - delivery time - // - preparing_time (the longest preparing time of all menu items) - // - buffer (5 mins) - // - now const availableDeliveryTime: TimeRange[] = []; - foodAvailabeTimeRanges.forEach((foodTimeRange) => { - let from = 0; - if (foodTimeRange.from < now) { - from = now; - } else if (foodTimeRange.from >= now) { - from = foodTimeRange.from; + + 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 timeRange: TimeRange = { - from: from + (longestPreparingTime + delivery_time_s + buffer_s) * 1000, - to: - foodTimeRange.to + - (longestPreparingTime + delivery_time_s + buffer_s) * 1000, - }; - availableDeliveryTime.push(timeRange); - }); + } //convert time ranges to time slots for (const timeRange of availableDeliveryTime) { diff --git a/src/feature/cart/dto/get-available-delivery-time-request.dto.ts b/src/feature/cart/dto/get-available-delivery-time-request.dto.ts index 75cc2be..30616b7 100644 --- a/src/feature/cart/dto/get-available-delivery-time-request.dto.ts +++ b/src/feature/cart/dto/get-available-delivery-time-request.dto.ts @@ -4,4 +4,5 @@ export class GetAvailableDeliveryTimeRequest { long: number; lat: number; utc_offset: number; + having_advanced_customization: boolean; } diff --git a/src/feature/common/common.service.ts b/src/feature/common/common.service.ts index 8abc657..957a13e 100644 --- a/src/feature/common/common.service.ts +++ b/src/feature/common/common.service.ts @@ -40,6 +40,7 @@ import { AhamoveService } from 'src/dependency/ahamove/ahamove.service'; import { ConfigService } from '@nestjs/config'; import { Packaging } from 'src/entity/packaging.entity'; import { MenuItemPackaging } from 'src/entity/menuitem-packaging.entity'; +import { ManualCutoffTime } from 'src/entity/manual-cutoff-time.entity'; @Injectable() export class CommonService { @@ -163,6 +164,14 @@ export class CommonService { }; }) || null; + let isAdvancedCustomizable: boolean = false; + if (!!menuItem.attribute_obj) { + if ( + menuItem.attribute_obj.filter((i) => i.type_id == 'taste').length > 0 + ) { + isAdvancedCustomizable = true; + } + } return { id: menuItem.menu_item_id, image: menuItem.image_obj.url, @@ -183,13 +192,13 @@ export class CommonService { menuItem.skus[0], ), promotion: menuItem.promotion, - cutoff_time: menuItem.cutoff_time, preparing_time_s: menuItem.preparing_time_s, cooking_time_s: menuItem.cooking_time_s, quantity_available: menuItem.quantity_available, is_vegetarian: Boolean(menuItem.is_vegetarian), cooking_schedule: menuItem.cooking_schedule, units_sold: menuItem.units_sold, + is_advanced_customizable: isAdvancedCustomizable, }; } // end of convertIntoFoodDTO @@ -852,4 +861,73 @@ export class CommonService { }) .getOne(); } //end of getStandardPackagingByMenuItem + + async checkIfFoodIsAdvancedCustomizable( + menu_item_id: number, + ): Promise { + let resutl: boolean = false; + + const advancedCustomization = await this.entityManager + .createQueryBuilder(MenuItemAttribute, 'attribute') + .where('attribute.menu_item_id = :menu_item_id', { menu_item_id }) + .andWhere("attribute.type_id = 'taste'") + .getMany(); + + resutl = advancedCustomization.length > 0 ? true : false; + return resutl; + } //end of checkIfFoodIsAdvancedCustomizable + + async getRestaurantById(restaurant_id: number): Promise { + const restaurant = await this.entityManager + .createQueryBuilder(Restaurant, 'restaurant') + .where('restaurant.restaurant_id = :restaurant_id', { restaurant_id }) + .getOne(); + return restaurant; + } //end of getRestaurantById + + async checkIfRestaurantHasAdvancedCustomizableFood( + restaurant_id: number, + ): Promise { + let resutl: boolean = false; + + const advancedCustomization = await this.entityManager + .createQueryBuilder(MenuItemAttribute, 'attribute') + .leftJoinAndSelect('attribute.menu_item_obj', 'menuItem') + .where('menuItem.restaurant_id = :restaurant_id', { restaurant_id }) + .andWhere("attribute.type_id = 'taste'") + .getMany(); + + resutl = advancedCustomization.length > 0 ? true : false; + return resutl; + } //end of checkIfFoodIsAdvancedCustomizable + + async getCutoffTimePoint( + now: number, + restaurant_id: number, + ): Promise { + const restaurantUtcTimeZone = await this.getUtcTimeZone(restaurant_id); + const timeZoneOffset = restaurantUtcTimeZone * 60 * 60 * 1000; // Offset in milliseconds for EST + + let cutoffTimeConfig: number = 0; + //get MANUAL cutoff time config from Manual_Cutoff_Time + const query = `SELECT * FROM Manual_Cutoff_Time where date = date(FROM_UNIXTIME(${ + (now + timeZoneOffset) / 1000 + })) ORDER BY logged_at DESC LIMIT 0,1`; + const data: ManualCutoffTime[] = await this.entityManager.query(query); + if (data.length > 0) { + cutoffTimeConfig = data[0].cutoff_time_m; + } else if (data.length <= 0) { + //get cutoff time config from restaurant + const restaurant = await this.getRestaurantById(restaurant_id); + cutoffTimeConfig = restaurant.cutoff_time_m; + } + + //Get starting point for local today + const localToday = new Date(now + timeZoneOffset); + localToday.setUTCHours(0, 0, 0, 0); + const startingPointOfLocalToday = localToday.getTime() - timeZoneOffset; + + //return data after adjusting with time offset and cutoff time config + return startingPointOfLocalToday + cutoffTimeConfig * 60 * 1000; + } } diff --git a/src/feature/food/dto/get-available-food-by-restaurant-response.dto.ts b/src/feature/food/dto/get-available-food-by-restaurant-response.dto.ts index 927286e..ef1f9d7 100644 --- a/src/feature/food/dto/get-available-food-by-restaurant-response.dto.ts +++ b/src/feature/food/dto/get-available-food-by-restaurant-response.dto.ts @@ -23,11 +23,11 @@ interface FoodDTO { price: number; price_after_discount: number; promotion: string; - cutoff_time: string; preparing_time_s: number; cooking_time_s: number; quantity_available: number; is_vegetarian: boolean; cooking_schedule: string; units_sold: number; + is_advanced_customizable: boolean; } diff --git a/src/feature/food/dto/get-food-detail-response.dto.ts b/src/feature/food/dto/get-food-detail-response.dto.ts index ded6f95..678c4b2 100644 --- a/src/feature/food/dto/get-food-detail-response.dto.ts +++ b/src/feature/food/dto/get-food-detail-response.dto.ts @@ -15,13 +15,14 @@ interface FoodDetail { review_number: number; promotion: string; packaging_info: PackagingInfo[]; - cutoff_time: string; + cutoff_time_m: number; ingredients: Ingredient[]; description: TextByLang[]; portion_customization: Option[]; taste_customization: Option[]; other_customizaton: BasicCustomization[]; reviews: Review[]; + is_advanced_customizable: boolean; } interface Ingredient { diff --git a/src/feature/food/dto/get-hot-food-response.dto.ts b/src/feature/food/dto/get-hot-food-response.dto.ts index f3c6ec0..018ae2d 100644 --- a/src/feature/food/dto/get-hot-food-response.dto.ts +++ b/src/feature/food/dto/get-hot-food-response.dto.ts @@ -21,13 +21,13 @@ interface FoodDTO { price: number; price_after_discount: number; promotion: string; - cutoff_time: string; preparing_time_s: number; cooking_time_s: number; quantity_available: number; is_vegetarian: boolean; cooking_schedule: string; units_sold: number; + is_advanced_customizable: boolean; } interface TextByLang { diff --git a/src/feature/food/dto/get-side-dish-response.dto.ts b/src/feature/food/dto/get-side-dish-response.dto.ts index ea8f748..f5e5192 100644 --- a/src/feature/food/dto/get-side-dish-response.dto.ts +++ b/src/feature/food/dto/get-side-dish-response.dto.ts @@ -23,11 +23,11 @@ interface FoodDTO { price: number; price_after_discount: number; promotion: string; - cutoff_time: string; preparing_time_s: number; cooking_time_s: number; quantity_available: number; is_vegetarian: boolean; cooking_schedule: string; units_sold: number; + is_advanced_customizable: boolean; } diff --git a/src/feature/food/food.service.ts b/src/feature/food/food.service.ts index 9b05ef4..d643729 100644 --- a/src/feature/food/food.service.ts +++ b/src/feature/food/food.service.ts @@ -68,6 +68,7 @@ export class FoodService { .leftJoinAndSelect('menuItem.menuItemExt', 'menuItemExt') .leftJoinAndSelect('menuItem.image_obj', 'media') .leftJoinAndSelect('menuItem.skus', 'sku') + .leftJoinAndSelect('menuItem.attribute_obj', 'attribute') .where('menuItem.restaurant_id IN (:...restaurantIds)', { restaurantIds }) .andWhere('menuItem.is_active = :active', { active: TRUE }) .andWhere('sku.is_standard = :standard', { standard: TRUE }) @@ -92,6 +93,7 @@ export class FoodService { .leftJoinAndSelect('menuItem.menuItemExt', 'menuItemExt') .leftJoinAndSelect('menuItem.image_obj', 'media') .leftJoinAndSelect('menuItem.skus', 'sku') + .leftJoinAndSelect('menuItem.attribute_obj', 'attribute') .where('menuItem.menu_item_id IN (:...menuItems)', { menuItems }) .andWhere('menuItem.is_active = :active', { active: TRUE }) .andWhere('sku.is_standard = :standard', { @@ -105,6 +107,7 @@ export class FoodService { .leftJoinAndSelect('menuItem.menuItemExt', 'menuItemExt') .leftJoinAndSelect('menuItem.image_obj', 'media') .leftJoinAndSelect('menuItem.skus', 'sku') + .leftJoinAndSelect('menuItem.attribute_obj', 'attribute') .where('menuItem.menu_item_id IN (:...menuItems)', { menuItems }) .andWhere('menuItem.is_active = :active', { active: TRUE }) .andWhere('sku.is_standard = :standard', { @@ -120,6 +123,7 @@ export class FoodService { .createQueryBuilder('menuItem') .leftJoinAndSelect('menuItem.menuItemExt', 'menuItemExt') .leftJoinAndSelect('menuItem.image_obj', 'media') + .leftJoinAndSelect('menuItem.attribute_obj', 'attribute') .where('menuItem.menu_item_id IN (:...menuItems)', { menuItems }) .andWhere('menuItem.is_active = :active', { active: TRUE }) .getMany(); @@ -128,6 +132,7 @@ export class FoodService { .createQueryBuilder('menuItem') .leftJoinAndSelect('menuItem.menuItemExt', 'menuItemExt') .leftJoinAndSelect('menuItem.image_obj', 'media') + .leftJoinAndSelect('menuItem.attribute_obj', 'attribute') .where('menuItem.menu_item_id IN (:...menuItems)', { menuItems }) .andWhere('menuItem.is_active = :active', { active: TRUE }) .andWhere('menuItemExt.ISO_language_code IN (:...langs)', { langs }) @@ -153,6 +158,9 @@ export class FoodService { result.message = 'Food not found'; return result; } + const restaurant = await this.commonService.getRestaurantById( + foods[0].restaurant_id, + ); const restaurantExt = await this.commonService.getRestaurantExtension( foods[0].restaurant_id, ); @@ -200,6 +208,14 @@ export class FoodService { convertedBasicCustomization.push(customizedItem); } + let isAdvancedCustomizable: boolean = false; + if (!!foods[0].attribute_obj) { + if ( + foods[0].attribute_obj.filter((i) => i.type_id == 'taste').length > 0 + ) { + isAdvancedCustomizable = true; + } + } //Mapping data to the result const data = { menu_item_id: menuItemId, @@ -219,7 +235,7 @@ export class FoodService { packaging, menuItemId, ), - cutoff_time: foods[0].cutoff_time, + cutoff_time_m: restaurant.cutoff_time_m, ingredients: recipe.map((item) => { return { item_name_vie: item.ingredient.vie_name, @@ -238,6 +254,7 @@ export class FoodService { taste_customization: convertedTasteCustomization, other_customizaton: convertedBasicCustomization, reviews: reviews, + is_advanced_customizable: isAdvancedCustomizable, }; result.statusCode = 200; @@ -647,6 +664,8 @@ export class FoodService { .getUTCSeconds() .toString() .padStart(2, '0')}`; + + //Step 1: Find the current day shift which matches the time of now const currentDayShift: DayShift = { day_id: adjustedNow.getUTCDay() + 1, day_name: '', @@ -699,7 +718,8 @@ export class FoodService { currentDayShift.to = Shift.NightTo; } - //Get earliest available day shift + //Step 2: Get earliest available day shift by loopping the schedule + // from the current day shift index const earliestAvailabeDayShift: DayShift = { day_id: null, day_name: null, diff --git a/src/feature/recommendation/dto/food-recommendation-response.dto.ts b/src/feature/recommendation/dto/food-recommendation-response.dto.ts index 65e9b6d..6f51f29 100644 --- a/src/feature/recommendation/dto/food-recommendation-response.dto.ts +++ b/src/feature/recommendation/dto/food-recommendation-response.dto.ts @@ -22,13 +22,13 @@ interface FoodDTO { price: number; price_after_discount: number; promotion: string; - cutoff_time: string; preparing_time_s: number; cooking_time_s: number; quantity_available: number; is_vegetarian: boolean; cooking_schedule: string; units_sold: number; + is_advanced_customizable: boolean; } interface TextByLang { diff --git a/src/feature/recommendation/dto/restaurant-recommendation-response.dto.ts b/src/feature/recommendation/dto/restaurant-recommendation-response.dto.ts index d091026..a7f5582 100644 --- a/src/feature/recommendation/dto/restaurant-recommendation-response.dto.ts +++ b/src/feature/recommendation/dto/restaurant-recommendation-response.dto.ts @@ -15,11 +15,11 @@ class RestaurantDTO { specialty: TextByLang[]; top_food: string; promotion: string; - cutoff_time: string[]; having_vegeterian_food: boolean; max_price: number; min_price: number; unit: string; + is_advanced_customizable: boolean; } interface TextByLang { ISO_language_code: string; diff --git a/src/feature/restaurant/dto/get-restaurant-detail-response.dto.ts b/src/feature/restaurant/dto/get-restaurant-detail-response.dto.ts index 42f50a7..d39f616 100644 --- a/src/feature/restaurant/dto/get-restaurant-detail-response.dto.ts +++ b/src/feature/restaurant/dto/get-restaurant-detail-response.dto.ts @@ -16,12 +16,13 @@ interface RestaurantDetailDTO { specialty: TextByLang[]; introduction: TextByLang[]; review_total_count: number; - cutoff_time: string[]; + cutoff_time_m: number; having_vegeterian_food: boolean; unit: string; menu: FoodDTO[]; distance_km: number; delivery_time_s: number; + is_advanced_customizable: boolean; } interface MediaItem { @@ -71,11 +72,11 @@ interface FoodDTO { price: number; price_after_discount: number; promotion: string; - cutoff_time: string; preparing_time_s: number; cooking_time_s: number; quantity_available: number; is_vegetarian: boolean; cooking_schedule: string; units_sold: number; + is_advanced_customizable: boolean; } diff --git a/src/feature/restaurant/restaurant.service.ts b/src/feature/restaurant/restaurant.service.ts index dc19c8d..e4d7281 100644 --- a/src/feature/restaurant/restaurant.service.ts +++ b/src/feature/restaurant/restaurant.service.ts @@ -144,7 +144,7 @@ export class RestaurantService { async convertIntoRestaurantDTO( restaurant: DeliveryRestaurant, - priceRange: PriceRange, + priceRange: PriceRange = { max: null, min: null }, ): Promise { const restaurantDTO = new RestaurantDTO(); const menuItems = await restaurant.menu_items; @@ -177,11 +177,14 @@ export class RestaurantService { restaurantDTO.specialty = specialty; restaurantDTO.top_food = restaurant.top_food; restaurantDTO.promotion = restaurant.promotion; - restaurantDTO.cutoff_time = menuItems.map((item) => item.cutoff_time); restaurantDTO.having_vegeterian_food = having_vegeterian_food; restaurantDTO.max_price = priceRange.max; restaurantDTO.min_price = priceRange.min; restaurantDTO.unit = restaurant.unit_obj.symbol; + restaurantDTO.is_advanced_customizable = + await this.commonService.checkIfRestaurantHasAdvancedCustomizableFood( + restaurant.restaurant_id, + ); return restaurantDTO; } // convertIntoRestaurantDTO @@ -201,6 +204,7 @@ export class RestaurantService { .leftJoinAndSelect('menuItem.menuItemExt', 'menuItemExt') .leftJoinAndSelect('menuItem.image_obj', 'image') .leftJoinAndSelect('menuItem.skus', 'sku') + .leftJoinAndSelect('menuItem.attribute_obj', 'attribute') .where('restaurant.restaurant_id = :restaurant_id', { restaurant_id }) .andWhere('restaurant.is_active = 1') .andWhere('sku.is_standard = :standard', { @@ -232,21 +236,31 @@ export class RestaurantService { const menuItems = await restaurant.menu_items; const convertedMenuItems: FoodDTO[] = []; let having_vegeterian_food: boolean = false; - const cutoff_time = []; + let isAdvancedCustomizable: boolean = false; + const cutoff_time_m = restaurant.cutoff_time_m; for (const menuItem of menuItems) { //Check if having vegeterian food if (menuItem.is_vegetarian === 1) { having_vegeterian_food = true; } - //push cutoff time - cutoff_time.push(menuItem.cutoff_time); + //check if having advanced customization + if (isAdvancedCustomizable == false && !!menuItem.attribute_obj) { + if ( + menuItem.attribute_obj.filter((i) => i.type_id == 'taste').length > 0 + ) { + isAdvancedCustomizable = true; + } + } + + // //push cutoff time + // cutoff_time.push(menuItem.cutoff_time); //Convert to FoodDTO const foodDTO = await this.commonService.convertIntoFoodDTO(menuItem); convertedMenuItems.push(foodDTO); } - cutoff_time.sort(); + // cutoff_time.sort(); //Calculate distance and time delivery const timeAnhDistance = await this.ahamoveService.estimateTimeAndDistance( @@ -287,12 +301,13 @@ export class RestaurantService { specialty: restaurant_ext.specialty, introduction: restaurant_ext.introduction, review_total_count: restaurant.review_total_count, - cutoff_time: cutoff_time, + cutoff_time_m: cutoff_time_m, having_vegeterian_food: having_vegeterian_food, unit: restaurant.unit_obj.symbol, menu: convertedMenuItems, distance_km: timeAnhDistance.distance_km, delivery_time_s: timeAnhDistance.duration_s, + is_advanced_customizable: isAdvancedCustomizable, }; return data; diff --git a/src/type/index.ts b/src/type/index.ts index 93ac28c..a8e5944 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -77,8 +77,8 @@ export interface DayShift { from: string; to: string; is_available?: boolean; - cutoff_time?: string; - waiting_time_s?: number; // the time a customer has to wait until the food is ready for delivery from the begining of the shift + // cutoff_time?: string; + // waiting_time_s?: number; // the time a customer has to wait until the food is ready for delivery from the begining of the shift } export interface OptionSelection {