Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha

This commit is contained in:
Abubeker Yasin
2026-07-16 15:37:25 +03:00
169 changed files with 7139 additions and 962 deletions

View File

@@ -152,7 +152,7 @@ export class BookingsService {
id: booking.id,
bookingRef: booking.bookingRef,
status: booking.status,
totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
currency: booking.displayCurrency,
displayCurrency: booking.displayCurrency,
displayTotalMinor: booking.displayTotalMinor,
@@ -296,7 +296,7 @@ export class BookingsService {
id: booking.id,
bookingRef: booking.bookingRef,
status: booking.status,
totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
currency: booking.displayCurrency,
displayCurrency: booking.displayCurrency,
displayTotalMinor: booking.displayTotalMinor,
@@ -409,7 +409,7 @@ export class BookingsService {
id: booking.id,
bookingRef: booking.bookingRef,
status: booking.status,
totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
currency: booking.displayCurrency,
displayCurrency: booking.displayCurrency,
displayTotalMinor: booking.displayTotalMinor,
@@ -554,7 +554,7 @@ export class BookingsService {
const uniquePassengers = Array.from(new Map(passengerDetails.map((p: any) => [p.name, p])).values());
return {
id: booking.id, bookingRef: booking.bookingRef, status: booking.status,
totalMinor: resolvePackageRoundTripTotal(booking, booking.priceTier?.priceMinor, booking.adultCount, booking.childCount),
totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, booking.priceTier?.priceMinor, booking.adultCount, booking.childCount),
currency: booking.displayCurrency,
displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor,
contactEmail: booking.contactEmail, contactPhone: booking.contactPhone,
@@ -683,7 +683,7 @@ export class BookingsService {
id: booking.id,
bookingRef: booking.bookingRef,
status: booking.status,
totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
currency: booking.displayCurrency,
displayCurrency: booking.displayCurrency,
displayTotalMinor: booking.displayTotalMinor,
@@ -1883,7 +1883,7 @@ export class BookingsService {
return {
id: booking.id, bookingRef: booking.bookingRef, status: booking.status,
totalMinor: resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
currency: booking.displayCurrency,
adultCount: booking.adultCount, childCount: booking.childCount,
displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor ?? undefined,

View File

@@ -72,6 +72,6 @@ function rabbitMQImport(): DynamicModule[] {
PaymentEventsConsumer,
ServiceAuthGuard,
],
exports: [PaymentClientService],
exports: [PaymentClientService, PaymentsService],
})
export class PaymentsModule {}

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,9 @@ 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 { 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';
// Retention windows
@@ -28,6 +31,8 @@ export class TasksService {
private readonly prisma: PrismaService,
private readonly sms: SmsClientService,
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.
// ─────────────────────────────────────────────────────────────────────────