import { Controller, Get, Post, Param, Query, Request, ParseIntPipe, HttpCode, HttpStatus, BadRequestException, } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiResponse, ApiOkResponse, ApiQuery, ApiBearerAuth, ApiParam, } from "@nestjs/swagger"; 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 type { Invoice, InvoiceList, InvoiceSsoLink, Subscription, PaymentMethodList, PaymentGatewayList, InvoicePaymentLink, } from "@customer-portal/domain"; interface AuthenticatedRequest { user: { id: string }; } @ApiTags("invoices") @Controller("invoices") @ApiBearerAuth() export class InvoicesController { constructor( private readonly invoicesService: InvoicesOrchestratorService, private readonly whmcsService: WhmcsService, private readonly mappingsService: MappingsService ) {} @Get() @ApiOperation({ summary: "Get paginated list of user invoices", description: "Retrieves invoices for the authenticated user with pagination and optional status filtering", }) @ApiQuery({ name: "page", required: false, type: Number, description: "Page number (default: 1)", }) @ApiQuery({ name: "limit", required: false, type: Number, description: "Items per page (default: 10)", }) @ApiQuery({ name: "status", required: false, type: String, description: "Filter by invoice status", }) @ApiOkResponse({ description: "List of invoices with pagination" }) async getInvoices( @Request() req: AuthenticatedRequest, @Query("page") page?: string, @Query("limit") limit?: string, @Query("status") status?: string ): Promise { const validStatuses = ["Paid", "Unpaid", "Overdue", "Cancelled", "Collections"] as const; type InvoiceStatus = (typeof validStatuses)[number]; // Validate and sanitize input const pageNum = this.validatePositiveInteger(page, 1, "page"); const limitNum = this.validatePositiveInteger(limit, 10, "limit"); // Limit max page size for performance if (limitNum > 100) { throw new BadRequestException("Limit cannot exceed 100 items per page"); } // Validate status if provided if (status && !validStatuses.includes(status as InvoiceStatus)) { throw new BadRequestException("Invalid status filter"); } const typedStatus = status ? (status as InvoiceStatus) : undefined; return this.invoicesService.getInvoices(req.user.id, { page: pageNum, limit: limitNum, status: typedStatus, }); } @Get("payment-methods") @ApiOperation({ summary: "Get user payment methods", description: "Retrieves all saved payment methods for the authenticated user", }) @ApiOkResponse({ description: "List of payment methods" }) async getPaymentMethods(@Request() req: AuthenticatedRequest): 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") @ApiOperation({ summary: "Get available payment gateways", description: "Retrieves all active payment gateways available for payments", }) @ApiOkResponse({ description: "List of payment gateways" }) async getPaymentGateways(): Promise { return this.whmcsService.getPaymentGateways(); } @Post("payment-methods/refresh") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Refresh payment methods cache", description: "Invalidates and refreshes payment methods cache for the current user", }) @ApiOkResponse({ description: "Payment methods cache refreshed" }) async refreshPaymentMethods(@Request() req: AuthenticatedRequest): 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") @ApiOperation({ summary: "Get invoice details by ID", description: "Retrieves detailed information for a specific invoice", }) @ApiParam({ name: "id", type: Number, description: "Invoice ID" }) @ApiOkResponse({ description: "Invoice details" }) @ApiResponse({ status: 404, description: "Invoice not found" }) async getInvoiceById( @Request() req: AuthenticatedRequest, @Param("id", ParseIntPipe) invoiceId: number ): Promise { if (invoiceId <= 0) { throw new BadRequestException("Invoice ID must be a positive number"); } return this.invoicesService.getInvoiceById(req.user.id, invoiceId); } @Get(":id/subscriptions") @ApiOperation({ summary: "Get subscriptions related to an invoice", description: "Retrieves all subscriptions that are referenced in the invoice items", }) @ApiParam({ name: "id", type: Number, description: "Invoice ID" }) @ApiOkResponse({ description: "List of related subscriptions" }) @ApiResponse({ status: 404, description: "Invoice not found" }) async getInvoiceSubscriptions( @Request() req: AuthenticatedRequest, @Param("id", ParseIntPipe) invoiceId: number ): Promise { if (invoiceId <= 0) { throw new BadRequestException("Invoice ID must be a positive number"); } // 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) @ApiOperation({ summary: "Create SSO link for invoice", description: "Generates a single sign-on link to view/pay the invoice or download PDF in WHMCS", }) @ApiParam({ name: "id", type: Number, description: "Invoice ID" }) @ApiQuery({ name: "target", required: false, enum: ["view", "download", "pay"], description: "Link target: view invoice, download PDF, or go to payment page (default: view)", }) @ApiOkResponse({ description: "SSO link created successfully" }) @ApiResponse({ status: 404, description: "Invoice not found" }) async createSsoLink( @Request() req: AuthenticatedRequest, @Param("id", ParseIntPipe) invoiceId: number, @Query("target") target?: "view" | "download" | "pay" ): Promise { if (invoiceId <= 0) { throw new BadRequestException("Invoice ID must be a positive number"); } // 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 ssoResult = await this.whmcsService.createSsoToken( mapping.whmcsClientId, invoiceId ? `index.php?rp=/invoice/${invoiceId}` : undefined ); return { url: ssoResult.url, expiresAt: ssoResult.expiresAt, }; } @Post(":id/payment-link") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Create payment link for invoice with payment method", description: "Generates a payment link for the invoice with a specific payment method or gateway", }) @ApiParam({ name: "id", type: Number, description: "Invoice ID" }) @ApiQuery({ name: "paymentMethodId", required: false, type: Number, description: "Payment method ID", }) @ApiQuery({ name: "gatewayName", required: false, type: String, description: "Payment gateway name", }) @ApiOkResponse({ description: "Payment link created successfully" }) @ApiResponse({ status: 404, description: "Invoice not found" }) async createPaymentLink( @Request() req: AuthenticatedRequest, @Param("id", ParseIntPipe) invoiceId: number, @Query("paymentMethodId") paymentMethodId?: string, @Query("gatewayName") gatewayName?: string ): Promise { if (invoiceId <= 0) { throw new BadRequestException("Invoice ID must be a positive number"); } 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", }; } private validatePositiveInteger( value: string | undefined, defaultValue: number, fieldName: string ): number { if (!value) { return defaultValue; } const parsed = parseInt(value, 10); if (isNaN(parsed) || parsed <= 0) { throw new BadRequestException(`${fieldName} must be a positive integer`); } return parsed; } }