remove reservation table

This commit is contained in:
2025-12-17 21:28:29 +03:30
parent 7f72d7d70f
commit b663bfa592
8 changed files with 57 additions and 143 deletions
@@ -1,7 +0,0 @@
import { PaymentMethodEnum } from "src/modules/payments/interface/payment";
export const STOCK_RESERVE_MINUTES: Record<PaymentMethodEnum, number> = {
[PaymentMethodEnum.Online]: 15,
[PaymentMethodEnum.Cash]: 120,
[PaymentMethodEnum.Wallet]: 15,
};
@@ -1,23 +0,0 @@
import { Entity, Enum, ManyToOne, Property } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Food } from '../../foods/entities/food.entity';
import { Order } from '../../orders/entities/order.entity';
import { ReservationStatus } from '../inteface/reservation';
@Entity({ tableName: 'reservations' })
export class Reservation extends BaseEntity {
@ManyToOne(() => Food)
food!: Food;
@ManyToOne(() => Order)
order!: Order;
@Property({ type: 'int' })
quantity!: number;
@Property({ type: 'date' })
expiresAt!: Date;
@Enum(() => ReservationStatus)
status: ReservationStatus = ReservationStatus.ACTIVE;
}
+15 -68
View File
@@ -4,17 +4,14 @@ import { SetStockDto } from './dto/set-stock.dto';
import { BulkSetStockDto } from './dto/bulk-set-stock.dto'; import { BulkSetStockDto } from './dto/bulk-set-stock.dto';
import { BulkReserveFoodDto } from './dto/bulk-reserve-food.dto'; import { BulkReserveFoodDto } from './dto/bulk-reserve-food.dto';
import { Inventory } from './entities/inventory.entity'; import { Inventory } from './entities/inventory.entity';
import { Reservation } from './entities/reservation.entity';
import { Food } from '../foods/entities/food.entity'; import { Food } from '../foods/entities/food.entity';
import { Restaurant } from '../restaurants/entities/restaurant.entity'; import { Restaurant } from '../restaurants/entities/restaurant.entity';
import { Order } from '../orders/entities/order.entity'; import { LockMode } from '@mikro-orm/core';
import { ReservationStatus } from './inteface/reservation';
import { STOCK_RESERVE_MINUTES } from './const/reservation';
import { PaymentMethodEnum } from '../payments/interface/payment';
@Injectable() @Injectable()
export class InventoryService { export class InventoryService {
constructor(private readonly em: EntityManager) {} constructor(private readonly em: EntityManager) { }
async setStockForFood(foodId: string, restaurantId: string, setStockDto: SetStockDto): Promise<Inventory> { async setStockForFood(foodId: string, restaurantId: string, setStockDto: SetStockDto): Promise<Inventory> {
// Validate that availableStock doesn't exceed totalStock // Validate that availableStock doesn't exceed totalStock
@@ -135,27 +132,22 @@ export class InventoryService {
return results; return results;
} }
async tempBulkReserveFood( async deductFromInventory(
em: EntityManager, em: EntityManager,
restaurantId: string,
orderId: string,
paymentMethod: PaymentMethodEnum,
bulkReserveFoodDto: BulkReserveFoodDto, bulkReserveFoodDto: BulkReserveFoodDto,
): Promise<Reservation[]> { ): Promise<Inventory[]> {
return em.transactional(async em => {
const { items } = bulkReserveFoodDto; const { items } = bulkReserveFoodDto;
const expiresAt = this.getReservationExpiry(paymentMethod);
// Get all unique food IDs // Get all unique food IDs
const foodIds = [...new Set(items.map(item => item.foodId))]; const foodIds = [...new Set(items.map(item => item.foodId))];
// Load all foods in one query // Load all foods in one query
const foods = await em.find(Food, { id: { $in: foodIds } }, { populate: ['restaurant'] }); const foods = await em.find(Food, { id: { $in: foodIds } }, { lockMode: LockMode.PESSIMISTIC_WRITE });
// Verify all foods exist and belong to the restaurant // Verify all foods exist and belong to the restaurant
const foodMap = new Map<string, Food>(); const foodMap = new Map<string, Food>();
for (const food of foods) { for (const food of foods) {
if (food.restaurant.id !== restaurantId) {
throw new BadRequestException(`Food ${food.id} does not belong to restaurant ${restaurantId}`);
}
foodMap.set(food.id, food); foodMap.set(food.id, food);
} }
@@ -165,15 +157,9 @@ export class InventoryService {
throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`); throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`);
} }
// Load order
const order = await em.findOne(Order, { id: orderId });
if (!order) {
throw new NotFoundException(`Order with ID ${orderId} not found`);
}
// Load all existing inventories in one query // Load all existing inventories in one query
const existingInventories = await em.find(Inventory, { const existingInventories = await em.find(Inventory, {
food: { id: { $in: foodIds }, restaurant: { id: restaurantId } }, food: { id: { $in: foodIds } },
}); });
const inventoryMap = new Map<string, Inventory>(); const inventoryMap = new Map<string, Inventory>();
@@ -182,9 +168,8 @@ export class InventoryService {
} }
// Validate stock availability and create reservations // Validate stock availability and create reservations
const reservations: Reservation[] = []; const inventories: Inventory[] = [];
for (const item of items) { for (const item of items) {
const food = foodMap.get(item.foodId)!;
const inventory = inventoryMap.get(item.foodId); const inventory = inventoryMap.get(item.foodId);
// Check if inventory exists // Check if inventory exists
@@ -198,51 +183,13 @@ export class InventoryService {
`Insufficient stock for food ${item.foodId}. Available: ${inventory.availableStock}, Requested: ${item.quantity}`, `Insufficient stock for food ${item.foodId}. Available: ${inventory.availableStock}, Requested: ${item.quantity}`,
); );
} }
inventory.availableStock -= item.quantity;
// Create reservation record inventories.push(inventory);
const reservation = em.create(Reservation, { em.persist(inventory);
food, }
order, return inventories;
quantity: item.quantity,
expiresAt,
status: ReservationStatus.ACTIVE,
}); });
reservations.push(reservation);
// Update available stock (decrease by quantity)
inventory.availableStock -= item.quantity;
}
return reservations;
}
async confirmReservationByOrderId(em: EntityManager, orderId: string): Promise<Reservation[]> {
const reservations = await em.find(Reservation, { order: { id: orderId } });
if (!reservations) {
throw new NotFoundException(`Reservations with order ID ${orderId} not found`);
}
for (const reservation of reservations) {
reservation.status = ReservationStatus.CONFIRMED;
}
return reservations;
}
async releaseReservationByOrderId(em: EntityManager, orderId: string): Promise<Reservation[]> {
const reservations = await em.find(Reservation, { order: { id: orderId } });
if (!reservations) {
throw new NotFoundException(`Reservations with order ID ${orderId} not found`);
}
for (const reservation of reservations) {
em.remove(reservation);
}
return reservations;
}
getReservationExpiry(method: PaymentMethodEnum): Date {
const minutes = STOCK_RESERVE_MINUTES[method] ?? 15;
return new Date(Date.now() + minutes * 60 * 1000);
} }
} }
@@ -121,7 +121,7 @@ export class OrdersService {
quantity: item.quantity, quantity: item.quantity,
})), })),
}; };
await this.inventoryService.tempBulkReserveFood(em, restaurantId, order.id, order.paymentMethod.method, bulkReserveFoodDto); await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
await em.flush(); await em.flush();
this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`); this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`);
return order; return order;
@@ -1,6 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { PaymentGatewayEnum } from '../interface/payment'; import { PaymentGatewayEnum } from '../interface/payment';
import { ZarinpalGateway } from '../gateways/zarinpal.gateway'; import { ZarinpalGateway } from './zarinpal.gateway';
import { IPaymentGateway } from '../interface/gateway'; import { IPaymentGateway } from '../interface/gateway';
@Injectable() @Injectable()
+1 -1
View File
@@ -10,7 +10,7 @@ import { AuthModule } from '../auth/auth.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { Payment } from './entities/payment.entity'; import { Payment } from './entities/payment.entity';
import { ZarinpalGateway } from './gateways/zarinpal.gateway'; import { ZarinpalGateway } from './gateways/zarinpal.gateway';
import { GatewayManager } from './services/gateway.manager'; import { GatewayManager } from './gateways/gateway.manager';
import { PaymentRepository } from './repositories/payment.repository'; import { PaymentRepository } from './repositories/payment.repository';
import { InventoryModule } from '../inventory/inventory.module'; import { InventoryModule } from '../inventory/inventory.module';
@@ -4,7 +4,7 @@ import { Payment } from '../entities/payment.entity';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { Order } from '../../orders/entities/order.entity'; import { Order } from '../../orders/entities/order.entity';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { GatewayManager } from './gateway.manager'; import { GatewayManager } from '../gateways/gateway.manager';
import { UserWallet } from 'src/modules/users/entities/user-wallet.entity'; import { UserWallet } from 'src/modules/users/entities/user-wallet.entity';
import { OrderPaymentContext } from '../interface/payment'; import { OrderPaymentContext } from '../interface/payment';
import { InventoryService } from 'src/modules/inventory/inventory.service'; import { InventoryService } from 'src/modules/inventory/inventory.service';
@@ -209,7 +209,6 @@ export class PaymentsService {
} }
this.markPaid(payment); this.markPaid(payment);
this.confirmOrder(payment.order); this.confirmOrder(payment.order);
await this.confirmStock(orderId, em);
await em.flush(); await em.flush();
return payment; return payment;
@@ -241,9 +240,7 @@ export class PaymentsService {
order.status = OrderStatus.PAID; order.status = OrderStatus.PAID;
} }
private async confirmStock(orderId: string, em: EntityManager) {
await this.inventoryService.confirmReservationByOrderId(em, orderId);
}
private async getOrCreateLatestPendingPayment( private async getOrCreateLatestPendingPayment(