2025-08-22 17:02:49 +09:00
|
|
|
import {
|
|
|
|
|
Controller,
|
|
|
|
|
Get,
|
|
|
|
|
Post,
|
|
|
|
|
Param,
|
|
|
|
|
Query,
|
2025-08-20 18:02:50 +09:00
|
|
|
Request,
|
|
|
|
|
ParseIntPipe,
|
|
|
|
|
HttpCode,
|
|
|
|
|
HttpStatus,
|
|
|
|
|
BadRequestException,
|
2025-08-22 17:02:49 +09:00
|
|
|
} from "@nestjs/common";
|
2025-12-10 16:08:34 +09:00
|
|
|
import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service.js";
|
|
|
|
|
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service.js";
|
|
|
|
|
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
2025-12-02 11:06:54 +09:00
|
|
|
import { ZodValidationPipe } from "nestjs-zod";
|
2025-12-10 16:08:34 +09:00
|
|
|
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
|
2025-08-28 16:57:57 +09:00
|
|
|
|
2025-10-22 10:58:16 +09:00
|
|
|
import type {
|
|
|
|
|
Invoice,
|
|
|
|
|
InvoiceList,
|
|
|
|
|
InvoiceSsoLink,
|
|
|
|
|
InvoiceListQuery,
|
|
|
|
|
} from "@customer-portal/domain/billing";
|
2025-10-08 16:31:42 +09:00
|
|
|
import { invoiceListQuerySchema, invoiceSchema } from "@customer-portal/domain/billing";
|
2025-10-08 10:33:33 +09:00
|
|
|
import type { Subscription } from "@customer-portal/domain/subscriptions";
|
2025-10-22 10:58:16 +09:00
|
|
|
import type {
|
|
|
|
|
PaymentMethodList,
|
|
|
|
|
PaymentGatewayList,
|
|
|
|
|
InvoicePaymentLink,
|
|
|
|
|
} from "@customer-portal/domain/payments";
|
2025-08-20 18:02:50 +09:00
|
|
|
|
2025-10-08 16:31:42 +09:00
|
|
|
/**
|
|
|
|
|
* Invoice Controller
|
2025-10-22 10:58:16 +09:00
|
|
|
*
|
2025-10-08 16:31:42 +09:00
|
|
|
* All request validation is handled by Zod schemas via ZodValidationPipe.
|
|
|
|
|
* Business logic is delegated to service layer.
|
|
|
|
|
*/
|
2025-08-22 17:02:49 +09:00
|
|
|
@Controller("invoices")
|
2025-08-20 18:02:50 +09:00
|
|
|
export class InvoicesController {
|
2025-09-25 16:38:21 +09:00
|
|
|
constructor(
|
|
|
|
|
private readonly invoicesService: InvoicesOrchestratorService,
|
|
|
|
|
private readonly whmcsService: WhmcsService,
|
|
|
|
|
private readonly mappingsService: MappingsService
|
|
|
|
|
) {}
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
@Get()
|
|
|
|
|
async getInvoices(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-10-08 16:31:42 +09:00
|
|
|
@Query(new ZodValidationPipe(invoiceListQuerySchema)) query: InvoiceListQuery
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<InvoiceList> {
|
2025-10-02 16:33:25 +09:00
|
|
|
return this.invoicesService.getInvoices(req.user.id, query);
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-30 15:10:24 +09:00
|
|
|
@Get("payment-methods")
|
2025-10-07 17:38:39 +09:00
|
|
|
async getPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
|
2025-09-25 16:38:21 +09:00
|
|
|
const mapping = await this.mappingsService.findByUserId(req.user.id);
|
|
|
|
|
if (!mapping?.whmcsClientId) {
|
|
|
|
|
throw new Error("WHMCS client mapping not found");
|
|
|
|
|
}
|
|
|
|
|
return this.whmcsService.getPaymentMethods(mapping.whmcsClientId, req.user.id);
|
2025-08-30 15:10:24 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Get("payment-gateways")
|
|
|
|
|
async getPaymentGateways(): Promise<PaymentGatewayList> {
|
2025-09-25 16:38:21 +09:00
|
|
|
return this.whmcsService.getPaymentGateways();
|
2025-08-30 15:10:24 +09:00
|
|
|
}
|
2025-08-30 15:41:08 +09:00
|
|
|
|
2025-08-30 15:10:24 +09:00
|
|
|
@Post("payment-methods/refresh")
|
|
|
|
|
@HttpCode(HttpStatus.OK)
|
2025-10-07 17:38:39 +09:00
|
|
|
async refreshPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
|
2025-08-30 15:10:24 +09:00
|
|
|
// Invalidate cache first
|
2025-09-25 16:38:21 +09:00
|
|
|
await this.whmcsService.invalidatePaymentMethodsCache(req.user.id);
|
2025-09-01 15:11:42 +09:00
|
|
|
|
2025-08-30 15:10:24 +09:00
|
|
|
// Return fresh payment methods
|
2025-09-25 16:38:21 +09:00
|
|
|
const mapping = await this.mappingsService.findByUserId(req.user.id);
|
|
|
|
|
if (!mapping?.whmcsClientId) {
|
|
|
|
|
throw new Error("WHMCS client mapping not found");
|
|
|
|
|
}
|
|
|
|
|
return this.whmcsService.getPaymentMethods(mapping.whmcsClientId, req.user.id);
|
2025-08-30 15:10:24 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Get(":id")
|
2025-08-20 18:02:50 +09:00
|
|
|
async getInvoiceById(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<Invoice> {
|
2025-10-08 16:31:42 +09:00
|
|
|
// Validate using domain schema
|
|
|
|
|
invoiceSchema.shape.id.parse(invoiceId);
|
2025-08-20 18:02:50 +09:00
|
|
|
return this.invoicesService.getInvoiceById(req.user.id, invoiceId);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Get(":id/subscriptions")
|
2025-09-25 18:59:07 +09:00
|
|
|
getInvoiceSubscriptions(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() _req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number
|
2025-09-25 18:59:07 +09:00
|
|
|
): Subscription[] {
|
2025-10-08 16:31:42 +09:00
|
|
|
// Validate using domain schema
|
|
|
|
|
invoiceSchema.shape.id.parse(invoiceId);
|
2025-10-22 10:58:16 +09:00
|
|
|
|
2025-09-25 16:23:24 +09:00
|
|
|
// This functionality has been moved to WHMCS directly
|
|
|
|
|
// For now, return empty array as subscriptions are managed in WHMCS
|
|
|
|
|
return [];
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Post(":id/sso-link")
|
2025-08-20 18:02:50 +09:00
|
|
|
@HttpCode(HttpStatus.OK)
|
|
|
|
|
async createSsoLink(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number,
|
|
|
|
|
@Query("target") target?: "view" | "download" | "pay"
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<InvoiceSsoLink> {
|
2025-10-08 16:31:42 +09:00
|
|
|
// Validate using domain schema
|
|
|
|
|
invoiceSchema.shape.id.parse(invoiceId);
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
// Validate target parameter
|
2025-08-22 17:02:49 +09:00
|
|
|
if (target && !["view", "download", "pay"].includes(target)) {
|
2025-08-20 18:02:50 +09:00
|
|
|
throw new BadRequestException('Target must be "view", "download", or "pay"');
|
|
|
|
|
}
|
2025-08-22 17:02:49 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
const mapping = await this.mappingsService.findByUserId(req.user.id);
|
|
|
|
|
if (!mapping?.whmcsClientId) {
|
|
|
|
|
throw new Error("WHMCS client mapping not found");
|
|
|
|
|
}
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-29 11:15:43 +09:00
|
|
|
const ssoUrl = await this.whmcsService.whmcsSsoForInvoice(
|
2025-09-25 16:38:21 +09:00
|
|
|
mapping.whmcsClientId,
|
2025-09-29 11:15:43 +09:00
|
|
|
invoiceId,
|
|
|
|
|
target || "view"
|
2025-09-25 16:38:21 +09:00
|
|
|
);
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
return {
|
2025-09-29 11:15:43 +09:00
|
|
|
url: ssoUrl,
|
|
|
|
|
expiresAt: new Date(Date.now() + 60000).toISOString(), // 60 seconds per WHMCS spec
|
2025-09-25 16:38:21 +09:00
|
|
|
};
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Post(":id/payment-link")
|
2025-08-20 18:02:50 +09:00
|
|
|
@HttpCode(HttpStatus.OK)
|
|
|
|
|
async createPaymentLink(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number,
|
|
|
|
|
@Query("paymentMethodId") paymentMethodId?: string,
|
|
|
|
|
@Query("gatewayName") gatewayName?: string
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<InvoicePaymentLink> {
|
2025-10-08 16:31:42 +09:00
|
|
|
// Validate using domain schema
|
|
|
|
|
invoiceSchema.shape.id.parse(invoiceId);
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
const paymentMethodIdNum = paymentMethodId ? parseInt(paymentMethodId, 10) : undefined;
|
|
|
|
|
if (paymentMethodId && (isNaN(paymentMethodIdNum!) || paymentMethodIdNum! <= 0)) {
|
2025-08-22 17:02:49 +09:00
|
|
|
throw new BadRequestException("Payment method ID must be a positive number");
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
2025-08-22 17:02:49 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
const mapping = await this.mappingsService.findByUserId(req.user.id);
|
|
|
|
|
if (!mapping?.whmcsClientId) {
|
|
|
|
|
throw new Error("WHMCS client mapping not found");
|
|
|
|
|
}
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
const ssoResult = await this.whmcsService.createPaymentSsoToken(
|
|
|
|
|
mapping.whmcsClientId,
|
2025-08-22 17:02:49 +09:00
|
|
|
invoiceId,
|
2025-09-25 16:38:21 +09:00
|
|
|
paymentMethodIdNum,
|
|
|
|
|
gatewayName || "stripe"
|
2025-08-20 18:02:50 +09:00
|
|
|
);
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
return {
|
|
|
|
|
url: ssoResult.url,
|
|
|
|
|
expiresAt: ssoResult.expiresAt,
|
|
|
|
|
gatewayName: gatewayName || "stripe",
|
|
|
|
|
};
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
}
|