Add PA05-18 semi-black registration before PA02-01 for physical SIM activation

Physical SIM activation was failing with error 210 "アカウント不在エラー"
(Account not found) because PA02-01 requires the SIM to be pre-registered
in Freebit's system. This adds PA05-18 (Semi-Black Account Registration)
as the first step before PA02-01.

New flow: PA05-18 → PA02-01 → PA05-05

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Temuulen Ankhbayar 2026-02-03 11:02:15 +09:00
parent b400b982f3
commit 9fbb6ed61e
5 changed files with 290 additions and 22 deletions

View File

@ -14,6 +14,7 @@ import { FreebitEsimService } from "./services/freebit-esim.service.js";
import { FreebitTestTrackerService } from "./services/freebit-test-tracker.service.js"; import { FreebitTestTrackerService } from "./services/freebit-test-tracker.service.js";
import { FreebitAccountRegistrationService } from "./services/freebit-account-registration.service.js"; import { FreebitAccountRegistrationService } from "./services/freebit-account-registration.service.js";
import { FreebitVoiceOptionsService } from "./services/freebit-voice-options.service.js"; import { FreebitVoiceOptionsService } from "./services/freebit-voice-options.service.js";
import { FreebitSemiBlackService } from "./services/freebit-semiblack.service.js";
import { SimManagementModule } from "../../modules/subscriptions/sim-management/sim-management.module.js"; import { SimManagementModule } from "../../modules/subscriptions/sim-management/sim-management.module.js";
@Module({ @Module({
@ -32,7 +33,8 @@ import { SimManagementModule } from "../../modules/subscriptions/sim-management/
FreebitVoiceService, FreebitVoiceService,
FreebitCancellationService, FreebitCancellationService,
FreebitEsimService, FreebitEsimService,
// Physical SIM activation services (PA02-01 + PA05-05) // Physical SIM activation services (PA05-18 + PA02-01 + PA05-05)
FreebitSemiBlackService,
FreebitAccountRegistrationService, FreebitAccountRegistrationService,
FreebitVoiceOptionsService, FreebitVoiceOptionsService,
// Facade (delegates to specialized services) // Facade (delegates to specialized services)
@ -51,7 +53,8 @@ import { SimManagementModule } from "../../modules/subscriptions/sim-management/
FreebitVoiceService, FreebitVoiceService,
FreebitCancellationService, FreebitCancellationService,
FreebitEsimService, FreebitEsimService,
// Physical SIM activation services (PA02-01 + PA05-05) // Physical SIM activation services (PA05-18 + PA02-01 + PA05-05)
FreebitSemiBlackService,
FreebitAccountRegistrationService, FreebitAccountRegistrationService,
FreebitVoiceOptionsService, FreebitVoiceOptionsService,
// Rate limiter (needed by SimController) // Rate limiter (needed by SimController)

View File

@ -0,0 +1,234 @@
/**
* Freebit Semi-Black Account Registration Service (PA05-18)
*
* 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.
*
* 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";
import { Logger } from "nestjs-pino";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import { FreebitClientService } from "./freebit-client.service.js";
/**
* PA05-18 Semi-Black Account Registration parameters
*/
export interface SemiBlackRegistrationParams {
/** MSISDN (phone number) - 11-14 digits */
account: string;
/** Manufacturing number (productNumber) - 15 chars, e.g., AXxxxxxxxxxxxxx */
productNumber: string;
/** Freebit plan code (e.g., "PASI_5G") */
planCode: string;
/** Ship date in YYYYMMDD format (defaults to today) */
shipDate?: string;
/** Create type: "new" for new master account, "add" for existing */
createType?: "new" | "add";
/** Global IP assignment: "20" = disabled (default) */
globalIp?: "20";
/** ALADIN operation flag: "10" = operated, "20" = not operated (default) */
aladinOperated?: "10" | "20";
/** Sales channel code (optional) */
deliveryCode?: string;
}
/**
* PA05-18 Request payload structure
*/
interface FreebitSemiBlackRequest {
authKey: string;
createType: "new" | "add";
account: string;
productNumber: string;
planCode: string;
shipDate: string;
mnp: {
method: "10"; // "10" = Semi-black SIM
};
globalIp?: string;
aladinOperated?: string;
deliveryCode?: string;
}
/**
* PA05-18 Response structure
*/
interface FreebitSemiBlackResponse {
resultCode: number;
status: {
message: string;
statusCode: number;
};
}
@Injectable()
export class FreebitSemiBlackService {
constructor(
private readonly client: FreebitClientService,
@Inject(Logger) private readonly logger: Logger
) {}
/**
* Register a semi-black SIM account (PA05-18)
*
* This registers a pre-provisioned (semi-black) physical SIM in Freebit's
* system. Must be called BEFORE PA02-01 account registration.
*
* @param params - Semi-black registration parameters
* @throws BadRequestException if registration fails
*/
async registerSemiBlackAccount(params: SemiBlackRegistrationParams): Promise<void> {
const {
account,
productNumber,
planCode,
shipDate,
createType = "new",
globalIp = "20",
aladinOperated = "20",
deliveryCode,
} = params;
// Validate phone number
if (!account || account.length < 11 || account.length > 14) {
throw new BadRequestException(
"Invalid phone number (account) for semi-black registration - must be 11-14 digits"
);
}
// Validate product number (manufacturing number)
if (!productNumber || productNumber.length !== 15) {
throw new BadRequestException(
"Invalid product number for semi-black registration - must be 15 characters (e.g., AXxxxxxxxxxxxxx)"
);
}
if (!planCode) {
throw new BadRequestException("Plan code is required for semi-black registration");
}
// Default to today's date if not provided
const effectiveShipDate = shipDate ?? this.formatTodayAsYYYYMMDD();
this.logger.log("Starting semi-black SIM registration (PA05-18)", {
account,
productNumber,
planCode,
shipDate: effectiveShipDate,
createType,
});
try {
const payload: Omit<FreebitSemiBlackRequest, "authKey"> = {
createType,
account,
productNumber,
planCode,
shipDate: effectiveShipDate,
mnp: {
method: "10", // Semi-black SIM method
},
globalIp,
aladinOperated,
...(deliveryCode && { deliveryCode }),
};
const response = await this.client.makeAuthenticatedRequest<
FreebitSemiBlackResponse,
Omit<FreebitSemiBlackRequest, "authKey">
>("/mvno/semiblack/addAcnt/", payload);
// Check response status
if (response.resultCode !== 100 || response.status?.statusCode !== 200) {
const errorCode = response.resultCode;
const errorMessage = this.getErrorMessage(errorCode, response.status?.message);
this.logger.error("Semi-black registration failed (PA05-18)", {
account,
productNumber,
planCode,
resultCode: response.resultCode,
statusCode: response.status?.statusCode,
message: response.status?.message,
});
throw new BadRequestException(
`Semi-black registration failed: ${errorMessage} (code: ${errorCode})`
);
}
this.logger.log("Semi-black SIM registration successful (PA05-18)", {
account,
productNumber,
planCode,
});
} catch (error: unknown) {
// Re-throw BadRequestException as-is
if (error instanceof BadRequestException) {
throw error;
}
const message = extractErrorMessage(error);
this.logger.error("Semi-black registration failed (PA05-18)", {
account,
productNumber,
planCode,
error: message,
});
throw new BadRequestException(`Semi-black registration failed: ${message}`);
}
}
/**
* Format today's date as YYYYMMDD
*/
private formatTodayAsYYYYMMDD(): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
return `${year}${month}${day}`;
}
/**
* Get human-readable error message for PA05-18 error codes
*/
private getErrorMessage(code: number, defaultMessage?: string): string {
const errorMessages: Record<number, string> = {
201: "Invalid account/phone number parameter",
202: "Invalid master password",
204: "Invalid parameter",
205: "Authentication key error",
208: "Account already exists (duplicate)",
210: "Master account not found",
211: "Account status does not allow this operation",
215: "Invalid plan code",
228: "Invalid authentication key",
230: "Account is in async processing queue",
231: "Invalid global IP parameter",
232: "Plan not found",
266: "Invalid product number (manufacturing number)",
269: "Invalid representative number",
274: "Invalid delivery code",
275: "No phone number stock for representative number",
276: "Invalid ship date",
279: "Invalid create type",
284: "Representative number is locked",
287: "Representative number does not exist or is unavailable",
288: "Product number does not exist, is already used, or not stocked",
289: "SIM type does not match representative number type",
306: "Invalid MNP method",
313: "MNP reservation expires within grace period",
900: "Unexpected system error",
};
return errorMessages[code] ?? defaultMessage ?? "Unknown error";
}
}

View File

@ -9,3 +9,7 @@ export { FreebitPlanService } from "./freebit-plan.service.js";
export { FreebitVoiceService } from "./freebit-voice.service.js"; export { FreebitVoiceService } from "./freebit-voice.service.js";
export { FreebitCancellationService } from "./freebit-cancellation.service.js"; export { FreebitCancellationService } from "./freebit-cancellation.service.js";
export { FreebitEsimService, type EsimActivationParams } from "./freebit-esim.service.js"; export { FreebitEsimService, type EsimActivationParams } from "./freebit-esim.service.js";
export {
FreebitSemiBlackService,
type SemiBlackRegistrationParams,
} from "./freebit-semiblack.service.js";

View File

@ -164,7 +164,7 @@ export class OrderFulfillmentOrchestrator {
} }
// Step 3: Execute the main fulfillment workflow as a distributed transaction // Step 3: Execute the main fulfillment workflow as a distributed transaction
// New flow: SIM activation (PA02-01 + PA05-05) → Activated status → WHMCS → Registration Completed // New flow: SIM activation (PA05-18 + PA02-01 + PA05-05) → Activated status → WHMCS → Registration Completed
let simFulfillmentResult: SimFulfillmentResult | undefined; let simFulfillmentResult: SimFulfillmentResult | undefined;
let mappingResult: WhmcsOrderItemMappingResult | undefined; let mappingResult: WhmcsOrderItemMappingResult | undefined;
let whmcsCreateResult: WhmcsOrderResult | undefined; let whmcsCreateResult: WhmcsOrderResult | undefined;
@ -212,10 +212,10 @@ export class OrderFulfillmentOrchestrator {
), ),
critical: false, critical: false,
}, },
// SIM fulfillment now runs BEFORE WHMCS (PA02-01 + PA05-05) // SIM fulfillment now runs BEFORE WHMCS (PA05-18 + PA02-01 + PA05-05)
{ {
id: "sim_fulfillment", id: "sim_fulfillment",
description: "SIM activation via Freebit (PA02-01 + PA05-05)", description: "SIM activation via Freebit (PA05-18 + PA02-01 + PA05-05)",
execute: this.createTrackedStep(context, "sim_fulfillment", async () => { execute: this.createTrackedStep(context, "sim_fulfillment", async () => {
if (context.orderDetails?.orderType === "SIM") { if (context.orderDetails?.orderType === "SIM") {
const sfOrder = context.validation?.sfOrder; const sfOrder = context.validation?.sfOrder;
@ -264,10 +264,13 @@ export class OrderFulfillmentOrchestrator {
rollback: () => { rollback: () => {
// SIM activation cannot be easily rolled back // SIM activation cannot be easily rolled back
// Log for manual intervention if needed // Log for manual intervention if needed
this.logger.warn("SIM fulfillment step needs rollback - manual intervention may be required", { this.logger.warn(
sfOrderId, "SIM fulfillment step needs rollback - manual intervention may be required",
simFulfillmentResult, {
}); sfOrderId,
simFulfillmentResult,
}
);
return Promise.resolve(); return Promise.resolve();
}, },
critical: context.validation?.sfOrder?.SIM_Type__c === "Physical SIM", critical: context.validation?.sfOrder?.SIM_Type__c === "Physical SIM",
@ -587,7 +590,7 @@ export class OrderFulfillmentOrchestrator {
* 1. validation * 1. validation
* 2. sf_status_update (Activating) * 2. sf_status_update (Activating)
* 3. order_details * 3. order_details
* 4. sim_fulfillment (PA02-01 + PA05-05) - SIM orders only * 4. sim_fulfillment (PA05-18 + PA02-01 + PA05-05) - SIM orders only
* 5. sf_activated_update - SIM orders only * 5. sf_activated_update - SIM orders only
* 6. mapping (with SIM data for WHMCS) * 6. mapping (with SIM data for WHMCS)
* 7. whmcs_create * 7. whmcs_create

View File

@ -3,6 +3,7 @@ import { Logger } from "nestjs-pino";
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service.js"; import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service.js";
import { FreebitAccountRegistrationService } from "@bff/integrations/freebit/services/freebit-account-registration.service.js"; import { FreebitAccountRegistrationService } from "@bff/integrations/freebit/services/freebit-account-registration.service.js";
import { FreebitVoiceOptionsService } from "@bff/integrations/freebit/services/freebit-voice-options.service.js"; import { FreebitVoiceOptionsService } from "@bff/integrations/freebit/services/freebit-voice-options.service.js";
import { FreebitSemiBlackService } from "@bff/integrations/freebit/services/freebit-semiblack.service.js";
import { SalesforceSIMInventoryService } from "@bff/integrations/salesforce/services/salesforce-sim-inventory.service.js"; import { SalesforceSIMInventoryService } from "@bff/integrations/salesforce/services/salesforce-sim-inventory.service.js";
import type { OrderDetails, OrderItemDetails } from "@customer-portal/domain/orders"; import type { OrderDetails, OrderItemDetails } from "@customer-portal/domain/orders";
import { mapProductToFreebitPlanCode } from "@customer-portal/domain/sim"; import { mapProductToFreebitPlanCode } from "@customer-portal/domain/sim";
@ -57,6 +58,7 @@ export interface SimFulfillmentResult {
export class SimFulfillmentService { export class SimFulfillmentService {
constructor( constructor(
private readonly freebit: FreebitOrchestratorService, private readonly freebit: FreebitOrchestratorService,
private readonly freebitSemiBlack: FreebitSemiBlackService,
private readonly freebitAccountReg: FreebitAccountRegistrationService, private readonly freebitAccountReg: FreebitAccountRegistrationService,
private readonly freebitVoiceOptions: FreebitVoiceOptionsService, private readonly freebitVoiceOptions: FreebitVoiceOptionsService,
private readonly simInventory: SalesforceSIMInventoryService, private readonly simInventory: SalesforceSIMInventoryService,
@ -160,7 +162,7 @@ export class SimFulfillmentService {
phoneNumber, phoneNumber,
}; };
} else { } else {
// Physical SIM activation flow (PA02-01 + PA05-05) // Physical SIM activation flow (PA05-18 + PA02-01 + PA05-05)
if (!assignedPhysicalSimId) { if (!assignedPhysicalSimId) {
throw new SimActivationException( throw new SimActivationException(
"Physical SIM requires an assigned SIM from inventory (Assign_Physical_SIM__c)", "Physical SIM requires an assigned SIM from inventory (Assign_Physical_SIM__c)",
@ -263,18 +265,19 @@ export class SimFulfillmentService {
} }
/** /**
* Activate Physical SIM via Freebit PA02-01 + PA05-05 APIs * Activate Physical SIM via Freebit PA05-18 + PA02-01 + PA05-05 APIs
* *
* New Flow (replaces PA05-33 OTA): * Flow for Semi-Black Physical SIMs:
* 1. Fetch SIM Inventory details from Salesforce * 1. Fetch SIM Inventory details from Salesforce
* 2. Validate SIM status is "Available" * 2. Validate SIM status is "Available"
* 3. Map product SKU to Freebit plan code * 3. Map product SKU to Freebit plan code
* 4. Call Freebit PA02-01 (Account Registration) to create MVNO account * 4. Call Freebit PA05-18 (Semi-Black Registration) to register the SIM
* 5. Call Freebit PA05-05 (Voice Options) to configure voice features * 5. Call Freebit PA02-01 (Account Registration) to create MVNO account
* 6. Update SIM Inventory status to "In Use" * 6. Call Freebit PA05-05 (Voice Options) to configure voice features
* 7. Update SIM Inventory status to "In Use"
* *
* Note: PA02-01 is asynchronous and may take up to 10 minutes to process. * Note: PA05-18 must be called before PA02-01, otherwise PA02-01 returns
* The account must be fully registered before PA05-05 can be called. * error 210 "アカウント不在エラー" (Account not found).
*/ */
private async activatePhysicalSim(params: { private async activatePhysicalSim(params: {
orderId: string; orderId: string;
@ -295,7 +298,7 @@ export class SimFulfillmentService {
contactIdentity, contactIdentity,
} = params; } = params;
this.logger.log("Starting Physical SIM activation (PA02-01 + PA05-05)", { this.logger.log("Starting Physical SIM activation (PA05-18 + PA02-01 + PA05-05)", {
orderId, orderId,
simInventoryId, simInventoryId,
planSku, planSku,
@ -328,7 +331,28 @@ export class SimFulfillmentService {
}); });
try { try {
// Step 4: Call Freebit PA02-01 (Account Registration) // Step 4: Call Freebit PA05-18 (Semi-Black Registration)
// This registers the semi-black SIM in Freebit's system
// Must be called BEFORE PA02-01 or we get error 210 "アカウント不在エラー"
this.logger.log("Calling PA05-18 Semi-Black Registration", {
orderId,
account: accountPhoneNumber,
productNumber: simRecord.ptNumber,
planCode,
});
await this.freebitSemiBlack.registerSemiBlackAccount({
account: accountPhoneNumber,
productNumber: simRecord.ptNumber,
planCode,
});
this.logger.log("PA05-18 Semi-Black Registration successful", {
orderId,
account: accountPhoneNumber,
});
// Step 5: Call Freebit PA02-01 (Account Registration)
this.logger.log("Calling PA02-01 Account Registration", { this.logger.log("Calling PA02-01 Account Registration", {
orderId, orderId,
account: accountPhoneNumber, account: accountPhoneNumber,
@ -345,7 +369,7 @@ export class SimFulfillmentService {
account: accountPhoneNumber, account: accountPhoneNumber,
}); });
// Step 5: Call Freebit PA05-05 (Voice Options Registration) // Step 6: Call Freebit PA05-05 (Voice Options Registration)
// Only call if we have contact identity data // Only call if we have contact identity data
if (contactIdentity) { if (contactIdentity) {
this.logger.log("Calling PA05-05 Voice Options Registration", { this.logger.log("Calling PA05-05 Voice Options Registration", {
@ -380,7 +404,7 @@ export class SimFulfillmentService {
}); });
} }
// Step 6: Update SIM Inventory status to "In Use" // Step 7: Update SIM Inventory status to "In Use"
await this.simInventory.markAsInUse(simInventoryId); await this.simInventory.markAsInUse(simInventoryId);
this.logger.log("Physical SIM activated successfully", { this.logger.log("Physical SIM activated successfully", {