order cleanup
This commit is contained in:
Generated
+33
-1
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user