|
|
|
@@ -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;
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|