order refactored
This commit is contained in:
@@ -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]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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],
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user