import { Injectable, Inject, OnModuleInit } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { getErrorMessage } from "@bff/core/utils/error.util"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service"; import type { WhmcsCurrenciesResponse, WhmcsCurrency } from "@customer-portal/domain/billing"; @Injectable() export class WhmcsCurrencyService implements OnModuleInit { private defaultCurrency: WhmcsCurrency | null = null; private currencies: WhmcsCurrency[] = []; constructor( @Inject(Logger) private readonly logger: Logger, private readonly connectionService: WhmcsConnectionOrchestratorService ) {} async onModuleInit() { try { // Check if WHMCS is available before trying to load currencies this.logger.debug("Checking WHMCS availability before loading currencies"); const isAvailable = await this.connectionService.isAvailable(); if (!isAvailable) { this.logger.warn("WHMCS service is not available, using fallback currency configuration"); this.setFallbackCurrency(); return; } this.logger.debug("WHMCS is available, attempting to load currencies"); await this.loadCurrencies(); } catch (error) { this.logger.error("Failed to load WHMCS currencies on startup", { error: getErrorMessage(error), }); // Set fallback default this.setFallbackCurrency(); } } /** * Set fallback currency configuration when WHMCS is not available */ private setFallbackCurrency(): void { this.defaultCurrency = { id: 1, code: "JPY", prefix: "¥", suffix: "", format: "1", rate: "1.00000", }; this.currencies = [this.defaultCurrency]; this.logger.log("Using fallback currency configuration", { defaultCurrency: this.defaultCurrency.code, }); } /** * Get the default currency (first currency from WHMCS or JPY fallback) */ getDefaultCurrency(): WhmcsCurrency { return ( this.defaultCurrency || { id: 1, code: "JPY", prefix: "¥", suffix: "", format: "1", rate: "1.00000", } ); } /** * Get all available currencies */ getAllCurrencies(): WhmcsCurrency[] { return this.currencies; } /** * Find currency by code */ getCurrencyByCode(code: string): WhmcsCurrency | null { return this.currencies.find(c => c.code.toUpperCase() === code.toUpperCase()) || null; } /** * Load currencies from WHMCS */ private async loadCurrencies(): Promise { try { // The connection service returns the raw WHMCS API response data // (the WhmcsResponse wrapper is unwrapped by the API methods service) const response = (await this.connectionService.getCurrencies()) as WhmcsCurrenciesResponse; // Check if response has currencies data (success case) or error fields if (response.result === "success" || (response.currencies && !response.error)) { // Parse the WHMCS response format into currency objects this.currencies = this.parseWhmcsCurrenciesResponse(response); if (this.currencies.length > 0) { // Set first currency as default (WHMCS typically returns the primary currency first) this.defaultCurrency = this.currencies[0]; this.logger.log(`Loaded ${this.currencies.length} currencies from WHMCS`, { defaultCurrency: this.defaultCurrency?.code, allCurrencies: this.currencies.map(c => c.code), }); } else { throw new Error("No currencies found in WHMCS response"); } } else { this.logger.error("WHMCS GetCurrencies returned error", { result: response?.result, message: response?.message, error: response?.error, errorcode: response?.errorcode, fullResponse: JSON.stringify(response, null, 2), }); throw new Error( `WHMCS GetCurrencies error: ${response?.message || response?.error || "Unknown error"}` ); } } catch (error) { this.logger.error("Failed to load currencies from WHMCS", { error: getErrorMessage(error), }); throw error; } } /** * Parse WHMCS response format into currency objects * Handles both flat format (currencies[currency][0][id]) and nested format (currencies.currency[]) */ private parseWhmcsCurrenciesResponse(response: WhmcsCurrenciesResponse): WhmcsCurrency[] { const currencies: WhmcsCurrency[] = []; // Check if response has nested currency structure if ( response.currencies && typeof response.currencies === "object" && "currency" in response.currencies ) { const currencyArray = Array.isArray(response.currencies.currency) ? response.currencies.currency : [response.currencies.currency]; for (const currencyData of currencyArray) { const currency: WhmcsCurrency = { id: parseInt(String(currencyData.id)) || 0, code: String(currencyData.code || ""), prefix: String(currencyData.prefix || ""), suffix: String(currencyData.suffix || ""), format: String(currencyData.format || "1"), rate: String(currencyData.rate || "1.00000"), }; // Validate that we have essential currency data if (currency.id && currency.code) { currencies.push(currency); } } } else { // Fallback: try to parse flat format (currencies[currency][0][id], etc.) const currencyKeys = Object.keys(response).filter( key => key.startsWith("currencies[currency][") && key.includes("][id]") ); // Extract currency indices const currencyIndices = currencyKeys .map(key => { const match = key.match(/currencies\[currency\]\[(\d+)\]\[id\]/); return match ? parseInt(match[1], 10) : null; }) .filter((index): index is number => index !== null); // Build currency objects from the flat response for (const index of currencyIndices) { const currency: WhmcsCurrency = { id: parseInt(String(response[`currencies[currency][${index}][id]`])) || 0, code: String(response[`currencies[currency][${index}][code]`] || ""), prefix: String(response[`currencies[currency][${index}][prefix]`] || ""), suffix: String(response[`currencies[currency][${index}][suffix]`] || ""), format: String(response[`currencies[currency][${index}][format]`] || "1"), rate: String(response[`currencies[currency][${index}][rate]`] || "1.00000"), }; // Validate that we have essential currency data if (currency.id && currency.code) { currencies.push(currency); } } } return currencies; } /** * Refresh currencies from WHMCS (can be called manually if needed) */ async refreshCurrencies(): Promise { await this.loadCurrencies(); } }