import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import type { FreebitAccountDetailsResponse, FreebitTrafficInfoResponse, FreebitQuotaHistoryResponse, SimDetails, SimUsage, SimTopUpHistory, } from "../interfaces/freebit.types"; import type { SimVoiceOptionsService } from "@bff/modules/subscriptions/sim-management/services/sim-voice-options.service"; @Injectable() export class FreebitMapperService { constructor( @Inject(Logger) private readonly logger: Logger, @Inject("SimVoiceOptionsService") private readonly voiceOptionsService?: SimVoiceOptionsService ) {} private parseOptionFlag(value: unknown, defaultValue: boolean = false): boolean { // If value is undefined or null, return the default if (value === undefined || value === null) { return defaultValue; } if (typeof value === "boolean") { return value; } if (typeof value === "number") { return value === 10 || value === 1; } if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "on" || normalized === "true") { return true; } if (normalized === "off" || normalized === "false") { return false; } const numeric = Number(normalized); if (!Number.isNaN(numeric)) { return numeric === 10 || numeric === 1; } } return defaultValue; } /** * Map SIM status from Freebit API to domain status */ mapSimStatus(status: string): "active" | "suspended" | "cancelled" | "pending" { switch (status) { case "active": return "active"; case "suspended": return "suspended"; case "temporary": case "waiting": return "pending"; case "obsolete": return "cancelled"; default: return "pending"; } } /** * Map Freebit account details response to SimDetails */ async mapToSimDetails(response: FreebitAccountDetailsResponse): Promise { const account = response.responseDatas[0]; if (!account) { throw new Error("No account data in response"); } let simType: "standard" | "nano" | "micro" | "esim" = "standard"; if (account.eid) { simType = "esim"; } else if (account.simSize) { simType = account.simSize; } // Try to get voice options from database first let voiceMailEnabled = true; let callWaitingEnabled = true; let internationalRoamingEnabled = true; let networkType = String(account.networkType ?? account.contractLine ?? "4G"); if (this.voiceOptionsService) { try { const storedOptions = await this.voiceOptionsService.getVoiceOptions( String(account.account ?? "") ); if (storedOptions) { voiceMailEnabled = storedOptions.voiceMailEnabled; callWaitingEnabled = storedOptions.callWaitingEnabled; internationalRoamingEnabled = storedOptions.internationalRoamingEnabled; networkType = storedOptions.networkType; this.logger.debug("[FreebitMapper] Loaded voice options from database", { account: account.account, options: storedOptions, }); } else { // No stored options, check API response voiceMailEnabled = this.parseOptionFlag(account.voicemail ?? account.voiceMail, true); callWaitingEnabled = this.parseOptionFlag( account.callwaiting ?? account.callWaiting, true ); internationalRoamingEnabled = this.parseOptionFlag( account.worldwing ?? account.worldWing, true ); this.logger.debug( "[FreebitMapper] No stored options found, using defaults or API values", { account: account.account, voiceMailEnabled, callWaitingEnabled, internationalRoamingEnabled, } ); } } catch (error) { this.logger.warn("[FreebitMapper] Failed to load voice options from database", { account: account.account, error, }); } } return { account: String(account.account ?? ""), status: this.mapSimStatus(String(account.state ?? account.status ?? "pending")), planCode: String(account.planCode ?? ""), planName: String(account.planName ?? ""), simType, iccid: String(account.iccid ?? ""), eid: String(account.eid ?? ""), msisdn: String(account.msisdn ?? account.account ?? ""), imsi: String(account.imsi ?? ""), remainingQuotaMb: Number(account.remainingQuotaMb ?? account.quota ?? 0), remainingQuotaKb: Number(account.remainingQuotaKb ?? 0), voiceMailEnabled, callWaitingEnabled, internationalRoamingEnabled, networkType, activatedAt: account.startDate ? String(account.startDate) : undefined, expiresAt: account.async ? String(account.async.date) : undefined, }; } /** * Map Freebit traffic info response to SimUsage */ mapToSimUsage(response: FreebitTrafficInfoResponse): SimUsage { if (!response.traffic) { throw new Error("No traffic data in response"); } const todayUsageKb = parseInt(response.traffic.today, 10) || 0; const recentDaysData = response.traffic.inRecentDays.split(",").map((usage, index) => ({ date: new Date(Date.now() - (index + 1) * 24 * 60 * 60 * 1000).toISOString().split("T")[0], usageKb: parseInt(usage, 10) || 0, usageMb: Math.round(((parseInt(usage, 10) || 0) / 1024) * 100) / 100, })); return { account: String(response.account ?? ""), todayUsageMb: Math.round((todayUsageKb / 1024) * 100) / 100, todayUsageKb, recentDaysUsage: recentDaysData, isBlacklisted: response.traffic.blackList === "10", }; } /** * Map Freebit quota history response to SimTopUpHistory */ mapToSimTopUpHistory(response: FreebitQuotaHistoryResponse, account: string): SimTopUpHistory { if (!response.quotaHistory) { throw new Error("No history data in response"); } return { account, totalAdditions: Number(response.total) || 0, additionCount: Number(response.count) || 0, history: response.quotaHistory.map(item => ({ quotaKb: parseInt(item.quota, 10), quotaMb: Math.round((parseInt(item.quota, 10) / 1024) * 100) / 100, addedDate: item.date, expiryDate: item.expire, campaignCode: item.quotaCode, })), }; } /** * Normalize account identifier (remove formatting) */ normalizeAccount(account: string): string { return account.replace(/[-\s()]/g, ""); } /** * Validate account format */ validateAccount(account: string): boolean { const normalized = this.normalizeAccount(account); // Basic validation - should be digits, typically 10-11 digits for Japanese phone numbers return /^\d{10,11}$/.test(normalized); } /** * Format date for Freebit API (YYYYMMDD) */ formatDateForApi(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}${month}${day}`; } /** * Parse date from Freebit API format (YYYYMMDD) */ parseDateFromApi(dateString: string): Date | null { if (!/^\d{8}$/.test(dateString)) { return null; } const year = parseInt(dateString.substring(0, 4), 10); const month = parseInt(dateString.substring(4, 6), 10) - 1; // Month is 0-indexed const day = parseInt(dateString.substring(6, 8), 10); return new Date(year, month, day); } }