refactor step 2

This commit is contained in:
2025-12-15 12:35:29 +03:30
parent b776f2b83a
commit 184ceb4800
10 changed files with 111 additions and 182 deletions
+92 -136
View File
@@ -92,6 +92,9 @@ export class OrdersService {
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`);
}
@@ -109,8 +112,8 @@ export class OrdersService {
em.persist(orderItem);
// Update food stock
food.stock -= quantity;
//reserve food stock
em.persist(food);
}
@@ -309,16 +312,10 @@ export class OrdersService {
if (!order) {
throw new NotFoundException('Order not found');
}
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) {
throw new BadRequestException('Order must be paid online before accepting');
}
// Cash, CardOnDelivery, and Wallet can be accepted even if payment status is pending
// (they're paid on delivery or already deducted)
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);
@@ -330,17 +327,14 @@ export class OrdersService {
if (!order) {
throw new NotFoundException('Order not found');
}
if (!this.canTransition(order.status, OrderStatus.PREPARING, order.paymentMethod?.method)) {
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;
await this.em.persistAndFlush(order);
return order;
}
// 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) {
@@ -356,26 +350,12 @@ export class OrdersService {
if (!order) {
throw new NotFoundException('Order not found');
}
let orderStatus: OrderStatus | undefined;
switch (order.deliveryMethod.method) {
case DeliveryMethodEnum.DeliveryCourier:
orderStatus = OrderStatus.ReadyForDeliveryCourier;
break;
case DeliveryMethodEnum.DeliveryCar:
orderStatus = OrderStatus.ReadyForDeliveryCar;
break;
case DeliveryMethodEnum.DineIn:
orderStatus = OrderStatus.ReadyForDineIn;
break;
case DeliveryMethodEnum.CustomerPickup:
orderStatus = OrderStatus.ReadyForCustomerPickup;
break;
default:
throw new BadRequestException('Invalid delivery method');
if (!this.canTransition(order.status, OrderStatus.READY, order.paymentMethod.method)) {
throw new BadRequestException('Invalid status transition');
}
order.status = OrderStatus.READY;
order.status = orderStatus;
await this.em.persistAndFlush(order);
return order;
}
@@ -385,8 +365,8 @@ export class OrdersService {
if (!order) {
throw new NotFoundException('Order not found');
}
if (order.status !== OrderStatus.) {
throw new BadRequestException('Order is not pending');
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);
@@ -399,143 +379,119 @@ export class OrdersService {
throw new NotFoundException('Order not found');
}
const readyStatuses = [
OrderStatus.ReadyForCustomerPickup,
OrderStatus.ReadyForDineIn,
OrderStatus.ReadyForDeliveryCar,
OrderStatus.ReadyForDeliveryCourier,
];
if (!readyStatuses.includes(order.status)) {
throw new BadRequestException(
`Order must be in a ready status to mark as delivered. Current status: ${order.status}`,
);
if (!this.canTransition(order.status, OrderStatus.COMPLETED, order.paymentMethod.method)) {
throw new BadRequestException('Invalid status transition');
}
order.status = OrderStatus.Delivered;
order.status = OrderStatus.COMPLETED;
await this.em.persistAndFlush(order);
return order;
}
async updateStatus(orderId: string, status: string, restId: string) {
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');
}
// Map string status to OrderStatus enum
const statusMap: Record<string, OrderStatus> = {
ReadyForCustomerPickup: OrderStatus.ReadyForCustomerPickup,
readyForCustomerPickup: OrderStatus.ReadyForCustomerPickup,
ReadyForDineIn: OrderStatus.ReadyForDineIn,
readyForDineIn: OrderStatus.ReadyForDineIn,
ReadyForDeliveryCar: OrderStatus.ReadyForDeliveryCar,
readyForDeliveryCar: OrderStatus.ReadyForDeliveryCar,
ReadyForDeliveryCourier: OrderStatus.ReadyForDeliveryCourier,
readyForDeliveryCourier: OrderStatus.ReadyForDeliveryCourier,
shipping: OrderStatus.Shipping,
Delivered: OrderStatus.Delivered,
delivered: OrderStatus.Delivered,
};
const newStatus = statusMap[status];
if (!newStatus) {
throw new BadRequestException(
`Invalid status: ${status}. Allowed statuses: shipping, Delivered, ReadyForCustomerPickup, ReadyForDineIn, ReadyForDeliveryCar, ReadyForDeliveryCourier`,
);
if (!this.canTransition(order.status, status, order.paymentMethod.method)) {
throw new BadRequestException('Invalid status transition');
}
order.status = newStatus;
order.status = status;
await this.em.persistAndFlush(order);
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) {
transitions: Record<OrderStatus, OrderStatus[]> = {
[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]: [],
};
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
*/
@Cron(CronExpression.EVERY_10_MINUTES)
async cleanupAbandonedOrders() {
this.logger.log('Starting cleanup of abandoned orders...');
// @Cron(CronExpression.EVERY_10_MINUTES)
// async cleanupAbandonedOrders() {
// this.logger.log('Starting cleanup of abandoned orders...');
try {
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
// try {
// const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
// Find abandoned orders: Pending status, Pending payment, created >30 minutes ago
const abandonedOrders = await this.em.find(
Order,
{
status: OrderStatus.Pending,
paymentStatus: PaymentStatusEnum.Pending,
createdAt: { $lt: thirtyMinutesAgo },
},
{ populate: ['items', 'items.food'] },
);
// // 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'] },
// );
if (abandonedOrders.length === 0) {
this.logger.log('No abandoned orders found');
return;
}
// if (abandonedOrders.length === 0) {
// this.logger.log('No abandoned orders found');
// return;
// }
this.logger.log(`Found ${abandonedOrders.length} abandoned order(s) to cleanup`);
// this.logger.log(`Found ${abandonedOrders.length} abandoned order(s) to cleanup`);
// 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();
// // 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();
// Restore stock for each item
for (const item of order.items) {
const food = item.food;
if (food) {
food.stock += item.quantity;
em.persist(food);
this.logger.debug(
`Restored ${item.quantity} units of stock for food ${food.id} (${food.title || 'N/A'})`,
);
}
}
// // 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'})`,
// );
// }
// }
// Update order status to Cancelled
order.status = OrderStatus.CancelledBySystem;
em.persist(order);
// // Update order status to Cancelled
// order.status = OrderStatus.CANCELED;
// em.persist(order);
await em.flush();
this.logger.log(`Cancelled abandoned order ${order.id}`);
});
}
// await em.flush();
// this.logger.log(`Cancelled abandoned order ${order.id}`);
// });
// }
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;
}
}
// 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;
// }
// }
}