import { Controller, Get, Post, Param, Query, UseGuards, Request, ParseIntPipe, HttpCode, HttpStatus, BadRequestException, ValidationPipe, UsePipes, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiQuery, ApiBearerAuth, ApiParam, } from '@nestjs/swagger'; import { InvoicesService } from './invoices.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { Invoice, InvoiceList, InvoiceSsoLink, Subscription, PaymentMethodList, PaymentGatewayList, InvoicePaymentLink } from '@customer-portal/shared'; @ApiTags('invoices') @Controller('invoices') @UseGuards(JwtAuthGuard) @ApiBearerAuth() export class InvoicesController { constructor(private readonly invoicesService: InvoicesService) {} @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' }) @ApiResponse({ status: 200, description: 'List of invoices with pagination', type: Object, // Would be InvoiceList if we had proper DTO decorators }) async getInvoices( @Request() req: any, @Query('page') page?: string, @Query('limit') limit?: string, @Query('status') status?: string, ): Promise { // 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 && !['Paid', 'Unpaid', 'Overdue', 'Cancelled'].includes(status)) { throw new BadRequestException('Invalid status filter'); } return this.invoicesService.getInvoices( req.user.id, { page: pageNum, limit: limitNum, status } ); } @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' }) @ApiResponse({ status: 200, description: 'Invoice details', type: Object, // Would be Invoice if we had proper DTO decorators }) @ApiResponse({ status: 404, description: 'Invoice not found' }) async getInvoiceById( @Request() req: any, @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' }) @ApiResponse({ status: 200, description: 'List of related subscriptions', type: [Object], // Would be Subscription[] if we had proper DTO decorators }) @ApiResponse({ status: 404, description: 'Invoice not found' }) async getInvoiceSubscriptions( @Request() req: any, @Param('id', ParseIntPipe) invoiceId: number, ): Promise { if (invoiceId <= 0) { throw new BadRequestException('Invoice ID must be a positive number'); } return this.invoicesService.getInvoiceSubscriptions(req.user.id, invoiceId); } @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)' }) @ApiResponse({ status: 200, description: 'SSO link created successfully', type: Object, // Would be InvoiceSsoLink if we had proper DTO decorators }) @ApiResponse({ status: 404, description: 'Invoice not found' }) async createSsoLink( @Request() req: any, @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"'); } return this.invoicesService.createSsoLink(req.user.id, invoiceId, target || 'view'); } @Get('payment-methods') @ApiOperation({ summary: 'Get user payment methods', description: 'Retrieves all saved payment methods for the authenticated user' }) @ApiResponse({ status: 200, description: 'List of payment methods', type: Object, // Would be PaymentMethodList if we had proper DTO decorators }) async getPaymentMethods( @Request() req: any, ): Promise { return this.invoicesService.getPaymentMethods(req.user.id); } @Get('payment-gateways') @ApiOperation({ summary: 'Get available payment gateways', description: 'Retrieves all active payment gateways available for payments' }) @ApiResponse({ status: 200, description: 'List of payment gateways', type: Object, // Would be PaymentGatewayList if we had proper DTO decorators }) async getPaymentGateways(): Promise { return this.invoicesService.getPaymentGateways(); } @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' }) @ApiResponse({ status: 200, description: 'Payment link created successfully', type: Object, // Would be InvoicePaymentLink if we had proper DTO decorators }) @ApiResponse({ status: 404, description: 'Invoice not found' }) async createPaymentLink( @Request() req: any, @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'); } return this.invoicesService.createPaymentSsoLink( req.user.id, invoiceId, paymentMethodIdNum, gatewayName ); } 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; } }