2025-08-20 18:02:50 +09:00
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
2025-08-21 15:24:40 +09:00
|
|
|
import { CacheService } from '../../../common/cache/cache.service';
|
2025-08-20 18:02:50 +09:00
|
|
|
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<string, CacheKeyConfig> = {
|
|
|
|
|
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<InvoiceList | null> {
|
|
|
|
|
const key = this.buildInvoicesKey(userId, page, limit, status);
|
|
|
|
|
return this.get<InvoiceList>(key, 'invoices');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cache invoices list for a user
|
|
|
|
|
*/
|
|
|
|
|
async setInvoicesList(
|
|
|
|
|
userId: string,
|
|
|
|
|
page: number,
|
|
|
|
|
limit: number,
|
|
|
|
|
status: string | undefined,
|
|
|
|
|
data: InvoiceList
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
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<Invoice | null> {
|
|
|
|
|
const key = this.buildInvoiceKey(userId, invoiceId);
|
|
|
|
|
return this.get<Invoice>(key, 'invoice');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cache individual invoice
|
|
|
|
|
*/
|
|
|
|
|
async setInvoice(userId: string, invoiceId: number, data: Invoice): Promise<void> {
|
|
|
|
|
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<SubscriptionList | null> {
|
|
|
|
|
const key = this.buildSubscriptionsKey(userId);
|
|
|
|
|
return this.get<SubscriptionList>(key, 'subscriptions');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cache subscriptions list for a user
|
|
|
|
|
*/
|
|
|
|
|
async setSubscriptionsList(userId: string, data: SubscriptionList): Promise<void> {
|
|
|
|
|
const key = this.buildSubscriptionsKey(userId);
|
|
|
|
|
await this.set(key, data, 'subscriptions', [`user:${userId}`]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cached individual subscription
|
|
|
|
|
*/
|
|
|
|
|
async getSubscription(userId: string, subscriptionId: number): Promise<Subscription | null> {
|
|
|
|
|
const key = this.buildSubscriptionKey(userId, subscriptionId);
|
|
|
|
|
return this.get<Subscription>(key, 'subscription');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cache individual subscription
|
|
|
|
|
*/
|
|
|
|
|
async setSubscription(userId: string, subscriptionId: number, data: Subscription): Promise<void> {
|
|
|
|
|
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<any | null> {
|
|
|
|
|
const key = this.buildClientKey(clientId);
|
|
|
|
|
return this.get<any>(key, 'client');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cache client data
|
|
|
|
|
*/
|
|
|
|
|
async setClientData(clientId: number, data: any): Promise<void> {
|
|
|
|
|
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<void> {
|
|
|
|
|
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<void> {
|
|
|
|
|
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<void> {
|
|
|
|
|
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<void> {
|
|
|
|
|
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<PaymentMethodList | null> {
|
|
|
|
|
const key = this.buildPaymentMethodsKey(userId);
|
|
|
|
|
return this.get<PaymentMethodList>(key, 'paymentMethods');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set payment methods cache for a user
|
|
|
|
|
*/
|
|
|
|
|
async setPaymentMethods(userId: string, paymentMethods: PaymentMethodList): Promise<void> {
|
|
|
|
|
const key = this.buildPaymentMethodsKey(userId);
|
|
|
|
|
await this.set(key, paymentMethods, 'paymentMethods', [userId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get cached payment gateways (global)
|
|
|
|
|
*/
|
|
|
|
|
async getPaymentGateways(): Promise<PaymentGatewayList | null> {
|
|
|
|
|
const key = 'whmcs:paymentgateways:global';
|
|
|
|
|
return this.get<PaymentGatewayList>(key, 'paymentGateways');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set payment gateways cache (global)
|
|
|
|
|
*/
|
|
|
|
|
async setPaymentGateways(paymentGateways: PaymentGatewayList): Promise<void> {
|
|
|
|
|
const key = 'whmcs:paymentgateways:global';
|
|
|
|
|
await this.set(key, paymentGateways, 'paymentGateways');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Invalidate payment methods cache for a user
|
|
|
|
|
*/
|
|
|
|
|
async invalidatePaymentMethods(userId: string): Promise<void> {
|
|
|
|
|
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<void> {
|
|
|
|
|
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<T>(key: string, configKey: string): Promise<T | null> {
|
|
|
|
|
try {
|
|
|
|
|
const data = await this.cacheService.get<T>(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<T>(
|
|
|
|
|
key: string,
|
|
|
|
|
data: T,
|
|
|
|
|
configKey: string,
|
|
|
|
|
additionalTags: string[] = []
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
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<string, number>;
|
|
|
|
|
}> {
|
|
|
|
|
// This would require Redis SCAN or similar functionality
|
|
|
|
|
// For now, return a placeholder
|
|
|
|
|
return {
|
|
|
|
|
totalKeys: 0,
|
|
|
|
|
keysByType: {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear all WHMCS cache
|
|
|
|
|
*/
|
|
|
|
|
async clearAllCache(): Promise<void> {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|