- Introduced WhmcsAccountDiscoveryService to streamline client account discovery processes. - Expanded WhmcsCacheService to include caching for subscription invoices and client email mappings, improving data retrieval efficiency. - Updated WhmcsClientService to utilize caching for client ID lookups by email, enhancing performance. - Implemented new internet cancellation features in SubscriptionsController, allowing users to preview and submit cancellation requests for internet services. - Added validation schemas for internet cancellation requests, ensuring data integrity and user guidance during the cancellation process. - Refactored various components and services to integrate new cancellation functionalities, improving user experience and operational flow.
503 lines
15 KiB
TypeScript
503 lines
15 KiB
TypeScript
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: {
|
|
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<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 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);
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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);
|
|
|
|
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<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: getErrorMessage(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: 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<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: getErrorMessage(error) });
|
|
}
|
|
}
|
|
}
|