Merge pull request #991 from Tria-plc/alpha

Alpha
This commit is contained in:
Abubeker Yasin
2026-07-28 11:41:33 +03:00
committed by GitHub
9 changed files with 120 additions and 47 deletions

View File

@@ -415,8 +415,18 @@ export class PaymentsController {
paySupplementaryCharge(
@Param('token') token: string,
@Body() dto: PaySupplementaryChargeDto,
@Headers('origin') origin?: string,
@Headers('referer') referer?: string,
@Headers('x-frontend-base-url') frontendBaseUrl?: string,
) {
return this.supplementaryService.pay(token, dto.method, dto.platform);
// Same domain-follows-the-user rule as /initiate — the self-pay page can be
// opened on either portal domain.
return this.supplementaryService.pay(
token,
dto.method,
dto.platform,
resolveAllowedOrigin(origin, referer, frontendBaseUrl),
);
}
@Post('supplementary/:id/mark-paid')

View File

@@ -48,12 +48,14 @@ const NON_TERMINAL_STATUSES: PaymentIntentStatus[] = [
];
// Methods whose return/failure URLs are browser-facing pages on the passenger
// portal, so they should follow whichever domain the user came in on. DMONEY is
// deliberately excluded — its return URL is a server-to-server webhook host, not
// a page the browser lands on.
// portal, so they should follow whichever domain the user came in on. For DMONEY
// this is the preOrder `redirect_url` (the page the browser lands on after
// checkout) — NOT `notify_url`, which is the server-to-server webhook and is
// configured provider-side, never rebased.
const DOMAIN_AWARE_METHODS = new Set<PaymentMethodType>([
PaymentMethodType.TELEBIRR,
PaymentMethodType.WAAFI,
PaymentMethodType.DMONEY,
]);
@Injectable()
@@ -279,22 +281,35 @@ export class PaymentsService {
return this.formatIntentResponse(intent);
}
// A web↔mobile switch needs a different clientAction (Telebirr: REDIRECT for web
// vs LAUNCH_APP for the native app). Detect the open session's platform from its
// clientAction shape so a platform change is NOT blocked below: it must flow
// through to re-initiate, where the payment service retires the stale session and
// opens a fresh one with the correct launch method for the requested platform.
const requestedMobile = (dto.platform ?? "web") === "mobile";
const storedAction = (snapshot?.clientAction ??
existingIntent.clientAction) as unknown as ClientAction | null;
const platformChanged =
(storedAction?.type === "LAUNCH_APP") !== requestedMobile;
// Still pending at the provider (REQUIRES_ACTION/PROCESSING) — or the payment
// service was unreachable and the local status is non-terminal. Block the switch:
// return the existing intent so the payer completes or waits out the open attempt
// rather than opening a second concurrent charge.
// rather than opening a second concurrent charge. A platform switch is exempt — it
// falls through so a session with the correct clientAction is opened for it.
if (
!snapshot ||
snapshot.status === ProviderPaymentStatus.REQUIRES_ACTION ||
snapshot.status === ProviderPaymentStatus.PROCESSING
!platformChanged &&
(!snapshot ||
snapshot.status === ProviderPaymentStatus.REQUIRES_ACTION ||
snapshot.status === ProviderPaymentStatus.PROCESSING)
) {
const intent = snapshot
? await this.syncIntentProjection(booking.id, snapshot)
: existingIntent;
return this.formatIntentResponse(intent);
}
// Otherwise the provider reports FAILED/CANCELLED — fall through and initiate
// the newly selected method below.
// Otherwise the provider reports FAILED/CANCELLED, or the payer switched platform —
// fall through and initiate the newly selected method below.
}
const { returnUrl, failureUrl } = this.resolveReturnUrls(

View File

@@ -116,11 +116,21 @@ export class SupplementaryChargesService {
return updated;
}
async pay(token: string, method: string, platform?: 'web' | 'mobile') {
async pay(
token: string,
method: string,
platform?: 'web' | 'mobile',
requestOrigin?: string | null,
) {
const charge = await this.getByToken(token); // validates status/expiry
const paymentMethod = method as ProviderMethod;
const portalUrl = process.env.PORTAL_URL ?? 'http://localhost:5174';
// Self-pay links are opened on whichever portal domain the recipient used
// (bookingedr.et vs passenger.edrsc.com), so the return pages must live on
// that same domain. `requestOrigin` is already allowlist-validated by the
// controller; PORTAL_URL is the fallback for non-browser callers.
const portalUrl =
requestOrigin ?? process.env.PORTAL_URL ?? 'http://localhost:5174';
const returnUrl = `${portalUrl}/pay-balance/${token}/success`;
const failureUrl = `${portalUrl}/pay-balance/${token}/failed`;