fix: comprehensive SIM audit fixes and MNP debug logging
Address critical, high, and medium issues found during SIM management audit: Critical: fix eSIM plan code mapping (SKU→PASI), PA05-41 endpoint typo, PA05-05 gender mapping (F→W) and katakana field names. High: fix double authKey injection, add MNP/porting fields to SF getOrder SOQL, add reissue params to eSIM addAcnt, remove console.error debug stmt. Medium: fix KB/MB conversion (1000→1024), birthday UTC timezone bug, plan code regex matching "5G" as 5GB, case-insensitive isMnp flag, domain schema enums (addKind +M, simkind E0/E2/E3), move identity into mnp Level 2. Frontend: fix SVG donut radius mismatch (r=88→96), fix FreebitError typo. Add comprehensive MNP debug logging across the entire data flow pipeline: SF order extraction, config mapping, MNP field parsing, API payload assembly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
be09c78491
commit
5e5bff12da
@ -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
|
||||
|
||||
@ -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<FreebitEsimAddAccountResponse, typeof request>(
|
||||
@ -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<FreebitEsimAccountActivationRequest, "authKey"> = {
|
||||
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<FreebitEsimAccountActivationRequest, "authKey">;
|
||||
|
||||
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<FreebitEsimAccountActivationRequest, "authKey">
|
||||
>("/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}`);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<void> {
|
||||
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<string, unknown>) {
|
||||
const nested = config["mnp"];
|
||||
const source =
|
||||
nested && typeof nested === "object" ? (nested as Record<string, unknown>) : config;
|
||||
const hasNestedMnp = nested && typeof nested === "object";
|
||||
const source = hasNestedMnp ? (nested as Record<string, unknown>) : 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user