order cleanup

This commit is contained in:
2025-12-03 12:01:23 +03:30
parent e8566aa389
commit 8a2390ea8e
4 changed files with 114 additions and 4 deletions
+33 -1
View File
@@ -30,6 +30,7 @@
"@nestjs/mapped-types": "*", "@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-fastify": "^11.1.8", "@nestjs/platform-fastify": "^11.1.8",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^11.2.1", "@nestjs/swagger": "^11.2.1",
"@nestjs/throttler": "^6.4.0", "@nestjs/throttler": "^6.4.0",
"axios": "^1.13.1", "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": { "node_modules/@nestjs/schematics": {
"version": "11.0.9", "version": "11.0.9",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz",
@@ -5867,6 +5881,12 @@
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT" "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": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -7936,6 +7956,19 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/cron-parser": {
"version": "4.9.0", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", "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", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
+1
View File
@@ -45,6 +45,7 @@
"@nestjs/mapped-types": "*", "@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-fastify": "^11.1.8", "@nestjs/platform-fastify": "^11.1.8",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^11.2.1", "@nestjs/swagger": "^11.2.1",
"@nestjs/throttler": "^6.4.0", "@nestjs/throttler": "^6.4.0",
"axios": "^1.13.1", "axios": "^1.13.1",
+2
View File
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs'; import { MikroOrmModule } from '@mikro-orm/nestjs';
import { ScheduleModule } from '@nestjs/schedule';
import { OrdersService } from './orders.service'; import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller'; import { OrdersController } from './orders.controller';
import { Order } from './entities/order.entity'; import { Order } from './entities/order.entity';
@@ -17,6 +18,7 @@ import { JwtModule } from '@nestjs/jwt';
@Module({ @Module({
imports: [ imports: [
ScheduleModule,
MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]), MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]),
CartModule, CartModule,
UtilsModule, UtilsModule,
+78 -3
View File
@@ -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 { EntityManager } from '@mikro-orm/postgresql';
import { Order } from './entities/order.entity'; import { Order } from './entities/order.entity';
import { OrderItem } from './entities/order-item.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 { PaymentsService } from '../payments/services/payments.service';
import { DeliveryMethodEnum } from '../delivery/interface/delivery'; import { DeliveryMethodEnum } from '../delivery/interface/delivery';
import { Delivery } from '../delivery/entities/delivery.entity'; import { Delivery } from '../delivery/entities/delivery.entity';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable() @Injectable()
export class OrdersService { export class OrdersService {
private readonly logger = new Logger(OrdersService.name);
constructor( constructor(
private readonly em: EntityManager, private readonly em: EntityManager,
private readonly cartService: CartService, private readonly cartService: CartService,
@@ -142,12 +145,19 @@ export class OrdersService {
throw new NotFoundException('Restaurant not found'); 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) { if (!delivery) {
throw new NotFoundException('Delivery not found'); 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) { if (delivery.method === DeliveryMethodEnum.DeliveryCourier && !cart.addressId) {
throw new BadRequestException('Address is required. Please set a delivery address before creating an order.'); 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) { remove(id: number) {
return `This action removes a #${id} order`; 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;
}
}
} }