- Deleted migration file that removed cached profile fields from the users table, centralizing profile data retrieval from WHMCS. - Updated CsrfMiddleware to include new public authentication endpoints for password reset, setting password, and WHMCS account linking. - Enhanced error handling in password and WHMCS linking workflows to provide clearer feedback on missing mappings and improve user experience. - Adjusted user creation and update methods in UsersFacade to handle cases where WHMCS mappings are not yet available, ensuring smoother account setup.
172 lines
5.5 KiB
TypeScript
172 lines
5.5 KiB
TypeScript
import { Injectable, Inject } from "@nestjs/common";
|
|
import { Logger } from "nestjs-pino";
|
|
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service";
|
|
import { SimValidationService } from "./sim-validation.service";
|
|
import { SimUsageStoreService } from "../../sim-usage-store.service";
|
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
|
import type { SimUsage, SimTopUpHistory } from "@bff/integrations/freebit/interfaces/freebit.types";
|
|
import type { SimTopUpHistoryRequest } from "../types/sim-requests.types";
|
|
import { BadRequestException } from "@nestjs/common";
|
|
|
|
@Injectable()
|
|
export class SimUsageService {
|
|
constructor(
|
|
private readonly freebitService: FreebitOrchestratorService,
|
|
private readonly simValidation: SimValidationService,
|
|
private readonly usageStore: SimUsageStoreService,
|
|
@Inject(Logger) private readonly logger: Logger
|
|
) {}
|
|
|
|
/**
|
|
* Get SIM data usage for a subscription
|
|
*/
|
|
async getSimUsage(userId: string, subscriptionId: number): Promise<SimUsage> {
|
|
let account = "";
|
|
|
|
try {
|
|
const validation = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
|
account = validation.account;
|
|
|
|
const simUsage = await this.freebitService.getSimUsage(account);
|
|
|
|
// Persist today's usage for monthly charts and cleanup previous months
|
|
try {
|
|
await this.usageStore.upsertToday(account, simUsage.todayUsageMb);
|
|
await this.usageStore.cleanupPreviousMonths();
|
|
const stored = await this.usageStore.getLastNDays(account, 30);
|
|
if (stored.length > 0) {
|
|
simUsage.recentDaysUsage = stored.map(d => ({
|
|
date: d.date,
|
|
usageKb: Math.round(d.usageMb * 1000),
|
|
usageMb: d.usageMb,
|
|
}));
|
|
}
|
|
} catch (e) {
|
|
const sanitizedError = getErrorMessage(e);
|
|
this.logger.warn("SIM usage persistence failed (non-fatal)", {
|
|
account,
|
|
error: sanitizedError,
|
|
});
|
|
}
|
|
|
|
this.logger.log(`Retrieved SIM usage for subscription ${subscriptionId}`, {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
todayUsageMb: simUsage.todayUsageMb,
|
|
});
|
|
|
|
return simUsage;
|
|
} catch (error) {
|
|
const sanitizedError = getErrorMessage(error);
|
|
this.logger.error(`Failed to get SIM usage for subscription ${subscriptionId}`, {
|
|
error: sanitizedError,
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
});
|
|
|
|
if (account && sanitizedError.toLowerCase().includes("failed to get sim usage")) {
|
|
try {
|
|
const fallback = await this.buildFallbackUsage(account);
|
|
this.logger.warn("Serving cached SIM usage after Freebit failure", {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
fallbackSource: fallback.recentDaysUsage.length > 0 ? "cache" : "default",
|
|
});
|
|
return fallback;
|
|
} catch (fallbackError) {
|
|
this.logger.warn("Unable to build fallback SIM usage", {
|
|
account,
|
|
error: getErrorMessage(fallbackError),
|
|
});
|
|
}
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async buildFallbackUsage(account: string): Promise<SimUsage> {
|
|
try {
|
|
const records = await this.usageStore.getLastNDays(account, 30);
|
|
if (records.length > 0) {
|
|
const todayIso = new Date().toISOString().slice(0, 10);
|
|
const todayRecord = records.find(r => r.date === todayIso) ?? records[records.length - 1];
|
|
const todayUsageMb = todayRecord?.usageMb ?? 0;
|
|
|
|
const mostRecentDate = records[0]?.date;
|
|
|
|
return {
|
|
account,
|
|
todayUsageMb,
|
|
todayUsageKb: Math.round(todayUsageMb * 1000),
|
|
recentDaysUsage: records.map(r => ({
|
|
date: r.date,
|
|
usageMb: r.usageMb,
|
|
usageKb: Math.round(r.usageMb * 1000),
|
|
})),
|
|
isBlacklisted: false,
|
|
lastUpdated: mostRecentDate ? `${mostRecentDate}T00:00:00.000Z` : new Date().toISOString(),
|
|
};
|
|
}
|
|
} catch (error) {
|
|
this.logger.warn("Failed to load cached SIM usage", {
|
|
account,
|
|
error: getErrorMessage(error),
|
|
});
|
|
}
|
|
|
|
return {
|
|
account,
|
|
todayUsageMb: 0,
|
|
todayUsageKb: 0,
|
|
recentDaysUsage: [],
|
|
isBlacklisted: false,
|
|
lastUpdated: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get SIM top-up history
|
|
*/
|
|
async getSimTopUpHistory(
|
|
userId: string,
|
|
subscriptionId: number,
|
|
request: SimTopUpHistoryRequest
|
|
): Promise<SimTopUpHistory> {
|
|
try {
|
|
const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
|
|
|
// Validate date format
|
|
if (!/^\d{8}$/.test(request.fromDate) || !/^\d{8}$/.test(request.toDate)) {
|
|
throw new BadRequestException("Dates must be in YYYYMMDD format");
|
|
}
|
|
|
|
const history = await this.freebitService.getSimTopUpHistory(
|
|
account,
|
|
request.fromDate,
|
|
request.toDate
|
|
);
|
|
|
|
this.logger.log(`Retrieved SIM top-up history for subscription ${subscriptionId}`, {
|
|
userId,
|
|
subscriptionId,
|
|
account,
|
|
totalAdditions: history.totalAdditions,
|
|
});
|
|
|
|
return history;
|
|
} catch (error) {
|
|
const sanitizedError = getErrorMessage(error);
|
|
this.logger.error(`Failed to get SIM top-up history for subscription ${subscriptionId}`, {
|
|
error: sanitizedError,
|
|
userId,
|
|
subscriptionId,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
}
|