Assist_Design/apps/bff/src/vendors/whmcs/services/whmcs-invoice.service.ts
tema ac259ce902 Enhance SIM management service with payment processing and API integration
- Implemented WHMCS invoice creation and payment capture in SimManagementService for top-ups.
- Updated top-up logic to calculate costs based on GB input, with pricing set at 500 JPY per GB.
- Simplified the Top Up Modal interface, removing unnecessary fields and improving user experience.
- Added new methods in WhmcsService for invoice and payment operations.
- Enhanced error handling for payment failures and added transaction logging for audit purposes.
- Updated documentation to reflect changes in the SIM management flow and API interactions.
2025-09-06 13:57:18 +09:00

346 lines
11 KiB
TypeScript

import { getErrorMessage } from "../../../common/utils/error.util";
import { Logger } from "nestjs-pino";
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { Invoice, InvoiceList } from "@customer-portal/shared";
import { WhmcsConnectionService } from "./whmcs-connection.service";
import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
import {
WhmcsGetInvoicesParams,
WhmcsCreateInvoiceParams,
WhmcsCreateInvoiceResponse,
WhmcsCapturePaymentParams,
WhmcsCapturePaymentResponse
} from "../types/whmcs-api.types";
export interface InvoiceFilters {
status?: "Paid" | "Unpaid" | "Cancelled" | "Overdue" | "Collections";
page?: number;
limit?: number;
}
@Injectable()
export class WhmcsInvoiceService {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly connectionService: WhmcsConnectionService,
private readonly dataTransformer: WhmcsDataTransformer,
private readonly cacheService: WhmcsCacheService
) {}
/**
* Get paginated invoices for a client with caching
*/
async getInvoices(
clientId: number,
userId: string,
filters: InvoiceFilters = {}
): Promise<InvoiceList> {
const { status, page = 1, limit = 10 } = filters;
try {
// Try cache first
const cached = await this.cacheService.getInvoicesList(userId, page, limit, status);
if (cached) {
this.logger.debug(`Cache hit for invoices: user ${userId}, page ${page}`);
return cached;
}
// Calculate pagination for WHMCS API
const limitstart = (page - 1) * limit;
// Fetch from WHMCS API
const params: WhmcsGetInvoicesParams = {
userid: clientId, // WHMCS API uses 'userid' parameter, not 'clientid'
limitstart,
limitnum: limit,
orderby: "date",
order: "DESC",
...(status && { status: status as WhmcsGetInvoicesParams["status"] }),
};
const response = await this.connectionService.getInvoices(params);
if (!response.invoices?.invoice) {
this.logger.warn(`No invoices found for client ${clientId}`);
return {
invoices: [],
pagination: {
page,
totalPages: 0,
totalItems: 0,
},
};
}
// Transform invoices (note: items are not included by GetInvoices API)
const invoices = response.invoices.invoice
.map(whmcsInvoice => {
try {
return this.dataTransformer.transformInvoice(whmcsInvoice);
} catch (error) {
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
error: getErrorMessage(error),
});
return null;
}
})
.filter((invoice): invoice is Invoice => invoice !== null);
// Build result with pagination
this.logger.debug(`WHMCS GetInvoices Response Analysis for Client ${clientId}:`, {
totalresults: response.totalresults,
numreturned: response.numreturned,
startnumber: response.startnumber,
actualInvoicesReturned: invoices.length,
requestParams: {
userid: clientId,
limitstart,
limitnum: limit,
orderby: "date",
order: "DESC",
},
});
const totalItems = response.totalresults || 0;
const totalPages = Math.ceil(totalItems / limit);
const result: InvoiceList = {
invoices,
pagination: {
page,
totalPages,
totalItems,
nextCursor: page < totalPages ? (page + 1).toString() : undefined,
},
};
// Cache the result
await this.cacheService.setInvoicesList(userId, page, limit, status, result);
this.logger.log(`Fetched ${invoices.length} invoices for client ${clientId}, page ${page}`);
return result;
} catch (error) {
this.logger.error(`Failed to fetch invoices for client ${clientId}`, {
error: getErrorMessage(error),
filters,
});
throw error;
}
}
/**
* Get invoices with items (for subscription linking)
* This method fetches invoices and then enriches them with item details
*/
async getInvoicesWithItems(
clientId: number,
userId: string,
filters: InvoiceFilters = {}
): Promise<InvoiceList> {
try {
// First get the basic invoices list
const invoiceList = await this.getInvoices(clientId, userId, filters);
// For each invoice, fetch the detailed version with items
const invoicesWithItems = await Promise.all(
invoiceList.invoices.map(async (invoice: Invoice) => {
try {
// Get detailed invoice with items
const detailedInvoice = await this.getInvoiceById(clientId, userId, invoice.id);
return detailedInvoice;
} catch (error) {
this.logger.warn(
`Failed to fetch details for invoice ${invoice.id}`,
getErrorMessage(error)
);
// Return the basic invoice if detailed fetch fails
return invoice;
}
})
);
const result: InvoiceList = {
invoices: invoicesWithItems,
pagination: invoiceList.pagination,
};
this.logger.log(
`Fetched ${invoicesWithItems.length} invoices with items for client ${clientId}`
);
return result;
} catch (error) {
this.logger.error(`Failed to fetch invoices with items for client ${clientId}`, {
error: getErrorMessage(error),
filters,
});
throw error;
}
}
/**
* Get individual invoice by ID with caching
*/
async getInvoiceById(clientId: number, userId: string, invoiceId: number): Promise<Invoice> {
try {
// Try cache first
const cached = await this.cacheService.getInvoice(userId, invoiceId);
if (cached) {
this.logger.debug(`Cache hit for invoice: user ${userId}, invoice ${invoiceId}`);
return cached;
}
// Fetch from WHMCS API
const response = await this.connectionService.getInvoice(invoiceId);
if (!response.invoiceid) {
throw new NotFoundException(`Invoice ${invoiceId} not found`);
}
// Verify the invoice belongs to this client
const invoiceClientId = response.userid;
if (invoiceClientId !== clientId) {
throw new NotFoundException(`Invoice ${invoiceId} not found`);
}
// Transform invoice
const invoice = this.dataTransformer.transformInvoice(response);
// Validate transformation
if (!this.dataTransformer.validateInvoice(invoice)) {
throw new Error(`Invalid invoice data after transformation`);
}
// Cache the result
await this.cacheService.setInvoice(userId, invoiceId, invoice);
this.logger.log(`Fetched invoice ${invoiceId} for client ${clientId}`);
return invoice;
} catch (error) {
this.logger.error(`Failed to fetch invoice ${invoiceId} for client ${clientId}`, {
error: getErrorMessage(error),
});
throw error;
}
}
/**
* Invalidate cache for a specific invoice
*/
async invalidateInvoiceCache(userId: string, invoiceId: number): Promise<void> {
await this.cacheService.invalidateInvoice(userId, invoiceId);
this.logger.log(`Invalidated invoice cache for user ${userId}, invoice ${invoiceId}`);
}
// ========================================
// NEW: Invoice Creation Methods
// ========================================
/**
* Create a new invoice for a client
*/
async createInvoice(params: {
clientId: number;
description: string;
amount: number;
currency?: string;
dueDate?: Date;
notes?: string;
}): Promise<{ id: number; number: string; total: number; status: string }> {
try {
const dueDateStr = params.dueDate
? params.dueDate.toISOString().split('T')[0]
: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; // 7 days from now
const whmcsParams: WhmcsCreateInvoiceParams = {
userid: params.clientId,
status: "Unpaid",
sendnotification: false, // Don't send email notification automatically
duedate: dueDateStr,
notes: params.notes,
itemdescription1: params.description,
itemamount1: params.amount,
itemtaxed1: false, // No tax for data top-ups for now
};
const response = await this.connectionService.createInvoice(whmcsParams);
if (response.result !== "success") {
throw new Error(`WHMCS invoice creation failed: ${response.message}`);
}
this.logger.log(`Created WHMCS invoice ${response.invoiceid} for client ${params.clientId}`, {
invoiceId: response.invoiceid,
amount: params.amount,
description: params.description,
});
return {
id: response.invoiceid,
number: `INV-${response.invoiceid}`,
total: params.amount,
status: response.status,
};
} catch (error) {
this.logger.error(`Failed to create invoice for client ${params.clientId}`, {
error: getErrorMessage(error),
params,
});
throw error;
}
}
/**
* Capture payment for an invoice using the client's default payment method
*/
async capturePayment(params: {
invoiceId: number;
amount: number;
currency?: string;
}): Promise<{ success: boolean; transactionId?: string; error?: string }> {
try {
const whmcsParams: WhmcsCapturePaymentParams = {
invoiceid: params.invoiceId,
};
const response = await this.connectionService.capturePayment(whmcsParams);
if (response.result === "success") {
this.logger.log(`Successfully captured payment for invoice ${params.invoiceId}`, {
invoiceId: params.invoiceId,
transactionId: response.transactionid,
amount: response.amount,
});
// Invalidate invoice cache since status changed
await this.cacheService.invalidateInvoice(`invoice-${params.invoiceId}`, params.invoiceId);
return {
success: true,
transactionId: response.transactionid,
};
} else {
this.logger.warn(`Payment capture failed for invoice ${params.invoiceId}`, {
invoiceId: params.invoiceId,
error: response.message || response.error,
});
return {
success: false,
error: response.message || response.error || "Payment capture failed",
};
}
} catch (error) {
this.logger.error(`Failed to capture payment for invoice ${params.invoiceId}`, {
error: getErrorMessage(error),
params,
});
return {
success: false,
error: getErrorMessage(error),
};
}
}
}