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