mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 20:10:56 +00:00
178 lines
6.1 KiB
TypeScript
178 lines
6.1 KiB
TypeScript
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<InitiateResponseDto> {
|
|
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 `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta http-equiv="refresh" content="0;url=${escaped}">
|
|
<title>Redirecting to payment…</title>
|
|
<style>
|
|
body { font-family: sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
|
.card { background: #fff; border-radius: 8px; padding: 40px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,.1); max-width: 400px; }
|
|
.spinner { width: 40px; height: 40px; border: 4px solid #e0e0e0; border-top-color: #1a73e8; border-radius: 50%; animation: spin .8s linear infinite; margin: 0 auto 20px; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
p { color: #555; margin: 0 0 16px; }
|
|
a { color: #1a73e8; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="spinner"></div>
|
|
<p>Redirecting to payment provider…</p>
|
|
<p><a href="${escaped}">Click here if you are not redirected</a></p>
|
|
</div>
|
|
<script>window.location.href = "${escaped}";</script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
private buildStatusHtml(status: string, intentId: string): string {
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Payment status</title>
|
|
<style>
|
|
body { font-family: sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
|
.card { background: #fff; border-radius: 8px; padding: 40px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,.1); max-width: 400px; }
|
|
.status { font-size: 1.1rem; font-weight: 600; color: #333; margin-bottom: 8px; }
|
|
small { color: #888; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="status">${status}</div>
|
|
<small>Intent: ${intentId}</small>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
private buildErrorHtml(message: string): string {
|
|
return `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Payment error</title>
|
|
<style>
|
|
body { font-family: sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
|
.card { background: #fff; border-radius: 8px; padding: 40px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,.1); max-width: 400px; }
|
|
.error { color: #d32f2f; font-weight: 600; margin-bottom: 8px; }
|
|
p { color: #555; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="error">Payment could not be initiated</div>
|
|
<p>${message}</p>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
}
|