feat: ( redirect ) add redirect url based on the domain name

This commit is contained in:
Abubeker Yasin
2026-07-14 14:16:48 +03:00
parent 1ace808b4a
commit ef5d643ba4
8 changed files with 179 additions and 11 deletions

View File

@@ -3,6 +3,7 @@ import {
Controller,
Delete,
Get,
Headers,
HttpStatus,
Param,
Patch,
@@ -37,6 +38,7 @@ import {
} from "./payments.dto";
import { PassengerStaff } from "../../common/passenger-guards";
import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
import { resolveAllowedOrigin } from "../../common/utils/redirect-origin.util";
@ApiTags("Payment")
@Controller("payments")
@@ -83,8 +85,19 @@ export class PaymentsController {
summary: "Initiate payment with nationality-based payment methods",
description: `Initiates payment for a booking with support for multiple payment providers:\n\n**Ethiopian Payment Methods:**\n- TELEBIRR - Ethiopia's leading mobile money\n- CBE_BIRR - Commercial Bank of Ethiopia\n- EBIRR - Electronic payment gateway\n\n**Djiboutian Payment Methods:**\n- WAAFI - Djibouti's mobile money service\n\n**International Payment Methods:**\n- CARD - Visa, Mastercard\n- WALLET - Internal wallet balance\n\n**Multi-Currency:**\n- All transactions processed in ETB\n- Display amounts in ETB, DJF, or USD\n- Real-time exchange rate conversion`,
})
initiatePayment(@Body() dto: InitiatePaymentDto) {
return this.service.initiatePayment(dto);
initiatePayment(
@Body() dto: InitiatePaymentDto,
@Headers("origin") origin?: string,
@Headers("referer") referer?: string,
@Headers("x-frontend-base-url") frontendBaseUrl?: string,
) {
// Browser-facing return URLs (telebirr/waafi) follow the domain the user is
// on — derived from Origin, then Referer, then the X-Frontend-Base-URL
// header the portal sets, all validated against the allowlist.
return this.service.initiatePayment(
dto,
resolveAllowedOrigin(origin, referer, frontendBaseUrl),
);
}
@Get("intents/:bookingId")

View File

@@ -26,6 +26,7 @@ import {
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
import { PaymentClientService } from "./payment-client.service";
import { CurrencyService } from "../currency/currency.service";
import { rebaseUrlOrigin } from "../../common/utils/redirect-origin.util";
import {
PaymentService as PaymentServiceEnum,
PaymentReferenceType,
@@ -41,6 +42,15 @@ const NON_TERMINAL_STATUSES: PaymentIntentStatus[] = [
PaymentIntentStatus.SUCCEEDED,
];
// 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.
const DOMAIN_AWARE_METHODS = new Set<PaymentMethodType>([
PaymentMethodType.TELEBIRR,
PaymentMethodType.WAAFI,
]);
@Injectable()
export class PaymentsService {
private readonly logger = new Logger(PaymentsService.name);
@@ -172,7 +182,10 @@ export class PaymentsService {
return correctTotal;
}
async initiatePayment(dto: InitiatePaymentDto): Promise<InitiateResponseDto> {
async initiatePayment(
dto: InitiatePaymentDto,
requestOrigin?: string | null,
): Promise<InitiateResponseDto> {
const booking = await this.prisma.booking.findUnique({
where: { id: dto.bookingId },
include: { seats: true },
@@ -213,7 +226,10 @@ export class PaymentsService {
return this.initiateWalletPayment(booking);
}
const { returnUrl, failureUrl } = this.resolveReturnUrls(method);
const { returnUrl, failureUrl } = this.resolveReturnUrls(
method,
requestOrigin,
);
// The selected method's settlement currency lives in the PaymentMethod table (WAAFI/DMONEY
// settle in DJF, CARD in USD, Ethiopian wallets in ETB). Convert the ETB booking total into
@@ -294,7 +310,10 @@ export class PaymentsService {
return this.formatIntentStatus(intent);
}
private resolveReturnUrls(method: PaymentMethodType): {
private resolveReturnUrls(
method: PaymentMethodType,
requestOrigin?: string | null,
): {
returnUrl?: string;
failureUrl?: string;
} {
@@ -323,9 +342,18 @@ export class PaymentsService {
};
const m = perMethod[method] ?? {};
const returnUrl = m.returnUrl || process.env.PAYMENT_RETURN_URL || undefined;
const failureUrl =
let returnUrl = m.returnUrl || process.env.PAYMENT_RETURN_URL || undefined;
let failureUrl =
m.failureUrl || process.env.PAYMENT_FAILURE_URL || returnUrl;
// For browser-facing methods, swap the configured URL's host for whichever
// allowlisted domain the user is currently on (bookingedr.et vs
// passenger.edrsc.com). `requestOrigin` is already validated against the
// allowlist by the controller; when it's null the configured URL is kept.
if (DOMAIN_AWARE_METHODS.has(method) && requestOrigin) {
returnUrl = rebaseUrlOrigin(returnUrl, requestOrigin);
failureUrl = rebaseUrlOrigin(failureUrl, requestOrigin);
}
return { returnUrl, failureUrl };
}