From b776f2b83a359e0ac7937d061bf8fea12dfbbc4f Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 15 Dec 2025 11:15:08 +0330 Subject: [PATCH] up --- src/app.module.ts | 2 - src/config/swagger.config.ts | 4 +- src/modules/cart/providers/cart.service.ts | 30 +++++----- src/modules/foods/entities/food.entity.ts | 12 ++-- src/modules/foods/providers/food.service.ts | 3 - .../inventory/entities/inventory.entity.ts | 9 +-- src/modules/inventory/inventory.service.ts | 11 +--- .../orders/controllers/orders.controller.ts | 2 +- src/modules/orders/entities/order.entity.ts | 5 +- src/modules/orders/interface/order-status.ts | 3 +- .../orders/providers/orders.service.ts | 58 ++++++++++++++----- src/modules/payments/interface/payment.ts | 1 - 12 files changed, 80 insertions(+), 60 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 8451a0b..d2df766 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -22,7 +22,6 @@ import { NotificationsModule } from './modules/notifications/notifications.modul import { EventEmitterModule } from '@nestjs/event-emitter'; import { PagerModule } from './modules/pager/pager.module'; import { ContactModule } from './modules/contact/contact.module'; -import { ReservationsModule } from './modules/reservations/reservations.module'; import { InventoryModule } from './modules/inventory/inventory.module'; @Module({ @@ -54,7 +53,6 @@ import { InventoryModule } from './modules/inventory/inventory.module'; EventEmitterModule.forRoot(), PagerModule, ContactModule, - ReservationsModule, InventoryModule, ], controllers: [], diff --git a/src/config/swagger.config.ts b/src/config/swagger.config.ts index c1bb3e2..fe3b7a8 100644 --- a/src/config/swagger.config.ts +++ b/src/config/swagger.config.ts @@ -3,8 +3,8 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; export function getSwaggerConfig(app: INestApplication) { const config = new DocumentBuilder() - .setTitle('Tahavol API') - .setDescription('API documentation for Tahavol backend') + .setTitle('D-menu API') + .setDescription('API documentation for D-menu backend') .setVersion('1.0') .addBearerAuth() // optional: for JWT endpoints .build(); diff --git a/src/modules/cart/providers/cart.service.ts b/src/modules/cart/providers/cart.service.ts index c2aee44..442e626 100644 --- a/src/modules/cart/providers/cart.service.ts +++ b/src/modules/cart/providers/cart.service.ts @@ -21,7 +21,6 @@ import { Cart, CartItem } from '../interfaces/cart.interface'; import { CouponService } from 'src/modules/coupons/providers/coupon.service'; import { OrderCouponDetail } from 'src/modules/orders/interface/order-status'; import { CouponType } from 'src/modules/coupons/interface/coupon'; -import { Order } from '../../orders/entities/order.entity'; @Injectable() export class CartService { @@ -95,7 +94,7 @@ export class CartService { const cart = await this.getOrCreateCart(userId, restaurantId); // Find food - const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant'] }); + const food = await this.em.findOne(Food, { id: foodId }, { populate: ['restaurant', 'inventory'] }); if (!food) { throw new NotFoundException(`Food with ID ${foodId} not found`); } @@ -106,8 +105,9 @@ export class CartService { } // Check stock availability - if (food.stock < addItemDto.quantity) { - throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${food.stock}`); + const availableStock = food.inventory?.availableStock ?? 0; + if (availableStock < addItemDto.quantity) { + throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`); } // Check if item already exists in cart @@ -119,8 +119,8 @@ export class CartService { const newQuantity = existingItem.quantity + addItemDto.quantity; // Check stock for new total quantity - if (food.stock < newQuantity) { - throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${food.stock}`); + if (availableStock < newQuantity) { + throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`); } const itemPrice = food.price || 0; @@ -221,7 +221,7 @@ export class CartService { // Process each item for (const addItemDto of bulkAddItemsDto.items) { // Find food - const food = await this.em.findOne(Food, { id: addItemDto.foodId }, { populate: ['restaurant'] }); + const food = await this.em.findOne(Food, { id: addItemDto.foodId }, { populate: ['restaurant', 'inventory'] }); if (!food) { throw new NotFoundException(`Food with ID ${addItemDto.foodId} not found`); } @@ -232,8 +232,9 @@ export class CartService { } // Check stock availability - if (food.stock < addItemDto.quantity) { - throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${food.stock}`); + const availableStock = food.inventory?.availableStock ?? 0; + if (availableStock < addItemDto.quantity) { + throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`); } // Check if item already exists in cart @@ -245,8 +246,8 @@ export class CartService { const newQuantity = existingItem.quantity + addItemDto.quantity; // Check stock for new total quantity - if (food.stock < newQuantity) { - throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${food.stock}`); + if (availableStock < newQuantity) { + throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`); } const itemPrice = food.price || 0; @@ -307,14 +308,15 @@ export class CartService { const item = cart.items[itemIndex]; // Find food to check stock - const food = await this.em.findOne(Food, { id: item.foodId }, { populate: ['restaurant'] }); + const food = await this.em.findOne(Food, { id: item.foodId }, { populate: ['restaurant', 'inventory'] }); if (!food) { throw new NotFoundException(`Food with ID ${item.foodId} not found`); } // Check stock availability - if (food.stock < updateItemDto.quantity) { - throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${food.stock}`); + const availableStock = food.inventory?.availableStock ?? 0; + if (availableStock < updateItemDto.quantity) { + throw new BadRequestException(`Insufficient stock for food ${food.title}. Available: ${availableStock}`); } // Update quantity diff --git a/src/modules/foods/entities/food.entity.ts b/src/modules/foods/entities/food.entity.ts index f7f9421..7175630 100644 --- a/src/modules/foods/entities/food.entity.ts +++ b/src/modules/foods/entities/food.entity.ts @@ -1,4 +1,4 @@ -import { Cascade, Collection, Entity, Index, ManyToOne, OneToMany, Property } from '@mikro-orm/core'; +import { Cascade, Collection, Entity, Index, ManyToOne, OneToMany, Property, OneToOne } from '@mikro-orm/core'; import { Category } from './category.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; @@ -14,13 +14,17 @@ export class Food extends BaseEntity { restaurant: Restaurant; @ManyToOne(() => Category) - category!: Category; + category: Category; @OneToMany(() => Review, review => review.food, { cascade: [Cascade.ALL], orphanRemoval: true }) reviews = new Collection(this); - @OneToMany(() => Inventory, inventory => inventory.food, { cascade: [Cascade.ALL], orphanRemoval: true }) - inventory = new Collection(this); + @OneToOne(() => Inventory, inventory => inventory.food, { + nullable: true, + cascade: [Cascade.ALL], + orphanRemoval: true, + }) + inventory?: Inventory; @Property({ nullable: true }) title?: string; diff --git a/src/modules/foods/providers/food.service.ts b/src/modules/foods/providers/food.service.ts index 1185c2a..31f4c47 100644 --- a/src/modules/foods/providers/food.service.ts +++ b/src/modules/foods/providers/food.service.ts @@ -44,9 +44,6 @@ export class FoodService { noon: rest.noon ?? false, dinner: rest.dinner ?? false, desc: rest.desc, - // pickup/stock - stock: rest.stock ?? 0, - stockDefault: rest.stockDefault ?? 0, isActive: rest.isActive ?? true, inPlaceServe: rest.inPlaceServe ?? false, pickupServe: rest.pickupServe ?? false, diff --git a/src/modules/inventory/entities/inventory.entity.ts b/src/modules/inventory/entities/inventory.entity.ts index 75d0bdd..97ac760 100644 --- a/src/modules/inventory/entities/inventory.entity.ts +++ b/src/modules/inventory/entities/inventory.entity.ts @@ -1,17 +1,12 @@ -import { Entity, ManyToOne, Property, Unique } from '@mikro-orm/core'; +import { Entity, Property, OneToOne } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; -import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Food } from '../../foods/entities/food.entity'; @Entity({ tableName: 'inventory' }) -@Unique({ properties: ['food', 'restaurant'] }) export class Inventory extends BaseEntity { - @ManyToOne(() => Food, { unique: true }) + @OneToOne(() => Food, food => food.inventory) food!: Food; - @ManyToOne(() => Restaurant) - restaurant!: Restaurant; - @Property({ type: 'int' }) totalStock!: number; diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts index 86ba5bf..d01135b 100644 --- a/src/modules/inventory/inventory.service.ts +++ b/src/modules/inventory/inventory.service.ts @@ -32,8 +32,7 @@ export class InventoryService { // Find or create inventory record let inventory = await this.em.findOne(Inventory, { - food: { id: foodId }, - restaurant: { id: restaurantId }, + food: { id: foodId, restaurant: { id: restaurantId } }, }); if (!inventory) { @@ -45,7 +44,6 @@ export class InventoryService { inventory = this.em.create(Inventory, { food, - restaurant, totalStock: setStockDto.totalStock, availableStock: setStockDto.availableStock, }); @@ -93,8 +91,7 @@ export class InventoryService { // Load all existing inventories in one query const existingInventories = await this.em.find(Inventory, { - food: { id: { $in: foodIds } }, - restaurant: { id: restaurantId }, + food: { id: { $in: foodIds }, restaurant: { id: restaurantId } }, }); const inventoryMap = new Map(); @@ -118,7 +115,6 @@ export class InventoryService { // Create new inventory record inventory = this.em.create(Inventory, { food, - restaurant, totalStock: item.totalStock, availableStock: item.availableStock, }); @@ -173,8 +169,7 @@ export class InventoryService { // Load all existing inventories in one query const existingInventories = await this.em.find(Inventory, { - food: { id: { $in: foodIds } }, - restaurant: { id: restaurantId }, + food: { id: { $in: foodIds }, restaurant: { id: restaurantId } }, }); const inventoryMap = new Map(); diff --git a/src/modules/orders/controllers/orders.controller.ts b/src/modules/orders/controllers/orders.controller.ts index b15e896..dc48c26 100644 --- a/src/modules/orders/controllers/orders.controller.ts +++ b/src/modules/orders/controllers/orders.controller.ts @@ -101,7 +101,7 @@ export class OrdersController { @ApiOperation({ summary: 'Accept an order' }) @ApiParam({ name: 'orderId', description: 'Order ID' }) acceptOrder(@Param('orderId') orderId: string, @RestId() restId: string) { - return this.ordersService.acceptOrder(orderId, restId); + return this.ordersService.confirmOrder(orderId, restId); } @UseGuards(AdminAuthGuard) diff --git a/src/modules/orders/entities/order.entity.ts b/src/modules/orders/entities/order.entity.ts index 686e352..2a11903 100644 --- a/src/modules/orders/entities/order.entity.ts +++ b/src/modules/orders/entities/order.entity.ts @@ -20,6 +20,7 @@ import { UserAddress } from '../../users/entities/user-address.entity'; import { PaymentMethod } from '../../payments/entities/payment-method.entity'; import { OrderItem } from './order-item.entity'; import { Delivery } from '../../delivery/entities/delivery.entity'; +import { Payment } from 'src/modules/payments/entities/payment.entity'; @Entity({ tableName: 'orders' }) @Unique({ properties: ['restaurant', 'orderNumber'] }) @@ -47,8 +48,8 @@ export class Order extends BaseEntity { @ManyToOne(() => UserAddress, { nullable: true }) address?: UserAddress; - @ManyToOne(() => PaymentMethod, { nullable: true }) - paymentMethod?: PaymentMethod; + @OneToOne(() => Payment, payment => payment.order) + paymentMethod: PaymentMethod; @Property({ type: 'int', nullable: true }) orderNumber?: number; diff --git a/src/modules/orders/interface/order-status.ts b/src/modules/orders/interface/order-status.ts index 2dae0a9..c80ce36 100644 --- a/src/modules/orders/interface/order-status.ts +++ b/src/modules/orders/interface/order-status.ts @@ -24,7 +24,7 @@ import type { CouponType } from 'src/modules/coupons/interface/coupon'; // Delivered = 'delivered', // } export enum OrderStatus { - CREATED = 'created', + CREATED = 'new', PENDING_PAYMENT = 'pendingPayment', PAID = 'paid', CONFIRMED = 'confirmed', @@ -32,6 +32,7 @@ export enum OrderStatus { READY = 'ready', SHIPPED = 'shipped', COMPLETED = 'completed', + CANCELED = 'canceled', FAILED = 'failed', REFUNDED = 'refunded', diff --git a/src/modules/orders/providers/orders.service.ts b/src/modules/orders/providers/orders.service.ts index 2c7a3dc..eb7f54f 100644 --- a/src/modules/orders/providers/orders.service.ts +++ b/src/modules/orders/providers/orders.service.ts @@ -20,7 +20,7 @@ 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'; - + @Injectable() export class OrdersService { private readonly logger = new Logger(OrdersService.name); @@ -81,7 +81,7 @@ export class OrdersService { totalItems: cart.totalItems || 0, description: cart.description, tableNumber: cart.tableNumber, - status: OrderStatus.Pending, + status: OrderStatus.NEW, paymentStatus: PaymentStatusEnum.Pending, }); @@ -92,7 +92,7 @@ export class OrdersService { const { food, quantity, unitPrice, discount } = itemData; // Stock check - if (food.stock < quantity) { + if (food.inventory.availableStock < quantity) { throw new BadRequestException(`${food.title} is out of stock`); } @@ -221,7 +221,7 @@ export class OrdersService { 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'] }); + 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`); } @@ -232,9 +232,10 @@ export class OrdersService { } // Final stock validation - if (food.stock < cartItem.quantity) { + const availableStock = food.inventory?.availableStock ?? 0; + if (availableStock < cartItem.quantity) { throw new BadRequestException( - `Insufficient stock for food ${food.title || food.id}. Available: ${food.stock}, Requested: ${cartItem.quantity}`, + `Insufficient stock for food ${food.title || food.id}. Available: ${availableStock}, Requested: ${cartItem.quantity}`, ); } @@ -303,13 +304,13 @@ export class OrdersService { return order; } - async acceptOrder(orderId: string, restId: string) { + 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 (order.status !== OrderStatus.Pending) { - throw new BadRequestException('Order must be pending before accepting'); + if (order.status !== OrderStatus.CREATED) { + throw new BadRequestException('Order must be created before confirming'); } // Wallet payments should be paid before accepting (like Online) if (order.paymentMethod?.method === PaymentMethodEnum.Online && order.paymentStatus !== PaymentStatusEnum.Paid) { @@ -319,7 +320,7 @@ export class OrdersService { // Cash, CardOnDelivery, and Wallet can be accepted even if payment status is pending // (they're paid on delivery or already deducted) - order.status = OrderStatus.Confirmed; + order.status = OrderStatus.CONFIRMED; await this.em.persistAndFlush(order); return order; } @@ -329,10 +330,13 @@ export class OrdersService { if (!order) { throw new NotFoundException('Order not found'); } - if (order.status !== OrderStatus.Confirmed) { + if (!this.canTransition(order.status, OrderStatus.PREPARING, order.paymentMethod?.method)) { + throw new BadRequestException('Invalid status transition'); + } + if (order.status !== OrderStatus.CONFIRMED) { throw new BadRequestException('Order must be confirmed before preparing'); } - order.status = OrderStatus.Preparing; + order.status = OrderStatus.PREPARING; await this.em.persistAndFlush(order); return order; } @@ -342,7 +346,7 @@ export class OrdersService { if (!order) { throw new NotFoundException('Order not found'); } - order.status = OrderStatus.RejectedByRestaurant; + order.status = OrderStatus.CANCELED; await this.em.persistAndFlush(order); return order; } @@ -381,10 +385,10 @@ export class OrdersService { if (!order) { throw new NotFoundException('Order not found'); } - if (order.status !== OrderStatus.Pending) { + if (order.status !== OrderStatus.) { throw new BadRequestException('Order is not pending'); } - order.status = OrderStatus.CancelledByUser; + order.status = OrderStatus.CANCELED; await this.em.persistAndFlush(order); return order; } @@ -446,6 +450,30 @@ export class OrdersService { return order; } + transitions = { + CREATED: [OrderStatus.PENDING_PAYMENT, OrderStatus.CONFIRMED, OrderStatus.CANCELED], + PENDING_PAYMENT: [OrderStatus.PAID, OrderStatus.FAILED, OrderStatus.CANCELED], + PAID: [OrderStatus.CONFIRMED, OrderStatus.REFUNDED], + CONFIRMED: [OrderStatus.PREPARING, OrderStatus.CANCELED], + PREPARING: [OrderStatus.READY, OrderStatus.SHIPPED, OrderStatus.CANCELED], + READY: [OrderStatus.COMPLETED, OrderStatus.FAILED], + SHIPPED: [OrderStatus.COMPLETED, OrderStatus.FAILED], + } + + canTransition(from: OrderStatus, to: OrderStatus, paymentMethod: PaymentMethodEnum) { + if (!this.transitions[from]?.includes(to)) return false; + + if (paymentMethod === PaymentMethodEnum.Cash) { + if ([OrderStatus.PENDING_PAYMENT, OrderStatus.PAID].includes(to)) return false; + } + + if (paymentMethod === PaymentMethodEnum.Online) { + if (to === OrderStatus.CONFIRMED && from !== OrderStatus.PAID) return false; + } + + return true; + } + /** * Cleanup job to handle abandoned orders (pending payment for >30 minutes) * Runs every 10 minutes to check for abandoned orders diff --git a/src/modules/payments/interface/payment.ts b/src/modules/payments/interface/payment.ts index 40be253..007e492 100644 --- a/src/modules/payments/interface/payment.ts +++ b/src/modules/payments/interface/payment.ts @@ -1,7 +1,6 @@ export enum PaymentMethodEnum { Online = 'Online', Cash = 'Cash', - CardOnDelivery = 'CardOnDelivery', Wallet = 'Wallet', } export enum PaymentStatusEnum {