diff --git a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts index 6ea96865..ae28bd49 100644 --- a/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts +++ b/apps/bff/src/integrations/freebit/interfaces/freebit.types.ts @@ -287,6 +287,8 @@ export interface FreebitEsimAddAccountRequest { shipDate?: string; planCode?: string; contractLine?: string; + oldProductNumber?: string; // Required for physical SIM → eSIM reissue + oldEid?: string; // Required for eSIM → eSIM reissue mnp?: { reserveNumber: string; reserveExpireDate: string; @@ -349,7 +351,7 @@ export interface FreebitEsimAccountActivationResponse { export interface FreebitError extends Error { resultCode: string; statusCode: string | number; - freebititMessage: string; + freebitMessage: string; } // Configuration 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 8ae52e04..dd41b541 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-esim.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-esim.service.ts @@ -2,7 +2,6 @@ import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { extractErrorMessage } from "@bff/core/utils/error.util.js"; import { FreebitClientService } from "./freebit-client.service.js"; -import { FreebitAuthService } from "./freebit-auth.service.js"; import type { FreebitEsimReissueRequest, FreebitEsimReissueResponse, @@ -44,7 +43,6 @@ export interface EsimActivationParams { export class FreebitEsimService { constructor( private readonly client: FreebitClientService, - private readonly auth: FreebitAuthService, @Inject(Logger) private readonly logger: Logger ) {} @@ -88,6 +86,8 @@ export class FreebitEsimService { eid: newEid, addKind: "R", ...(options.planCode !== undefined && { planCode: options.planCode }), + ...(options.oldProductNumber && { oldProductNumber: options.oldProductNumber }), + ...(options.oldEid && { oldEid: options.oldEid }), }; await this.client.makeAuthenticatedRequest( @@ -158,10 +158,11 @@ export class FreebitEsimService { }); try { - const payload: FreebitEsimAccountActivationRequest = { - authKey: await this.auth.getAuthKey(), + // Note: authKey is injected by makeAuthenticatedJsonRequest + // createType is not required when addKind is 'R' (reissue) + const payload: Omit = { aladinOperated, - createType: "new", + ...(finalAddKind !== "R" && { createType: "new" }), eid, account, simkind: simKind || "E0", @@ -174,21 +175,39 @@ export class FreebitEsimService { globalIp, // MNP object includes both reservation info AND identity fields (all Level 2 per PA05-41 spec) ...(mnp ? { mnp } : {}), - } as FreebitEsimAccountActivationRequest; + } as Omit; - this.logger.log("PA05-41 sending request", { + // Log full payload details (excluding authKey) for MNP debugging + this.logger.log("PA05-41 final payload to Freebit API", { account, + eid, addKind: finalAddKind, + simkind: simKind || "E0", aladinOperated, - mnpReserveNumber: mnp?.reserveNumber, - mnpHasIdentity: !!(mnp?.lastnameKanji || mnp?.firstnameKanji), - mnpGender: mnp?.gender, + createType: finalAddKind === "R" ? "omitted" : "new", + planCode, + contractLine, + shipDate: shipDate ?? "not-set", + hasMnpObject: !!mnp, + mnpFields: mnp + ? { + reserveNumber: mnp.reserveNumber ?? "MISSING", + reserveExpireDate: mnp.reserveExpireDate ?? "not-set", + lastnameKanji: mnp.lastnameKanji ? "present" : "missing", + firstnameKanji: mnp.firstnameKanji ? "present" : "missing", + lastnameZenKana: mnp.lastnameZenKana ? "present" : "missing", + firstnameZenKana: mnp.firstnameZenKana ? "present" : "missing", + gender: mnp.gender ?? "not-set", + birthday: mnp.birthday ? "present" : "missing", + totalFields: Object.keys(mnp).length, + } + : "no-mnp", }); await this.client.makeAuthenticatedJsonRequest< FreebitEsimAccountActivationResponse, - FreebitEsimAccountActivationRequest - >("/mvno/esim/addAcct/", payload); + Omit + >("/mvno/esim/addAcnt/", payload); this.logger.log("Successfully activated new eSIM account via PA05-41", { account, @@ -207,6 +226,8 @@ export class FreebitEsimService { addKind: finalAddKind, aladinOperated, isMnp: finalAddKind === "M", + hasMnpReserveNumber: !!mnp?.reserveNumber, + mnpFieldCount: mnp ? Object.keys(mnp).length : 0, error: message, }); throw new BadRequestException(`Failed to activate new eSIM account: ${message}`); diff --git a/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts b/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts index 7430d54d..4d6b1c97 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts @@ -160,15 +160,15 @@ export class FreebitMapperService { if (account.remainingQuotaMb != null) { // If API explicitly provides remainingQuotaMb, use it directly remainingQuotaMb = Number(account.remainingQuotaMb); - remainingQuotaKb = Number(account.remainingQuotaKb ?? remainingQuotaMb * 1000); + remainingQuotaKb = Number(account.remainingQuotaKb ?? remainingQuotaMb * 1024); } else if (account.quota != null) { - // If only quota is provided, it's in KB - convert to MB + // If only quota is provided, it's in KB - convert to MB (1 MB = 1024 KB) remainingQuotaKb = Number(account.quota); - remainingQuotaMb = remainingQuotaKb / 1000; + remainingQuotaMb = remainingQuotaKb / 1024; } else if (account.remainingQuotaKb != null) { - // If only remainingQuotaKb is provided, convert to MB + // If only remainingQuotaKb is provided, convert to MB (1 MB = 1024 KB) remainingQuotaKb = Number(account.remainingQuotaKb); - remainingQuotaMb = remainingQuotaKb / 1000; + remainingQuotaMb = remainingQuotaKb / 1024; } // Log raw account data in dev to debug MSISDN availability diff --git a/apps/bff/src/integrations/freebit/services/freebit-voice-options.service.ts b/apps/bff/src/integrations/freebit/services/freebit-voice-options.service.ts index 6644b49f..30d59971 100644 --- a/apps/bff/src/integrations/freebit/services/freebit-voice-options.service.ts +++ b/apps/bff/src/integrations/freebit/services/freebit-voice-options.service.ts @@ -25,7 +25,7 @@ export interface VoiceOptionIdentityData { lastnameKana: string; /** First name in Katakana (full-width, max 50 chars) */ firstnameKana: string; - /** Gender: "M" = Male, "F" = Female */ + /** Gender: "M" = Male, "F" = Female (mapped to Freebit "W" on send) */ gender: "M" | "F"; /** Birthday in YYYYMMDD format */ birthday: string; @@ -67,9 +67,9 @@ interface FreebitVoiceOptionRequest { identificationData: { lastnameKanji: string; firstnameKanji: string; - lastnameKana: string; - firstnameKana: string; - gender: string; + lastnameZenKana: string; + firstnameZenKana: string; + gender: string; // 'M'/'W'/'C' per Freebit spec birthday: string; }; } @@ -167,9 +167,11 @@ export class FreebitVoiceOptionsService { identificationData: { lastnameKanji: identificationData.lastnameKanji, firstnameKanji: identificationData.firstnameKanji, - lastnameKana: identificationData.lastnameKana, - firstnameKana: identificationData.firstnameKana, - gender: identificationData.gender, + // PA05-05 spec uses lastnameZenKana/firstnameZenKana (full-width katakana) + lastnameZenKana: identificationData.lastnameKana, + firstnameZenKana: identificationData.firstnameKana, + // Map Salesforce gender 'F' → Freebit gender 'W' (PA05-05 uses M/W/C) + gender: identificationData.gender === "F" ? "W" : identificationData.gender, birthday: identificationData.birthday, }, }; diff --git a/apps/bff/src/integrations/salesforce/facades/salesforce.facade.ts b/apps/bff/src/integrations/salesforce/facades/salesforce.facade.ts index 525fb526..c1bc9a1e 100644 --- a/apps/bff/src/integrations/salesforce/facades/salesforce.facade.ts +++ b/apps/bff/src/integrations/salesforce/facades/salesforce.facade.ts @@ -157,7 +157,15 @@ export class SalesforceFacade implements OnModuleInit { const soql = ` SELECT Id, Status, Type, Activation_Status__c, WHMCS_Order_ID__c, Activation_Error_Code__c, Activation_Error_Message__c, - AccountId, Account.Name, SIM_Type__c, Assign_Physical_SIM__c + AccountId, Account.Name, SIM_Type__c, Assign_Physical_SIM__c, + EID__c, SIM_Voice_Mail__c, SIM_Call_Waiting__c, + Activation_Type__c, Activation_Scheduled_At__c, + MNP_Application__c, MNP_Reservation_Number__c, + MNP_Expiry_Date__c, MNP_Phone_Number__c, + MVNO_Account_Number__c, + Porting_FirstName__c, Porting_LastName__c, + Porting_FirstName_Katakana__c, Porting_LastName_Katakana__c, + Porting_Gender__c, Porting_DateOfBirth__c FROM Order WHERE Id = '${orderId}' LIMIT 1 @@ -169,11 +177,6 @@ export class SalesforceFacade implements OnModuleInit { return result.records?.[0] || null; } catch (error) { - // Temporary: Raw console log to see full error - console.error( - ">>> SALESFORCE getOrder ERROR >>>", - JSON.stringify(error, Object.getOwnPropertyNames(error as object), 2) - ); this.logger.error("Failed to get order from Salesforce", { orderId, error: extractErrorMessage(error), diff --git a/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts b/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts index 15c81980..0bfdceb1 100644 --- a/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts +++ b/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts @@ -80,6 +80,53 @@ export class FulfillmentContextMapper { } } + // Log MNP field extraction results for debugging + const mnpFieldsFound = [ + config["isMnp"] != null && "isMnp", + config["mnpNumber"] != null && "mnpNumber", + config["mnpExpiry"] != null && "mnpExpiry", + config["mnpPhone"] != null && "mnpPhone", + config["mvnoAccountNumber"] != null && "mvnoAccountNumber", + config["portingFirstName"] != null && "portingFirstName", + config["portingLastName"] != null && "portingLastName", + config["portingFirstNameKatakana"] != null && "portingFirstNameKatakana", + config["portingLastNameKatakana"] != null && "portingLastNameKatakana", + config["portingGender"] != null && "portingGender", + config["portingDateOfBirth"] != null && "portingDateOfBirth", + ].filter(Boolean); + + if (mnpFieldsFound.length > 0 || config["isMnp"] != null) { + this.logger.log("MNP configuration fields extracted", { + source: sfOrder ? "payload+salesforce" : "payload-only", + fieldsFound: mnpFieldsFound, + isMnp: config["isMnp"], + mnpNumber: config["mnpNumber"] ? "present" : "missing", + mnpExpiry: config["mnpExpiry"] ? "present" : "missing", + mnpPhone: config["mnpPhone"] ? "present" : "missing", + portingIdentityComplete: !!( + config["portingFirstName"] && + config["portingLastName"] && + config["portingFirstNameKatakana"] && + config["portingLastNameKatakana"] && + config["portingGender"] && + config["portingDateOfBirth"] + ), + // Log SF field presence separately so we know which side contributed + sfFields: sfOrder + ? { + MNP_Application__c: sfOrder.MNP_Application__c ?? "null", + MNP_Reservation_Number__c: sfOrder.MNP_Reservation_Number__c ? "present" : "missing", + MNP_Expiry_Date__c: sfOrder.MNP_Expiry_Date__c ? "present" : "missing", + MNP_Phone_Number__c: sfOrder.MNP_Phone_Number__c ? "present" : "missing", + Porting_FirstName__c: sfOrder.Porting_FirstName__c ? "present" : "missing", + Porting_LastName__c: sfOrder.Porting_LastName__c ? "present" : "missing", + Porting_Gender__c: sfOrder.Porting_Gender__c ?? "null", + Porting_DateOfBirth__c: sfOrder.Porting_DateOfBirth__c ?? "null", + } + : "no-sf-order", + }); + } + return config; } @@ -162,13 +209,13 @@ export class FulfillmentContextMapper { return `${isoMatch[1]}${isoMatch[2]}${isoMatch[3]}`; } - // Try parsing as Date object + // Try parsing as Date object — use UTC methods to avoid timezone shift try { const date = new Date(dateStr); if (!Number.isNaN(date.getTime())) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, "0"); + const day = String(date.getUTCDate()).padStart(2, "0"); return `${year}${month}${day}`; } } catch { 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 c54b3f7c..5a28ac2b 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 @@ -98,6 +98,17 @@ export class FulfillmentStepExecutors { voiceMailEnabled, callWaitingEnabled, hasContactIdentity: !!contactIdentity, + // MNP debug: log what made it into the configurations + mnpInConfig: { + isMnp: configurations["isMnp"] ?? "not-set", + mnpNumber: configurations["mnpNumber"] ? "present" : "missing", + mnpExpiry: configurations["mnpExpiry"] ? "present" : "missing", + mnpPhone: configurations["mnpPhone"] ? "present" : "missing", + portingFirstName: configurations["portingFirstName"] ? "present" : "missing", + portingLastName: configurations["portingLastName"] ? "present" : "missing", + portingGender: configurations["portingGender"] ?? "not-set", + portingDateOfBirth: configurations["portingDateOfBirth"] ? "present" : "missing", + }, }); // Build assignment details for SIM Inventory record (only include defined properties) 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 b1b7e9c2..eb8c976b 100644 --- a/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts +++ b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts @@ -187,10 +187,19 @@ export class SimFulfillmentService { }); } + // Map product SKU to Freebit plan code (same as physical SIM path) + const planCode = mapProductToFreebitPlanCode(planSku, planName); + if (!planCode) { + throw new SimActivationException( + `Unable to map product to Freebit plan code. SKU: ${planSku}, Name: ${planName}`, + { orderId: orderDetails.id, planSku, planName } + ); + } + await this.activateEsim({ account: phoneNumber, eid, - planSku, + planCode, activationType, ...(scheduledAt && { scheduledAt }), ...(mnp && { mnp }), @@ -258,17 +267,17 @@ export class SimFulfillmentService { private async activateEsim(params: { account: string; eid: string; - planSku: string; + planCode: string; activationType: "Immediate" | "Scheduled"; scheduledAt?: string; mnp?: MnpConfig; }): Promise { - const { account, eid, planSku, activationType, scheduledAt, mnp } = params; + const { account, eid, planCode, activationType, scheduledAt, mnp } = params; const isMnp = !!mnp?.reserveNumber; this.logger.log("eSIM activation starting", { account, - planSku, + planCode, isMnp, addKind: isMnp ? "M" : "N", aladinOperated: isMnp ? "20" : "10", @@ -294,29 +303,52 @@ export class SimFulfillmentService { } : undefined; - await this.freebitFacade.activateEsimAccountNew({ + const addKind = isMnp ? ("M" as const) : ("N" as const); + const aladinOperated = isMnp ? ("20" as const) : ("10" as const); + const pa0541Params = { account, eid, - planCode: planSku, - contractLine: "5G", - simKind: "E0", // Voice eSIM - addKind: isMnp ? "M" : "N", - aladinOperated: isMnp ? "20" : "10", // '20' = we provide identity for ALADIN + planCode, + contractLine: "5G" as const, + simKind: "E0" as const, + addKind, + aladinOperated, ...(activationType === "Scheduled" && scheduledAt && { shipDate: scheduledAt }), ...(mnpPayload && { mnp: mnpPayload }), + }; + + this.logger.log("PA05-41 full request payload", { + account: pa0541Params.account, + eid: pa0541Params.eid, + planCode: pa0541Params.planCode, + contractLine: pa0541Params.contractLine, + simKind: pa0541Params.simKind, + addKind: pa0541Params.addKind, + aladinOperated: pa0541Params.aladinOperated, + shipDate: pa0541Params.shipDate ?? "not-set", + hasMnpPayload: !!mnpPayload, + mnpPayloadFields: mnpPayload ? Object.keys(mnpPayload) : [], + mnpReserveNumber: mnpPayload?.reserveNumber ?? "not-set", + mnpGenderMapped: mnpPayload?.gender ?? "not-set", }); - this.logger.log("eSIM activated successfully", { + await this.freebitFacade.activateEsimAccountNew(pa0541Params); + + this.logger.log("eSIM activated successfully via PA05-41", { account, - planSku, + planCode, isMnp, + addKind: pa0541Params.addKind, scheduled: activationType === "Scheduled", }); } catch (error: unknown) { - this.logger.error("eSIM activation failed", { + this.logger.error("eSIM activation failed via PA05-41", { account, - planSku, + planCode, isMnp, + addKind: isMnp ? "M" : "N", + mnpReserveNumber: mnp?.reserveNumber ?? "not-set", + mnpFieldCount: mnp ? Object.keys(mnp).length : 0, error: extractErrorMessage(error), }); throw error; @@ -518,11 +550,15 @@ export class SimFulfillmentService { private extractMnpConfig(config: Record) { const nested = config["mnp"]; - const source = - nested && typeof nested === "object" ? (nested as Record) : config; + const hasNestedMnp = nested && typeof nested === "object"; + const source = hasNestedMnp ? (nested as Record) : config; const isMnpFlag = this.readString(source["isMnp"] ?? config["isMnp"]); - if (isMnpFlag && isMnpFlag !== "true") { + if (isMnpFlag && isMnpFlag.toLowerCase() !== "true") { + this.logger.log("MNP extraction skipped: isMnp flag is not 'true'", { + isMnpFlag, + isMnpFlagType: typeof (source["isMnp"] ?? config["isMnp"]), + }); return; } @@ -551,11 +587,24 @@ export class SimFulfillmentService { !gender && !birthday ) { + this.logger.log("MNP extraction: no MNP fields found in config", { + hasNestedMnp, + isMnpFlag: isMnpFlag ?? "not-set", + checkedKeys: [ + "mnpNumber", + "reserveNumber", + "mnpExpiry", + "portingFirstName", + "portingLastName", + "portingGender", + "portingDateOfBirth", + ], + }); return; } // Build object with only defined properties (for exactOptionalPropertyTypes) - return { + const result = { ...(reserveNumber && { reserveNumber }), ...(reserveExpireDate && { reserveExpireDate }), ...(account && { account }), @@ -566,5 +615,19 @@ export class SimFulfillmentService { ...(gender && { gender }), ...(birthday && { birthday }), }; + + this.logger.log("MNP config extracted", { + hasReserveNumber: !!reserveNumber, + reserveNumberLength: reserveNumber?.length, + hasReserveExpireDate: !!reserveExpireDate, + hasAccount: !!account, + hasIdentity: !!(firstnameKanji && lastnameKanji), + hasKana: !!(firstnameZenKana && lastnameZenKana), + gender: gender ?? "not-set", + hasBirthday: !!birthday, + totalFieldsExtracted: Object.keys(result).length, + }); + + return result; } } diff --git a/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx b/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx index 7b71d701..dd566785 100644 --- a/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx +++ b/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx @@ -261,7 +261,7 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro // Calculate percentage for circle const totalMB = Number.parseFloat(remainingMB) + Number.parseFloat(usedMB); const usagePercentage = totalMB > 0 ? (Number.parseFloat(usedMB) / totalMB) * 100 : 0; - const circumference = 2 * Math.PI * 88; + const circumference = 2 * Math.PI * 96; const strokeDashoffset = circumference - (usagePercentage / 100) * circumference; return ( diff --git a/packages/domain/sim/helpers.ts b/packages/domain/sim/helpers.ts index 9f7581f9..fa642142 100644 --- a/packages/domain/sim/helpers.ts +++ b/packages/domain/sim/helpers.ts @@ -111,8 +111,8 @@ export function mapProductToFreebitPlanCode( // Try to extract data tier from SKU or name const source = productSku || productName || ""; - // Match patterns like "50GB", "50G", "50gb", or just "50" in context of GB - const gbMatch = source.match(/(\d+)\s*G(?:B)?/i); + // Match patterns like "50GB", "50gb" — require the 'B' to distinguish from "5G" network type + const gbMatch = source.match(/(\d+)\s*GB/i); if (gbMatch?.[1]) { const tier = gbMatch[1]; const planCode = PLAN_CODE_MAPPING[tier]; @@ -121,8 +121,8 @@ export function mapProductToFreebitPlanCode( } } - // Try matching standalone numbers in SKU patterns like "sim-50gb" - const skuMatch = source.match(/[-_](\d+)[-_]?(?:gb|g)?/i); + // Try matching numbers in SKU patterns like "sim-50gb" or "sim-50-gb" + const skuMatch = source.match(/[-_](\d+)[-_]?gb/i); if (skuMatch?.[1]) { const tier = skuMatch[1]; const planCode = PLAN_CODE_MAPPING[tier]; @@ -131,5 +131,15 @@ export function mapProductToFreebitPlanCode( } } + // Fallback: match "NNg" only when preceded by delimiter (not "5G" network type) + const delimiterMatch = source.match(/[-_](\d+)g(?:\b|[-_])/i); + if (delimiterMatch?.[1]) { + const tier = delimiterMatch[1]; + const planCode = PLAN_CODE_MAPPING[tier]; + if (planCode) { + return planCode; + } + } + return null; } diff --git a/packages/domain/sim/providers/freebit/requests.ts b/packages/domain/sim/providers/freebit/requests.ts index 3f52bd87..24bc3f2c 100644 --- a/packages/domain/sim/providers/freebit/requests.ts +++ b/packages/domain/sim/providers/freebit/requests.ts @@ -126,7 +126,20 @@ export const freebitQuotaHistoryResponseSchema = z.object({ export const freebitEsimMnpSchema = z.object({ reserveNumber: z.string().min(1, "Reserve number is required"), - reserveExpireDate: z.string().regex(/^\d{8}$/, "Reserve expire date must be in YYYYMMDD format"), + reserveExpireDate: z + .string() + .regex(/^\d{8}$/, "Reserve expire date must be in YYYYMMDD format") + .optional(), + // Identity fields (Level 2 per PA05-41 spec — nested inside mnp) + lastnameKanji: z.string().optional(), + firstnameKanji: z.string().optional(), + lastnameZenKana: z.string().optional(), + firstnameZenKana: z.string().optional(), + gender: z.enum(["M", "W", "C"]).optional(), // M: Male, W: Female (Weiblich), C: Corporate + birthday: z + .string() + .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") + .optional(), }); export const freebitEsimReissueRequestSchema = z.object({ @@ -142,7 +155,7 @@ export const freebitEsimAddAccountRequestSchema = z.object({ aladinOperated: z.enum(["10", "20"]).default("10"), account: z.string().min(1, MSG_ACCOUNT_REQUIRED), eid: z.string().min(1, "EID is required"), - addKind: z.enum(["N", "R"]).default("N"), + addKind: z.enum(["N", "R", "M"]).default("N"), // N: New, R: Reissue, M: MNP shipDate: z.string().regex(YYYYMMDD_REGEX, MSG_SHIP_DATE_FORMAT).optional(), planCode: z.string().optional(), contractLine: z.enum(["4G", "5G"]).optional(), @@ -185,7 +198,7 @@ export const freebitEsimIdentitySchema = z.object({ lastnameKanji: z.string().optional(), firstnameZenKana: z.string().optional(), lastnameZenKana: z.string().optional(), - gender: z.enum(["M", "F"]).optional(), + gender: z.enum(["M", "W", "C"]).optional(), // Freebit: M=Male, W=Female, C=Corporate birthday: z .string() .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") @@ -199,30 +212,20 @@ export const freebitEsimIdentitySchema = z.object({ export const freebitEsimActivationRequestSchema = z.object({ authKey: z.string().min(1, "Auth key is required"), aladinOperated: z.enum(["10", "20"]).default("10"), // 10: issue profile, 20: no-issue - createType: z.enum(["new", "reissue", "exchange"]).default("new"), + createType: z.enum(["new", "reissue", "exchange"]).optional(), // Not required for addKind='R' account: z.string().min(1, "Account (MSISDN) is required"), eid: z.string().min(1, "EID is required for eSIM"), - simkind: z.enum(["esim", "psim"]).default("esim"), + simkind: z.enum(["E0", "E2", "E3"]).default("E0"), // E0: voice, E2: data-only, E3: SMS planCode: z.string().optional(), contractLine: z.enum(["4G", "5G"]).optional(), shipDate: z.string().regex(YYYYMMDD_REGEX, MSG_SHIP_DATE_FORMAT).optional(), - mnp: freebitEsimMnpSchema.optional(), - // Identity fields (flattened for API) - firstnameKanji: z.string().optional(), - lastnameKanji: z.string().optional(), - firstnameZenKana: z.string().optional(), - lastnameZenKana: z.string().optional(), - gender: z.enum(["M", "F"]).optional(), - birthday: z - .string() - .regex(/^\d{8}$/, "Birthday must be in YYYYMMDD format") - .optional(), + mnp: freebitEsimMnpSchema.optional(), // MNP + identity fields (Level 2 per PA05-41) // Additional fields for reissue/exchange masterAccount: z.string().optional(), masterPassword: z.string().optional(), repAccount: z.string().optional(), size: z.string().optional(), - addKind: z.string().optional(), // 'R' for reissue + addKind: z.enum(["N", "M", "R"]).optional(), // N: New, M: MNP, R: Reissue oldEid: z.string().optional(), oldProductNumber: z.string().optional(), deliveryCode: z.string().optional(), @@ -258,8 +261,9 @@ export const freebitEsimActivationParamsSchema = z.object({ contractLine: z.enum(["4G", "5G"]).optional(), aladinOperated: z.enum(["10", "20"]).default("10"), shipDate: z.string().regex(YYYYMMDD_REGEX, MSG_SHIP_DATE_FORMAT).optional(), - mnp: freebitEsimMnpSchema.optional(), - identity: freebitEsimIdentitySchema.optional(), + addKind: z.enum(["N", "M", "R"]).optional(), + simKind: z.enum(["E0", "E2", "E3"]).optional(), + mnp: freebitEsimMnpSchema.optional(), // MNP reservation + identity (Level 2) }); // ============================================================================