import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service"; import type { OrderDetails, OrderItemDetails } from "@customer-portal/domain/orders"; import { getErrorMessage } from "@bff/core/utils/error.util"; import { SimActivationException, OrderValidationException, } from "@bff/core/exceptions/domain-exceptions"; export interface SimFulfillmentRequest { orderDetails: OrderDetails; configurations: Record; } @Injectable() export class SimFulfillmentService { constructor( private readonly freebit: FreebitOrchestratorService, @Inject(Logger) private readonly logger: Logger ) {} async fulfillSimOrder(request: SimFulfillmentRequest): Promise { const { orderDetails, configurations } = request; this.logger.log("Starting SIM fulfillment", { orderId: orderDetails.id, orderType: orderDetails.orderType, }); const simType = this.readEnum(configurations.simType, ["eSIM", "Physical SIM"]) ?? "eSIM"; const eid = this.readString(configurations.eid); const activationType = this.readEnum(configurations.activationType, ["Immediate", "Scheduled"]) ?? "Immediate"; const scheduledAt = this.readString(configurations.scheduledAt); const phoneNumber = this.readString(configurations.mnpPhone); const mnp = this.extractMnpConfig(configurations); const simPlanItem = orderDetails.items.find( (item: OrderItemDetails) => item.product?.itemClass === "Plan" || item.product?.sku?.toLowerCase().includes("sim") ); if (!simPlanItem) { throw new OrderValidationException("No SIM plan found in order items", { orderId: orderDetails.id, }); } const planSku = simPlanItem.product?.sku; if (!planSku) { throw new OrderValidationException("SIM plan SKU not found", { orderId: orderDetails.id, itemId: simPlanItem.id, }); } if (simType === "eSIM" && (!eid || eid.length < 15)) { throw new SimActivationException("EID is required for eSIM and must be valid", { orderId: orderDetails.id, simType, eidLength: eid?.length, }); } if (!phoneNumber) { throw new SimActivationException("Phone number is required for SIM activation", { orderId: orderDetails.id, }); } if (simType === "eSIM") { if (!eid) { throw new SimActivationException("EID is required for eSIM activation", { orderId: orderDetails.id, }); } await this.activateSim({ account: phoneNumber, eid, planSku, simType: "eSIM", activationType, scheduledAt, mnp, }); } else { await this.activateSim({ account: phoneNumber, planSku, simType: "Physical SIM", activationType, scheduledAt, mnp, }); } this.logger.log("SIM fulfillment completed successfully", { orderId: orderDetails.id, account: phoneNumber, planSku, }); } private async activateSim( params: | { account: string; eid: string; planSku: string; simType: "eSIM"; activationType: "Immediate" | "Scheduled"; scheduledAt?: string; mnp?: { reserveNumber?: string; reserveExpireDate?: string; account?: string; firstnameKanji?: string; lastnameKanji?: string; firstnameZenKana?: string; lastnameZenKana?: string; gender?: string; birthday?: string; }; } | { account: string; eid?: string; planSku: string; simType: "Physical SIM"; activationType: "Immediate" | "Scheduled"; scheduledAt?: string; mnp?: { reserveNumber?: string; reserveExpireDate?: string; account?: string; firstnameKanji?: string; lastnameKanji?: string; firstnameZenKana?: string; lastnameZenKana?: string; gender?: string; birthday?: string; }; } ): Promise { const { account, planSku, simType, activationType, scheduledAt, mnp } = params; try { if (simType === "eSIM") { const { eid } = params; await this.freebit.activateEsimAccountNew({ account, eid, planCode: planSku, contractLine: "5G", shipDate: activationType === "Scheduled" ? scheduledAt : undefined, mnp: mnp && mnp.reserveNumber && mnp.reserveExpireDate ? { reserveNumber: mnp.reserveNumber, reserveExpireDate: mnp.reserveExpireDate, } : undefined, identity: mnp ? { firstnameKanji: mnp.firstnameKanji, lastnameKanji: mnp.lastnameKanji, firstnameZenKana: mnp.firstnameZenKana, lastnameZenKana: mnp.lastnameZenKana, gender: mnp.gender, birthday: mnp.birthday, } : undefined, }); this.logger.log("eSIM activated successfully", { account, planSku, scheduled: activationType === "Scheduled", }); } else { await this.freebit.topUpSim(account, 0, { scheduledAt: activationType === "Scheduled" ? scheduledAt : undefined, }); this.logger.log("Physical SIM activation scheduled", { account, planSku, }); } } catch (error: unknown) { this.logger.error("SIM activation failed", { account, planSku, error: getErrorMessage(error), }); throw error; } } private readString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } private readEnum(value: unknown, allowed: readonly T[]): T | undefined { return typeof value === "string" && allowed.includes(value as T) ? (value as T) : undefined; } private extractMnpConfig(config: Record) { const nested = config.mnp; const source = nested && typeof nested === "object" ? (nested as Record) : config; const isMnpFlag = this.readString(source.isMnp ?? config.isMnp); if (isMnpFlag && isMnpFlag !== "true") { return undefined; } const reserveNumber = this.readString(source.mnpNumber ?? source.reserveNumber); const reserveExpireDate = this.readString(source.mnpExpiry ?? source.reserveExpireDate); const account = this.readString(source.mvnoAccountNumber ?? source.account); const firstnameKanji = this.readString(source.portingFirstName ?? source.firstnameKanji); const lastnameKanji = this.readString(source.portingLastName ?? source.lastnameKanji); const firstnameZenKana = this.readString( source.portingFirstNameKatakana ?? source.firstnameZenKana ); const lastnameZenKana = this.readString( source.portingLastNameKatakana ?? source.lastnameZenKana ); const gender = this.readString(source.portingGender ?? source.gender); const birthday = this.readString(source.portingDateOfBirth ?? source.birthday); if ( !reserveNumber && !reserveExpireDate && !account && !firstnameKanji && !lastnameKanji && !firstnameZenKana && !lastnameZenKana && !gender && !birthday ) { return undefined; } return { reserveNumber, reserveExpireDate, account, firstnameKanji, lastnameKanji, firstnameZenKana, lastnameZenKana, gender, birthday, }; } }