From e5850fbc63241dfb6617d1577cd2ef9639cadd07 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 16 Jun 2026 15:23:32 +0300 Subject: [PATCH 1/3] feat: ( payment ) return url based on payment method --- apps/edr-passenger-api/.env.example | 13 +++++ .../src/modules/payments/payments.service.ts | 51 ++++++++++++++----- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/apps/edr-passenger-api/.env.example b/apps/edr-passenger-api/.env.example index 1a9c71990..b2500160d 100644 --- a/apps/edr-passenger-api/.env.example +++ b/apps/edr-passenger-api/.env.example @@ -88,6 +88,19 @@ WAAFI_INSECURE_TLS=false # Payment Configuration PAYMENT_PROVIDERS_ENABLED=TELEBIRR,CBE_BIRR,EBIRR,CARD,WALLET,WAAFI +# Browser return targets after a hosted payment page (UX only — payment is confirmed by the +# webhook/queryStatus, never this redirect). Global fallback used when a method-specific URL +# below is unset. Most providers use a single redirect; Waafi takes separate success/failure. +PAYMENT_RETURN_URL= +PAYMENT_FAILURE_URL= +TELEBIRR_RETURN_URL= +WAAFI_SUCCESS_REDIRECT= +WAAFI_FAIL_REDIRECT= +DMONEY_RETURN_URL= +CBE_RETURN_URL= +EBIRR_RETURN_URL= +CARD_RETURN_URL= + # Session Configuration SESSION_INACTIVITY_MINUTES=30 diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index 59438c4ad..c017c6332 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -42,13 +42,6 @@ const NON_TERMINAL_STATUSES: PaymentIntentStatus[] = [ @Injectable() export class PaymentsService { private readonly logger = new Logger(PaymentsService.name); - - /** - * DEMO ONLY: when true, a WALLET "payment" is treated as instantly successful — the wallet - * balance check and debit are skipped and the booking is confirmed + ticket issued as if fully - * paid. Lets the happy-path be demoed while a real provider (e.g. Telebirr) is unavailable. - * Never enable in production. Toggle with WALLET_DEMO_AUTO_SUCCEED in the env. - */ private readonly walletDemoAutoSucceed = true; constructor( @@ -136,9 +129,7 @@ export class PaymentsService { return this.initiateWalletPayment(booking); } - // Provider methods go through the payment microservice (docs/payment-service §7.1): - // it owns the intent, the provider session, and the single webhook per provider. - // Re-initiating is safe — the service returns the existing active intent (idempotent). + const { returnUrl, failureUrl } = this.resolveReturnUrls(method); const snapshot = await this.paymentClient.initiate({ service: PaymentServiceEnum.PASSENGER, referenceType: PaymentReferenceType.BOOKING, @@ -148,10 +139,8 @@ export class PaymentsService { currency: booking.currency, provider: method as unknown as ProviderMethod, platform: dto.platform, - // PASSENGER-owned browser bounce-back after the hosted page (freight passes its own). - // UX only — payment is confirmed by the webhook/mark-paid event, never this redirect. - returnUrl: process.env.PAYMENT_RETURN_URL || undefined, - failureUrl: process.env.PAYMENT_FAILURE_URL || undefined, + returnUrl, + failureUrl, }); let intent = await this.syncIntentProjection(booking.id, snapshot); @@ -168,6 +157,40 @@ export class PaymentsService { } return this.formatIntentResponse(intent); } + private resolveReturnUrls(method: PaymentMethodType): { + returnUrl?: string; + failureUrl?: string; + } { + const perMethod: Partial< + Record + > = { + [PaymentMethodType.TELEBIRR]: { + returnUrl: process.env.TELEBIRR_RETURN_URL, + }, + [PaymentMethodType.WAAFI]: { + returnUrl: process.env.WAAFI_SUCCESS_REDIRECT, + failureUrl: process.env.WAAFI_FAIL_REDIRECT, + }, + [PaymentMethodType.DMONEY]: { + returnUrl: process.env.DMONEY_RETURN_URL, + }, + [PaymentMethodType.CBE_BIRR]: { + returnUrl: process.env.CBE_RETURN_URL, + }, + [PaymentMethodType.EBIRR]: { + returnUrl: process.env.EBIRR_RETURN_URL, + }, + [PaymentMethodType.CARD]: { + returnUrl: process.env.CARD_RETURN_URL, + }, + }; + + const m = perMethod[method] ?? {}; + const returnUrl = m.returnUrl || process.env.PAYMENT_RETURN_URL || undefined; + const failureUrl = + m.failureUrl || process.env.PAYMENT_FAILURE_URL || returnUrl; + return { returnUrl, failureUrl }; + } private async syncIntentProjection( bookingId: string, From c56e0e794ce38ff8acd825accfb475635106ed12 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 16 Jun 2026 15:25:14 +0300 Subject: [PATCH 2/3] Update payments.module.ts --- .../src/modules/payments/payments.module.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/edr-passenger-api/src/modules/payments/payments.module.ts b/apps/edr-passenger-api/src/modules/payments/payments.module.ts index 74dc3e764..056986613 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.module.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.module.ts @@ -1,23 +1,60 @@ import { Module } from "@nestjs/common"; import { HttpModule } from "@nestjs/axios"; +import { ConfigService } from "@nestjs/config"; +import { RabbitMQModule } from "@golevelup/nestjs-rabbitmq"; +import { + PAYMENT_EVENTS_DLX, + PAYMENT_EVENTS_EXCHANGE, + PAYMENT_QUEUES, + PaymentService, + paymentServiceBindingPattern, +} from "@edr/types"; import { PaymentsController } from "./payments.controller"; import { PaymentsService } from "./payments.service"; import { InternalPaymentsController } from "./internal-payments.controller"; import { PaymentClientService } from "./payment-client.service"; +import { PaymentEventsConsumer } from "./payment-events.consumer"; import { ServiceAuthGuard } from "../../common/guards/service-auth.guard"; import { SeatsModule } from "../seats/seats.module"; import { TicketsModule } from "../tickets/tickets.module"; +const PASSENGER_QUEUE = PAYMENT_QUEUES[PaymentService.PASSENGER]; + @Module({ imports: [ SeatsModule, TicketsModule, HttpModule.register({ timeout: 10_000 }), + RabbitMQModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + uri: config.get("rabbitmq.url") as string, + exchanges: [ + { + name: PAYMENT_EVENTS_EXCHANGE, + type: "topic", + options: { durable: true }, + }, + { name: PAYMENT_EVENTS_DLX, type: "topic", options: { durable: true } }, + ], + queues: [ + { + name: PASSENGER_QUEUE.dlq, + exchange: PAYMENT_EVENTS_DLX, + routingKey: paymentServiceBindingPattern(PaymentService.PASSENGER), + options: { durable: true }, + }, + ], + prefetchCount: config.get("rabbitmq.prefetch") ?? 10, + connectionInitOptions: { wait: false }, + }), + }), ], controllers: [PaymentsController, InternalPaymentsController], providers: [ PaymentsService, PaymentClientService, + PaymentEventsConsumer, ServiceAuthGuard, ], }) From 30fecc3627d5d6fb1691b00aa2cd188e106c2aa9 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Tue, 16 Jun 2026 15:33:03 +0300 Subject: [PATCH 3/3] fix ( telebirr ) remove booking ref from title --- .../src/providers/telebirr/telebirr.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts b/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts index d4be867da..d785fc65b 100644 --- a/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts +++ b/packages/payment-providers/src/providers/telebirr/telebirr.provider.ts @@ -210,7 +210,7 @@ export class TelebirrProvider implements PaymentProvider { merch_code: this.merchantCode, merch_order_id: input.merchantOrderId, trade_type: "Checkout" as const, - title: `EDR ${input.orderRef}`, + title: `EDR booking payment`, total_amount: totalAmount, trans_currency: input.currency, timeout_express: this.timeoutExpress,