feat: (payment) add telebirr mini-app in-app payment flow

This commit is contained in:
Abubeker Yasin
2026-08-07 13:48:58 +03:00
parent 1c1fcb28a9
commit f51ee7cf59
9 changed files with 393 additions and 29 deletions

View File

@@ -56,7 +56,7 @@ class WaiveSupplementaryChargeDto {
class PaySupplementaryChargeDto {
@ApiProperty({ enum: PaymentMethodTypeEnum, example: 'TELEBIRR' }) @IsEnum(PaymentMethodTypeEnum) method: PaymentMethodTypeEnum;
@ApiPropertyOptional({ enum: ['web', 'mobile'], default: 'web' }) @IsOptional() @IsIn(['web', 'mobile']) platform?: 'web' | 'mobile';
@ApiPropertyOptional({ enum: ['web', 'mobile', 'inapp'], default: 'web' }) @IsOptional() @IsIn(['web', 'mobile', 'inapp']) platform?: PaymentPlatformDto;
}
@ApiTags("Payment")
@@ -307,7 +307,14 @@ export class PaymentsController {
})
@ApiQuery({ name: "bookingId", required: true })
@ApiQuery({ name: "method", enum: PaymentMethodTypeEnum, required: true })
@ApiQuery({ name: "platform", enum: ["web", "mobile"], required: false })
@ApiQuery({
name: "platform",
enum: ["web", "mobile"],
required: false,
description:
"Browser-only endpoint — `inapp` is not offered here. A mini-app payer has no browser " +
"to redirect and must go through POST /payments/initiate for the bridge payload.",
})
@ApiProduces("text/html")
async checkout(
@Query("bookingId") bookingId: string,

View File

@@ -28,7 +28,8 @@ export enum PaymentMethodTypeEnum {
CBE_BILL = "CBE_BILL", // Ethiopia (pay at any CBE channel by bill number)
}
export type PaymentPlatformDto = "web" | "mobile";
/** Mirrors `PaymentPlatform` in @edr/types — see there for what each surface means. */
export type PaymentPlatformDto = "web" | "mobile" | "inapp";
export class InitiatePaymentDto {
@ApiProperty({ example: "booking-uuid" }) @IsString() bookingId: string;
@@ -45,12 +46,14 @@ export class InitiatePaymentDto {
@IsString()
paymentMethodId?: string;
@ApiPropertyOptional({
enum: ["web", "mobile"],
enum: ["web", "mobile", "inapp"],
default: "web",
description: "Payment platform (web or mobile)",
description:
"Payer surface. `inapp` = the portal is running inside a SuperApp mini-app WebView " +
"(Telebirr), which cannot follow redirect flows and gets a bridge payload instead.",
})
@IsOptional()
@IsIn(["web", "mobile"])
@IsIn(["web", "mobile", "inapp"])
platform?: PaymentPlatformDto;
@ApiPropertyOptional({
description:
@@ -117,9 +120,20 @@ export class SupportedPaymentMethodDto {
export class ClientActionDto {
@ApiProperty({
enum: ["REDIRECT", "LAUNCH_APP", "COLLECT_OTP", "SHOW_BILL_REFERENCE"],
enum: [
"REDIRECT",
"LAUNCH_APP",
"INVOKE_BRIDGE",
"COLLECT_OTP",
"SHOW_BILL_REFERENCE",
],
})
type: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP" | "SHOW_BILL_REFERENCE";
type:
| "REDIRECT"
| "LAUNCH_APP"
| "INVOKE_BRIDGE"
| "COLLECT_OTP"
| "SHOW_BILL_REFERENCE";
@ApiPropertyOptional({ description: "Set when type=REDIRECT (web flow)" })
url?: string;
@ApiPropertyOptional({
@@ -134,6 +148,18 @@ export class ClientActionDto {
description: "Set when type=LAUNCH_APP (mobile flow)",
})
shortCode?: string;
@ApiPropertyOptional({
description:
"Set when type=INVOKE_BRIDGE (telebirr mini app) — which SuperApp host bridge to call",
enum: ["TELEBIRR"],
})
bridge?: "TELEBIRR";
@ApiPropertyOptional({
description:
"Set when type=INVOKE_BRIDGE (telebirr mini app). Signed query string handed verbatim " +
"to the host bridge (js_fun_start_pay). NOT a URL — never navigate to it.",
})
rawRequest?: string;
@ApiPropertyOptional({ description: "Set when type=COLLECT_OTP (e.g. CAC Bank)" })
providerOrderId?: string;
@ApiPropertyOptional({ description: "Set when type=COLLECT_OTP" })

View File

@@ -5,6 +5,7 @@ import { SmsClientService } from '../notifications/sms-client.service';
import { EmailClientService } from '../notifications/email-client.service';
import { PaymentClientService } from './payment-client.service';
import { PaymentReferenceType, PaymentService as PaymentServiceEnum, ProviderMethod } from '@edr/types';
import { PaymentPlatformDto } from './payments.dto';
const CHARGE_TTL_MS = 72 * 60 * 60 * 1000; // 72 hours
@@ -119,7 +120,7 @@ export class SupplementaryChargesService {
async pay(
token: string,
method: string,
platform?: 'web' | 'mobile',
platform?: PaymentPlatformDto,
requestOrigin?: string | null,
) {
const charge = await this.getByToken(token); // validates status/expiry