232 lines
7.8 KiB
TypeScript
232 lines
7.8 KiB
TypeScript
|
|
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<InvoiceList> {
|
||
|
|
// 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<Invoice> {
|
||
|
|
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<Subscription[]> {
|
||
|
|
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<InvoiceSsoLink> {
|
||
|
|
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<PaymentMethodList> {
|
||
|
|
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<PaymentGatewayList> {
|
||
|
|
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<InvoicePaymentLink> {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|