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

This commit is contained in:
Abubeker Yasin
2026-07-24 13:59:21 +03:00
445 changed files with 33383 additions and 7513 deletions

View File

@@ -1,6 +1,7 @@
import {
BadGatewayException,
BadRequestException,
ConflictException,
Injectable,
Logger,
} from "@nestjs/common";
@@ -154,11 +155,16 @@ export class PaymentClientService {
} catch (err) {
if (err instanceof AxiosError && err.response) {
// 4xx/5xx from the payment service: propagate 404 to callers that handle it;
// everything else is a gateway-level failure from the client's perspective.
// 409 = a legitimate conflict (e.g. another provider's payment is already in
// flight for this booking) — surface its message as-is rather than masking it as
// a gateway failure; everything else is a genuine gateway-level failure.
if (err.response.status === 404) throw err;
const detail =
(err.response.data as { message?: string | string[] })?.message ??
err.message;
if (err.response.status === 409) {
throw new ConflictException(detail);
}
this.logger.error(
`payment service ${method} ${path}${err.response.status}: ${detail}`,
);

View File

@@ -1072,6 +1072,19 @@ export class PaymentsService {
return { processed: false, reason: "booking-not-found" };
}
// C-4 guard: a settlement must cover what the passenger was quoted. Compare the provider-settled
// amount against the booking's display-currency total (the amount the customer agreed to pay);
// a short payment must NOT confirm the booking. Amount-only — the display↔charge currency
// divergence is tracked separately under the USD/DJF findings. The 1% tolerance absorbs rounding.
const expectedMinor = booking.displayTotalMinor ?? booking.totalMinor;
const shortPayTolerance = Math.max(1, Math.round(expectedMinor * 0.01));
if (event.amountMinor < expectedMinor - shortPayTolerance) {
this.logger.error(
`mark-paid: short payment for booking ${booking.id} — settled ${event.amountMinor} ${event.currency} < expected ${expectedMinor} ${booking.displayCurrency}; not confirming`,
);
return { processed: false, reason: "amount-mismatch" };
}
// Local intent row is a projection during the strangler migration: reuse it when the
// legacy initiate path created one, otherwise materialize it from the event.
let intent = await this.prisma.paymentIntent.findUnique({