import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { Logger } from "nestjs-pino"; import { Injectable, NotFoundException, Inject } from "@nestjs/common"; import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js"; import { Providers } from "@customer-portal/domain/subscriptions"; import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions"; import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js"; import { WhmcsCurrencyService } from "./whmcs-currency.service.js"; import { WhmcsCacheService } from "../cache/whmcs-cache.service.js"; import type { WhmcsGetClientsProductsParams } 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) { return Providers.Whmcs.filterSubscriptionsByStatus(cached, filters.status); } 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 defaultCurrency = this.currencyService.getDefaultCurrency(); let result: SubscriptionList; try { result = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, { defaultCurrencyCode: defaultCurrency.code, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, onItemError: (error, product) => { this.logger.error(`Failed to transform subscription ${product.id}`, { error: getErrorMessage(error), }); }, }); } catch (error) { const message = error instanceof Error ? error.message : "GetClientsProducts call failed"; this.logger.error("WHMCS GetClientsProducts returned error result", { clientId, error: getErrorMessage(error), }); throw new WhmcsOperationException(message, { clientId }); } // Cache the result await this.cacheService.setSubscriptionsList(userId, result); this.logger.log( `Fetched ${result.subscriptions.length} subscriptions for client ${clientId}` ); // Apply status filter if needed if (filters.status) { return Providers.Whmcs.filterSubscriptionsByStatus(result, filters.status); } 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; } // 2. Check if we have the FULL list cached. // If we do, searching memory is faster than an API call. const cachedList = await this.cacheService.getSubscriptionsList(userId); if (cachedList) { const found = cachedList.subscriptions.find((s: Subscription) => s.id === subscriptionId); if (found) { this.logger.debug( `Cache hit (via list) for subscription: user ${userId}, subscription ${subscriptionId}` ); // Cache this individual item for faster direct access next time await this.cacheService.setSubscription(userId, subscriptionId, found); return found; } // If list is cached but item not found, it might be new or not in that list? // Proceed to fetch single item. } // 3. Fetch ONLY this subscription from WHMCS (Optimized) // Instead of fetching all products, use serviceid filter const params: WhmcsGetClientsProductsParams = { clientid: clientId, serviceid: subscriptionId, }; const rawResponse = await this.connectionService.getClientsProducts(params); // Transform response const defaultCurrency = this.currencyService.getDefaultCurrency(); const resultList = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, { defaultCurrencyCode: defaultCurrency.code, defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix, }); const subscription = resultList.subscriptions.find(s => 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}` ); } }