order refactored

This commit is contained in:
2026-02-09 14:47:03 +03:30
parent 626d8194d8
commit ff7d2c912a
12 changed files with 52 additions and 412 deletions
@@ -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;
@@ -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<Product> {
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<User> {
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<UserAddress> {
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<Product> {
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<Shop> {
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<void> {
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<Delivery> {
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<Delivery> {
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<PaymentMethod> {
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<void> {
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<void> {
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<void> {
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<Map<string, Product>> {
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]));
}
}
+2 -90
View File
@@ -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<Cart> {
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<Cart> {
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;
}
}
/**
+2 -6
View File
@@ -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;
@@ -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;
+1 -1
View File
@@ -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' })
@@ -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]: 'لغو شده',
+7 -2
View File
@@ -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],
+14 -13
View File
@@ -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 <= ?
@@ -118,30 +118,30 @@ export class OrderRepository extends EntityRepository<Order> {
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<string, string> = 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<Order> {
},
);
// 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;
}
}
}
@@ -48,7 +48,7 @@ export class ProductRepository extends EntityRepository<Product> {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['category'],
populate: ['category','variants'],
});
const totalPages = Math.ceil(total / limit);
@@ -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');