import { Injectable, Inject, BadRequestException } 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 { SimNotificationService } from "./sim-notification.service"; import { getErrorMessage } from "@bff/core/utils/error.util"; import type { SimPlanChangeRequest, SimFeaturesUpdateRequest } from "../types/sim-requests.types"; @Injectable() export class SimPlanService { constructor( private readonly freebitService: FreebitOrchestratorService, private readonly simValidation: SimValidationService, private readonly simNotification: SimNotificationService, @Inject(Logger) private readonly logger: Logger ) {} /** * Change SIM plan */ async changeSimPlan( userId: string, subscriptionId: number, request: SimPlanChangeRequest ): Promise<{ ipv4?: string; ipv6?: string }> { try { const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId); // Validate plan code format - simplified for 5GB, 10GB, 25GB, 50GB plans if (!request.newPlanCode || !["5GB", "10GB", "25GB", "50GB"].includes(request.newPlanCode)) { throw new BadRequestException("Invalid plan code. Must be one of: 5GB, 10GB, 25GB, 50GB"); } // Map simplified plan codes to Freebit plan codes const planCodeMapping: Record = { "5GB": "PASI_5G", "10GB": "PASI_10G", "25GB": "PASI_25G", "50GB": "PASI_50G", }; const freebitPlanCode = planCodeMapping[request.newPlanCode]; if (!freebitPlanCode) { throw new BadRequestException(`Unable to map plan code ${request.newPlanCode} to Freebit format`); } // Automatically set to 1st of next month const nextMonth = new Date(); nextMonth.setMonth(nextMonth.getMonth() + 1); nextMonth.setDate(1); // Set to 1st of the month // Format as YYYYMMDD for Freebit API const year = nextMonth.getFullYear(); const month = String(nextMonth.getMonth() + 1).padStart(2, "0"); const day = String(nextMonth.getDate()).padStart(2, "0"); const scheduledAt = `${year}${month}${day}`; this.logger.log(`Auto-scheduled plan change to 1st of next month: ${scheduledAt}`, { userId, subscriptionId, account, newPlanCode: request.newPlanCode, freebitPlanCode, }); // First try immediate change, fallback to scheduled if that fails let result: { ipv4?: string; ipv6?: string }; try { result = await this.freebitService.changeSimPlan(account, freebitPlanCode, { assignGlobalIp: false, // No global IP assignment // Try immediate first }); this.logger.log(`Immediate plan change successful for account ${account}`, { account, newPlanCode: request.newPlanCode, freebitPlanCode, }); } catch (immediateError) { this.logger.warn(`Immediate plan change failed, trying scheduled: ${getErrorMessage(immediateError)}`, { account, newPlanCode: request.newPlanCode, freebitPlanCode, error: getErrorMessage(immediateError), }); // Fallback to scheduled change result = await this.freebitService.changeSimPlan(account, freebitPlanCode, { assignGlobalIp: false, // No global IP assignment scheduledAt: scheduledAt, }); this.logger.log(`Scheduled plan change successful for account ${account}`, { account, newPlanCode: request.newPlanCode, freebitPlanCode, scheduledAt, }); } this.logger.log(`Successfully changed SIM plan for subscription ${subscriptionId}`, { userId, subscriptionId, account, newPlanCode: request.newPlanCode, freebitPlanCode, scheduledAt: scheduledAt, assignGlobalIp: false, }); await this.simNotification.notifySimAction("Change Plan", "SUCCESS", { userId, subscriptionId, account, newPlanCode: request.newPlanCode, freebitPlanCode, scheduledAt, }); return result; } catch (error) { const sanitizedError = getErrorMessage(error); this.logger.error(`Failed to change SIM plan for subscription ${subscriptionId}`, { error: sanitizedError, userId, subscriptionId, newPlanCode: request.newPlanCode, }); await this.simNotification.notifySimAction("Change Plan", "ERROR", { userId, subscriptionId, account: "unknown", newPlanCode: request.newPlanCode, error: sanitizedError, }); throw error; } } /** * Update SIM features (voicemail, call waiting, roaming, network type) */ async updateSimFeatures( userId: string, subscriptionId: number, request: SimFeaturesUpdateRequest ): Promise { try { const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId); // Validate network type if provided if (request.networkType && !["4G", "5G"].includes(request.networkType)) { throw new BadRequestException('networkType must be either "4G" or "5G"'); } // Log the request for debugging this.logger.log(`Updating SIM features for subscription ${subscriptionId}`, { userId, subscriptionId, account, request, }); // Update all features in one call - the FreebitOperationsService will handle the complexity await this.freebitService.updateSimFeatures(account, { voiceMailEnabled: request.voiceMailEnabled, callWaitingEnabled: request.callWaitingEnabled, internationalRoamingEnabled: request.internationalRoamingEnabled, networkType: request.networkType, }); this.logger.log(`Successfully updated SIM features for subscription ${subscriptionId}`, { userId, subscriptionId, account, ...request, }); await this.simNotification.notifySimAction("Update Features", "SUCCESS", { userId, subscriptionId, account, ...request, }); } catch (error) { const sanitizedError = getErrorMessage(error); this.logger.error(`Failed to update SIM features for subscription ${subscriptionId}`, { error: sanitizedError, userId, subscriptionId, account: "unknown", ...request, errorStack: error instanceof Error ? error.stack : undefined, }); await this.simNotification.notifySimAction("Update Features", "ERROR", { userId, subscriptionId, ...request, error: sanitizedError, }); throw error; } } }