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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|