import { Body, Controller, Get, HttpStatus, Param, Patch, Post, Query, Res, SetMetadata, UseGuards, } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiOkResponse, ApiProduces, } from "@nestjs/swagger"; import { SkipThrottle, Throttle } from "@nestjs/throttler"; import { Response } from "express"; import { PaymentsService } from "./payments.service"; import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto, PaymentRegionEnum, SupportedPaymentMethodDto, PaymentMethodTypeEnum, PaymentPlatformDto, BookingAmountResponseDto, } from "./payments.dto"; import { PassengerStaff } from "../../common/passenger-guards"; import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry"; @ApiTags("Payment") @Controller("payments") @Throttle({ strict: { limit: 20, ttl: 60_000 } }) export class PaymentsController { constructor(private service: PaymentsService) {} @Get("all") @PassengerStaff([PASSENGER_PERMS.payments.viewAll, PASSENGER_PERMS.admin]) @ApiBearerAuth("IAM-auth") @ApiOperation({ summary: "Get all payments with filters (staff/admin only)" }) @ApiQuery({ name: "search", required: false }) @ApiQuery({ name: "status", required: false }) @ApiQuery({ name: "method", required: false }) @ApiQuery({ name: "page", required: false }) @ApiQuery({ name: "pageSize", required: false }) async getAll( @Query("search") search?: string, @Query("status") status?: string, @Query("method") method?: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string, ) { return this.service.getAll({ search, status, method, page: page ? parseInt(page) : 1, pageSize: pageSize ? parseInt(pageSize) : 10, }); } @Post("initiate") @SetMetadata('isPublic', true) @ApiOperation({ 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); } @Get("intents/:bookingId") @SetMetadata('isPublic', true) @ApiOperation({ summary: "Get payment intent status for a booking" }) getIntent(@Param("bookingId") bookingId: string) { return this.service.getIntentByBookingId(bookingId); } @Get("waafi/return") @SetMetadata('isPublic', true) @ApiOperation({ summary: "DEMO ONLY — confirm a Waafi payment from the browser-return params and return JSON for the " + "UI to display. The frontend success page forwards the Waafi query params here. Gated by " + "WAAFI_DEMO_TRUST_RETURN (INSECURE; real confirmation is the webhook/HPP_GETTRANINFO).", }) @ApiQuery({ name: "referenceId", required: true }) @ApiQuery({ name: "state", required: true }) @ApiQuery({ name: "transactionId", required: false }) waafiReturn( @Query("referenceId") referenceId: string, @Query("state") state: string, @Query("transactionId") transactionId: string, ) { return this.service.confirmWaafiReturnDemo({ referenceId, state, transactionId, }); } @Post("refund") @PassengerStaff([PASSENGER_PERMS.payments.refund, PASSENGER_PERMS.admin]) @ApiBearerAuth("IAM-auth") @ApiOperation({ summary: "Refund a confirmed booking (staff/agent only)" }) refund(@Body() dto: RefundDto) { return this.service.refund(dto); } @Post("methods") @PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin]) @ApiBearerAuth("IAM-auth") @ApiOperation({ summary: "Add a payment system to the platform catalog (admin only)", }) addMethod(@Body() dto: AddPaymentMethodDto) { return this.service.addPaymentMethod(dto); } @Patch("methods/:id") @PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin]) @ApiBearerAuth("IAM-auth") @ApiOperation({ summary: "Update a payment method configuration (admin only)", }) updateMethod(@Param("id") id: string, @Body() dto: Partial) { return this.service.updatePaymentMethod(id, dto); } @Get("methods") @SetMetadata('isPublic', true) @ApiOperation({ summary: "List payment systems supported by the platform", description: "Returns all enabled payment methods. Optionally filter by `region` to narrow to methods available for a passenger's nationality.", }) @ApiQuery({ name: "region", enum: PaymentRegionEnum, required: false }) @ApiOkResponse({ type: [SupportedPaymentMethodDto] }) getMethods( @Query("region") region?: PaymentRegionEnum, ) { return this.service.getSupportedPaymentMethods(region); } @Get("booking-amount") @SetMetadata('isPublic', true) @ApiOperation({ summary: "Get booking amount in a specific currency", description: "Returns the booking total converted from ETB to the requested currency using the latest exchange rate. " + "If currency is ETB the stored amount is returned as-is (no conversion). " + "Amounts are returned in major currency units (e.g. 162.50 DJF, not centimes).", }) @ApiQuery({ name: "bookingId", required: true, description: "Booking UUID" }) @ApiQuery({ name: "currency", required: true, example: "DJF", description: "Target currency: ETB, DJF, or USD" }) @ApiOkResponse({ type: BookingAmountResponseDto }) getBookingAmount( @Query("bookingId") bookingId: string, @Query("currency") currency: string, ) { return this.service.getBookingAmountByCurrency(bookingId, currency); } @Get("checkout") @SetMetadata('isPublic', true) @ApiOperation({ summary: "Browser checkout redirect", description: "Initiates payment and returns an HTML page that auto-redirects the browser to the provider checkout URL. Designed to be opened directly in a browser tab.", }) @ApiQuery({ name: "bookingId", required: true }) @ApiQuery({ name: "method", enum: PaymentMethodTypeEnum, required: true }) @ApiQuery({ name: "platform", enum: ["web", "mobile"], required: false }) @ApiProduces("text/html") async checkout( @Query("bookingId") bookingId: string, @Query("method") method: PaymentMethodTypeEnum, @Query("platform") platform: PaymentPlatformDto = "web", @Res() res: Response, ) { if (!bookingId) { return res .status(HttpStatus.BAD_REQUEST) .type("html") .send( this.buildErrorHtml("Missing required query parameter: bookingId"), ); } if (!method || !Object.values(PaymentMethodTypeEnum).includes(method)) { return res .status(HttpStatus.BAD_REQUEST) .type("html") .send( this.buildErrorHtml("Missing or invalid query parameter: method"), ); } try { const result = await this.service.initiatePayment({ bookingId, method, platform, }); const url = result.clientAction?.type === "REDIRECT" ? result.clientAction.url : undefined; if (url) { return res .status(HttpStatus.OK) .type("html") .send(this.buildRedirectHtml(url)); } return res .status(HttpStatus.OK) .type("html") .send(this.buildStatusHtml(result.status, result.intentId)); } catch (err: unknown) { const message = err instanceof Error ? err.message : "An unexpected error occurred"; return res .status(HttpStatus.OK) .type("html") .send(this.buildErrorHtml(message)); } } private buildRedirectHtml(url: string): string { const escaped = url.replace(/\"/g, """); return ` Redirecting to payment…

Redirecting to payment provider…

Click here if you are not redirected

`; } private buildStatusHtml(status: string, intentId: string): string { return ` Payment status
${status}
Intent: ${intentId}
`; } private buildErrorHtml(message: string): string { return ` Payment error
Payment could not be initiated

${message}

`; } }