import { getErrorMessage } from "@bff/core/utils/error.util"; import { Logger } from "nestjs-pino"; import { Injectable, NotFoundException, Inject } from "@nestjs/common"; import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions"; import { Subscription, SubscriptionList, Providers } from "@customer-portal/domain/subscriptions"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service"; import { WhmcsCurrencyService } from "./whmcs-currency.service"; import { WhmcsCacheService } from "../cache/whmcs-cache.service"; import { type WhmcsGetClientsProductsParams, whmcsProductListResponseSchema, } from "@customer-portal/domain/subscriptions"; export interface SubscriptionFilters { status?: string; } @Injectable() export class WhmcsSubscriptionService { constructor( @Inject(Logger) private readonly logger: Logger, private readonly connectionService: WhmcsConnectionOrchestratorService, private readonly currencyService: WhmcsCurrencyService, private readonly cacheService: WhmcsCacheService ) {} /** * Get client subscriptions/services with caching */ async getSubscriptions( clientId: number, userId: string, filters: SubscriptionFilters = {} ): Promise { try { // Try cache first const cached = await this.cacheService.getSubscriptionsList(userId); if (cached) { this.logger.debug(`Cache hit for subscriptions: user ${userId}`); // Apply status filter if needed if (filters.status) { const statusFilter = filters.status.toLowerCase(); const filtered = cached.subscriptions.filter( (sub: Subscription) => sub.status.toLowerCase() === statusFilter ); return { subscriptions: filtered, totalCount: filtered.length, }; } return cached; } // Fetch from WHMCS API const params: WhmcsGetClientsProductsParams = { clientid: clientId, orderby: "regdate", order: "DESC", }; const rawResponse = await this.connectionService.getClientsProducts(params); if (!rawResponse) { this.logger.error("WHMCS GetClientsProducts returned empty response", { clientId, }); throw new WhmcsOperationException("GetClientsProducts call failed", { clientId, }); } const response = whmcsProductListResponseSchema.parse(rawResponse); if (response.result === "error") { const message = response.message || "GetClientsProducts call failed"; this.logger.error("WHMCS GetClientsProducts returned error result", { clientId, response, }); throw new WhmcsOperationException(message, { clientId, }); } const productContainer = response.products?.product; const products = Array.isArray(productContainer) ? productContainer : productContainer ? [productContainer] : []; const totalResults = response.totalresults !== undefined ? Number(response.totalresults) : products.length; this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, { totalresults: totalResults, startnumber: response.startnumber ?? 0, numreturned: response.numreturned ?? products.length, productCount: products.length, }); if (products.length === 0) { return { subscriptions: [], totalCount: totalResults, }; } const defaultCurrency = this.currencyService.getDefaultCurrency(); const subscriptions = products .map(whmcsProduct => { try { return Providers.Whmcs.transformWhmcsSubscription(whmcsProduct, { defaultCurrencyCode: defaultCurrency.code, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, }); } catch (error) { this.logger.error(`Failed to transform subscription ${whmcsProduct.id}`, { error: getErrorMessage(error), }); return null; } }) .filter((subscription): subscription is Subscription => subscription !== null); const result: SubscriptionList = { subscriptions, totalCount: totalResults, }; // Cache the result await this.cacheService.setSubscriptionsList(userId, result); this.logger.log(`Fetched ${subscriptions.length} subscriptions for client ${clientId}`); // Apply status filter if needed if (filters.status) { const statusFilter = filters.status.toLowerCase(); const filtered = result.subscriptions.filter( (sub: Subscription) => sub.status.toLowerCase() === statusFilter ); return { subscriptions: filtered, totalCount: filtered.length, }; } return result; } catch (error) { this.logger.error(`Failed to fetch subscriptions for client ${clientId}`, { error: getErrorMessage(error), filters, }); throw error; } } /** * Get individual subscription by ID */ async getSubscriptionById( clientId: number, userId: string, subscriptionId: number ): Promise { try { // Try cache first const cached = await this.cacheService.getSubscription(userId, subscriptionId); if (cached) { this.logger.debug( `Cache hit for subscription: user ${userId}, subscription ${subscriptionId}` ); return cached; } // Get all subscriptions and find the specific one const subscriptionList = await this.getSubscriptions(clientId, userId); const subscription = subscriptionList.subscriptions.find( (s: Subscription) => s.id === subscriptionId ); if (!subscription) { throw new NotFoundException(`Subscription ${subscriptionId} not found`); } // Cache the individual subscription await this.cacheService.setSubscription(userId, subscriptionId, subscription); this.logger.log(`Fetched subscription ${subscriptionId} for client ${clientId}`); return subscription; } catch (error) { this.logger.error(`Failed to fetch subscription ${subscriptionId} for client ${clientId}`, { error: getErrorMessage(error), }); throw error; } } /** * Invalidate cache for a specific subscription */ async invalidateSubscriptionCache(userId: string, subscriptionId: number): Promise { await this.cacheService.invalidateSubscription(userId, subscriptionId); this.logger.log( `Invalidated subscription cache for user ${userId}, subscription ${subscriptionId}` ); } }