diff --git a/src/modules/orders/dto/create-order.dto.ts b/src/modules/orders/dto/create-order.dto.ts deleted file mode 100644 index 0eafaef..0000000 --- a/src/modules/orders/dto/create-order.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreateOrderDto {} diff --git a/src/modules/orders/orders.controller.ts b/src/modules/orders/orders.controller.ts index b46b64e..21ab626 100644 --- a/src/modules/orders/orders.controller.ts +++ b/src/modules/orders/orders.controller.ts @@ -1,15 +1,25 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { OrdersService } from './orders.service'; -import { CreateOrderDto } from './dto/create-order.dto'; import { UpdateOrderDto } from './dto/update-order.dto'; +import { AuthGuard } from '../auth/guards/auth.guard'; +import { UserId } from '../../common/decorators/user-id.decorator'; +import { RestId } from 'src/common/decorators/rest-id.decorator'; -@Controller('orders') +@ApiTags('orders') +@Controller() export class OrdersController { constructor(private readonly ordersService: OrdersService) {} - @Post() - create(@Body() createOrderDto: CreateOrderDto) { - return this.ordersService.create(createOrderDto); + @UseGuards(AuthGuard) + @ApiBearerAuth() + @Post('public/orders') + @ApiOperation({ summary: 'Create a new order from cart' }) + @ApiResponse({ status: 201, description: 'Order created successfully' }) + @ApiResponse({ status: 400, description: 'Bad request - cart validation failed' }) + @ApiResponse({ status: 404, description: 'Cart, user, restaurant, address, or payment method not found' }) + create(@UserId() userId: string, @RestId() restaurantId: string) { + return this.ordersService.create(userId, restaurantId); } @Get() diff --git a/src/modules/orders/orders.module.ts b/src/modules/orders/orders.module.ts index de72044..e2834ef 100644 --- a/src/modules/orders/orders.module.ts +++ b/src/modules/orders/orders.module.ts @@ -1,8 +1,31 @@ import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; import { OrdersService } from './orders.service'; import { OrdersController } from './orders.controller'; +import { Order } from './entities/order.entity'; +import { OrderItem } from './entities/order-item.entity'; +import { User } from '../users/entities/user.entity'; +import { Restaurant } from '../restaurants/entities/restaurant.entity'; +import { Food } from '../foods/entities/food.entity'; +import { UserAddress } from '../users/entities/user-address.entity'; +import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment-method.entity'; +import { CartModule } from '../cart/cart.module'; +import { UtilsModule } from '../utils/utils.module'; @Module({ + imports: [ + MikroOrmModule.forFeature([ + Order, + OrderItem, + User, + Restaurant, + Food, + UserAddress, + RestaurantPaymentMethod, + ]), + CartModule, + UtilsModule, + ], controllers: [OrdersController], providers: [OrdersService], }) diff --git a/src/modules/orders/orders.service.ts b/src/modules/orders/orders.service.ts index ddc00ed..ada4e76 100644 --- a/src/modules/orders/orders.service.ts +++ b/src/modules/orders/orders.service.ts @@ -1,11 +1,199 @@ -import { Injectable } from '@nestjs/common'; -import { CreateOrderDto } from './dto/create-order.dto'; -import { UpdateOrderDto } from './dto/update-order.dto'; +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { Order } from './entities/order.entity'; +import { OrderItem } from './entities/order-item.entity'; +import { User } from '../users/entities/user.entity'; +import { Restaurant } from '../restaurants/entities/restaurant.entity'; +import { Food } from '../foods/entities/food.entity'; +import { UserAddress } from '../users/entities/user-address.entity'; +import { RestaurantPaymentMethod } from '../payments/entities/restaurant-payment-method.entity'; +import { CartService } from '../cart/providers/cart.service'; +import { CacheService } from '../utils/cache.service'; +import { OrderStatus } from './interface/order-status'; +import { PaymentStatus } from '../payments/interface/payment-status'; +import { Cart } from '../cart/interfaces/cart.interface'; @Injectable() export class OrdersService { - create(createOrderDto: CreateOrderDto) { - return 'This action adds a new order'; + private readonly CART_KEY_PREFIX = 'cart'; + + constructor( + private readonly em: EntityManager, + private readonly cartService: CartService, + private readonly cacheService: CacheService, + ) {} + + async create(userId: string, restaurantId: string): Promise { + // Get cart + const cart = await this.cartService.findOne(userId, restaurantId); + + // Validate cart and prepare order data + const validationResult = await this.validateCartForOrder(userId, restaurantId, cart); + + // Create order within a transaction + return await this.em.transactional(async em => { + // Create order entity + const order = em.create(Order, { + user: validationResult.user, + restaurant: validationResult.restaurant, + address: validationResult.address, + paymentMethod: validationResult.paymentMethod, + couponDiscount: cart.couponDiscount || 0, + itemsDiscount: cart.itemsDiscount || 0, + totalDiscount: cart.totalDiscount || 0, + subTotal: cart.subTotal || 0, + tax: cart.tax || 0, + deliveryFee: cart.deliveryFee || 0, + total: cart.total || 0, + totalItems: cart.totalItems || 0, + status: OrderStatus.Pending, + paymentStatus: PaymentStatus.Pending, + }); + + em.persist(order); + + // Create order items and update stock + for (const itemData of validationResult.orderItemsData) { + const { food, quantity, unitPrice, discount } = itemData; + const totalPrice = (unitPrice - discount) * quantity; + + const orderItem = em.create(OrderItem, { + order, + food, + quantity, + unitPrice, + discount, + totalPrice, + }); + + em.persist(orderItem); + + // Update food stock + food.stock -= quantity; + em.persist(food); + } + + // Flush all changes + await em.flush(); + + // Clear cart from cache after successful order creation + const cacheKey = `${this.CART_KEY_PREFIX}:${userId}:${restaurantId}`; + await this.cacheService.del(cacheKey); + + return order; + }); + } + + /** + * Validates cart and prepares all required data for order creation + */ + private async validateCartForOrder( + userId: string, + restaurantId: string, + cart: Cart, + ): Promise<{ + user: User; + restaurant: Restaurant; + address: UserAddress; + paymentMethod: RestaurantPaymentMethod; + 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.'); + } + + // Validate address is set + if (!cart.addressId) { + throw new BadRequestException('Address is required. Please set a delivery address before creating an order.'); + } + + // Validate payment method is set + if (!cart.paymentMethodId) { + throw new BadRequestException( + 'Payment method is required. Please set a payment method before creating an order.', + ); + } + + // Validate and load entities + const user = await this.em.findOne(User, { id: userId }); + if (!user) { + throw new NotFoundException('User not found'); + } + + const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); + if (!restaurant) { + throw new NotFoundException('Restaurant not found'); + } + + const address = await this.em.findOne(UserAddress, { id: cart.addressId }, { populate: ['user'] }); + if (!address) { + throw new NotFoundException('Address not found'); + } + + // Verify address belongs to the user + if (address.user.id !== userId) { + throw new BadRequestException('Address does not belong to the current user'); + } + + const paymentMethod = await this.em.findOne( + RestaurantPaymentMethod, + { + restaurant: { id: restaurantId }, + paymentMethod: { id: cart.paymentMethodId }, + }, + { populate: ['restaurant', 'paymentMethod'] }, + ); + + if (!paymentMethod) { + throw new NotFoundException( + `Payment method with ID ${cart.paymentMethodId} not found for restaurant ${restaurantId}`, + ); + } + + if (!paymentMethod.isActive) { + throw new BadRequestException('Payment method is not active 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'] }); + 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 + if (food.stock < cartItem.quantity) { + throw new BadRequestException( + `Insufficient stock for food ${food.title || food.id}. Available: ${food.stock}, Requested: ${cartItem.quantity}`, + ); + } + + const unitPrice = food.price || 0; + const discount = food.discount || 0; + + orderItemsData.push({ + food, + quantity: cartItem.quantity, + unitPrice, + discount, + }); + } + + return { + user, + restaurant, + address, + paymentMethod, + orderItemsData, + }; } findAll() { @@ -16,7 +204,8 @@ export class OrdersService { return `This action returns a #${id} order`; } - update(id: number, updateOrderDto: UpdateOrderDto) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + update(id: number, _updateOrderDto: unknown) { return `This action updates a #${id} order`; }