diff --git a/apps/bff/src/integrations/freebit/facades/freebit.facade.ts b/apps/bff/src/integrations/freebit/facades/freebit.facade.ts index ac3b47e7..9ee158b4 100644 --- a/apps/bff/src/integrations/freebit/facades/freebit.facade.ts +++ b/apps/bff/src/integrations/freebit/facades/freebit.facade.ts @@ -208,14 +208,11 @@ export class FreebitFacade { // ============================================================================ /** - * Register a semi-black SIM account (PA05-18) + * Register a semi-black SIM account (PA05-19) * - * This MUST be called BEFORE PA02-01 for physical SIMs. + * For MNP transfers on physical SIMs, this replaces PA02-01. * Semi-black SIMs are pre-provisioned physical SIMs that need to be registered * to associate them with a customer account and plan. - * - * Error 210 "アカウント不在エラー" on PA02-01 indicates that PA05-18 - * was not called first. */ async registerSemiBlackAccount(params: { account: string; diff --git a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts index bf1a40c5..6ea96865 100644 --- a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts +++ b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts @@ -319,15 +319,15 @@ export interface FreebitEsimAccountActivationRequest { oldEid?: string; // Row 14: 元eSIM識別番号 (Conditional - for exchange) mnp?: { // Row 15: MNP情報 (Conditional) - reserveNumber: string; // Row 16: MNP予約番号 (Conditional) - reserveExpireDate?: string; // (Conditional) YYYYMMDD + reserveNumber: string; // Row 16: MNP予約番号 (Level 2) + reserveExpireDate?: string; // (Level 2) YYYYMMDD + lastnameKanji?: string; // Row 17: 名前(漢字) (Level 2) + firstnameKanji?: string; // Row 18: 由字(漢字) (Level 2) + lastnameZenKana?: string; // Row 19: 名前(全角カタカナ) (Level 2) + firstnameZenKana?: string; // Row 20: 由字(全角カタカナ) (Level 2) + gender?: string; // Row 21: 性別 ('M'/'W'/'C') (Level 2) + birthday?: string; // Row 22: 生年月日 YYYYMMDD (Level 2) }; - firstnameKanji?: string; // Row 17: 由字(漢字) (Conditional) - lastnameKanji?: string; // Row 18: 名前(漢字) (Conditional) - firstnameZenKana?: string; // Row 19: 由字(全角カタカナ) (Conditional) - lastnameZenKana?: string; // Row 20: 名前(全角カタカナ) (Conditional) - gender?: string; // Row 21: 性別 ('M', 'F') (Required for identification) - birthday?: string; // Row 22: 生年月日 YYYYMMDD (Conditional) shipDate?: string; // Row 23: 出荷日 YYYYMMDD (Conditional) planCode?: string; // Row 24: プランコード (Max 32 chars) (Conditional) deliveryCode?: string; // Row 25: 顧客コード (Max 10 chars) (Conditional - OEM specific) diff --git a/apps/bff/src/integrations/freebit/services/freebit-esim.service.ts b/apps/bff/src/integrations/freebit/services/freebit-esim.service.ts index dc11c1d1..8ae52e04 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-esim.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-esim.service.ts @@ -24,13 +24,14 @@ export interface EsimActivationParams { repAccount?: string; deliveryCode?: string; globalIp?: "10" | "20"; - mnp?: { reserveNumber: string; reserveExpireDate?: string }; - identity?: { - firstnameKanji?: string; + mnp?: { + reserveNumber: string; + reserveExpireDate?: string; lastnameKanji?: string; - firstnameZenKana?: string; + firstnameKanji?: string; lastnameZenKana?: string; - gender?: string; + firstnameZenKana?: string; + gender?: string; // Freebit uses 'M'/'W'/'C' (caller should map 'F' → 'W') birthday?: string; }; } @@ -128,7 +129,6 @@ export class FreebitEsimService { deliveryCode, globalIp, mnp, - identity, } = params; if (!account || !eid) { @@ -144,6 +144,19 @@ export class FreebitEsimService { ); } + this.logger.log("PA05-41 preparing eSIM activation payload", { + account, + addKind: finalAddKind, + simKind: simKind || "E0", + aladinOperated, + hasMnp: !!mnp, + hasReserveNumber: !!mnp?.reserveNumber, + hasIdentity: !!(mnp?.lastnameKanji || mnp?.firstnameKanji), + gender: mnp?.gender, + planCode, + contractLine, + }); + try { const payload: FreebitEsimAccountActivationRequest = { authKey: await this.auth.getAuthKey(), @@ -159,10 +172,19 @@ export class FreebitEsimService { repAccount, deliveryCode, globalIp, + // MNP object includes both reservation info AND identity fields (all Level 2 per PA05-41 spec) ...(mnp ? { mnp } : {}), - ...(identity ? identity : {}), } as FreebitEsimAccountActivationRequest; + this.logger.log("PA05-41 sending request", { + account, + addKind: finalAddKind, + aladinOperated, + mnpReserveNumber: mnp?.reserveNumber, + mnpHasIdentity: !!(mnp?.lastnameKanji || mnp?.firstnameKanji), + mnpGender: mnp?.gender, + }); + await this.client.makeAuthenticatedJsonRequest< FreebitEsimAccountActivationResponse, FreebitEsimAccountActivationRequest @@ -172,9 +194,9 @@ export class FreebitEsimService { account, planCode, contractLine, - addKind: addKind || "N", + addKind: finalAddKind, scheduled: !!shipDate, - mnp: !!mnp, + isMnp: finalAddKind === "M", }); } catch (error) { const message = extractErrorMessage(error); @@ -182,7 +204,9 @@ export class FreebitEsimService { account, eid, planCode, - addKind, + addKind: finalAddKind, + aladinOperated, + isMnp: finalAddKind === "M", error: message, }); throw new BadRequestException(`Failed to activate new eSIM account: ${message}`); diff --git a/apps/bff/src/integrations/freebit/services/freebit-semiblack.service.ts b/apps/bff/src/integrations/freebit/services/freebit-semiblack.service.ts index 328744be..6c7ecfea 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-semiblack.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-semiblack.service.ts @@ -1,15 +1,11 @@ /** - * Freebit Semi-Black Account Registration Service (PA05-18) + * Freebit Semi-Black Account Registration Service (PA05-19) * - * Handles MVNO semi-black (半黒) SIM registration via the Freebit PA05-18 API. - * This must be called BEFORE PA02-01 for physical SIMs to create the account - * in Freebit's system. + * Handles MVNO semi-black (半黒) SIM registration via the Freebit PA05-19 API. + * For MNP transfers, this replaces PA02-01. For non-MNP, PA02-01 is used instead. * * Semi-black SIMs are pre-provisioned SIMs that need to be registered * to associate them with a customer account and plan. - * - * Error 210 "アカウント不在エラー" on PA02-01 indicates that PA05-18 - * was not called first. */ import { Injectable, Inject, BadRequestException } from "@nestjs/common"; @@ -18,7 +14,7 @@ import { extractErrorMessage } from "@bff/core/utils/error.util.js"; import { FreebitClientService } from "./freebit-client.service.js"; /** - * PA05-18 Semi-Black Account Registration parameters + * PA05-19 Semi-Black Account Registration parameters */ export interface SemiBlackRegistrationParams { /** MSISDN (phone number) - 11-14 digits */ @@ -40,7 +36,7 @@ export interface SemiBlackRegistrationParams { } /** - * PA05-18 Request payload structure + * PA05-19 Request payload structure */ interface FreebitSemiBlackRequest { authKey: string; @@ -58,7 +54,7 @@ interface FreebitSemiBlackRequest { } /** - * PA05-18 Response structure + * PA05-19 Response structure */ interface FreebitSemiBlackResponse { resultCode: number; @@ -76,10 +72,10 @@ export class FreebitSemiBlackService { ) {} /** - * Register a semi-black SIM account (PA05-18) + * Register a semi-black SIM account (PA05-19) * * This registers a pre-provisioned (semi-black) physical SIM in Freebit's - * system. Must be called BEFORE PA02-01 account registration. + * system. For MNP transfers, this replaces PA02-01. * * @param params - Semi-black registration parameters * @throws BadRequestException if registration fails @@ -117,7 +113,7 @@ export class FreebitSemiBlackService { // Default to today's date if not provided const effectiveShipDate = shipDate ?? this.formatTodayAsYYYYMMDD(); - this.logger.log("Starting semi-black SIM registration (PA05-18)", { + this.logger.log("Starting semi-black SIM registration (PA05-19)", { account, productNumber, planCode, @@ -146,7 +142,7 @@ export class FreebitSemiBlackService { Omit >("/mvno/semiblack/addAcnt/", payload); - this.logger.log("Semi-black SIM registration successful (PA05-18)", { + this.logger.log("Semi-black SIM registration successful (PA05-19)", { account, productNumber, planCode, @@ -158,7 +154,7 @@ export class FreebitSemiBlackService { } const message = extractErrorMessage(error); - this.logger.error("Semi-black registration failed (PA05-18)", { + this.logger.error("Semi-black registration failed (PA05-19)", { account, productNumber, planCode, diff --git a/apps/bff/src/modules/orders/services/fulfillment-step-executors.service.ts b/apps/bff/src/modules/orders/services/fulfillment-step-executors.service.ts index cf96da1f..c54b3f7c 100644 --- a/apps/bff/src/modules/orders/services/fulfillment-step-executors.service.ts +++ b/apps/bff/src/modules/orders/services/fulfillment-step-executors.service.ts @@ -64,7 +64,7 @@ export class FulfillmentStepExecutors { } /** - * SIM fulfillment via Freebit (PA05-18 + PA02-01 + PA05-05) + * SIM fulfillment via Freebit (PA05-19/PA02-01 + PA05-05) */ async executeSimFulfillment( ctx: OrderFulfillmentContext, diff --git a/apps/bff/src/modules/orders/services/fulfillment-step-factory.service.ts b/apps/bff/src/modules/orders/services/fulfillment-step-factory.service.ts index 8cde253f..d33bc2ea 100644 --- a/apps/bff/src/modules/orders/services/fulfillment-step-factory.service.ts +++ b/apps/bff/src/modules/orders/services/fulfillment-step-factory.service.ts @@ -37,7 +37,7 @@ export class FulfillmentStepFactory { * Step order: * 1. sf_status_update (Activating) * 2. order_details (retain in context) - * 3. sim_fulfillment (SIM orders only - PA05-18 + PA02-01 + PA05-05) + * 3. sim_fulfillment (SIM orders only - PA05-19/PA02-01 + PA05-05) * 4. sf_activated_update (SIM orders only) * 5. mapping (with SIM data for WHMCS) * 6. whmcs_create @@ -111,7 +111,7 @@ export class FulfillmentStepFactory { ): DistributedStep { return { id: "sim_fulfillment", - description: "SIM activation via Freebit (PA05-18 + PA02-01 + PA05-05)", + description: "SIM activation via Freebit (PA05-19/PA02-01 + PA05-05)", execute: this.createTrackedStep(ctx, "sim_fulfillment", async () => { const result = await this.executors.executeSimFulfillment(ctx, payload); state.simFulfillmentResult = result; diff --git a/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts index 1e7a7cec..b1b7e9c2 100644 --- a/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts +++ b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts @@ -67,6 +67,31 @@ export interface SimFulfillmentResult { eid?: string; } +/** + * MNP configuration extracted from Salesforce order/porting fields + */ +interface MnpConfig { + reserveNumber?: string; + reserveExpireDate?: string; + account?: string; + firstnameKanji?: string; + lastnameKanji?: string; + firstnameZenKana?: string; + lastnameZenKana?: string; + gender?: string; + birthday?: string; +} + +/** + * Map Salesforce gender code to Freebit gender code. + * Salesforce: 'M' (Male), 'F' (Female) + * Freebit: 'M' (Male), 'W' (Weiblich/Female), 'C' (Corporation) + */ +function mapGenderToFreebit(gender: string): string { + if (gender === "F") return "W"; + return gender; +} + @Injectable() export class SimFulfillmentService { constructor( @@ -115,6 +140,16 @@ export class SimFulfillmentService { const scheduledAt = this.readString(configurations["scheduledAt"]); const phoneNumber = this.readString(configurations["mnpPhone"]); const mnp = this.extractMnpConfig(configurations); + const isMnp = !!mnp?.reserveNumber; + + this.logger.log("MNP detection result", { + orderId: orderDetails.id, + isMnp, + simType, + mnpReserveNumber: mnp?.reserveNumber, + mnpHasIdentity: !!(mnp?.lastnameKanji || mnp?.firstnameKanji), + mnpGender: mnp?.gender, + }); const simPlanItem = orderDetails.items.find( (item: OrderItemDetails) => @@ -174,7 +209,9 @@ export class SimFulfillmentService { eid, }; } else { - // Physical SIM activation flow (PA05-18 + PA02-01 + PA05-05) + // Physical SIM activation flow: + // Non-MNP: PA02-01 + PA05-05 + // MNP: PA05-19 + PA05-05 if (!assignedPhysicalSimId) { throw new SimActivationException( "Physical SIM requires an assigned SIM from inventory (Assign_Physical_SIM__c)", @@ -191,6 +228,8 @@ export class SimFulfillmentService { callWaitingEnabled, contactIdentity, assignmentDetails, + isMnp, + ...(mnp && { mnp }), }); this.logger.log("Physical SIM fulfillment completed successfully", { @@ -222,55 +261,62 @@ export class SimFulfillmentService { planSku: string; activationType: "Immediate" | "Scheduled"; scheduledAt?: string; - mnp?: { - reserveNumber?: string; - reserveExpireDate?: string; - account?: string; - firstnameKanji?: string; - lastnameKanji?: string; - firstnameZenKana?: string; - lastnameZenKana?: string; - gender?: string; - birthday?: string; - }; + mnp?: MnpConfig; }): Promise { const { account, eid, planSku, activationType, scheduledAt, mnp } = params; + const isMnp = !!mnp?.reserveNumber; + + this.logger.log("eSIM activation starting", { + account, + planSku, + isMnp, + addKind: isMnp ? "M" : "N", + aladinOperated: isMnp ? "20" : "10", + mnpReserveNumber: mnp?.reserveNumber, + mnpHasIdentity: !!(mnp?.lastnameKanji || mnp?.firstnameKanji), + mnpGender: mnp?.gender, + }); try { + // Build unified MNP object with both reservation and identity data (all Level 2 per PA05-41) + const mnpPayload = + isMnp && mnp?.reserveNumber + ? { + reserveNumber: mnp.reserveNumber, + ...(mnp.reserveExpireDate && { reserveExpireDate: mnp.reserveExpireDate }), + ...(mnp.lastnameKanji && { lastnameKanji: mnp.lastnameKanji }), + ...(mnp.firstnameKanji && { firstnameKanji: mnp.firstnameKanji }), + ...(mnp.lastnameZenKana && { lastnameZenKana: mnp.lastnameZenKana }), + ...(mnp.firstnameZenKana && { firstnameZenKana: mnp.firstnameZenKana }), + // Map Salesforce gender 'F' → Freebit gender 'W' (Weiblich) + ...(mnp.gender && { gender: mapGenderToFreebit(mnp.gender) }), + ...(mnp.birthday && { birthday: mnp.birthday }), + } + : undefined; + await this.freebitFacade.activateEsimAccountNew({ account, eid, planCode: planSku, contractLine: "5G", + simKind: "E0", // Voice eSIM + addKind: isMnp ? "M" : "N", + aladinOperated: isMnp ? "20" : "10", // '20' = we provide identity for ALADIN ...(activationType === "Scheduled" && scheduledAt && { shipDate: scheduledAt }), - ...(mnp?.reserveNumber && - mnp?.reserveExpireDate && { - mnp: { - reserveNumber: mnp.reserveNumber, - reserveExpireDate: mnp.reserveExpireDate, - }, - }), - ...(mnp && { - identity: { - ...(mnp.firstnameKanji && { firstnameKanji: mnp.firstnameKanji }), - ...(mnp.lastnameKanji && { lastnameKanji: mnp.lastnameKanji }), - ...(mnp.firstnameZenKana && { firstnameZenKana: mnp.firstnameZenKana }), - ...(mnp.lastnameZenKana && { lastnameZenKana: mnp.lastnameZenKana }), - ...(mnp.gender && { gender: mnp.gender }), - ...(mnp.birthday && { birthday: mnp.birthday }), - }, - }), + ...(mnpPayload && { mnp: mnpPayload }), }); this.logger.log("eSIM activated successfully", { account, planSku, + isMnp, scheduled: activationType === "Scheduled", }); } catch (error: unknown) { this.logger.error("eSIM activation failed", { account, planSku, + isMnp, error: extractErrorMessage(error), }); throw error; @@ -278,15 +324,21 @@ export class SimFulfillmentService { } /** - * Activate Physical SIM (Black SIM) via Freebit PA02-01 + PA05-05 APIs + * Activate Physical SIM (Black SIM) via Freebit APIs * - * Flow for Physical SIMs (Black SIMs): + * Non-MNP flow: * 1. Fetch SIM Inventory details from Salesforce * 2. Validate SIM status is "Available" * 3. Map product SKU to Freebit plan code * 4. Call Freebit PA02-01 (Account Registration) with createType="new" * 5. Call Freebit PA05-05 (Voice Options) to configure voice features * 6. Update SIM Inventory status to "Assigned" + * + * MNP flow: + * 1-3. Same as above + * 4. Call Freebit PA05-19 (Semi-Black MNP Registration) — replaces PA02-01 + * 5. Call Freebit PA05-05 (Voice Options) + * 6. Update SIM Inventory status to "Assigned" */ private async activatePhysicalSim(params: { orderId: string; @@ -297,6 +349,8 @@ export class SimFulfillmentService { callWaitingEnabled: boolean; contactIdentity?: ContactIdentityData | undefined; assignmentDetails?: SimAssignmentDetails | undefined; + isMnp?: boolean; + mnp?: MnpConfig; }): Promise<{ phoneNumber: string; serialNumber: string }> { const { orderId, @@ -307,15 +361,20 @@ export class SimFulfillmentService { callWaitingEnabled, contactIdentity, assignmentDetails, + isMnp = false, + mnp, } = params; - this.logger.log("Starting Physical SIM activation (PA02-01 + PA05-05)", { + this.logger.log("Starting Physical SIM activation", { orderId, simInventoryId, planSku, + isMnp, + path: isMnp ? "PA05-19 (MNP)" : "PA02-01 (new)", voiceMailEnabled, callWaitingEnabled, hasContactIdentity: !!contactIdentity, + mnpReserveNumber: mnp?.reserveNumber, }); // Step 1 & 2: Fetch and validate SIM Inventory @@ -337,28 +396,54 @@ export class SimFulfillmentService { orderId, simInventoryId, accountPhoneNumber, + ptNumber: simRecord.ptNumber, planCode, + isMnp, }); try { - // Step 4: Call Freebit PA02-01 (Account Registration) for Black SIM - this.logger.log("Calling PA02-01 Account Registration", { - orderId, - account: accountPhoneNumber, - planCode, - createType: "new", - }); + if (isMnp) { + // Step 4 (MNP): Call Freebit PA05-19 (Semi-Black MNP Registration) + // PA05-19 replaces PA02-01 for MNP transfers — it registers the account + // and initiates the MNP transfer in a single call + this.logger.log("Calling PA05-19 Semi-Black MNP Registration", { + orderId, + account: accountPhoneNumber, + productNumber: simRecord.ptNumber, + planCode, + mnpMethod: "10", + }); - await this.freebitFacade.registerAccount({ - account: accountPhoneNumber, - planCode, - createType: "new", - }); + await this.freebitFacade.registerSemiBlackAccount({ + account: accountPhoneNumber, + productNumber: simRecord.ptNumber, + planCode, + }); - this.logger.log("PA02-01 Account Registration successful", { - orderId, - account: accountPhoneNumber, - }); + this.logger.log("PA05-19 Semi-Black MNP Registration successful", { + orderId, + account: accountPhoneNumber, + }); + } else { + // Step 4 (non-MNP): Call Freebit PA02-01 (Account Registration) + this.logger.log("Calling PA02-01 Account Registration", { + orderId, + account: accountPhoneNumber, + planCode, + createType: "new", + }); + + await this.freebitFacade.registerAccount({ + account: accountPhoneNumber, + planCode, + createType: "new", + }); + + this.logger.log("PA02-01 Account Registration successful", { + orderId, + account: accountPhoneNumber, + }); + } // Step 5: Call Freebit PA05-05 (Voice Options Registration) // Only call if we have contact identity data