From 8a2390ea8e53d51b296452687701943854de70e1 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 3 Dec 2025 12:01:23 +0330 Subject: [PATCH] order cleanup --- package-lock.json | 34 +++++++++++- package.json | 1 + src/modules/orders/orders.module.ts | 2 + src/modules/orders/orders.service.ts | 81 ++++++++++++++++++++++++++-- 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8f4792..57e7dfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-fastify": "^11.1.8", + "@nestjs/schedule": "^6.0.1", "@nestjs/swagger": "^11.2.1", "@nestjs/throttler": "^6.4.0", "axios": "^1.13.1", @@ -4493,6 +4494,19 @@ } } }, + "node_modules/@nestjs/schedule": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.1.tgz", + "integrity": "sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==", + "license": "MIT", + "dependencies": { + "cron": "4.3.3" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.9", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", @@ -5867,6 +5881,12 @@ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "license": "MIT" }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -7936,6 +7956,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cron": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz", + "integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + } + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -11435,7 +11468,6 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" } diff --git a/package.json b/package.json index ccefcc0..0f0c6c2 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", "@nestjs/platform-fastify": "^11.1.8", + "@nestjs/schedule": "^6.0.1", "@nestjs/swagger": "^11.2.1", "@nestjs/throttler": "^6.4.0", "axios": "^1.13.1", diff --git a/src/modules/orders/orders.module.ts b/src/modules/orders/orders.module.ts index 7c6eb05..c968724 100644 --- a/src/modules/orders/orders.module.ts +++ b/src/modules/orders/orders.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { ScheduleModule } from '@nestjs/schedule'; import { OrdersService } from './orders.service'; import { OrdersController } from './orders.controller'; import { Order } from './entities/order.entity'; @@ -17,6 +18,7 @@ import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ + ScheduleModule, MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]), CartModule, UtilsModule, diff --git a/src/modules/orders/orders.service.ts b/src/modules/orders/orders.service.ts index f2a4109..7b554fd 100644 --- a/src/modules/orders/orders.service.ts +++ b/src/modules/orders/orders.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; import { EntityManager } from '@mikro-orm/postgresql'; import { Order } from './entities/order.entity'; import { OrderItem } from './entities/order-item.entity'; @@ -15,9 +15,12 @@ import { PaymentMethod } from '../payments/entities/payment-method.entity'; import { PaymentsService } from '../payments/services/payments.service'; import { DeliveryMethodEnum } from '../delivery/interface/delivery'; import { Delivery } from '../delivery/entities/delivery.entity'; +import { Cron, CronExpression } from '@nestjs/schedule'; @Injectable() export class OrdersService { + private readonly logger = new Logger(OrdersService.name); + constructor( private readonly em: EntityManager, private readonly cartService: CartService, @@ -142,12 +145,19 @@ export class OrdersService { throw new NotFoundException('Restaurant not found'); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const delivery = await this.em.findOne(Delivery, { id: cart.deliveryMethodId! }); + const delivery = await this.em.findOne(Delivery, { id: cart.deliveryMethodId }); if (!delivery) { throw new NotFoundException('Delivery not found'); } + // Validate minimum order price for delivery method + const minOrderPrice = Number(delivery.minOrderPrice) || 0; + if (minOrderPrice > 0 && cart.total < minOrderPrice) { + throw new BadRequestException( + `Minimum order amount for this delivery method is ${minOrderPrice}. Current total is ${cart.total}.`, + ); + } + if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.addressId) { throw new BadRequestException('Address is required. Please set a delivery address before creating an order.'); } @@ -248,4 +258,69 @@ export class OrdersService { remove(id: number) { return `This action removes a #${id} order`; } + + /** + * 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...'); + + 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'] }, + ); + + if (abandonedOrders.length === 0) { + this.logger.log('No abandoned orders found'); + return; + } + + 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(); + + // 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'})`, + ); + } + } + + // Update order status to Cancelled + order.status = OrderStatus.Cancelled; + em.persist(order); + + 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; + } + } }