2025-08-21 15:24:40 +09:00
|
|
|
import { getErrorMessage } from "../../../common/utils/error.util";
|
2025-08-22 17:02:49 +09:00
|
|
|
import { Logger } from "nestjs-pino";
|
|
|
|
|
import { Injectable, Inject } from "@nestjs/common";
|
|
|
|
|
import { PaymentMethodList, PaymentGateway, PaymentGatewayList } from "@customer-portal/shared";
|
2025-08-21 15:24:40 +09:00
|
|
|
import { WhmcsConnectionService } from "./whmcs-connection.service";
|
|
|
|
|
import { WhmcsDataTransformer } from "../transformers/whmcs-data.transformer";
|
|
|
|
|
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
|
|
|
|
|
import { WhmcsCreateSsoTokenParams } from "../types/whmcs-api.types";
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class WhmcsPaymentService {
|
|
|
|
|
constructor(
|
2025-08-22 17:02:49 +09:00
|
|
|
@Inject(Logger) private readonly logger: Logger,
|
2025-08-20 18:02:50 +09:00
|
|
|
private readonly connectionService: WhmcsConnectionService,
|
|
|
|
|
private readonly dataTransformer: WhmcsDataTransformer,
|
2025-08-22 17:02:49 +09:00
|
|
|
private readonly cacheService: WhmcsCacheService
|
2025-08-20 18:02:50 +09:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get payment methods for a client
|
|
|
|
|
*/
|
2025-08-22 17:02:49 +09:00
|
|
|
async getPaymentMethods(clientId: number, userId: string): Promise<PaymentMethodList> {
|
2025-08-20 18:02:50 +09:00
|
|
|
try {
|
|
|
|
|
// Try cache first
|
|
|
|
|
const cached = await this.cacheService.getPaymentMethods(userId);
|
|
|
|
|
if (cached) {
|
|
|
|
|
this.logger.debug(`Cache hit for payment methods: user ${userId}`);
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 17:24:37 +09:00
|
|
|
const response = await this.connectionService.getPayMethods({ clientid: clientId });
|
|
|
|
|
const methods = (response.paymethods?.paymethod || []).map(pm =>
|
|
|
|
|
this.dataTransformer.transformPaymentMethod(pm)
|
|
|
|
|
);
|
|
|
|
|
const result: PaymentMethodList = { paymentMethods: methods, totalCount: methods.length };
|
2025-08-20 18:02:50 +09:00
|
|
|
await this.cacheService.setPaymentMethods(userId, result);
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.error(`Failed to fetch payment methods for client ${clientId}`, {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
userId,
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get available payment gateways
|
|
|
|
|
*/
|
|
|
|
|
async getPaymentGateways(): Promise<PaymentGatewayList> {
|
|
|
|
|
try {
|
|
|
|
|
// Try cache first
|
|
|
|
|
const cached = await this.cacheService.getPaymentGateways();
|
|
|
|
|
if (cached) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.debug("Cache hit for payment gateways");
|
2025-08-20 18:02:50 +09:00
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch from WHMCS API
|
|
|
|
|
const response = await this.connectionService.getPaymentGateways();
|
|
|
|
|
|
|
|
|
|
if (!response.gateways?.gateway) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.warn("No payment gateways found");
|
2025-08-20 18:02:50 +09:00
|
|
|
return {
|
|
|
|
|
gateways: [],
|
|
|
|
|
totalCount: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform payment gateways
|
2025-08-21 15:24:40 +09:00
|
|
|
const gateways = response.gateways.gateway
|
2025-08-22 17:02:49 +09:00
|
|
|
.map(whmcsGateway => {
|
2025-08-21 15:24:40 +09:00
|
|
|
try {
|
|
|
|
|
return this.dataTransformer.transformPaymentGateway(whmcsGateway);
|
|
|
|
|
} catch (error) {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.error(`Failed to transform payment gateway ${whmcsGateway.name}`, {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-21 15:24:40 +09:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.filter((gateway): gateway is PaymentGateway => gateway !== null);
|
2025-08-20 18:02:50 +09:00
|
|
|
|
|
|
|
|
const result: PaymentGatewayList = {
|
|
|
|
|
gateways,
|
|
|
|
|
totalCount: gateways.length,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Cache the result (cache for 1 hour since gateways don't change often)
|
|
|
|
|
await this.cacheService.setPaymentGateways(result);
|
|
|
|
|
|
|
|
|
|
this.logger.log(`Fetched ${gateways.length} payment gateways`);
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.error("Failed to fetch payment gateways", {
|
2025-08-20 18:02:50 +09:00
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create SSO token with payment method for invoice payment
|
|
|
|
|
* This creates a direct link to the payment form with gateway pre-selected
|
|
|
|
|
*/
|
|
|
|
|
async createPaymentSsoToken(
|
|
|
|
|
clientId: number,
|
|
|
|
|
invoiceId: number,
|
|
|
|
|
paymentMethodId?: number,
|
2025-08-22 17:02:49 +09:00
|
|
|
gatewayName?: string
|
2025-08-20 18:02:50 +09:00
|
|
|
): Promise<{ url: string; expiresAt: string }> {
|
|
|
|
|
try {
|
|
|
|
|
// Use WHMCS Friendly URL format for direct payment page access
|
|
|
|
|
// This goes directly to the payment page, not just invoice view
|
|
|
|
|
let invoiceUrl = `index.php?rp=/invoice/${invoiceId}/pay`;
|
2025-08-21 15:24:40 +09:00
|
|
|
|
2025-08-20 18:02:50 +09:00
|
|
|
if (paymentMethodId) {
|
|
|
|
|
// Pre-select specific saved payment method
|
|
|
|
|
invoiceUrl += `&paymentmethod=${paymentMethodId}`;
|
|
|
|
|
} else if (gatewayName) {
|
|
|
|
|
// Pre-select specific gateway
|
|
|
|
|
invoiceUrl += `&gateway=${gatewayName}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const params: WhmcsCreateSsoTokenParams = {
|
|
|
|
|
client_id: clientId,
|
2025-08-21 15:24:40 +09:00
|
|
|
destination: "sso:custom_redirect",
|
2025-08-20 18:02:50 +09:00
|
|
|
sso_redirect_path: invoiceUrl,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const response = await this.connectionService.createSsoToken(params);
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
url: response.redirect_url,
|
|
|
|
|
expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.log(`Created payment SSO token for client ${clientId}, invoice ${invoiceId}`, {
|
|
|
|
|
paymentMethodId,
|
|
|
|
|
gatewayName,
|
|
|
|
|
invoiceUrl,
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
2025-08-22 17:02:49 +09:00
|
|
|
this.logger.error(`Failed to create payment SSO token for client ${clientId}`, {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
invoiceId,
|
|
|
|
|
paymentMethodId,
|
|
|
|
|
gatewayName,
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get products catalog
|
|
|
|
|
*/
|
|
|
|
|
async getProducts(): Promise<any> {
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.connectionService.getProducts();
|
|
|
|
|
return response;
|
|
|
|
|
} catch (error) {
|
2025-08-21 15:24:40 +09:00
|
|
|
this.logger.error("Failed to get products", {
|
|
|
|
|
error: getErrorMessage(error),
|
|
|
|
|
});
|
2025-08-20 18:02:50 +09:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Transform product data (delegate to transformer)
|
|
|
|
|
*/
|
|
|
|
|
transformProduct(whmcsProduct: any): any {
|
|
|
|
|
return this.dataTransformer.transformProduct(whmcsProduct);
|
|
|
|
|
}
|
|
|
|
|
}
|