- 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.
211 lines
6.7 KiB
TypeScript
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}`
|
|
);
|
|
}
|
|
}
|