refactor step 2
This commit is contained in:
@@ -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;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user