mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 17:45:42 +00:00
Fallback cron job for ticketing if rabbitmq disconnected.
This commit is contained in:
@@ -72,6 +72,6 @@ function rabbitMQImport(): DynamicModule[] {
|
|||||||
PaymentEventsConsumer,
|
PaymentEventsConsumer,
|
||||||
ServiceAuthGuard,
|
ServiceAuthGuard,
|
||||||
],
|
],
|
||||||
exports: [PaymentClientService],
|
exports: [PaymentClientService, PaymentsService],
|
||||||
})
|
})
|
||||||
export class PaymentsModule {}
|
export class PaymentsModule {}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
|
|||||||
import { PrismaModule } from '../../common/prisma.module';
|
import { PrismaModule } from '../../common/prisma.module';
|
||||||
import { NotificationsModule } from '../notifications/notifications.module';
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
import { CurrencyModule } from '../currency/currency.module';
|
import { CurrencyModule } from '../currency/currency.module';
|
||||||
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
import { TasksService } from './tasks.service';
|
import { TasksService } from './tasks.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, NotificationsModule, CurrencyModule],
|
imports: [PrismaModule, NotificationsModule, CurrencyModule, PaymentsModule],
|
||||||
providers: [TasksService],
|
providers: [TasksService],
|
||||||
})
|
})
|
||||||
export class TasksModule {}
|
export class TasksModule {}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { Cron } from '@nestjs/schedule';
|
|||||||
import { PrismaService } from '../../common/prisma.service';
|
import { PrismaService } from '../../common/prisma.service';
|
||||||
import { SmsClientService } from '../notifications/sms-client.service';
|
import { SmsClientService } from '../notifications/sms-client.service';
|
||||||
import { CurrencyService } from '../currency/currency.service';
|
import { CurrencyService } from '../currency/currency.service';
|
||||||
|
import { PaymentsService } from '../payments/payments.service';
|
||||||
|
import { PaymentClientService } from '../payments/payment-client.service';
|
||||||
|
import { PaymentReferenceType, ProviderPaymentStatus } from '@edr/types';
|
||||||
import { MAX_PAYMENT_HOURS, CUTOFF_MINUTES, computePaymentDeadline } from '../../common/utils/payment-deadline.utils';
|
import { MAX_PAYMENT_HOURS, CUTOFF_MINUTES, computePaymentDeadline } from '../../common/utils/payment-deadline.utils';
|
||||||
|
|
||||||
// Retention windows
|
// Retention windows
|
||||||
@@ -28,6 +31,8 @@ export class TasksService {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly sms: SmsClientService,
|
private readonly sms: SmsClientService,
|
||||||
private readonly currencyService: CurrencyService,
|
private readonly currencyService: CurrencyService,
|
||||||
|
private readonly paymentsService: PaymentsService,
|
||||||
|
private readonly paymentClient: PaymentClientService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
@@ -253,6 +258,87 @@ export class TasksService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// Every 1 min: poll the payment service for any PENDING_PAYMENT bookings
|
||||||
|
// whose payment intent has moved to SUCCEEDED on the gateway but whose
|
||||||
|
// confirmation event was never delivered (missed RabbitMQ message, network
|
||||||
|
// blip, etc.). finalizePaymentSuccess() is fully idempotent so re-running
|
||||||
|
// it for an already-confirmed booking is safe.
|
||||||
|
//
|
||||||
|
// Processes at most 50 bookings per cycle to avoid hammering the payment
|
||||||
|
// service; the next tick picks up the remainder.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
@Cron('*/1 * * * *')
|
||||||
|
async syncPaymentStatuses() {
|
||||||
|
const BATCH_SIZE = 50;
|
||||||
|
|
||||||
|
const bookings = await this.prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
status: 'PENDING_PAYMENT',
|
||||||
|
paymentIntent: { status: { in: ['REQUIRES_ACTION', 'PROCESSING'] } },
|
||||||
|
},
|
||||||
|
include: { paymentIntent: true },
|
||||||
|
take: BATCH_SIZE,
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bookings.length === 0) return;
|
||||||
|
|
||||||
|
let confirmed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
let errored = 0;
|
||||||
|
|
||||||
|
for (const booking of bookings) {
|
||||||
|
if (!booking.paymentIntent) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const snapshot = await this.paymentClient.getIntentByReference(
|
||||||
|
PaymentReferenceType.BOOKING,
|
||||||
|
booking.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!snapshot) continue;
|
||||||
|
|
||||||
|
if (snapshot.status === ProviderPaymentStatus.SUCCEEDED) {
|
||||||
|
const result = await this.paymentsService.finalizePaymentSuccess({
|
||||||
|
intentId: booking.paymentIntent.id,
|
||||||
|
providerTxnId: snapshot.providerTxnId,
|
||||||
|
paidAt: snapshot.paidAt ? new Date(snapshot.paidAt) : undefined,
|
||||||
|
});
|
||||||
|
if (!result.alreadyFinalized) {
|
||||||
|
this.logger.log(`Payment sync confirmed: ${booking.bookingRef}`);
|
||||||
|
confirmed++;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
snapshot.status === ProviderPaymentStatus.FAILED ||
|
||||||
|
snapshot.status === ProviderPaymentStatus.CANCELLED
|
||||||
|
) {
|
||||||
|
// The payment deadline enforcer will cancel the booking when its
|
||||||
|
// window expires; log now so operations can see failed intents early.
|
||||||
|
this.logger.warn(
|
||||||
|
`Payment sync: ${booking.bookingRef} intent is ${snapshot.status} — ` +
|
||||||
|
`booking will be auto-cancelled at payment deadline`,
|
||||||
|
);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
// REQUIRES_ACTION / PROCESSING → still pending, retry next cycle
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Payment sync error for ${booking.bookingRef}: ` +
|
||||||
|
`${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
errored++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmed > 0 || failed > 0 || errored > 0) {
|
||||||
|
this.logger.log(
|
||||||
|
`Payment sync run: ${bookings.length} checked, ` +
|
||||||
|
`${confirmed} confirmed, ${failed} failed/cancelled, ${errored} errors`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// Daily at 02:00 EAT: purge expired/stale records to enforce data retention.
|
// Daily at 02:00 EAT: purge expired/stale records to enforce data retention.
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user