Assist_Design/apps/bff/src/integrations/whmcs/cache/whmcs-cache.service.ts

503 lines
15 KiB
TypeScript
Raw Normal View History

2025-08-22 17:02:49 +09:00
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<string, CacheKeyConfig> = {
invoices: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:invoices",
ttl: 90, // 90 seconds - invoices change frequently
2025-08-22 17:02:49 +09:00
tags: ["invoices", "billing"],
},
invoice: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:invoice",
ttl: 300, // 5 minutes - individual invoices change less frequently
2025-08-22 17:02:49 +09:00
tags: ["invoice", "billing"],
},
subscriptions: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:subscriptions",
ttl: 300, // 5 minutes - subscriptions change less frequently
2025-08-22 17:02:49 +09:00
tags: ["subscriptions", "services"],
},
subscription: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:subscription",
ttl: 600, // 10 minutes - individual subscriptions rarely change
2025-08-22 17:02:49 +09:00
tags: ["subscription", "services"],
},
subscriptionInvoices: {
prefix: "whmcs:subscription:invoices",
ttl: 300, // 5 minutes
tags: ["subscription", "invoices"],
},
client: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:client",
ttl: 1800, // 30 minutes - client data rarely changes
2025-08-22 17:02:49 +09:00
tags: ["client", "user"],
},
clientEmail: {
prefix: "whmcs:client:email",
ttl: 1800, // 30 minutes
tags: ["client", "email"],
},
sso: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:sso",
ttl: 3600, // 1 hour - SSO tokens have their own expiry
2025-08-22 17:02:49 +09:00
tags: ["sso", "auth"],
},
paymentMethods: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:paymentmethods",
ttl: 900, // 15 minutes - payment methods change occasionally
2025-08-22 17:02:49 +09:00
tags: ["paymentmethods", "billing"],
},
paymentGateways: {
2025-08-22 17:02:49 +09:00
prefix: "whmcs:paymentgateways",
ttl: 3600, // 1 hour - payment gateways rarely change
2025-08-22 17:02:49 +09:00
tags: ["paymentgateways", "config"],
},
};
2025-08-22 17:02:49 +09:00
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<InvoiceList | null> {
const key = this.buildInvoicesKey(userId, page, limit, status);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
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<InvoiceList | null> {
const key = this.buildSubscriptionInvoicesKey(userId, subscriptionId, page, limit);
return this.get<InvoiceList>(key, "subscriptionInvoices");
}
/**
* Cache subscription invoices
*/
async setSubscriptionInvoices(
userId: string,
subscriptionId: number,
page: number,
limit: number,
data: InvoiceList
): Promise<void> {
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<WhmcsClient | null> {
const key = this.buildClientKey(clientId);
return this.get<WhmcsClient>(key, "client");
}
/**
* Cache client data
*/
async setClientData(clientId: number, data: WhmcsClient) {
const key = this.buildClientKey(clientId);
2025-08-22 17:02:49 +09:00
await this.set(key, data, "client", [`client:${clientId}`]);
}
/**
* Get cached client ID by email
*/
async getClientIdByEmail(email: string): Promise<number | null> {
const key = this.buildClientEmailKey(email);
return this.get<number>(key, "clientEmail");
}
/**
* Cache client ID for email
*/
async setClientIdByEmail(email: string, clientId: number): Promise<void> {
const key = this.buildClientEmailKey(email);
await this.set(key, clientId, "clientEmail");
}
/**
* 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) {
2025-08-22 17:02:49 +09:00
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<void> {
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<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) {
2025-08-22 17:02:49 +09:00
this.logger.error(`Failed to invalidate cache by tag ${tag}`, {
error: getErrorMessage(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) {
2025-08-22 17:02:49 +09:00
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<void> {
try {
const specificKey = this.buildSubscriptionKey(userId, subscriptionId);
const listKey = this.buildSubscriptionsKey(userId);
2025-08-22 17:02:49 +09:00
await Promise.all([this.cacheService.del(specificKey), this.cacheService.del(listKey)]);
2025-08-22 17:02:49 +09:00
this.logger.log(
`Invalidated subscription cache for user ${userId}, subscription ${subscriptionId}`
);
} catch (error) {
2025-08-22 17:02:49 +09:00
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<PaymentMethodList | null> {
const key = this.buildPaymentMethodsKey(userId);
2025-08-22 17:02:49 +09:00
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);
2025-08-22 17:02:49 +09:00
await this.set(key, paymentMethods, "paymentMethods", [userId]);
}
/**
* Get cached payment gateways (global)
*/
async getPaymentGateways(): Promise<PaymentGatewayList | null> {
2025-08-22 17:02:49 +09:00
const key = "whmcs:paymentgateways:global";
return this.get<PaymentGatewayList>(key, "paymentGateways");
}
/**
* Set payment gateways cache (global)
*/
async setPaymentGateways(paymentGateways: PaymentGatewayList): Promise<void> {
2025-08-22 17:02:49 +09:00
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 {
2025-08-22 17:02:49 +09:00
const key = "whmcs:paymentgateways:global";
await this.cacheService.del(key);
2025-08-22 17:02:49 +09:00
this.logger.log("Invalidated payment gateways cache");
} catch (error) {
2025-08-22 17:02:49 +09:00
this.logger.error("Failed to invalidate payment gateways cache", error);
}
}
/**
* Generic get method with configuration
*/
2025-08-22 17:02:49 +09:00
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) {
2025-08-22 17:02:49 +09:00
this.logger.error(`Cache get error for key ${key}`, { error: getErrorMessage(error) });
return null;
}
}
/**
* Generic set method with configuration
*/
private async set<T>(
key: string,
data: T,
2025-08-22 17:02:49 +09:00
_configKey: string,
_additionalTags: string[] = []
): Promise<void> {
try {
2025-08-22 17:02:49 +09:00
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) {
2025-08-22 17:02:49 +09:00
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 {
2025-08-22 17:02:49 +09:00
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<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)));
2025-08-22 17:02:49 +09:00
this.logger.warn("Cleared all WHMCS cache");
} catch (error) {
2025-08-22 17:02:49 +09:00
this.logger.error("Failed to clear all WHMCS cache", { error: getErrorMessage(error) });
}
}
}