import { Body, Controller, Get, HttpStatus, Post, Query, Res, } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiQuery, ApiOkResponse, ApiProduces, } from "@nestjs/swagger"; import { Response } from "express"; import { Public } from "@edr/api-common"; import { BillingService } from "./billing.service"; import { InitiatePaymentDto, InitiateResponseDto, PaymentMethodTypeEnum, PaymentPlatformDto, } from "../payment/payments.dto"; /** * Central payment entrypoints. Domain-agnostic — the caller supplies an * invoice ID and the billing service resolves the amount and drives the * gateway. The domain never talks to the payment service directly. * Routes are unchanged (`/payments/*`) so the portal is unaffected. */ @ApiTags("Payment") @Controller("payments") export class PaymentController { constructor(private readonly billing: BillingService) { } @Post("initiate") @ApiOperation({ summary: "Initiate payment for an invoice", description: "Charges the invoice through the payment gateway.", }) @ApiOkResponse({ type: InitiateResponseDto }) initiate(@Body() dto: InitiatePaymentDto): Promise { return this.billing.payInvoice(dto.invoiceId, { method: dto.method, platform: dto.platform, payerAccount: dto.payerAccount, returnUrl: dto.returnUrl, failureUrl: dto.failureUrl, }); } @Get("checkout") @Public() @ApiOperation({ summary: "Browser checkout redirect", description: "Charges the invoice and returns an HTML page that auto-redirects to the provider checkout URL. Open directly in a browser tab.", }) @ApiQuery({ name: "invoiceId", required: true }) @ApiQuery({ name: "method", enum: PaymentMethodTypeEnum, required: true }) @ApiQuery({ name: "platform", enum: ["web", "mobile"], required: false }) @ApiProduces("text/html") async checkout( @Query("invoiceId") invoiceId: string, @Query("method") method: PaymentMethodTypeEnum, @Query("platform") platform: PaymentPlatformDto = "web", @Res() res: Response, ) { if (!invoiceId) { return res .status(HttpStatus.BAD_REQUEST) .type("html") .send(this.buildErrorHtml("Missing required query parameter: invoiceId")); } 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.billing.payInvoice( invoiceId, { 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}

`; } }