diff --git a/src/modules/cart/interfaces/cart.interface.ts b/src/modules/cart/interfaces/cart.interface.ts index b281661..67b611e 100644 --- a/src/modules/cart/interfaces/cart.interface.ts +++ b/src/modules/cart/interfaces/cart.interface.ts @@ -1,4 +1,4 @@ -import type { OrderCouponDetail, OrderUserAddress, OrderCarAddress } from 'src/modules/orders/interface/order.interface'; +import type { OrderCouponDetail, OrderUserAddress } from 'src/modules/orders/interface/order.interface'; export interface CartItem { foodId: string; @@ -29,7 +29,6 @@ export interface Cart { totalDiscount: number; total: number; - carAddress?: OrderCarAddress | null; userAddress?: OrderUserAddress | null; totalItems: number; diff --git a/src/modules/cart/providers/cart-validation.service.ts.backup b/src/modules/cart/providers/cart-validation.service.ts.backup deleted file mode 100644 index 34362cf..0000000 --- a/src/modules/cart/providers/cart-validation.service.ts.backup +++ /dev/null @@ -1,273 +0,0 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; -import { Product } from ../../../products/entities/product.entity'; -import { Shop } from ../../../shops/entities/shop.entity'; -import { UserAddress } from '../../../users/entities/user-address.entity'; -import { User } from '../../../users/entities/user.entity'; -import { PaymentMethod } from '../../../payments/entities/payment-method.entity'; -import { Delivery } from '../../../delivery/entities/delivery.entity'; -import { DeliveryMethodEnum } from '../../../delivery/interface/delivery'; -import { PointTransaction } from '../../../users/entities/point-transaction.entity'; -import { Order } from '../../../orders/entities/order.entity'; -import { OrderStatus } from '../../../orders/interface/order.interface'; -import { Cart } from '../interfaces/cart.interface'; -import { GeographicUtils } from '../utils/geographic.utils'; -import { CartMessage } from 'src/common/enums/message.enum'; -import { WalletTransactionRepository } from 'src/modules/users/repositories/wallet-transaction.repository'; - -@Injectable() -export class CartValidationService { - constructor( - private readonly em: EntityManager, - private readonly walletTransactionRepository: WalletTransactionRepository, - ) { } - - /** - * Validate product exists and belongs to shop - */ - async validateAndGetFood(foodId: string, restaurantId: string, quantity: number): Promise { - const product = await this.em.findOne(Product, { id: foodId }, { populate: ['shop'] }); - if (!product) { - throw new NotFoundException(CartMessage.FOOD_NOT_FOUND); - } - - if (product.shop.id !== restaurantId) { - throw new BadRequestException(CartMessage.FOOD_NOT_BELONGS_TO_RESTAURANT); - } - - return product; - } - - - /** - * Get user or throw if not found - */ - async getUserOrFail(userId: string): Promise { - const user = await this.em.findOne(User, { id: userId }); - if (!user) { - throw new NotFoundException(CartMessage.USER_NOT_FOUND); - } - return user; - } - - /** - * Get user address or throw if not found - */ - async getUserAddressOrFail(addressId: string): Promise { - const address = await this.em.findOne(UserAddress, { id: addressId }, { populate: ['user'] }); - if (!address) { - throw new NotFoundException(CartMessage.ADDRESS_NOT_FOUND); - } - return address; - } - - /** - * Get product or throw if not found - */ - async getFoodOrFail(foodId: string): Promise { - const product = await this.em.findOne(Product, { id: foodId }); - if (!product) { - throw new NotFoundException(CartMessage.FOOD_NOT_FOUND); - } - return product; - } - - /** - * Get shop or throw if not found - */ - async getRestaurantOrFail(restaurantId: string): Promise { - const shop = await this.em.findOne(Shop, { id: restaurantId }); - if (!shop) { - throw new NotFoundException(CartMessage.RESTAURANT_NOT_FOUND); - } - return shop; - } - - /** - * Validate address belongs to user - */ - validateAddressOwnership(address: UserAddress, userId: string): void { - if (address.user.id !== userId) { - throw new BadRequestException(CartMessage.ADDRESS_NOT_BELONGS_TO_USER); - } - } - - /** - * Assert address is inside shop service area - */ - async assertAddressInsideServiceArea( - restaurantId: string, - latitude?: number | null, - longitude?: number | null, - ): Promise { - if (latitude === undefined || latitude === null || longitude === undefined || longitude === null) { - throw new BadRequestException(CartMessage.ADDRESS_NO_COORDINATES); - } - - const shop = await this.getRestaurantOrFail(restaurantId); - - const serviceArea = shop.serviceArea; - // If no service area is defined, assume service is available everywhere - if (!serviceArea || !serviceArea.coordinates || !Array.isArray(serviceArea.coordinates)) return; - - // GeoJSON coordinates: [ [ [lng, lat], ... ] ] -> take first ring - const rings = serviceArea.coordinates; - if (!rings || rings.length === 0 || !Array.isArray(rings[0])) return; - - const ring = rings[0]; - const point: [number, number] = [Number(longitude), Number(latitude)]; - - // Ensure polygon has the correct tuple typing ([lng, lat] pairs) - const polygon: [number, number][] = ring.map( - coord => [Number(coord[0] ?? 0), Number(coord[1] ?? 0)] as [number, number], - ); - - if (!GeographicUtils.isPointInPolygon(point, polygon)) { - throw new BadRequestException(CartMessage.ADDRESS_OUTSIDE_SERVICE_AREA); - } - } - - /** - * Get delivery method for shop or throw if not found - */ - async getDeliveryMethodForRestaurantOrFail( - restaurantId: string, - deliveryMethodId: string, - ): Promise { - const deliveryMethod = await this.em.findOne(Delivery, { - id: deliveryMethodId, - shop: { id: restaurantId }, - }); - - if (!deliveryMethod) { - throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND_FOR_RESTAURANT); - } - - return deliveryMethod; - } - - /** - * Get enabled delivery method or throw if not found or disabled - */ - async getEnabledDeliveryMethodOrFail(restaurantId: string, deliveryMethodId: string): Promise { - const deliveryMethod = await this.em.findOne( - Delivery, - { id: deliveryMethodId, shop: { id: restaurantId } }, - { populate: ['shop'] }, - ); - if (!deliveryMethod) { - throw new NotFoundException(CartMessage.DELIVERY_METHOD_NOT_FOUND); - } - if (!deliveryMethod.enabled) { - throw new BadRequestException(CartMessage.DELIVERY_METHOD_NOT_ENABLED); - } - return deliveryMethod; - } - - /** - * Assert delivery method matches expected type - */ - assertDeliveryMethod(actual: DeliveryMethodEnum, expected: DeliveryMethodEnum, message: string): void { - if (actual !== expected) { - throw new BadRequestException(message); - } - } - - /** - * Require delivery method ID or throw - */ - requireDeliveryMethodId(cart: Cart, message: string): string { - if (!cart.deliveryMethodId) { - throw new BadRequestException(message); - } - return cart.deliveryMethodId; - } - - /** - * Get enabled payment method or throw if not found or disabled - */ - async getEnabledPaymentMethodOrFail(restaurantId: string, paymentMethodId: string): Promise { - const paymentMethod = await this.em.findOne( - PaymentMethod, - { id: paymentMethodId, shop: { id: restaurantId } }, - { populate: ['shop'] }, - ); - if (!paymentMethod) { - throw new NotFoundException(CartMessage.PAYMENT_METHOD_NOT_FOUND); - } - if (!paymentMethod.enabled) { - throw new BadRequestException(CartMessage.PAYMENT_METHOD_NOT_ENABLED); - } - return paymentMethod; - } - - /** - * Assert wallet has enough balance - */ - async assertWalletHasEnoughBalance(userId: string, restaurantId: string, amount: number): Promise { - const balance = await this.walletTransactionRepository.getCurrentWalletBalance(userId, restaurantId); - - if (balance < amount) { - throw new BadRequestException(CartMessage.WALLET_INSUFFICIENT); - } - } - - /** - * Assert coupon usage limit - */ - async assertCouponUsageLimit(userId: string, couponId: string, maxUsesPerUser?: number): Promise { - if (!maxUsesPerUser) return; - - const ordersThatUsedTheCouponCount = await this.em.count(Order, { - user: { id: userId }, - couponDetail: { couponId: couponId }, - status: { $ne: OrderStatus.CANCELED }, - deletedAt: null, - }); - - if (ordersThatUsedTheCouponCount >= maxUsesPerUser) { - throw new BadRequestException(CartMessage.COUPON_MAX_USES_REACHED); - } - } - - /** - * Assert coupon matches cart if restricted - */ - async assertCouponMatchesCartIfRestricted( - cart: Cart, - foodCategories?: string[] | null, - products?: string[] | null, - ): Promise { - const categoryRestriction = foodCategories?.filter(Boolean) ?? []; - const foodRestriction = products?.filter(Boolean) ?? []; - const hasCategoryRestriction = categoryRestriction.length > 0; - const hasFoodRestriction = foodRestriction.length > 0; - if (!hasCategoryRestriction && !hasFoodRestriction) return; - - const foodMap = await this.getFoodsInCartWithCategories(cart); - - for (const item of cart.items) { - const product = foodMap.get(item.foodId); - if (!product) continue; - - const matchesCategory = hasCategoryRestriction && product.category && categoryRestriction.includes(product.category.id); - const matchesFood = hasFoodRestriction && foodRestriction.includes(product.id); - if (matchesCategory || matchesFood) return; - } - - throw new BadRequestException(CartMessage.COUPON_CANNOT_BE_APPLIED); - } - - /** - * Get products in cart with categories - */ - async getFoodsInCartWithCategories(cart: Cart): Promise> { - const foodIds = cart.items.map(item => item.foodId); - if (foodIds.length === 0) return new Map(); - - const products = await this.em.find(Product, { id: { $in: foodIds } }, { populate: ['category'] }); - return new Map(products.map(f => [f.id, f])); - } - -} - diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index 6de164e..fb9a2a3 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -11,7 +11,6 @@ import { CartRepository } from '../repositories/cart.repository'; import { CartValidationService } from './cart-validation.service'; import { CartCalculationService } from './cart-calculation.service'; import { CartItemService } from './cart-item.service'; -import { SetCarDeliveryDto } from '../dto/set-car-delivery.dto'; import { CartMessage } from 'src/common/enums/message.enum'; @Injectable() @@ -42,34 +41,12 @@ export class CartService { // Handle each delivery method with its specific requirements switch (deliveryMethod.method) { - case DeliveryMethodEnum.DineIn: - // DineIn requires table number and clears incompatible fields - if (!tableNumber) { - throw new BadRequestException(CartMessage.TABLE_NUMBER_REQUIRED); - } - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn); - cart.tableNumber = tableNumber; - break; case DeliveryMethodEnum.CustomerPickup: // CustomerPickup just clears incompatible fields this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.CustomerPickup); break; - case DeliveryMethodEnum.DeliveryCar: - // DeliveryCar requires carAddress and clears incompatible fields - if (!carAddress) { - throw new BadRequestException(CartMessage.CAR_ADDRESS_REQUIRED); - } - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar); - const user = await this.validationService.getUserOrFail(userId); - cart.carAddress = { - carModel: carAddress.carModel, - carColor: carAddress.carColor, - plateNumber: carAddress.plateNumber, - phone: user.phone, - }; - break; case DeliveryMethodEnum.DeliveryCourier: // DeliveryCourier requires addressId and clears incompatible fields @@ -364,61 +341,6 @@ export class CartService { return this.saveTouchedCart(cart); } - /** - * Set table number for cart - */ - async setTableNumber(userId: string, restaurantId: string, tableNumber: string): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const deliveryMethodId = this.validationService.requireDeliveryMethodId( - cart, - 'Delivery method must be set before setting table number', - ); - const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail( - restaurantId, - deliveryMethodId, - ); - this.validationService.assertDeliveryMethod( - deliveryMethod.method, - DeliveryMethodEnum.DineIn, - 'Delivery method must be DineIn', - ); - - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DineIn); - cart.tableNumber = tableNumber; - return this.saveTouchedCart(cart); - } - - /** - * Set car delivery for cart - */ - async setCarDelivery(userId: string, restaurantId: string, setCarDeliveryDto: SetCarDeliveryDto): Promise { - const cart = await this.findOneOrFail(userId, restaurantId); - - const deliveryMethodId = this.validationService.requireDeliveryMethodId( - cart, - 'Delivery method must be set before setting car delivery', - ); - const deliveryMethod = await this.validationService.getDeliveryMethodForRestaurantOrFail( - restaurantId, - deliveryMethodId, - ); - this.validationService.assertDeliveryMethod( - deliveryMethod.method, - DeliveryMethodEnum.DeliveryCar, - 'Delivery method must be DeliveryCar', - ); - const user = await this.validationService.getUserOrFail(userId); - - this.clearFieldsIncompatibleWithDeliveryMethod(cart, DeliveryMethodEnum.DeliveryCar); - cart.carAddress = { - carModel: setCarDeliveryDto.carModel, - carColor: setCarDeliveryDto.carColor, - plateNumber: setCarDeliveryDto.plateNumber, - phone: user.phone, - }; - return this.saveTouchedCart(cart); - } /** * Clear cart from cache @@ -431,30 +353,20 @@ export class CartService { * Clears cart fields that are not applicable for a given delivery method. */ private clearFieldsIncompatibleWithDeliveryMethod(cart: Cart, method: DeliveryMethodEnum): void { - if (method === DeliveryMethodEnum.DineIn) { - cart.userAddress = null; - cart.carAddress = null; - return; - } + if (method === DeliveryMethodEnum.CustomerPickup) { cart.userAddress = null; - cart.carAddress = null; cart.tableNumber = undefined; return; } if (method === DeliveryMethodEnum.DeliveryCourier) { - cart.carAddress = null; cart.tableNumber = undefined; return; } - if (method === DeliveryMethodEnum.DeliveryCar) { - cart.userAddress = null; - cart.tableNumber = undefined; - return; - } + } /** diff --git a/src/modules/orders/crone/order.crone.ts b/src/modules/orders/crone/order.crone.ts index 4152a9f..50042e8 100644 --- a/src/modules/orders/crone/order.crone.ts +++ b/src/modules/orders/crone/order.crone.ts @@ -35,7 +35,7 @@ export class OrdersCrone { status: PaymentStatusEnum.Pending, createdAt: { $lte: cutoff }, }, - { populate: ['order', 'order.items', 'order.items.product'] }, + { populate: ['order', 'order.items', 'order.items.variant'] }, ); if (!payments || payments.length === 0) { @@ -51,7 +51,7 @@ export class OrdersCrone { const payment = await em.findOne( Payment, { id: p.id }, - { populate: ['order', 'order.items', 'order.items.product'] }, + { populate: ['order', 'order.items', 'order.items.variant'] }, ); if (!payment) return; if (payment.status !== PaymentStatusEnum.Pending) return; @@ -96,8 +96,6 @@ export class OrdersCrone { status: { $in: [ OrderStatus.SHIPPED, - OrderStatus.DELIVERED_TO_WAITER, - OrderStatus.DELIVERED_TO_RECEPTIONIST, ], }, updatedAt: { $lte: cutoff }, @@ -124,8 +122,6 @@ export class OrdersCrone { if ( ![ OrderStatus.SHIPPED, - OrderStatus.DELIVERED_TO_WAITER, - OrderStatus.DELIVERED_TO_RECEPTIONIST, ].includes(reloadedOrder.status) ) { return; diff --git a/src/modules/orders/entities/order-item.entity.ts b/src/modules/orders/entities/order-item.entity.ts index ec04bb4..0541125 100644 --- a/src/modules/orders/entities/order-item.entity.ts +++ b/src/modules/orders/entities/order-item.entity.ts @@ -1,17 +1,15 @@ import { Entity, Index, ManyToOne, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Order } from './order.entity'; -import { Product } from '../../products/entities/product.entity'; +import { Variant } from '../../products/entities/variant.entity'; @Entity({ tableName: 'order_items' }) -@Index({ properties: ['order'] }) -@Index({ properties: ['product'] }) export class OrderItem extends BaseEntity { @ManyToOne(() => Order) order!: Order; - @ManyToOne(() => Product) - product!: Product; + @ManyToOne(() => Variant) + variant!: Variant; @Property({ type: 'int' }) quantity!: number; diff --git a/src/modules/orders/entities/order.entity.ts b/src/modules/orders/entities/order.entity.ts index f7cebd1..7c7a8be 100644 --- a/src/modules/orders/entities/order.entity.ts +++ b/src/modules/orders/entities/order.entity.ts @@ -16,7 +16,7 @@ import { Shop } from '../../shops/entities/shop.entity'; import { PaymentMethod } from '../../payments/entities/payment-method.entity'; import { OrderItem } from './order-item.entity'; import { Delivery } from '../../delivery/entities/delivery.entity'; -import { OrderUserAddress, OrderCarAddress } from '../interface/order.interface'; +import { OrderUserAddress } from '../interface/order.interface'; import { Payment } from 'src/modules/payments/entities/payment.entity'; @Entity({ tableName: 'orders' }) diff --git a/src/modules/orders/listeners/order.listeners.ts b/src/modules/orders/listeners/order.listeners.ts index 79fadd9..63db9e2 100644 --- a/src/modules/orders/listeners/order.listeners.ts +++ b/src/modules/orders/listeners/order.listeners.ts @@ -35,8 +35,7 @@ export class OrderListeners { [OrderStatus.PENDING_PAYMENT]: 'در انتظار پرداخت', [OrderStatus.PAID]: 'پرداخت شده', [OrderStatus.PREPARING]: 'در حال آماده‌سازی', - [OrderStatus.DELIVERED_TO_RECEPTIONIST]: 'تحویل به پذیرش', - [OrderStatus.DELIVERED_TO_WAITER]: 'تحویل به گارسون', + [OrderStatus.DELIVERED]: 'تحویل شده', [OrderStatus.SHIPPED]: 'ارسال شده', [OrderStatus.COMPLETED]: 'تکمیل شده', [OrderStatus.CANCELED]: 'لغو شده', diff --git a/src/modules/orders/orders.module.ts b/src/modules/orders/orders.module.ts index 038768c..11e7062 100644 --- a/src/modules/orders/orders.module.ts +++ b/src/modules/orders/orders.module.ts @@ -7,6 +7,7 @@ import { OrderItem } from './entities/order-item.entity'; import { User } from '../users/entities/user.entity'; import { Shop } from '../shops/entities/shop.entity'; import { Product } from '../products/entities/product.entity'; +import { Variant } from '../products/entities/variant.entity'; import { UserAddress } from '../users/entities/user-address.entity'; import { PaymentMethod } from '../payments/entities/payment-method.entity'; import { CartModule } from '../cart/cart.module'; @@ -20,10 +21,12 @@ import { OrdersCrone } from './crone/order.crone'; import { AdminModule } from '../admin/admin.module'; import { NotificationsModule } from '../notifications/notifications.module'; import { UserModule } from '../users/user.module'; +import { ShopsModule } from '../shops/shops.module'; +import { DeliveryModule } from '../delivery/delivery.module'; @Module({ imports: [ - MikroOrmModule.forFeature([Order, OrderItem, User, Shop, Product, UserAddress, PaymentMethod]), + MikroOrmModule.forFeature([Order, OrderItem, User, Shop, Product, Variant, UserAddress, PaymentMethod]), CartModule, UtilsModule, AuthModule, @@ -31,7 +34,9 @@ import { UserModule } from '../users/user.module'; JwtModule, AdminModule, NotificationsModule, - forwardRef(() => UserModule) + forwardRef(() => UserModule), + ShopsModule, + DeliveryModule ], controllers: [OrdersController], providers: [OrdersService, OrderRepository, OrderListeners, OrdersCrone], diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts index 3d343ba..989cbb6 100644 --- a/src/modules/orders/providers/orders.service.ts +++ b/src/modules/orders/providers/orders.service.ts @@ -4,7 +4,7 @@ import { Order } from '../entities/order.entity'; import { OrderItem } from '../entities/order-item.entity'; import { User } from '../../users/entities/user.entity'; import { Shop } from '../../shops/entities/shop.entity'; -import { Product } from '../../products/entities/product.entity'; +import { Variant } from '../../products/entities/variant.entity'; import { CartService } from '../../cart/providers/cart.service'; import { OrderStatus, OrderUserAddress } from '../interface/order.interface'; import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment'; @@ -26,7 +26,7 @@ import { ShopService } from 'src/modules/shops/providers/shops.service'; import { DeliveryService } from 'src/modules/delivery/providers/delivery.service'; import { PaymentMethodService } from 'src/modules/payments/services/payment-method.service'; -type OrderItemData = { product: Product; quantity: number; unitPrice: number; discount: number }; +type OrderItemData = { variant: Variant; quantity: number; unitPrice: number; discount: number }; type ValidatedCartForOrder = { user: User; @@ -91,14 +91,14 @@ export class OrdersService { em.persist(order); for (const itemData of validated.orderItemsData) { - const { product, quantity, unitPrice, discount } = itemData; + const { variant, quantity, unitPrice, discount } = itemData; const totalPrice = (unitPrice - discount) * quantity; const orderItem = em.create(OrderItem, { order, - product, + variant, quantity, unitPrice, discount, @@ -211,7 +211,7 @@ export class OrdersService { 'paymentMethod', 'payments', 'items', - 'items.product', + 'items.variant', ], }, ); @@ -366,18 +366,18 @@ export class OrdersService { const orderItemsData: OrderItemData[] = []; for (const cartItem of cart.items) { - const product = await this.em.findOne(Product, { id: cartItem.foodId }, { populate: ['shop'] }); - if (!product) throw new NotFoundException(OrderMessage.PRODUCT_NOT_FOUND); + const variant = await this.em.findOne(Variant, { id: cartItem.foodId }, { populate: ['product', 'product.shop'] }); + if (!variant) throw new NotFoundException(OrderMessage.PRODUCT_NOT_FOUND); - if (product.shop.id !== shopId) { + if (variant.product.shop.id !== shopId) { throw new BadRequestException(OrderMessage.PRODUCT_NOT_BELONGS_TO_SHOP); } orderItemsData.push({ - product, + variant, quantity: cartItem.quantity, - unitPrice: product.price || 0, - discount: product.discount || 0, + unitPrice: variant.price || 0, + discount: variant.product.discount || 0, }); } @@ -401,7 +401,7 @@ export class OrdersService { }); // 2. Active products count - const activeFoods = await em.count(Product, { + const activeFoods = await em.count('Product', { shop: { id: shopId }, isActive: true, }); @@ -490,7 +490,8 @@ export class OrdersService { COALESCE(SUM(oi.total_price), 0)::numeric as total_revenue FROM order_items oi INNER JOIN orders o ON oi.order_id = o.id - INNER JOIN products f ON oi.food_id = f.id + INNER JOIN variants v ON oi.variant_id = v.id + INNER JOIN products f ON v.product_id = f.id WHERE o.restaurant_id = ? AND o.created_at >= ? AND o.created_at <= ? diff --git a/src/modules/orders/repositories/order.repository.ts b/src/modules/orders/repositories/order.repository.ts index 116a3c4..d1e5aab 100644 --- a/src/modules/orders/repositories/order.repository.ts +++ b/src/modules/orders/repositories/order.repository.ts @@ -118,30 +118,30 @@ export class OrderRepository extends EntityRepository { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['user', 'shop', 'deliveryMethod', 'paymentMethod', 'items', 'items.product'] as never, + populate: ['user', 'shop', 'deliveryMethod', 'paymentMethod', 'items', 'items.variant'] as never, }); - // Collect all (orderId, foodId) pairs for efficient review lookup - const orderFoodPairs: Array<{ orderId: string; foodId: string }> = []; + // Collect all (orderId, variantId) pairs for efficient review lookup + const orderVariantPairs: Array<{ orderId: string; variantId: string }> = []; for (const order of data) { for (const item of order.items.getItems()) { - if (item.product?.id) { - orderFoodPairs.push({ orderId: order.id, foodId: item.product.id }); + if (item.variant?.id) { + orderVariantPairs.push({ orderId: order.id, variantId: item.variant.id }); } } } // Fetch all relevant reviews in a single query const reviewsMap: Map = new Map(); - if (orderFoodPairs.length > 0) { - const orderIds = [...new Set(orderFoodPairs.map(p => p.orderId))]; - const foodIds = [...new Set(orderFoodPairs.map(p => p.foodId))]; + if (orderVariantPairs.length > 0) { + const orderIds = [...new Set(orderVariantPairs.map(p => p.orderId))]; + const variantIds = [...new Set(orderVariantPairs.map(p => p.variantId))]; const reviews = await this.em.find( Review, { order: { id: { $in: orderIds } }, - product: { id: { $in: foodIds } }, + product: { id: { $in: variantIds } }, }, { fields: ['id', 'order', 'product'], @@ -149,22 +149,23 @@ export class OrderRepository extends EntityRepository { }, ); - // Create a map: key = `${orderId}-${foodId}`, value = reviewId + // Create a map: key = `${orderId}-${variantId}`, value = reviewId + // Note: Reviews still reference products, so we need to map through variant.product for (const review of reviews) { const key = `${review.order.id}-${review.product.id}`; reviewsMap.set(key, review.id); } } - // Map reviewIds to products + // Map reviewIds to variants (through variant.product for now, as reviews still reference products) for (const order of data) { for (const item of order.items.getItems()) { - if (item.product) { - const key = `${order.id}-${item.product.id}`; + if (item.variant?.product) { + const key = `${order.id}-${item.variant.product.id}`; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const product = item.product as any; + const variant = item.variant as any; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - product.reviewId = reviewsMap.get(key) || null; + variant.reviewId = reviewsMap.get(key) || null; } } } diff --git a/src/modules/products/repositories/product.repository.ts b/src/modules/products/repositories/product.repository.ts index 2ec5434..b5fa487 100644 --- a/src/modules/products/repositories/product.repository.ts +++ b/src/modules/products/repositories/product.repository.ts @@ -48,7 +48,7 @@ export class ProductRepository extends EntityRepository { limit, offset, orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, - populate: ['category'], + populate: ['category','variants'], }); const totalPages = Math.ceil(total / limit); diff --git a/src/modules/review/providers/review.service.ts b/src/modules/review/providers/review.service.ts index 06b26d5..8408d4b 100644 --- a/src/modules/review/providers/review.service.ts +++ b/src/modules/review/providers/review.service.ts @@ -46,10 +46,12 @@ export class ReviewService { throw new NotFoundException('Order not found or does not belong to the current user'); } - // Verify the product is in the order + // Verify the product is in the order (check through variant.product) const orderItem = await this.em.findOne(OrderItem, { order: { id: orderId }, - product: { id: foodId }, + variant: { product: { id: foodId } }, + }, { + populate: ['variant', 'variant.product'], }); if (!orderItem) { throw new BadRequestException('Product is not in the specified order');