import { Injectable, Logger } from '@nestjs/common'; import { CacheService } from '../../../common/cache/cache.service'; import { Invoice, InvoiceList, Subscription, SubscriptionList, PaymentMethodList, PaymentGatewayList } from '@customer-portal/shared'; export interface CacheOptions { ttl?: number; tags?: string[]; } export interface CacheKeyConfig { prefix: string; ttl: number; tags: string[]; } @Injectable() export class WhmcsCacheService { private readonly logger = new Logger(WhmcsCacheService.name); // 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'], }, client: { prefix: 'whmcs:client', ttl: 1800, // 30 minutes - client data rarely changes tags: ['client', 'user'], }, 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) {} /** * 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 client data */ async getClientData(clientId: number): Promise { const key = this.buildClientKey(clientId); return this.get(key, 'client'); } /** * Cache client data */ async setClientData(clientId: number, data: any): Promise { const key = this.buildClientKey(clientId); await this.set(key, data, 'client', [`client:${clientId}`]); } /** * 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); } } /** * 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); } } /** * 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); } } /** * 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); } } /** * 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); 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); } } /** * 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 client data */ private buildClientKey(clientId: number): string { return `${this.cacheConfigs.client.prefix}:${clientId}`; } /** * Build cache key for payment methods */ private buildPaymentMethodsKey(userId: string): string { return `${this.cacheConfigs.paymentMethods.prefix}:${userId}`; } /** * Get cache statistics */ async getCacheStats(): Promise<{ 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); } } }