import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { CacheService } from "@bff/infra/cache/cache.service.js"; import type { Invoice, InvoiceList } from "@customer-portal/domain/billing"; import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions"; import type { PaymentMethodList, PaymentGatewayList } from "@customer-portal/domain/payments"; import type { WhmcsClient } from "@customer-portal/domain/customer"; export interface CacheOptions { ttl?: number; tags?: string[]; } export interface CacheKeyConfig { prefix: string; ttl: number; tags: string[]; } @Injectable() export class WhmcsCacheService { // Cache configuration for different data types private readonly cacheConfigs: Record = { invoices: { prefix: "whmcs:invoices", ttl: 90, // 90 seconds - invoices change frequently tags: ["invoices", "billing"], }, invoice: { prefix: "whmcs:invoice", ttl: 300, // 5 minutes - individual invoices change less frequently tags: ["invoice", "billing"], }, subscriptions: { prefix: "whmcs:subscriptions", ttl: 300, // 5 minutes - subscriptions change less frequently tags: ["subscriptions", "services"], }, subscription: { prefix: "whmcs:subscription", ttl: 600, // 10 minutes - individual subscriptions rarely change tags: ["subscription", "services"], }, subscriptionInvoices: { prefix: "whmcs:subscription:invoices", ttl: 300, // 5 minutes tags: ["subscription", "invoices"], }, client: { prefix: "whmcs:client", ttl: 1800, // 30 minutes - client data rarely changes tags: ["client", "user"], }, clientEmail: { prefix: "whmcs:client:email", ttl: 1800, // 30 minutes tags: ["client", "email"], }, sso: { prefix: "whmcs:sso", ttl: 3600, // 1 hour - SSO tokens have their own expiry tags: ["sso", "auth"], }, paymentMethods: { prefix: "whmcs:paymentmethods", ttl: 900, // 15 minutes - payment methods change occasionally tags: ["paymentmethods", "billing"], }, paymentGateways: { prefix: "whmcs:paymentgateways", ttl: 3600, // 1 hour - payment gateways rarely change tags: ["paymentgateways", "config"], }, }; constructor( private readonly cacheService: CacheService, @Inject(Logger) private readonly logger: Logger ) {} /** * Get cached invoices list for a user */ async getInvoicesList( userId: string, page: number, limit: number, status?: string ): Promise { const key = this.buildInvoicesKey(userId, page, limit, status); return this.get(key, "invoices"); } /** * Cache invoices list for a user */ async setInvoicesList( userId: string, page: number, limit: number, status: string | undefined, data: InvoiceList ): Promise { const key = this.buildInvoicesKey(userId, page, limit, status); await this.set(key, data, "invoices", [`user:${userId}`]); } /** * Get cached individual invoice */ async getInvoice(userId: string, invoiceId: number): Promise { const key = this.buildInvoiceKey(userId, invoiceId); return this.get(key, "invoice"); } /** * Cache individual invoice */ async setInvoice(userId: string, invoiceId: number, data: Invoice): Promise { const key = this.buildInvoiceKey(userId, invoiceId); await this.set(key, data, "invoice", [`user:${userId}`, `invoice:${invoiceId}`]); } /** * Get cached subscriptions list for a user */ async getSubscriptionsList(userId: string): Promise { const key = this.buildSubscriptionsKey(userId); return this.get(key, "subscriptions"); } /** * Cache subscriptions list for a user */ async setSubscriptionsList(userId: string, data: SubscriptionList): Promise { const key = this.buildSubscriptionsKey(userId); await this.set(key, data, "subscriptions", [`user:${userId}`]); } /** * Get cached individual subscription */ async getSubscription(userId: string, subscriptionId: number): Promise { const key = this.buildSubscriptionKey(userId, subscriptionId); return this.get(key, "subscription"); } /** * Cache individual subscription */ async setSubscription(userId: string, subscriptionId: number, data: Subscription): Promise { const key = this.buildSubscriptionKey(userId, subscriptionId); await this.set(key, data, "subscription", [`user:${userId}`, `subscription:${subscriptionId}`]); } /** * Get cached subscription invoices */ async getSubscriptionInvoices( userId: string, subscriptionId: number, page: number, limit: number ): Promise { const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit); return this.get(key, "subscriptionInvoices"); } /** * Cache subscription invoices */ async setSubscriptionInvoices( userId: string, subscriptionId: number, page: number, limit: number, data: InvoiceList ): Promise { const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit); await this.set(key, data, "subscriptionInvoices", [ `user:${userId}`, `subscription:${subscriptionId}`, ]); } /** * Get cached client data * Returns WhmcsClient (type inferred from domain) */ async getClientData(clientId: number): Promise { const key = this.buildClientKey(clientId); return this.get(key, "client"); } /** * Cache client data */ async setClientData(clientId: number, data: WhmcsClient) { const key = this.buildClientKey(clientId); await this.set(key, data, "client", [`client:${clientId}`]); } /** * Get cached client ID by email */ async getClientIdByEmail(email: string): Promise { const key = this.buildClientEmailKey(email); return this.get(key, "clientEmail"); } /** * Cache client ID for email */ async setClientIdByEmail(email: string, clientId: number): Promise { const key = this.buildClientEmailKey(email); await this.set(key, clientId, "clientEmail"); } /** * Invalidate all cache for a specific user */ async invalidateUserCache(userId: string): Promise { try { const patterns = [ `${this.cacheConfigs.invoices.prefix}:${userId}:*`, `${this.cacheConfigs.invoice.prefix}:${userId}:*`, `${this.cacheConfigs.subscriptions.prefix}:${userId}:*`, `${this.cacheConfigs.subscription.prefix}:${userId}:*`, ]; await Promise.all(patterns.map(pattern => this.cacheService.delPattern(pattern))); this.logger.log(`Invalidated all cache for user ${userId}`); } catch (error) { this.logger.error(`Failed to invalidate cache for user ${userId}`, { error: getErrorMessage(error), }); } } /** * Invalidate client data cache for a specific client */ async invalidateClientCache(clientId: number): Promise { try { const key = this.buildClientKey(clientId); await this.cacheService.del(key); this.logger.log(`Invalidated client cache for client ${clientId}`); } catch (error) { this.logger.error(`Failed to invalidate client cache for client ${clientId}`, { error: getErrorMessage(error), }); } } /** * Invalidate cache by tags */ async invalidateByTag(tag: string): Promise { try { // This would require a more sophisticated cache implementation with tag support // For now, we'll use pattern-based invalidation const patterns = Object.values(this.cacheConfigs) .filter(config => config.tags.includes(tag)) .map(config => `${config.prefix}:*`); await Promise.all(patterns.map(pattern => this.cacheService.delPattern(pattern))); this.logger.log(`Invalidated cache by tag: ${tag}`); } catch (error) { this.logger.error(`Failed to invalidate cache by tag ${tag}`, { error: getErrorMessage(error), }); } } /** * Invalidate specific invoice cache */ async invalidateInvoice(userId: string, invoiceId: number): Promise { try { const specificKey = this.buildInvoiceKey(userId, invoiceId); const listPattern = `${this.cacheConfigs.invoices.prefix}:${userId}:*`; await Promise.all([ this.cacheService.del(specificKey), this.cacheService.delPattern(listPattern), ]); this.logger.log(`Invalidated invoice cache for user ${userId}, invoice ${invoiceId}`); } catch (error) { this.logger.error( `Failed to invalidate invoice cache for user ${userId}, invoice ${invoiceId}`, { error: getErrorMessage(error) } ); } } /** * Invalidate specific subscription cache */ async invalidateSubscription(userId: string, subscriptionId: number): Promise { try { const specificKey = this.buildSubscriptionKey(userId, subscriptionId); const listKey = this.buildSubscriptionsKey(userId); await Promise.all([this.cacheService.del(specificKey), this.cacheService.del(listKey)]); this.logger.log( `Invalidated subscription cache for user ${userId}, subscription ${subscriptionId}` ); } catch (error) { this.logger.error( `Failed to invalidate subscription cache for user ${userId}, subscription ${subscriptionId}`, { error: getErrorMessage(error) } ); } } /** * Get cached payment methods for a user */ async getPaymentMethods(userId: string): Promise { const key = this.buildPaymentMethodsKey(userId); return this.get(key, "paymentMethods"); } /** * Set payment methods cache for a user */ async setPaymentMethods(userId: string, paymentMethods: PaymentMethodList): Promise { const key = this.buildPaymentMethodsKey(userId); await this.set(key, paymentMethods, "paymentMethods", [userId]); } /** * Get cached payment gateways (global) */ async getPaymentGateways(): Promise { const key = "whmcs:paymentgateways:global"; return this.get(key, "paymentGateways"); } /** * Set payment gateways cache (global) */ async setPaymentGateways(paymentGateways: PaymentGatewayList): Promise { const key = "whmcs:paymentgateways:global"; await this.set(key, paymentGateways, "paymentGateways"); } /** * Invalidate payment methods cache for a user */ async invalidatePaymentMethods(userId: string): Promise { try { const key = this.buildPaymentMethodsKey(userId); await this.cacheService.del(key); this.logger.log(`Invalidated payment methods cache for user ${userId}`); } catch (error) { this.logger.error(`Failed to invalidate payment methods cache for user ${userId}`, error); } } /** * Invalidate payment gateways cache (global) */ async invalidatePaymentGateways(): Promise { try { const key = "whmcs:paymentgateways:global"; await this.cacheService.del(key); this.logger.log("Invalidated payment gateways cache"); } catch (error) { this.logger.error("Failed to invalidate payment gateways cache", error); } } /** * Generic get method with configuration */ private async get(key: string, _configKey: string): Promise { try { const data = await this.cacheService.get(key); if (data) { this.logger.debug(`Cache hit: ${key}`); } return data; } catch (error) { this.logger.error(`Cache get error for key ${key}`, { error: getErrorMessage(error) }); return null; } } /** * Generic set method with configuration */ private async set( key: string, data: T, _configKey: string, _additionalTags: string[] = [] ): Promise { try { const config = this.cacheConfigs[_configKey]; await this.cacheService.set(key, data, config.ttl); this.logger.debug(`Cache set: ${key} (TTL: ${config.ttl}s)`); } catch (error) { this.logger.error(`Cache set error for key ${key}`, { error: getErrorMessage(error) }); } } /** * Build cache key for invoices list */ private buildInvoicesKey(userId: string, page: number, limit: number, status?: string): string { return `${this.cacheConfigs.invoices.prefix}:${userId}:${page}:${limit}:${status || "all"}`; } /** * Build cache key for individual invoice */ private buildInvoiceKey(userId: string, invoiceId: number): string { return `${this.cacheConfigs.invoice.prefix}:${userId}:${invoiceId}`; } /** * Build cache key for subscriptions list */ private buildSubscriptionsKey(userId: string): string { return `${this.cacheConfigs.subscriptions.prefix}:${userId}`; } /** * Build cache key for individual subscription */ private buildSubscriptionKey(userId: string, subscriptionId: number): string { return `${this.cacheConfigs.subscription.prefix}:${userId}:${subscriptionId}`; } /** * Build cache key for subscription invoices */ private buildSubscriptionInvoicesKey( userId: string, subscriptionId: number, page: number, limit: number ): string { return `${this.cacheConfigs.subscriptionInvoices.prefix}:${userId}:${subscriptionId}:${page}:${limit}`; } /** * Build cache key for client data */ private buildClientKey(clientId: number): string { return `${this.cacheConfigs.client.prefix}:${clientId}`; } /** * Build cache key for client email mapping */ private buildClientEmailKey(email: string): string { return `${this.cacheConfigs.clientEmail.prefix}:${email.toLowerCase()}`; } /** * Build cache key for payment methods */ private buildPaymentMethodsKey(userId: string): string { return `${this.cacheConfigs.paymentMethods.prefix}:${userId}`; } /** * Get cache statistics */ getCacheStats(): { totalKeys: number; keysByType: Record; } { // This would require Redis SCAN or similar functionality // For now, return a placeholder return { totalKeys: 0, keysByType: {}, }; } /** * Clear all WHMCS cache */ async clearAllCache(): Promise { try { const patterns = Object.values(this.cacheConfigs).map(config => `${config.prefix}:*`); await Promise.all(patterns.map(pattern => this.cacheService.delPattern(pattern))); this.logger.warn("Cleared all WHMCS cache"); } catch (error) { this.logger.error("Failed to clear all WHMCS cache", { error: getErrorMessage(error) }); } } }