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

307 lines
9.5 KiB
TypeScript
Raw Normal View History

2025-08-22 17:02:49 +09:00
import {
Controller,
Get,
Post,
Param,
Query,
Request,
ParseIntPipe,
HttpCode,
HttpStatus,
BadRequestException,
2025-08-22 17:02:49 +09:00
} from "@nestjs/common";
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiOkResponse,
2025-08-22 17:02:49 +09:00
ApiQuery,
ApiBearerAuth,
ApiParam,
2025-08-22 17:02:49 +09:00
} 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";
2025-08-28 16:57:57 +09:00
import type {
2025-08-22 17:02:49 +09:00
Invoice,
InvoiceList,
InvoiceSsoLink,
Subscription,
PaymentMethodList,
PaymentGatewayList,
InvoicePaymentLink,
} from "@customer-portal/domain";
2025-08-22 17:02:49 +09:00
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()
2025-08-22 17:02:49 +09:00
@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(
2025-08-22 17:02:49 +09:00
@Request() req: AuthenticatedRequest,
@Query("page") page?: string,
@Query("limit") limit?: string,
@Query("status") status?: string
): Promise<InvoiceList> {
2025-08-27 10:54:05 +09:00
const validStatuses = ["Paid", "Unpaid", "Overdue", "Cancelled", "Collections"] as const;
type InvoiceStatus = (typeof validStatuses)[number];
// Validate and sanitize input
2025-08-22 17:02:49 +09:00
const pageNum = this.validatePositiveInteger(page, 1, "page");
const limitNum = this.validatePositiveInteger(limit, 10, "limit");
// Limit max page size for performance
if (limitNum > 100) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Limit cannot exceed 100 items per page");
}
// Validate status if provided
2025-08-27 10:54:05 +09:00
if (status && !validStatuses.includes(status as InvoiceStatus)) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Invalid status filter");
}
2025-08-22 17:02:49 +09:00
2025-08-27 10:54:05 +09:00
const typedStatus = status ? (status as InvoiceStatus) : undefined;
2025-08-22 17:02:49 +09:00
return this.invoicesService.getInvoices(req.user.id, {
page: pageNum,
limit: limitNum,
2025-08-27 10:54:05 +09:00
status: typedStatus,
2025-08-22 17:02:49 +09:00
});
}
@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> {
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<PaymentGatewayList> {
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<PaymentMethodList> {
// 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);
}
2025-08-22 17:02:49 +09:00
@Get(":id")
@ApiOperation({
summary: "Get invoice details by ID",
description: "Retrieves detailed information for a specific invoice",
})
2025-08-22 17:02:49 +09:00
@ApiParam({ name: "id", type: Number, description: "Invoice ID" })
@ApiOkResponse({ description: "Invoice details" })
2025-08-22 17:02:49 +09:00
@ApiResponse({ status: 404, description: "Invoice not found" })
async getInvoiceById(
2025-08-22 17:02:49 +09:00
@Request() req: AuthenticatedRequest,
@Param("id", ParseIntPipe) invoiceId: number
): Promise<Invoice> {
if (invoiceId <= 0) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Invoice ID must be a positive number");
}
2025-08-22 17:02:49 +09:00
return this.invoicesService.getInvoiceById(req.user.id, invoiceId);
}
2025-08-22 17:02:49 +09:00
@Get(":id/subscriptions")
@ApiOperation({
summary: "Get subscriptions related to an invoice",
description: "Retrieves all subscriptions that are referenced in the invoice items",
})
2025-08-22 17:02:49 +09:00
@ApiParam({ name: "id", type: Number, description: "Invoice ID" })
@ApiOkResponse({ description: "List of related subscriptions" })
2025-08-22 17:02:49 +09:00
@ApiResponse({ status: 404, description: "Invoice not found" })
getInvoiceSubscriptions(
2025-08-22 17:02:49 +09:00
@Request() req: AuthenticatedRequest,
@Param("id", ParseIntPipe) invoiceId: number
): Subscription[] {
if (invoiceId <= 0) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Invoice ID must be a positive number");
}
2025-08-22 17:02:49 +09:00
// This functionality has been moved to WHMCS directly
// For now, return empty array as subscriptions are managed in WHMCS
return [];
}
2025-08-22 17:02:49 +09:00
@Post(":id/sso-link")
@HttpCode(HttpStatus.OK)
2025-08-22 17:02:49 +09:00
@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" })
2025-08-22 17:02:49 +09:00
@ApiResponse({ status: 404, description: "Invoice not found" })
async createSsoLink(
2025-08-22 17:02:49 +09:00
@Request() req: AuthenticatedRequest,
@Param("id", ParseIntPipe) invoiceId: number,
@Query("target") target?: "view" | "download" | "pay"
): Promise<InvoiceSsoLink> {
if (invoiceId <= 0) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Invoice ID must be a positive number");
}
// Validate target parameter
2025-08-22 17:02:49 +09:00
if (target && !["view", "download", "pay"].includes(target)) {
throw new BadRequestException('Target must be "view", "download", or "pay"');
}
2025-08-22 17:02:49 +09:00
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,
};
}
2025-08-22 17:02:49 +09:00
@Post(":id/payment-link")
@HttpCode(HttpStatus.OK)
2025-08-22 17:02:49 +09:00
@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" })
2025-08-22 17:02:49 +09:00
@ApiResponse({ status: 404, description: "Invoice not found" })
async createPaymentLink(
2025-08-22 17:02:49 +09:00
@Request() req: AuthenticatedRequest,
@Param("id", ParseIntPipe) invoiceId: number,
@Query("paymentMethodId") paymentMethodId?: string,
@Query("gatewayName") gatewayName?: string
): Promise<InvoicePaymentLink> {
if (invoiceId <= 0) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Invoice ID must be a positive number");
}
const paymentMethodIdNum = paymentMethodId ? parseInt(paymentMethodId, 10) : undefined;
if (paymentMethodId && (isNaN(paymentMethodIdNum!) || paymentMethodIdNum! <= 0)) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Payment method ID must be a positive number");
}
2025-08-22 17:02:49 +09:00
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,
2025-08-22 17:02:49 +09:00
invoiceId,
paymentMethodIdNum,
gatewayName || "stripe"
);
return {
url: ssoResult.url,
expiresAt: ssoResult.expiresAt,
gatewayName: gatewayName || "stripe",
};
}
2025-08-22 17:02:49 +09:00
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;
}
}