import { Controller, Get, Post, Param, Query, Request, ParseIntPipe, HttpCode, HttpStatus, } from "@nestjs/common"; 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"; import { createZodDto, ZodResponse } from "nestjs-zod"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import type { Invoice, InvoiceList, InvoiceSsoLink } from "@customer-portal/domain/billing"; import { invoiceListQuerySchema, invoiceListSchema, invoiceSchema, invoiceSsoLinkSchema, invoiceSsoQuerySchema, invoicePaymentLinkQuerySchema, } from "@customer-portal/domain/billing"; import type { Subscription } from "@customer-portal/domain/subscriptions"; import type { PaymentMethodList, PaymentGatewayList, InvoicePaymentLink, } from "@customer-portal/domain/payments"; import { paymentMethodListSchema, paymentGatewayListSchema, invoicePaymentLinkSchema, } from "@customer-portal/domain/payments"; class InvoiceListQueryDto extends createZodDto(invoiceListQuerySchema) {} class InvoiceListDto extends createZodDto(invoiceListSchema) {} class InvoiceDto extends createZodDto(invoiceSchema) {} class InvoiceSsoLinkDto extends createZodDto(invoiceSsoLinkSchema) {} class InvoiceSsoQueryDto extends createZodDto(invoiceSsoQuerySchema) {} class InvoicePaymentLinkQueryDto extends createZodDto(invoicePaymentLinkQuerySchema) {} class PaymentMethodListDto extends createZodDto(paymentMethodListSchema) {} class PaymentGatewayListDto extends createZodDto(paymentGatewayListSchema) {} class InvoicePaymentLinkDto extends createZodDto(invoicePaymentLinkSchema) {} /** * Invoice Controller * * All request validation is handled by Zod schemas via global 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() @ZodResponse({ description: "List invoices", type: InvoiceListDto }) async getInvoices( @Request() req: RequestWithUser, @Query() query: InvoiceListQueryDto ): Promise { return this.invoicesService.getInvoices(req.user.id, query); } @Get("payment-methods") @ZodResponse({ description: "List payment methods", type: PaymentMethodListDto }) 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") @ZodResponse({ description: "List payment gateways", type: PaymentGatewayListDto }) async getPaymentGateways(): Promise { return this.whmcsService.getPaymentGateways(); } @Post("payment-methods/refresh") @HttpCode(HttpStatus.OK) @ZodResponse({ description: "Refresh payment methods", type: PaymentMethodListDto }) 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") @ZodResponse({ description: "Get invoice by id", type: InvoiceDto }) async getInvoiceById( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) invoiceId: number ): Promise { return this.invoicesService.getInvoiceById(req.user.id, invoiceId); } @Get(":id/subscriptions") getInvoiceSubscriptions( @Request() _req: RequestWithUser, @Param("id", ParseIntPipe) _invoiceId: number ): Subscription[] { // 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) @ZodResponse({ description: "Create invoice SSO link", type: InvoiceSsoLinkDto }) async createSsoLink( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) invoiceId: number, @Query() query: InvoiceSsoQueryDto ): Promise { const mapping = await this.mappingsService.findByUserId(req.user.id); if (!mapping?.whmcsClientId) { throw new Error("WHMCS client mapping not found"); } const parsedQuery = invoiceSsoQuerySchema.parse(query as unknown); const ssoUrl = await this.whmcsService.whmcsSsoForInvoice( mapping.whmcsClientId, invoiceId, parsedQuery.target ); return { url: ssoUrl, expiresAt: new Date(Date.now() + 60000).toISOString(), // 60 seconds per WHMCS spec }; } @Post(":id/payment-link") @HttpCode(HttpStatus.OK) @ZodResponse({ description: "Create invoice payment link", type: InvoicePaymentLinkDto }) async createPaymentLink( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) invoiceId: number, @Query() query: InvoicePaymentLinkQueryDto ): Promise { const mapping = await this.mappingsService.findByUserId(req.user.id); if (!mapping?.whmcsClientId) { throw new Error("WHMCS client mapping not found"); } const parsedQuery = invoicePaymentLinkQuerySchema.parse(query as unknown); const ssoResult = await this.whmcsService.createPaymentSsoToken( mapping.whmcsClientId, invoiceId, parsedQuery.paymentMethodId, parsedQuery.gatewayName ); return { url: ssoResult.url, expiresAt: ssoResult.expiresAt, gatewayName: parsedQuery.gatewayName, }; } }