Assist_Design/apps/bff/src/modules/invoices/invoices.controller.ts

267 lines
8.1 KiB
TypeScript

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 { InvoicesService } from "./invoices.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: 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",
})
@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<InvoiceList> {
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<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",
})
@ApiOkResponse({ description: "List of payment gateways" })
async getPaymentGateways(): Promise<PaymentGatewayList> {
return this.invoicesService.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<PaymentMethodList> {
// Invalidate cache first
await this.invoicesService.invalidatePaymentMethodsCache(req.user.id);
// Return fresh payment methods
return this.invoicesService.getPaymentMethods(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<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" })
@ApiOkResponse({ description: "List of related subscriptions" })
@ApiResponse({ status: 404, description: "Invoice not found" })
async getInvoiceSubscriptions(
@Request() req: AuthenticatedRequest,
@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)",
})
@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<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");
}
@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<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;
}
}