tema 1283880f7d Revamp Physical SIM activation to use PA02-01 + PA05-05
Replace PA05-33 OTA API with the proper two-step activation flow:
- PA02-01: Account Registration (/master/addAcnt/)
- PA05-05: Voice Options Registration (/mvno/talkoption/addOrder/)

Changes:
- Add FreebitAccountRegistrationService for PA02-01 account registration
- Add FreebitVoiceOptionsService for PA05-05 voice options
- Update SimFulfillmentService to use new APIs instead of PA05-33 OTA
- Add SalesforceSIMInventoryService for fetching SIM inventory data
- Remove deprecated FreebitOtaService (PA05-33 no longer used)
- Remove debug console.log statements

The new flow:
1. Fetch SIM inventory from Salesforce (phone number, PT number)
2. Call PA02-01 to register MVNO account with plan code
3. Call PA05-05 to configure voice options with customer identity
4. Update SIM inventory status to "In Use"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 18:22:00 +09:00

134 lines
3.8 KiB
TypeScript

import { SIM_PLAN_CODES, SIM_PLAN_LABELS, SIM_STATUS } from "./contract.js";
import type { SimStatus, SimFeaturesUpdateRequest } from "./schema.js";
export function canManageActiveSim(status: SimStatus): boolean {
return status === SIM_STATUS.ACTIVE;
}
export function canReissueEsim(status: SimStatus): boolean {
return canManageActiveSim(status);
}
export function canCancelSim(status: SimStatus): boolean {
return canManageActiveSim(status);
}
export function canTopUpSim(status: SimStatus): boolean {
return canManageActiveSim(status);
}
export function formatSimPlanShort(planCode?: string): string {
if (!planCode) return "—";
const normalized = planCode.trim();
const matchPrimary = normalized.match(/(?:^|[_-])(\d+(?:\.\d+)?)\s*G(?:B)?\b/i);
if (matchPrimary?.[1]) {
return `${matchPrimary[1]}G`;
}
const matchFallback = normalized.match(/(\d+(?:\.\d+)?)\s*G(?:B)?\b/i);
if (matchFallback?.[1]) {
return `${matchFallback[1]}G`;
}
return normalized;
}
export type SimPlanOption = {
code: (typeof SIM_PLAN_CODES)[number];
label: string;
shortLabel: string;
};
export const SIM_PLAN_OPTIONS: SimPlanOption[] = SIM_PLAN_CODES.map(code => ({
code,
label: SIM_PLAN_LABELS[code],
shortLabel: formatSimPlanShort(code),
}));
export function getSimPlanLabel(planCode?: string): string {
if (!planCode) return "Unknown plan";
if (planCode in SIM_PLAN_LABELS) {
return SIM_PLAN_LABELS[planCode as keyof typeof SIM_PLAN_LABELS];
}
return formatSimPlanShort(planCode);
}
export type SimFeatureToggleSnapshot = {
voiceMailEnabled: boolean;
callWaitingEnabled: boolean;
internationalRoamingEnabled: boolean;
networkType: "4G" | "5G";
};
export function buildSimFeaturesUpdatePayload(
current: SimFeatureToggleSnapshot,
next: SimFeatureToggleSnapshot
): SimFeaturesUpdateRequest | null {
const payload: SimFeaturesUpdateRequest = {};
if (current.voiceMailEnabled !== next.voiceMailEnabled) {
payload.voiceMailEnabled = next.voiceMailEnabled;
}
if (current.callWaitingEnabled !== next.callWaitingEnabled) {
payload.callWaitingEnabled = next.callWaitingEnabled;
}
if (current.internationalRoamingEnabled !== next.internationalRoamingEnabled) {
payload.internationalRoamingEnabled = next.internationalRoamingEnabled;
}
if (current.networkType !== next.networkType) {
payload.networkType = next.networkType;
}
return Object.keys(payload).length > 0 ? payload : null;
}
/**
* Plan code mapping from product SKU/name to Freebit plan code
* Maps common data tiers: 5GB, 10GB, 25GB, 50GB
*/
const PLAN_CODE_MAPPING: Record<string, string> = {
"5": "PASI_5G",
"10": "PASI_10G",
"25": "PASI_25G",
"50": "PASI_50G",
};
/**
* Maps a product SKU or name to the corresponding Freebit plan code.
*
* Extracts the data tier (e.g., "50" from "SIM Data+Voice 50GB" or "sim-50gb")
* and maps it to the appropriate PASI plan code.
*
* @param productSku - The product SKU (e.g., "sim-data-voice-50gb")
* @param productName - The product name (e.g., "SIM Data+Voice 50GB")
* @returns The Freebit plan code (e.g., "PASI_50G") or null if not mappable
*/
export function mapProductToFreebitPlanCode(
productSku?: string,
productName?: string
): string | null {
// 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);
if (gbMatch?.[1]) {
const tier = gbMatch[1];
if (tier in PLAN_CODE_MAPPING) {
return PLAN_CODE_MAPPING[tier];
}
}
// Try matching standalone numbers in SKU patterns like "sim-50gb"
const skuMatch = source.match(/[-_](\d+)[-_]?(?:gb|g)?/i);
if (skuMatch?.[1]) {
const tier = skuMatch[1];
if (tier in PLAN_CODE_MAPPING) {
return PLAN_CODE_MAPPING[tier];
}
}
return null;
}