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