From 53c108734f622d82722b0d6195c579f6a68cf8b0 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 17 Dec 2025 10:31:33 +0330 Subject: [PATCH] order refactor --- .../orders/controllers/orders.controller.ts | 53 +- .../orders/providers/orders.service.ts | 490 +++++++----------- 2 files changed, 210 insertions(+), 333 deletions(-) diff --git a/src/modules/orders/controllers/orders.controller.ts b/src/modules/orders/controllers/orders.controller.ts index 5bcf458..ca805f7 100644 --- a/src/modules/orders/controllers/orders.controller.ts +++ b/src/modules/orders/controllers/orders.controller.ts @@ -9,53 +9,35 @@ import { FindOrdersDto } from '../dto/find-orders.dto'; import { OrderStatus } from '../interface/order-status'; @ApiTags('orders') +@ApiBearerAuth() +@ApiHeader({ + name: 'X-Slug', + required: true, + schema: { + type: 'string', + default: 'zhivan', + }, +}) @Controller() export class OrdersController { - constructor(private readonly ordersService: OrdersService) {} + constructor(private readonly ordersService: OrdersService) { } @UseGuards(AuthGuard) - @ApiBearerAuth() @Post('public/checkout') @ApiOperation({ summary: 'Checkout : create order and payment record' }) - @ApiHeader({ - name: 'X-Slug', - required: true, - schema: { - type: 'string', - default: 'zhivan', - }, - }) checkout(@UserId() userId: string, @RestId() restaurantId: string) { return this.ordersService.checkout(userId, restaurantId); } @UseGuards(AuthGuard) - @ApiBearerAuth() @Get('public/orders') @ApiOperation({ summary: 'Get all orders with pagination and filters' }) - @ApiHeader({ - name: 'X-Slug', - required: true, - schema: { - type: 'string', - default: 'zhivan', - }, - }) findAll(@RestId() restId: string, @Query() dto: FindOrdersDto, @UserId() userId: string) { return this.ordersService.findAllForUser(restId, dto, userId); } @UseGuards(AuthGuard) - @ApiBearerAuth() @ApiOperation({ summary: 'Get an order By id for User' }) - @ApiHeader({ - name: 'X-Slug', - required: true, - schema: { - type: 'string', - default: 'zhivan', - }, - }) @ApiParam({ name: 'orderId', description: 'Order ID' }) @Get('public/orders/:orderId') findOne(@Param('orderId') orderId: string, @RestId() restId: string) { @@ -63,24 +45,14 @@ export class OrdersController { } @UseGuards(AuthGuard) - @ApiBearerAuth() @Patch('public/orders/:id/cancel') @ApiOperation({ summary: 'Cancel an order By User' }) - @ApiHeader({ - name: 'X-Slug', - required: true, - schema: { - type: 'string', - default: 'zhivan', - }, - }) @ApiParam({ name: 'id', description: 'Order ID' }) cancelOrder(@Param('id') id: string, @RestId() restId: string) { return this.ordersService.cancelOrderAsUser(id, restId); } @UseGuards(AdminAuthGuard) - @ApiBearerAuth() @Get('admin/orders') @ApiOperation({ summary: 'Get all orders with pagination and filters' }) findAllAdmin(@RestId() restId: string, @Query() dto: FindOrdersDto) { @@ -88,7 +60,6 @@ export class OrdersController { } @UseGuards(AdminAuthGuard) - @ApiBearerAuth() @ApiOperation({ summary: 'Get an order By id for User' }) @ApiParam({ name: 'orderId', description: 'Order ID' }) @Get('admin/orders/:orderId') @@ -97,7 +68,6 @@ export class OrdersController { } @UseGuards(AdminAuthGuard) - @ApiBearerAuth() @Patch('admin/orders/:orderId/confirm') @ApiOperation({ summary: 'Accept an order' }) @ApiParam({ name: 'orderId', description: 'Order ID' }) @@ -106,7 +76,6 @@ export class OrdersController { } @UseGuards(AdminAuthGuard) - @ApiBearerAuth() @Patch('admin/orders/:id/prepare') @ApiOperation({ summary: 'Prepare an order By Admin' }) @ApiParam({ name: 'id', description: 'Order ID' }) @@ -115,7 +84,6 @@ export class OrdersController { } @UseGuards(AdminAuthGuard) - @ApiBearerAuth() @Patch('admin/orders/:orderId/reject') @ApiOperation({ summary: 'Reject an order' }) @ApiParam({ name: 'orderId', description: 'Order ID' }) @@ -142,7 +110,6 @@ export class OrdersController { // } @UseGuards(AdminAuthGuard) - @ApiBearerAuth() @Patch('admin/orders/:orderId/:status') @ApiOperation({ summary: 'Update an order status' }) @ApiParam({ name: 'orderId', description: 'Order ID' }) diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts index d350796..b366c63 100644 --- a/src/modules/orders/providers/orders.service.ts +++ b/src/modules/orders/providers/orders.service.ts @@ -6,31 +6,56 @@ import { User } from '../../users/entities/user.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Food } from '../../foods/entities/food.entity'; import { CartService } from '../../cart/providers/cart.service'; -import { OrderStatus } from '../interface/order-status'; +import { OrderStatus, OrderUserAddress, OrderCarAddress } from '../interface/order-status'; import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment'; import { Cart } from '../../cart/interfaces/cart.interface'; import { PaymentMethod } from '../../payments/entities/payment-method.entity'; import { PaymentsService } from '../../payments/services/payments.service'; import { DeliveryMethodEnum } from '../../delivery/interface/delivery'; import { Delivery } from '../../delivery/entities/delivery.entity'; -import { Cron, CronExpression } from '@nestjs/schedule'; import { OrderRepository } from '../repositories/order.repository'; import { FindOrdersDto } from '../dto/find-orders.dto'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { OrderUserAddress, OrderCarAddress } from '../interface/order-status'; + +type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number }; + +type ValidatedCartForOrder = { + user: User; + restaurant: Restaurant; + delivery: Delivery; + userAddress: OrderUserAddress | null; + carAddress: OrderCarAddress | null; + paymentMethod: PaymentMethod; + orderItemsData: OrderItemData[]; +}; @Injectable() export class OrdersService { private readonly logger = new Logger(OrdersService.name); + private static readonly STATUS_TRANSITIONS: Record = { + [OrderStatus.NEW]: [OrderStatus.PENDING_PAYMENT, OrderStatus.CONFIRMED, OrderStatus.CANCELED], + [OrderStatus.PENDING_PAYMENT]: [OrderStatus.PAID, OrderStatus.FAILED, OrderStatus.CANCELED], + [OrderStatus.PAID]: [OrderStatus.CONFIRMED, OrderStatus.REFUNDED], + [OrderStatus.CONFIRMED]: [OrderStatus.PREPARING, OrderStatus.CANCELED], + [OrderStatus.PREPARING]: [OrderStatus.READY, OrderStatus.SHIPPED, OrderStatus.CANCELED], + [OrderStatus.READY]: [OrderStatus.COMPLETED, OrderStatus.FAILED], + [OrderStatus.SHIPPED]: [OrderStatus.COMPLETED, OrderStatus.FAILED], + [OrderStatus.COMPLETED]: [], + [OrderStatus.CANCELED]: [], + [OrderStatus.FAILED]: [], + [OrderStatus.REFUNDED]: [], + }; + constructor( private readonly em: EntityManager, private readonly cartService: CartService, - private readonly paymentsService: PaymentsService, private readonly orderRepository: OrderRepository, - private readonly eventEmitter2: EventEmitter2, - // private readonly paymentGatewayService: PaymentGatewayService, + // NOTE: kept for future payment integration + private readonly _paymentsService: PaymentsService, + // NOTE: kept for future event emission + private readonly _eventEmitter2: EventEmitter2, ) { } async checkout(userId: string, restaurantId: string) { @@ -38,31 +63,16 @@ export class OrdersService { const { order } = await this.createOrder(userId, restaurantId, cart); - // if (!order.paymentMethod) { - // throw new BadRequestException('Payment method is required for checkout'); - // } - - // const { paymentUrl } = await this.paymentsService.startPayment(order.id); - await this.cartService.clearCart(userId, restaurantId); - // this.eventEmitter2.emit( - // OrderCreatedEvent.name, - // new OrderCreatedEvent(restaurantId, order.user.id, order.id, order.total), - // ); - // this.eventEmitter2.emit( - // OrderPaymentSuccessEvent.name, - // new OrderPaymentSuccessEvent(restaurantId, order.user.id, order.id, order.total), - // ); - console.log('Order created event emitted 111'); + this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`); return { order }; } async createOrder(userId: string, restaurantId: string, cart: Cart) { const validationResult = await this.validateCartForOrder(userId, restaurantId, cart); - // Create order within a transaction + return this.em.transactional(async em => { - // Create order entity const order = em.create(Order, { user: validationResult.user, restaurant: validationResult.restaurant, @@ -86,17 +96,10 @@ export class OrdersService { em.persist(order); - // Create order items and update stock for (const itemData of validationResult.orderItemsData) { const { food, quantity, unitPrice, discount } = itemData; - // Stock check - if (!food.inventory) { - throw new BadRequestException(`Food ${food.title} does not have inventory`); - } - if (food.inventory.availableStock < quantity) { - throw new BadRequestException(`${food.title} is out of stock`); - } + this.assertFoodHasSufficientStock(food, quantity); const totalPrice = (unitPrice - discount) * quantity; @@ -111,12 +114,9 @@ export class OrdersService { em.persist(orderItem); - //reserve food stock - em.persist(food); } - // Flush all changes await em.flush(); return { order }; }); @@ -125,126 +125,24 @@ export class OrdersService { /** * Validates cart and prepares all required data for order creation */ - private async validateCartForOrder( - userId: string, - restaurantId: string, - cart: Cart, - ): Promise<{ - user: User; - restaurant: Restaurant; - delivery: Delivery; - userAddress: OrderUserAddress | null; - carAddress: OrderCarAddress | null; - paymentMethod: PaymentMethod; - orderItemsData: Array<{ food: Food; quantity: number; unitPrice: number; discount: number }>; - }> { - // Validate cart has items - if (!cart.items || cart.items.length === 0) { - throw new BadRequestException('Cart is empty. Add items to cart before creating an order.'); - } + private async validateCartForOrder(userId: string, restaurantId: string, cart: Cart): Promise { + this.assertCartHasItems(cart); + this.assertCartHasDeliveryMethod(cart); + this.assertCartHasPaymentMethod(cart); - // Validate address is set - if (!cart.deliveryMethodId) { - throw new NotFoundException('Delivery method not found'); - } + const [user, restaurant, delivery] = await Promise.all([ + this.getUserOrFail(userId), + this.getRestaurantOrFail(restaurantId), + this.getDeliveryOrFail(cart.deliveryMethodId!), + ]); - // Validate payment method is set - if (!cart.paymentMethodId) { - throw new BadRequestException( - 'Payment method is required. Please set a payment method before creating an order.', - ); - } + this.assertMeetsMinOrderForDelivery(cart, delivery); + this.assertDeliveryMethodRequirements(cart, delivery); - // Validate and load entities - const user = await this.em.findOne(User, { id: userId }); - if (!user) { - throw new NotFoundException('User not found'); - } + const paymentMethod = await this.getPaymentMethodOrFail(cart.paymentMethodId!, restaurantId); + this.assertPaymentMethodEnabled(paymentMethod); - const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); - if (!restaurant) { - throw new NotFoundException('Restaurant not found'); - } - - const delivery = await this.em.findOne(Delivery, { id: cart.deliveryMethodId }); - if (!delivery) { - throw new NotFoundException('Delivery not found'); - } - - // Validate minimum order price for delivery method - const minOrderPrice = Number(delivery.minOrderPrice) || 0; - if (minOrderPrice > 0 && cart.total < minOrderPrice) { - throw new BadRequestException( - `Minimum order amount for this delivery method is ${minOrderPrice}. Current total is ${cart.total}.`, - ); - } - - // Validate table number is required for DineIn - if (delivery.method === DeliveryMethodEnum.DineIn) { - if (!cart.tableNumber || cart.tableNumber.trim() === '') { - throw new BadRequestException('Table number is required when delivery method is DineIn'); - } - } - - if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.userAddress) { - throw new BadRequestException('Address is required. Please set a delivery address before creating an order.'); - } - - if (delivery.method === DeliveryMethodEnum.DeliveryCar && !cart.carAddress) { - throw new BadRequestException('Car address is required. Please set a car address before creating an order.'); - } - - const paymentMethod = await this.em.findOne( - PaymentMethod, - { - id: cart.paymentMethodId, - restaurant: { id: restaurantId }, - }, - { populate: ['restaurant'] }, - ); - - if (!paymentMethod) { - throw new NotFoundException( - `Payment method with ID ${cart.paymentMethodId} not found for restaurant ${restaurantId}`, - ); - } - - if (!paymentMethod.enabled) { - throw new BadRequestException('Payment method is not enabled for this restaurant'); - } - - // Validate stock and prepare order items data - const orderItemsData: Array<{ food: Food; quantity: number; unitPrice: number; discount: number }> = []; - - for (const cartItem of cart.items) { - const food = await this.em.findOne(Food, { id: cartItem.foodId }, { populate: ['restaurant', 'inventory'] }); - if (!food) { - throw new NotFoundException(`Food with ID ${cartItem.foodId} not found`); - } - - // Verify food belongs to the restaurant - if (food.restaurant.id !== restaurantId) { - throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`); - } - - // Final stock validation - const availableStock = food.inventory?.availableStock ?? 0; - if (availableStock < cartItem.quantity) { - throw new BadRequestException( - `Insufficient stock for food ${food.title || food.id}. Available: ${availableStock}, Requested: ${cartItem.quantity}`, - ); - } - - const unitPrice = food.price || 0; - const discount = food.discount || 0; - - orderItemsData.push({ - food, - quantity: cartItem.quantity, - unitPrice, - discount, - }); - } + const orderItemsData = await this.buildOrderItemsData(cart, restaurantId); return { user, @@ -313,116 +211,64 @@ export class OrdersService { } async confirmOrder(orderId: string, restId: string) { - const order = await this.em.findOne(Order, { id: orderId, restaurant: { id: restId } }); - if (!order) { - throw new NotFoundException('Order not found'); - } - - if (!this.canTransition(order.status, OrderStatus.CONFIRMED, order.paymentMethod.method)) { - throw new BadRequestException('Invalid status transition'); - } - - order.status = OrderStatus.CONFIRMED; - await this.em.persistAndFlush(order); - return order; + return this.changeOrderStatus(orderId, restId, OrderStatus.CONFIRMED); } async prepareOrder(orderId: string, restId: string) { - const order = await this.em.findOne(Order, { id: orderId, restaurant: { id: restId } }); - if (!order) { - throw new NotFoundException('Order not found'); - } - if (!this.canTransition(order.status, OrderStatus.PREPARING, order.paymentMethod.method)) { - throw new BadRequestException('Invalid status transition'); - } - order.status = OrderStatus.PREPARING; - await this.em.persistAndFlush(order); - return order; + return this.changeOrderStatus(orderId, restId, OrderStatus.PREPARING); } + // just admin can reject the order any time async rejectOrder(orderId: string, restId: string) { - const order = await this.em.findOne(Order, { id: orderId, restaurant: { id: restId } }); - if (!order) { - throw new NotFoundException('Order not found'); - } - order.status = OrderStatus.CANCELED; - await this.em.persistAndFlush(order); - return order; + return this.changeOrderStatus(orderId, restId, OrderStatus.CANCELED, { skipTransitionValidation: true }); } async readyForDelivery(orderId: string, restId: string) { - const order = await this.em.findOne(Order, { id: orderId, restaurant: { id: restId } }); - if (!order) { - throw new NotFoundException('Order not found'); - } - - if (!this.canTransition(order.status, OrderStatus.READY, order.paymentMethod.method)) { - throw new BadRequestException('Invalid status transition'); - } - order.status = OrderStatus.READY; - - await this.em.persistAndFlush(order); - return order; + return this.changeOrderStatus(orderId, restId, OrderStatus.READY); } async cancelOrderAsUser(orderId: string, restId: string) { - const order = await this.em.findOne(Order, { id: orderId, restaurant: { id: restId } }); - if (!order) { - throw new NotFoundException('Order not found'); - } - if (!this.canTransition(order.status, OrderStatus.CANCELED, order.paymentMethod.method)) { - throw new BadRequestException('Invalid status transition'); - } - order.status = OrderStatus.CANCELED; - await this.em.persistAndFlush(order); - return order; + return this.changeOrderStatus(orderId, restId, OrderStatus.CANCELED); } async markAsDelivered(orderId: string, restId: string) { - const order = await this.em.findOne(Order, { id: orderId, restaurant: { id: restId } }); - if (!order) { - throw new NotFoundException('Order not found'); - } - - if (!this.canTransition(order.status, OrderStatus.COMPLETED, order.paymentMethod.method)) { - throw new BadRequestException('Invalid status transition'); - } - order.status = OrderStatus.COMPLETED; - await this.em.persistAndFlush(order); - return order; + return this.changeOrderStatus(orderId, restId, OrderStatus.COMPLETED); } async updateStatus(orderId: string, status: OrderStatus, restId: string) { - const order = await this.em.findOne(Order, { id: orderId, restaurant: { id: restId } }); - if (!order) { - throw new NotFoundException('Order not found'); + return this.changeOrderStatus(orderId, restId, status); + } + + private async changeOrderStatus( + orderId: string, + restId: string, + toStatus: OrderStatus, + options?: { skipTransitionValidation?: boolean }, + ): Promise { + const order = await this.getOrderOrFail(orderId, restId); + + if (!options?.skipTransitionValidation) { + this.assertStatusTransitionAllowed(order, toStatus); } - if (!this.canTransition(order.status, status, order.paymentMethod.method)) { - throw new BadRequestException('Invalid status transition'); - } - - order.status = status; + order.status = toStatus; await this.em.persistAndFlush(order); return order; } - transitions: Record = { - [OrderStatus.NEW]: [OrderStatus.PENDING_PAYMENT, OrderStatus.CONFIRMED, OrderStatus.CANCELED], - [OrderStatus.PENDING_PAYMENT]: [OrderStatus.PAID, OrderStatus.FAILED, OrderStatus.CANCELED], - [OrderStatus.PAID]: [OrderStatus.CONFIRMED, OrderStatus.REFUNDED], - [OrderStatus.CONFIRMED]: [OrderStatus.PREPARING, OrderStatus.CANCELED], - [OrderStatus.PREPARING]: [OrderStatus.READY, OrderStatus.SHIPPED, OrderStatus.CANCELED], - [OrderStatus.READY]: [OrderStatus.COMPLETED, OrderStatus.FAILED], - [OrderStatus.SHIPPED]: [OrderStatus.COMPLETED, OrderStatus.FAILED], - [OrderStatus.COMPLETED]: [], - [OrderStatus.CANCELED]: [], - [OrderStatus.FAILED]: [], - [OrderStatus.REFUNDED]: [], - }; + private assertStatusTransitionAllowed(order: Order, to: OrderStatus) { + const paymentMethod = order.paymentMethod?.method; + if (!paymentMethod) { + throw new BadRequestException('Order payment method is missing'); + } - canTransition(from: OrderStatus, to: OrderStatus, paymentMethod: PaymentMethodEnum) { - if (!this.transitions[from]?.includes(to)) return false; + if (!this.canTransition(order.status, to, paymentMethod)) { + throw new BadRequestException(`Invalid status transition: ${order.status} -> ${to}`); + } + } + + private canTransition(from: OrderStatus, to: OrderStatus, paymentMethod: PaymentMethodEnum) { + if (!OrdersService.STATUS_TRANSITIONS[from]?.includes(to)) return false; if (paymentMethod === PaymentMethodEnum.Cash) { if ([OrderStatus.PENDING_PAYMENT, OrderStatus.PAID].includes(to)) return false; @@ -435,68 +281,132 @@ export class OrdersService { return true; } - /** - * Cleanup job to handle abandoned orders (pending payment for >30 minutes) - * Runs every 10 minutes to check for abandoned orders - */ - // @Cron(CronExpression.EVERY_10_MINUTES) - // async cleanupAbandonedOrders() { - // this.logger.log('Starting cleanup of abandoned orders...'); + private async getOrderOrFail(orderId: string, restId: string): Promise { + const order = await this.em.findOne( + Order, + { id: orderId, restaurant: { id: restId } }, + { populate: ['paymentMethod'] }, + ); + if (!order) throw new NotFoundException('Order not found'); + return order; + } - // try { - // const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000); + private assertCartHasItems(cart: Cart) { + if (!cart.items || cart.items.length === 0) { + throw new BadRequestException('Cart is empty. Add items to cart before creating an order.'); + } + } - // // Find abandoned orders: Pending status, Pending payment, created >30 minutes ago - // const abandonedOrders = await this.em.find( - // Order, - // { - // status: OrderStatus.PENDING_PAYMENT, - // paymentStatus: PaymentStatusEnum.Pending, - // createdAt: { $lt: thirtyMinutesAgo }, - // }, - // { populate: ['items', 'items.food'] }, - // ); + private assertCartHasDeliveryMethod(cart: Cart) { + if (!cart.deliveryMethodId) { + throw new NotFoundException('Delivery method not found'); + } + } - // if (abandonedOrders.length === 0) { - // this.logger.log('No abandoned orders found'); - // return; - // } + private assertCartHasPaymentMethod(cart: Cart) { + if (!cart.paymentMethodId) { + throw new BadRequestException( + 'Payment method is required. Please set a payment method before creating an order.', + ); + } + } - // this.logger.log(`Found ${abandonedOrders.length} abandoned order(s) to cleanup`); + private async getUserOrFail(userId: string): Promise { + const user = await this.em.findOne(User, { id: userId }); + if (!user) throw new NotFoundException('User not found'); + return user; + } - // // Process each abandoned order in a transaction - // for (const order of abandonedOrders) { - // await this.em.transactional(async em => { - // // Load order items with food relations - // await order.items.loadItems(); + private async getRestaurantOrFail(restaurantId: string): Promise { + const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); + if (!restaurant) throw new NotFoundException('Restaurant not found'); + return restaurant; + } - // // Restore stock for each item - // for (const item of order.items) { - // const food = item.food; - // if (food) { - // food.inventory?.availableStock += item.quantity; - // em.persist(food); - // this.logger.debug( - // `Restored ${item.quantity} units of stock for food ${food.id} (${food.title || 'N/A'})`, - // ); - // } - // } + private async getDeliveryOrFail(deliveryId: string): Promise { + const delivery = await this.em.findOne(Delivery, { id: deliveryId }); + if (!delivery) throw new NotFoundException('Delivery not found'); + return delivery; + } - // // Update order status to Cancelled - // order.status = OrderStatus.CANCELED; - // em.persist(order); + private assertMeetsMinOrderForDelivery(cart: Cart, delivery: Delivery) { + const minOrderPrice = Number(delivery.minOrderPrice) || 0; + if (minOrderPrice > 0 && cart.total < minOrderPrice) { + throw new BadRequestException( + `Minimum order amount for this delivery method is ${minOrderPrice}. Current total is ${cart.total}.`, + ); + } + } - // await em.flush(); - // this.logger.log(`Cancelled abandoned order ${order.id}`); - // }); - // } + private assertDeliveryMethodRequirements(cart: Cart, delivery: Delivery) { + if (delivery.method === DeliveryMethodEnum.DineIn) { + if (!cart.tableNumber || cart.tableNumber.trim() === '') { + throw new BadRequestException('Table number is required when delivery method is DineIn'); + } + } - // this.logger.log(`Successfully cleaned up ${abandonedOrders.length} abandoned order(s)`); - // } catch (error) { - // const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - // const errorStack = error instanceof Error ? error.stack : undefined; - // this.logger.error(`Error during cleanup of abandoned orders: ${errorMessage}`, errorStack); - // throw error; - // } - // } + if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.userAddress) { + throw new BadRequestException('Address is required. Please set a delivery address before creating an order.'); + } + + if (delivery.method === DeliveryMethodEnum.DeliveryCar && !cart.carAddress) { + throw new BadRequestException('Car address is required. Please set a car address before creating an order.'); + } + } + + private async getPaymentMethodOrFail(paymentMethodId: string, restaurantId: string): Promise { + const paymentMethod = await this.em.findOne( + PaymentMethod, + { + id: paymentMethodId, + restaurant: { id: restaurantId }, + }, + { populate: ['restaurant'] }, + ); + + if (!paymentMethod) { + throw new NotFoundException(`Payment method with ID ${paymentMethodId} not found for restaurant ${restaurantId}`); + } + + return paymentMethod; + } + + private assertPaymentMethodEnabled(paymentMethod: PaymentMethod) { + if (!paymentMethod.enabled) { + throw new BadRequestException('Payment method is not enabled for this restaurant'); + } + } + + private async buildOrderItemsData(cart: Cart, restaurantId: string): Promise { + const orderItemsData: OrderItemData[] = []; + + for (const cartItem of cart.items) { + const food = await this.em.findOne(Food, { id: cartItem.foodId }, { populate: ['restaurant', 'inventory'] }); + if (!food) throw new NotFoundException(`Food with ID ${cartItem.foodId} not found`); + + if (food.restaurant.id !== restaurantId) { + throw new BadRequestException(`Food ${food.title || food.id} does not belong to restaurant ${restaurantId}`); + } + + this.assertFoodHasSufficientStock(food, cartItem.quantity); + + orderItemsData.push({ + food, + quantity: cartItem.quantity, + unitPrice: food.price || 0, + discount: food.discount || 0, + }); + } + + return orderItemsData; + } + + private assertFoodHasSufficientStock(food: Food, quantity: number) { + const availableStock = food.inventory?.availableStock ?? 0; + if (availableStock < quantity) { + throw new BadRequestException( + `Insufficient stock for food ${food.title || food.id}. Available: ${availableStock}, Requested: ${quantity}`, + ); + } + } }