- Introduced `testPaymentMethods` endpoint in InvoicesController to directly test WHMCS payment methods for a specific client ID. - Added `testWhmcsPaymentMethods` method in InvoicesService for detailed logging and error handling during WHMCS API calls. - Implemented cache bypassing and enhanced logging in `getPaymentMethods` method of WhmcsPaymentService for debugging purposes. - Updated payment method transformation logic in WhmcsDataTransformer to handle variations in WHMCS API responses. - Added debug information in Checkout and AddressConfirmation components to assist in troubleshooting address confirmation flow.
308 lines
9.3 KiB
TypeScript
308 lines
9.3 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Param,
|
|
Query,
|
|
Request,
|
|
ParseIntPipe,
|
|
HttpCode,
|
|
HttpStatus,
|
|
BadRequestException,
|
|
} from "@nestjs/common";
|
|
import {
|
|
ApiTags,
|
|
ApiOperation,
|
|
ApiResponse,
|
|
ApiQuery,
|
|
ApiBearerAuth,
|
|
ApiParam,
|
|
} from "@nestjs/swagger";
|
|
import { InvoicesService } from "./invoices.service";
|
|
|
|
import {
|
|
Invoice,
|
|
InvoiceList,
|
|
InvoiceSsoLink,
|
|
Subscription,
|
|
PaymentMethodList,
|
|
PaymentGatewayList,
|
|
InvoicePaymentLink,
|
|
} from "@customer-portal/shared";
|
|
|
|
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",
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: "List of invoices with pagination",
|
|
type: Object, // Would be InvoiceList if we had proper DTO decorators
|
|
})
|
|
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",
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: "List of payment methods",
|
|
type: Object, // Would be PaymentMethodList if we had proper DTO decorators
|
|
})
|
|
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",
|
|
})
|
|
@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();
|
|
}
|
|
|
|
@Get("test-payment-methods/:clientId")
|
|
@ApiOperation({
|
|
summary: "Test WHMCS payment methods API for specific client ID",
|
|
description: "Direct test of WHMCS GetPayMethods API - TEMPORARY DEBUG ENDPOINT",
|
|
})
|
|
@ApiParam({ name: "clientId", type: Number, description: "WHMCS Client ID to test" })
|
|
async testPaymentMethods(@Param("clientId", ParseIntPipe) clientId: number): Promise<any> {
|
|
return this.invoicesService.testWhmcsPaymentMethods(clientId);
|
|
}
|
|
|
|
@Post("payment-methods/refresh")
|
|
@HttpCode(HttpStatus.OK)
|
|
@ApiOperation({
|
|
summary: "Refresh payment methods cache",
|
|
description: "Invalidates and refreshes payment methods cache for the current user",
|
|
})
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: "Payment methods cache refreshed",
|
|
type: Object,
|
|
})
|
|
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" })
|
|
@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: 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" })
|
|
@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: 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)",
|
|
})
|
|
@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: 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",
|
|
})
|
|
@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: 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;
|
|
}
|
|
}
|