remove reservation table
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -4,17 +4,14 @@ import { SetStockDto } from './dto/set-stock.dto';
|
||||
import { BulkSetStockDto } from './dto/bulk-set-stock.dto';
|
||||
import { BulkReserveFoodDto } from './dto/bulk-reserve-food.dto';
|
||||
import { Inventory } from './entities/inventory.entity';
|
||||
import { Reservation } from './entities/reservation.entity';
|
||||
import { Food } from '../foods/entities/food.entity';
|
||||
import { Restaurant } from '../restaurants/entities/restaurant.entity';
|
||||
import { Order } from '../orders/entities/order.entity';
|
||||
import { ReservationStatus } from './inteface/reservation';
|
||||
import { STOCK_RESERVE_MINUTES } from './const/reservation';
|
||||
import { PaymentMethodEnum } from '../payments/interface/payment';
|
||||
import { LockMode } from '@mikro-orm/core';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class InventoryService {
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
constructor(private readonly em: EntityManager) { }
|
||||
|
||||
async setStockForFood(foodId: string, restaurantId: string, setStockDto: SetStockDto): Promise<Inventory> {
|
||||
// Validate that availableStock doesn't exceed totalStock
|
||||
@@ -61,7 +58,7 @@ export class InventoryService {
|
||||
}
|
||||
|
||||
async bulkSetStockForFoods(restaurantId: string, bulkSetStockDto: BulkSetStockDto): Promise<Inventory[]> {
|
||||
|
||||
|
||||
const { items } = bulkSetStockDto;
|
||||
// Validate all items first
|
||||
for (const item of items) {
|
||||
@@ -135,114 +132,64 @@ export class InventoryService {
|
||||
return results;
|
||||
}
|
||||
|
||||
async tempBulkReserveFood(
|
||||
async deductFromInventory(
|
||||
em: EntityManager,
|
||||
restaurantId: string,
|
||||
orderId: string,
|
||||
paymentMethod: PaymentMethodEnum,
|
||||
bulkReserveFoodDto: BulkReserveFoodDto,
|
||||
): Promise<Reservation[]> {
|
||||
const { items } = bulkReserveFoodDto;
|
||||
const expiresAt = this.getReservationExpiry(paymentMethod);
|
||||
// Get all unique food IDs
|
||||
const foodIds = [...new Set(items.map(item => item.foodId))];
|
||||
): Promise<Inventory[]> {
|
||||
return em.transactional(async em => {
|
||||
|
||||
const { items } = bulkReserveFoodDto;
|
||||
// Get all unique food IDs
|
||||
const foodIds = [...new Set(items.map(item => item.foodId))];
|
||||
|
||||
// Load all foods in one query
|
||||
const foods = await em.find(Food, { id: { $in: foodIds } }, { populate: ['restaurant'] });
|
||||
// Load all foods in one query
|
||||
const foods = await em.find(Food, { id: { $in: foodIds } }, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
||||
|
||||
// Verify all foods exist and belong to the restaurant
|
||||
const foodMap = new Map<string, Food>();
|
||||
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);
|
||||
}
|
||||
|
||||
// Check for missing foods
|
||||
const missingFoodIds = foodIds.filter(id => !foodMap.has(id));
|
||||
if (missingFoodIds.length > 0) {
|
||||
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
|
||||
const existingInventories = await em.find(Inventory, {
|
||||
food: { id: { $in: foodIds }, restaurant: { id: restaurantId } },
|
||||
});
|
||||
|
||||
const inventoryMap = new Map<string, Inventory>();
|
||||
for (const inventory of existingInventories) {
|
||||
inventoryMap.set(inventory.food.id, inventory);
|
||||
}
|
||||
|
||||
// Validate stock availability and create reservations
|
||||
const reservations: Reservation[] = [];
|
||||
for (const item of items) {
|
||||
const food = foodMap.get(item.foodId)!;
|
||||
const inventory = inventoryMap.get(item.foodId);
|
||||
|
||||
// Check if inventory exists
|
||||
if (!inventory) {
|
||||
throw new NotFoundException(`Inventory not found for food ${item.foodId}`);
|
||||
// Verify all foods exist and belong to the restaurant
|
||||
const foodMap = new Map<string, Food>();
|
||||
for (const food of foods) {
|
||||
foodMap.set(food.id, food);
|
||||
}
|
||||
|
||||
// Check if available stock is sufficient
|
||||
if (inventory.availableStock < item.quantity) {
|
||||
throw new BadRequestException(
|
||||
`Insufficient stock for food ${item.foodId}. Available: ${inventory.availableStock}, Requested: ${item.quantity}`,
|
||||
);
|
||||
// Check for missing foods
|
||||
const missingFoodIds = foodIds.filter(id => !foodMap.has(id));
|
||||
if (missingFoodIds.length > 0) {
|
||||
throw new NotFoundException(`Foods not found: ${missingFoodIds.join(', ')}`);
|
||||
}
|
||||
|
||||
// Create reservation record
|
||||
const reservation = em.create(Reservation, {
|
||||
food,
|
||||
order,
|
||||
quantity: item.quantity,
|
||||
expiresAt,
|
||||
status: ReservationStatus.ACTIVE,
|
||||
// Load all existing inventories in one query
|
||||
const existingInventories = await em.find(Inventory, {
|
||||
food: { id: { $in: foodIds } },
|
||||
});
|
||||
|
||||
reservations.push(reservation);
|
||||
const inventoryMap = new Map<string, Inventory>();
|
||||
for (const inventory of existingInventories) {
|
||||
inventoryMap.set(inventory.food.id, inventory);
|
||||
}
|
||||
|
||||
// Update available stock (decrease by quantity)
|
||||
inventory.availableStock -= item.quantity;
|
||||
}
|
||||
|
||||
// Validate stock availability and create reservations
|
||||
const inventories: Inventory[] = [];
|
||||
for (const item of items) {
|
||||
const inventory = inventoryMap.get(item.foodId);
|
||||
|
||||
// Check if inventory exists
|
||||
if (!inventory) {
|
||||
throw new NotFoundException(`Inventory not found for food ${item.foodId}`);
|
||||
}
|
||||
|
||||
// Check if available stock is sufficient
|
||||
if (inventory.availableStock < item.quantity) {
|
||||
throw new BadRequestException(
|
||||
`Insufficient stock for food ${item.foodId}. Available: ${inventory.availableStock}, Requested: ${item.quantity}`,
|
||||
);
|
||||
}
|
||||
inventory.availableStock -= item.quantity;
|
||||
inventories.push(inventory);
|
||||
em.persist(inventory);
|
||||
}
|
||||
return inventories;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { OrderListeners } from './listeners/order.listeners';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { InventoryModule } from '../inventory/inventory.module';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]),
|
||||
|
||||
@@ -121,7 +121,7 @@ export class OrdersService {
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
};
|
||||
await this.inventoryService.tempBulkReserveFood(em, restaurantId, order.id, order.paymentMethod.method, bulkReserveFoodDto);
|
||||
await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
|
||||
await em.flush();
|
||||
this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`);
|
||||
return order;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { PaymentGatewayEnum } from '../interface/payment';
|
||||
import { ZarinpalGateway } from '../gateways/zarinpal.gateway';
|
||||
import { ZarinpalGateway } from './zarinpal.gateway';
|
||||
import { IPaymentGateway } from '../interface/gateway';
|
||||
|
||||
@Injectable()
|
||||
@@ -10,7 +10,7 @@ import { AuthModule } from '../auth/auth.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { Payment } from './entities/payment.entity';
|
||||
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 { InventoryModule } from '../inventory/inventory.module';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Payment } from '../entities/payment.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Order } from '../../orders/entities/order.entity';
|
||||
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 { OrderPaymentContext } from '../interface/payment';
|
||||
import { InventoryService } from 'src/modules/inventory/inventory.service';
|
||||
@@ -209,8 +209,7 @@ export class PaymentsService {
|
||||
}
|
||||
this.markPaid(payment);
|
||||
this.confirmOrder(payment.order);
|
||||
await this.confirmStock(orderId, em);
|
||||
|
||||
|
||||
await em.flush();
|
||||
return payment;
|
||||
});
|
||||
@@ -241,9 +240,7 @@ export class PaymentsService {
|
||||
order.status = OrderStatus.PAID;
|
||||
}
|
||||
|
||||
private async confirmStock(orderId: string, em: EntityManager) {
|
||||
await this.inventoryService.confirmReservationByOrderId(em, orderId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async getOrCreateLatestPendingPayment(
|
||||
|
||||
Reference in New Issue
Block a user