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); } } }