2025-08-22 17:02:49 +09:00
|
|
|
import {
|
|
|
|
|
Controller,
|
|
|
|
|
Get,
|
|
|
|
|
Post,
|
|
|
|
|
Param,
|
|
|
|
|
Query,
|
2025-08-20 18:02:50 +09:00
|
|
|
Request,
|
|
|
|
|
ParseIntPipe,
|
|
|
|
|
HttpCode,
|
|
|
|
|
HttpStatus,
|
|
|
|
|
BadRequestException,
|
2025-10-02 16:33:25 +09:00
|
|
|
UsePipes,
|
2025-08-22 17:02:49 +09:00
|
|
|
} from "@nestjs/common";
|
2025-09-25 16:38:21 +09:00
|
|
|
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-10-02 16:33:25 +09:00
|
|
|
import { ZodValidationPipe } from "@bff/core/validation";
|
2025-10-07 17:38:39 +09:00
|
|
|
import type { RequestWithUser } from "@bff/modules/auth/auth.types";
|
2025-08-28 16:57:57 +09:00
|
|
|
|
2025-10-08 10:33:33 +09:00
|
|
|
import type { Invoice, InvoiceList, InvoiceSsoLink, InvoiceListQuery } from "@customer-portal/domain/billing";
|
2025-10-03 17:08:42 +09:00
|
|
|
import { invoiceListQuerySchema } from "@customer-portal/domain/billing";
|
2025-10-08 10:33:33 +09:00
|
|
|
import type { Subscription } from "@customer-portal/domain/subscriptions";
|
|
|
|
|
import type { PaymentMethodList, PaymentGatewayList, InvoicePaymentLink } from "@customer-portal/domain/payments";
|
2025-08-20 18:02:50 +09:00
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Controller("invoices")
|
2025-08-20 18:02:50 +09:00
|
|
|
export class InvoicesController {
|
2025-09-25 16:38:21 +09:00
|
|
|
constructor(
|
|
|
|
|
private readonly invoicesService: InvoicesOrchestratorService,
|
|
|
|
|
private readonly whmcsService: WhmcsService,
|
|
|
|
|
private readonly mappingsService: MappingsService
|
|
|
|
|
) {}
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
@Get()
|
2025-10-02 16:33:25 +09:00
|
|
|
@UsePipes(new ZodValidationPipe(invoiceListQuerySchema))
|
2025-08-20 18:02:50 +09:00
|
|
|
async getInvoices(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-10-02 16:33:25 +09:00
|
|
|
@Query() query: InvoiceListQuery
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<InvoiceList> {
|
2025-10-02 16:33:25 +09:00
|
|
|
return this.invoicesService.getInvoices(req.user.id, query);
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-30 15:10:24 +09:00
|
|
|
@Get("payment-methods")
|
2025-10-07 17:38:39 +09:00
|
|
|
async getPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
|
2025-09-25 16:38:21 +09:00
|
|
|
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-30 15:10:24 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Get("payment-gateways")
|
|
|
|
|
async getPaymentGateways(): Promise<PaymentGatewayList> {
|
2025-09-25 16:38:21 +09:00
|
|
|
return this.whmcsService.getPaymentGateways();
|
2025-08-30 15:10:24 +09:00
|
|
|
}
|
2025-08-30 15:41:08 +09:00
|
|
|
|
2025-08-30 15:10:24 +09:00
|
|
|
@Post("payment-methods/refresh")
|
|
|
|
|
@HttpCode(HttpStatus.OK)
|
2025-10-07 17:38:39 +09:00
|
|
|
async refreshPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
|
2025-08-30 15:10:24 +09:00
|
|
|
// Invalidate cache first
|
2025-09-25 16:38:21 +09:00
|
|
|
await this.whmcsService.invalidatePaymentMethodsCache(req.user.id);
|
2025-09-01 15:11:42 +09:00
|
|
|
|
2025-08-30 15:10:24 +09:00
|
|
|
// Return fresh payment methods
|
2025-09-25 16:38:21 +09:00
|
|
|
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-30 15:10:24 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Get(":id")
|
2025-08-20 18:02:50 +09:00
|
|
|
async getInvoiceById(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number
|
2025-08-20 18:02:50 +09:00
|
|
|
): 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-20 18:02:50 +09:00
|
|
|
}
|
2025-08-22 17:02:49 +09:00
|
|
|
|
2025-08-20 18:02:50 +09:00
|
|
|
return this.invoicesService.getInvoiceById(req.user.id, invoiceId);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Get(":id/subscriptions")
|
2025-09-25 18:59:07 +09:00
|
|
|
getInvoiceSubscriptions(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() _req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number
|
2025-09-25 18:59:07 +09:00
|
|
|
): Subscription[] {
|
2025-08-20 18:02:50 +09:00
|
|
|
if (invoiceId <= 0) {
|
2025-08-22 17:02:49 +09:00
|
|
|
throw new BadRequestException("Invoice ID must be a positive number");
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
2025-08-22 17:02:49 +09:00
|
|
|
|
2025-09-25 16:23:24 +09:00
|
|
|
// This functionality has been moved to WHMCS directly
|
|
|
|
|
// For now, return empty array as subscriptions are managed in WHMCS
|
|
|
|
|
return [];
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Post(":id/sso-link")
|
2025-08-20 18:02:50 +09:00
|
|
|
@HttpCode(HttpStatus.OK)
|
|
|
|
|
async createSsoLink(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number,
|
|
|
|
|
@Query("target") target?: "view" | "download" | "pay"
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<InvoiceSsoLink> {
|
|
|
|
|
if (invoiceId <= 0) {
|
2025-08-22 17:02:49 +09:00
|
|
|
throw new BadRequestException("Invoice ID must be a positive number");
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate target parameter
|
2025-08-22 17:02:49 +09:00
|
|
|
if (target && !["view", "download", "pay"].includes(target)) {
|
2025-08-20 18:02:50 +09:00
|
|
|
throw new BadRequestException('Target must be "view", "download", or "pay"');
|
|
|
|
|
}
|
2025-08-22 17:02:49 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
const mapping = await this.mappingsService.findByUserId(req.user.id);
|
|
|
|
|
if (!mapping?.whmcsClientId) {
|
|
|
|
|
throw new Error("WHMCS client mapping not found");
|
|
|
|
|
}
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-29 11:15:43 +09:00
|
|
|
const ssoUrl = await this.whmcsService.whmcsSsoForInvoice(
|
2025-09-25 16:38:21 +09:00
|
|
|
mapping.whmcsClientId,
|
2025-09-29 11:15:43 +09:00
|
|
|
invoiceId,
|
|
|
|
|
target || "view"
|
2025-09-25 16:38:21 +09:00
|
|
|
);
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
return {
|
2025-09-29 11:15:43 +09:00
|
|
|
url: ssoUrl,
|
|
|
|
|
expiresAt: new Date(Date.now() + 60000).toISOString(), // 60 seconds per WHMCS spec
|
2025-09-25 16:38:21 +09:00
|
|
|
};
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
@Post(":id/payment-link")
|
2025-08-20 18:02:50 +09:00
|
|
|
@HttpCode(HttpStatus.OK)
|
|
|
|
|
async createPaymentLink(
|
2025-10-07 17:38:39 +09:00
|
|
|
@Request() req: RequestWithUser,
|
2025-08-22 17:02:49 +09:00
|
|
|
@Param("id", ParseIntPipe) invoiceId: number,
|
|
|
|
|
@Query("paymentMethodId") paymentMethodId?: string,
|
|
|
|
|
@Query("gatewayName") gatewayName?: string
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<InvoicePaymentLink> {
|
|
|
|
|
if (invoiceId <= 0) {
|
2025-08-22 17:02:49 +09:00
|
|
|
throw new BadRequestException("Invoice ID must be a positive number");
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-20 18:02:50 +09:00
|
|
|
}
|
2025-08-22 17:02:49 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
const mapping = await this.mappingsService.findByUserId(req.user.id);
|
|
|
|
|
if (!mapping?.whmcsClientId) {
|
|
|
|
|
throw new Error("WHMCS client mapping not found");
|
|
|
|
|
}
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
const ssoResult = await this.whmcsService.createPaymentSsoToken(
|
|
|
|
|
mapping.whmcsClientId,
|
2025-08-22 17:02:49 +09:00
|
|
|
invoiceId,
|
2025-09-25 16:38:21 +09:00
|
|
|
paymentMethodIdNum,
|
|
|
|
|
gatewayName || "stripe"
|
2025-08-20 18:02:50 +09:00
|
|
|
);
|
2025-09-25 17:42:36 +09:00
|
|
|
|
2025-09-25 16:38:21 +09:00
|
|
|
return {
|
|
|
|
|
url: ssoResult.url,
|
|
|
|
|
expiresAt: ssoResult.expiresAt,
|
|
|
|
|
gatewayName: gatewayName || "stripe",
|
|
|
|
|
};
|
2025-08-20 18:02:50 +09:00
|
|
|
}
|
|
|
|
|
}
|