feat: ( payment ): verify-before-cancel never cancel a paid booking

This commit is contained in:
Abubeker Yasin
2026-07-29 17:29:59 +03:00
parent 9a98d460b5
commit d7168a8d77
9 changed files with 290 additions and 41 deletions

View File

@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import { PrismaModule } from '../../common/prisma.module';
import { NotificationsModule } from '../notifications/notifications.module';
import { CurrencyModule } from '../currency/currency.module';
import { PaymentsModule } from '../payments/payments.module';
import { TasksService } from './tasks.service';
@Module({
imports: [PrismaModule, NotificationsModule, CurrencyModule],
imports: [PrismaModule, NotificationsModule, CurrencyModule, PaymentsModule],
providers: [TasksService],
})
export class TasksModule {}

View File

@@ -3,6 +3,7 @@ import { Cron } from '@nestjs/schedule';
import { PrismaService } from '../../common/prisma.service';
import { SmsClientService } from '../notifications/sms-client.service';
import { CurrencyService } from '../currency/currency.service';
import { PaymentsService } from '../payments/payments.service';
import { MAX_PAYMENT_HOURS, CUTOFF_MINUTES, computePaymentDeadline } from '../../common/utils/payment-deadline.utils';
// Retention windows
@@ -28,6 +29,7 @@ export class TasksService {
private readonly prisma: PrismaService,
private readonly sms: SmsClientService,
private readonly currencyService: CurrencyService,
private readonly paymentsService: PaymentsService,
) {}
// ─────────────────────────────────────────────────────────────────────────
@@ -308,6 +310,18 @@ export class TasksService {
const paymentDeadline = computePaymentDeadline(createdAt, dep, checkinMinutes);
if (now < paymentDeadline) continue;
// Deadline passed — but NEVER cancel a booking that is actually paid. The payment.succeeded
// event may have been lost (RabbitMQ down) or arrived late, leaving a paid booking stuck
// PENDING_PAYMENT. Ask the payment service over HTTP; it confirms the booking synchronously
// if paid. Only proceed to cancel when settlement is VERIFIED unpaid.
const settlement = await this.paymentsService.reconcileAndConfirmIfPaid(booking.id);
if (settlement.paid || !settlement.verified) {
this.logger.log(
`Skip auto-cancel ${booking.bookingRef}: ${settlement.paid ? 'PAID → confirmed' : 'unverifiable → deferred'}`,
);
continue;
}
// 1a. Release held seats (Journey rows are the occupancy source of truth once paid)
await this.prisma.journey.deleteMany({ where: { bookingId: booking.id } as any });