Files
dkala-api/src/modules/orders/crone/order.crone.ts
T
2026-02-08 10:49:16 +03:30

176 lines
5.6 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { EntityManager } from '@mikro-orm/postgresql';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Payment } from '../../../payments/entities/payment.entity';
import { PaymentMethodEnum, PaymentStatusEnum } from '../../../payments/interface/payment';
import { OrderStatus } from '../interface/order.interface';
import { Order } from '../entities/order.entity';
import { OrderStatusChangedEvent } from '../events/order.events';
@Injectable()
export class OrdersCrone {
private readonly logger = new Logger(OrdersCrone.name);
constructor(
private readonly em: EntityManager,
private readonly eventEmitter: EventEmitter2,
) { }
// run every minute and fail pending online payments older than 15 minutes
@Cron('*/1 * * * *', {
name: 'failOldOnlinePayments',
timeZone: 'UTC',
})
async cancelOldOnlinePendingOrders() {
try {
const cutoff = new Date(Date.now() - 15 * 60 * 1000);
this.logger.debug('Searching for pending online payments older than 15 minutes');
const payments = await this.em.find(
Payment,
{
method: PaymentMethodEnum.Online,
status: PaymentStatusEnum.Pending,
createdAt: { $lte: cutoff },
},
{ populate: ['order', 'order.items', 'order.items.product'] },
);
if (!payments || payments.length === 0) {
return;
}
this.logger.log(`Found ${payments.length} stale pending online payments`);
for (const p of payments) {
try {
await this.em.transactional(async em => {
// reload inside transaction to avoid concurrency issues
const payment = await em.findOne(
Payment,
{ id: p.id },
{ populate: ['order', 'order.items', 'order.items.product'] },
);
if (!payment) return;
if (payment.status !== PaymentStatusEnum.Pending) return;
payment.status = PaymentStatusEnum.Failed;
payment.failedAt = new Date();
if (payment.order) {
payment.order.status = OrderStatus.CANCELED;
}
em.persist(payment);
if (payment.order) em.persist(payment.order);
await em.flush();
this.logger.log(
`Marked payment ${payment.id} and order ${payment.order?.id} as failed`,
);
});
} catch (err) {
this.logger.error(`Error processing payment ${p.id}: ${err.message}`, err.stack);
}
}
} catch (err) {
this.logger.error(`OrdersCrone failed: ${err.message}`, err.stack);
}
}
// run every 15 minutes to complete orders that have been in shipped/delivered statuses for more than 3 hours
@Cron('*/15 * * * *', {
name: 'completeOldDeliveredOrders',
timeZone: 'UTC',
})
async completeOldDeliveredOrders() {
try {
const cutoff = new Date(Date.now() - 3 * 60 * 60 * 1000); // 3 hours ago
this.logger.debug('Searching for orders in shipped/delivered statuses older than 3 hours');
const orders = await this.em.find(
Order,
{
status: {
$in: [
OrderStatus.SHIPPED,
OrderStatus.DELIVERED_TO_WAITER,
OrderStatus.DELIVERED_TO_RECEPTIONIST,
],
},
updatedAt: { $lte: cutoff },
},
{ populate: ['shop'] },
);
if (!orders || orders.length === 0) {
return;
}
this.logger.log(`Found ${orders.length} orders to mark as completed`);
for (const order of orders) {
try {
await this.em.transactional(async em => {
// reload inside transaction to avoid concurrency issues
const reloadedOrder = await em.findOne(
Order,
{ id: order.id },
{ populate: ['shop', 'user'] },
);
if (!reloadedOrder) return;
if (
![
OrderStatus.SHIPPED,
OrderStatus.DELIVERED_TO_WAITER,
OrderStatus.DELIVERED_TO_RECEPTIONIST,
].includes(reloadedOrder.status)
) {
return;
}
const previousStatus = reloadedOrder.status;
const restaurantId =
typeof reloadedOrder.shop === 'string'
? reloadedOrder.shop
: reloadedOrder.shop.id;
// Update order status and history
reloadedOrder.status = OrderStatus.COMPLETED;
reloadedOrder.history.push({
status: OrderStatus.COMPLETED,
changedAt: new Date(),
desc: 'تکمیل سفارش توسط سیستم',
});
em.persist(reloadedOrder);
await em.flush();
// // Emit event after transaction completes
this.eventEmitter.emit(
OrderStatusChangedEvent.name,
new OrderStatusChangedEvent(
reloadedOrder.id,
reloadedOrder.user?.id || '',
String(reloadedOrder.orderNumber) || '',
restaurantId,
previousStatus,
OrderStatus.COMPLETED,
'admin',
),
);
this.logger.log(`Marked order ${reloadedOrder.id} as completed`);
});
} catch (err) {
this.logger.error(`Error processing order ${order.id}: ${err.message}`, err.stack);
}
}
} catch (err) {
this.logger.error(`completeOldDeliveredOrders cron failed: ${err.message}`, err.stack);
}
}
}