Assist_Design/apps/bff/src/integrations/whmcs/services/whmcs-subscription.service.ts
barsa 9f8d5fe4f1 Enhance error handling and response structure across filters and services
- Updated error response structures in AuthErrorFilter, HttpExceptionFilter, and ZodValidationExceptionFilter to include detailed information such as timestamp and request path.
- Replaced generic error messages with domain-specific exceptions in Freebit and WHMCS services to improve clarity and maintainability.
- Improved logging and error handling in various services to provide better context for failures and enhance debugging capabilities.
- Refactored JWT strategy to include explicit expiration checks for improved security and user feedback.
2025-10-28 13:43:45 +09:00

211 lines
6.7 KiB
TypeScript

import { getErrorMessage } from "@bff/core/utils/error.util";
import { Logger } from "nestjs-pino";
import { Injectable, NotFoundException, Inject } from "@nestjs/common";
import { WhmcsOperationException } from "@bff/core/exceptions/domain-exceptions";
import { Subscription, SubscriptionList, Providers } from "@customer-portal/domain/subscriptions";
import { WhmcsConnectionOrchestratorService } from "../connection/services/whmcs-connection-orchestrator.service";
import { WhmcsCurrencyService } from "./whmcs-currency.service";
import { WhmcsCacheService } from "../cache/whmcs-cache.service";
import {
type WhmcsGetClientsProductsParams,
whmcsProductListResponseSchema,
} 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) {
const statusFilter = filters.status.toLowerCase();
const filtered = cached.subscriptions.filter(
(sub: Subscription) => sub.status.toLowerCase() === statusFilter
);
return {
subscriptions: filtered,
totalCount: filtered.length,
};
}
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 response = whmcsProductListResponseSchema.parse(rawResponse);
if (response.result === "error") {
const message = response.message || "GetClientsProducts call failed";
this.logger.error("WHMCS GetClientsProducts returned error result", {
clientId,
response,
});
throw new WhmcsOperationException(message, {
clientId,
});
}
const productContainer = response.products?.product;
const products = Array.isArray(productContainer)
? productContainer
: productContainer
? [productContainer]
: [];
const totalResults =
response.totalresults !== undefined ? Number(response.totalresults) : products.length;
this.logger.debug(`WHMCS GetClientsProducts response structure for client ${clientId}`, {
totalresults: totalResults,
startnumber: response.startnumber ?? 0,
numreturned: response.numreturned ?? products.length,
productCount: products.length,
});
if (products.length === 0) {
return {
subscriptions: [],
totalCount: totalResults,
};
}
const defaultCurrency = this.currencyService.getDefaultCurrency();
const subscriptions = products
.map(whmcsProduct => {
try {
return Providers.Whmcs.transformWhmcsSubscription(whmcsProduct, {
defaultCurrencyCode: defaultCurrency.code,
defaultCurrencySymbol: defaultCurrency.prefix || defaultCurrency.suffix,
});
} catch (error) {
this.logger.error(`Failed to transform subscription ${whmcsProduct.id}`, {
error: getErrorMessage(error),
});
return null;
}
})
.filter((subscription): subscription is Subscription => subscription !== null);
const result: SubscriptionList = {
subscriptions,
totalCount: totalResults,
};
// Cache the result
await this.cacheService.setSubscriptionsList(userId, result);
this.logger.log(`Fetched ${subscriptions.length} subscriptions for client ${clientId}`);
// Apply status filter if needed
if (filters.status) {
const statusFilter = filters.status.toLowerCase();
const filtered = result.subscriptions.filter(
(sub: Subscription) => sub.status.toLowerCase() === statusFilter
);
return {
subscriptions: filtered,
totalCount: filtered.length,
};
}
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;
}
// Get all subscriptions and find the specific one
const subscriptionList = await this.getSubscriptions(clientId, userId);
const subscription = subscriptionList.subscriptions.find(
(s: Subscription) => 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}`
);
}
}