Assist_Design/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts
barsa 4573b94484 Enhance WHMCS Integration and Add Internet Cancellation Features
- 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.
2025-12-23 15:19:20 +09:00

188 lines
6.6 KiB
TypeScript

import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { Logger } from "nestjs-pino";
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions.js";
import { Providers } from "@customer-portal/domain/subscriptions";
import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service.js";
import { WhmcsCurrencyService } from "./whmcs-currency.service.js";
import { WhmcsCacheService } from "../cache/whmcs-cache.service.js";
import type { WhmcsGetClientsProductsParams } from "@customer-portal/domain/subscriptions";
export interface SubscriptionFilters {
status?: string;
}
@Injectable()
export class WhmcsSubscriptionService {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly connectionService: WhmcsConnectionOrchestratorService,
private readonly currencyService: WhmcsCurrencyService,
private readonly cacheService: WhmcsCacheService
) {}
/**
* Get client subscriptions/services with caching
*/
async getSubscriptions(
clientId: number,
userId: string,
filters: SubscriptionFilters = {}
): Promise<SubscriptionList> {
try {
// Try cache first
const cached = await this.cacheService.getSubscriptionsList(userId);
if (cached) {
this.logger.debug(`Cache hit for subscriptions: user ${userId}`);
// Apply status filter if needed
if (filters.status) {
return Providers.Whmcs.filterSubscriptionsByStatus(cached, filters.status);
}
return cached;
}
// Fetch from WHMCS API
const params: WhmcsGetClientsProductsParams = {
clientid: clientId,
orderby: "regdate",
order: "DESC",
};
const rawResponse = await this.connectionService.getClientsProducts(params);
if (!rawResponse) {
this.logger.error("WHMCS GetClientsProducts returned empty response", {
clientId,
});
throw new WhmcsOperationException("GetClientsProducts call failed", {
clientId,
});
}
const defaultCurrency = this.currencyService.getDefaultCurrency();
let result: SubscriptionList;
try {
result = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, {
defaultCurrencyCode: defaultCurrency.code,
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
onItemError: (error, product) => {
this.logger.error(`Failed to transform subscription ${product.id}`, {
error: getErrorMessage(error),
});
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "GetClientsProducts call failed";
this.logger.error("WHMCS GetClientsProducts returned error result", {
clientId,
error: getErrorMessage(error),
});
throw new WhmcsOperationException(message, { clientId });
}
// Cache the result
await this.cacheService.setSubscriptionsList(userId, result);
this.logger.log(
`Fetched ${result.subscriptions.length} subscriptions for client ${clientId}`
);
// Apply status filter if needed
if (filters.status) {
return Providers.Whmcs.filterSubscriptionsByStatus(result, filters.status);
}
return result;
} catch (error) {
this.logger.error(`Failed to fetch subscriptions for client ${clientId}`, {
error: getErrorMessage(error),
filters,
});
throw error;
}
}
/**
* Get individual subscription by ID
*/
async getSubscriptionById(
clientId: number,
userId: string,
subscriptionId: number
): Promise<Subscription> {
try {
// Try cache first
const cached = await this.cacheService.getSubscription(userId, subscriptionId);
if (cached) {
this.logger.debug(
`Cache hit for subscription: user ${userId}, subscription ${subscriptionId}`
);
return cached;
}
// 2. Check if we have the FULL list cached.
// If we do, searching memory is faster than an API call.
const cachedList = await this.cacheService.getSubscriptionsList(userId);
if (cachedList) {
const found = cachedList.subscriptions.find((s: Subscription) => s.id === subscriptionId);
if (found) {
this.logger.debug(
`Cache hit (via list) for subscription: user ${userId}, subscription ${subscriptionId}`
);
// Cache this individual item for faster direct access next time
await this.cacheService.setSubscription(userId, subscriptionId, found);
return found;
}
// If list is cached but item not found, it might be new or not in that list?
// Proceed to fetch single item.
}
// 3. Fetch ONLY this subscription from WHMCS (Optimized)
// Instead of fetching all products, use serviceid filter
const params: WhmcsGetClientsProductsParams = {
clientid: clientId,
serviceid: subscriptionId,
};
const rawResponse = await this.connectionService.getClientsProducts(params);
// Transform response
const defaultCurrency = this.currencyService.getDefaultCurrency();
const resultList = Providers.Whmcs.transformWhmcsSubscriptionListResponse(rawResponse, {
defaultCurrencyCode: defaultCurrency.code,
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
});
const subscription = resultList.subscriptions.find(s => s.id === subscriptionId);
if (!subscription) {
throw new NotFoundException(`Subscription ${subscriptionId} not found`);
}
// Cache the individual subscription
await this.cacheService.setSubscription(userId, subscriptionId, subscription);
this.logger.log(`Fetched subscription ${subscriptionId} for client ${clientId}`);
return subscription;
} catch (error) {
this.logger.error(`Failed to fetch subscription ${subscriptionId} for client ${clientId}`, {
error: getErrorMessage(error),
});
throw error;
}
}
/**
* Invalidate cache for a specific subscription
*/
async invalidateSubscriptionCache(userId: string, subscriptionId: number): Promise<void> {
await this.cacheService.invalidateSubscription(userId, subscriptionId);
this.logger.log(
`Invalidated subscription cache for user ${userId}, subscription ${subscriptionId}`
);
}
}