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:
parent
b400b982f3
commit
9fbb6ed61e
@ -14,6 +14,7 @@ import { FreebitEsimService } from "./services/freebit-esim.service.js";
|
||||
import { FreebitTestTrackerService } from "./services/freebit-test-tracker.service.js";
|
||||
import { FreebitAccountRegistrationService } from "./services/freebit-account-registration.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";
|
||||
|
||||
@Module({
|
||||
@ -32,7 +33,8 @@ import { SimManagementModule } from "../../modules/subscriptions/sim-management/
|
||||
FreebitVoiceService,
|
||||
FreebitCancellationService,
|
||||
FreebitEsimService,
|
||||
// Physical SIM activation services (PA02-01 + PA05-05)
|
||||
// Physical SIM activation services (PA05-18 + PA02-01 + PA05-05)
|
||||
FreebitSemiBlackService,
|
||||
FreebitAccountRegistrationService,
|
||||
FreebitVoiceOptionsService,
|
||||
// Facade (delegates to specialized services)
|
||||
@ -51,7 +53,8 @@ import { SimManagementModule } from "../../modules/subscriptions/sim-management/
|
||||
FreebitVoiceService,
|
||||
FreebitCancellationService,
|
||||
FreebitEsimService,
|
||||
// Physical SIM activation services (PA02-01 + PA05-05)
|
||||
// Physical SIM activation services (PA05-18 + PA02-01 + PA05-05)
|
||||
FreebitSemiBlackService,
|
||||
FreebitAccountRegistrationService,
|
||||
FreebitVoiceOptionsService,
|
||||
// Rate limiter (needed by SimController)
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -9,3 +9,7 @@ export { FreebitPlanService } from "./freebit-plan.service.js";
|
||||
export { FreebitVoiceService } from "./freebit-voice.service.js";
|
||||
export { FreebitCancellationService } from "./freebit-cancellation.service.js";
|
||||
export { FreebitEsimService, type EsimActivationParams } from "./freebit-esim.service.js";
|
||||
export {
|
||||
FreebitSemiBlackService,
|
||||
type SemiBlackRegistrationParams,
|
||||
} from "./freebit-semiblack.service.js";
|
||||
|
||||
@ -164,7 +164,7 @@ export class OrderFulfillmentOrchestrator {
|
||||
}
|
||||
|
||||
// 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 mappingResult: WhmcsOrderItemMappingResult | undefined;
|
||||
let whmcsCreateResult: WhmcsOrderResult | undefined;
|
||||
@ -212,10 +212,10 @@ export class OrderFulfillmentOrchestrator {
|
||||
),
|
||||
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",
|
||||
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 () => {
|
||||
if (context.orderDetails?.orderType === "SIM") {
|
||||
const sfOrder = context.validation?.sfOrder;
|
||||
@ -264,10 +264,13 @@ export class OrderFulfillmentOrchestrator {
|
||||
rollback: () => {
|
||||
// SIM activation cannot be easily rolled back
|
||||
// Log for manual intervention if needed
|
||||
this.logger.warn("SIM fulfillment step needs rollback - manual intervention may be required", {
|
||||
sfOrderId,
|
||||
simFulfillmentResult,
|
||||
});
|
||||
this.logger.warn(
|
||||
"SIM fulfillment step needs rollback - manual intervention may be required",
|
||||
{
|
||||
sfOrderId,
|
||||
simFulfillmentResult,
|
||||
}
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
critical: context.validation?.sfOrder?.SIM_Type__c === "Physical SIM",
|
||||
@ -587,7 +590,7 @@ export class OrderFulfillmentOrchestrator {
|
||||
* 1. validation
|
||||
* 2. sf_status_update (Activating)
|
||||
* 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
|
||||
* 6. mapping (with SIM data for WHMCS)
|
||||
* 7. whmcs_create
|
||||
|
||||
@ -3,6 +3,7 @@ import { Logger } from "nestjs-pino";
|
||||
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.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 { FreebitSemiBlackService } from "@bff/integrations/freebit/services/freebit-semiblack.service.js";
|
||||
import { SalesforceSIMInventoryService } from "@bff/integrations/salesforce/services/salesforce-sim-inventory.service.js";
|
||||
import type { OrderDetails, OrderItemDetails } from "@customer-portal/domain/orders";
|
||||
import { mapProductToFreebitPlanCode } from "@customer-portal/domain/sim";
|
||||
@ -57,6 +58,7 @@ export interface SimFulfillmentResult {
|
||||
export class SimFulfillmentService {
|
||||
constructor(
|
||||
private readonly freebit: FreebitOrchestratorService,
|
||||
private readonly freebitSemiBlack: FreebitSemiBlackService,
|
||||
private readonly freebitAccountReg: FreebitAccountRegistrationService,
|
||||
private readonly freebitVoiceOptions: FreebitVoiceOptionsService,
|
||||
private readonly simInventory: SalesforceSIMInventoryService,
|
||||
@ -160,7 +162,7 @@ export class SimFulfillmentService {
|
||||
phoneNumber,
|
||||
};
|
||||
} else {
|
||||
// Physical SIM activation flow (PA02-01 + PA05-05)
|
||||
// Physical SIM activation flow (PA05-18 + PA02-01 + PA05-05)
|
||||
if (!assignedPhysicalSimId) {
|
||||
throw new SimActivationException(
|
||||
"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
|
||||
* 2. Validate SIM status is "Available"
|
||||
* 3. Map product SKU to Freebit plan code
|
||||
* 4. Call Freebit PA02-01 (Account Registration) to create MVNO account
|
||||
* 5. Call Freebit PA05-05 (Voice Options) to configure voice features
|
||||
* 6. Update SIM Inventory status to "In Use"
|
||||
* 4. Call Freebit PA05-18 (Semi-Black Registration) to register the SIM
|
||||
* 5. Call Freebit PA02-01 (Account Registration) to create MVNO account
|
||||
* 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.
|
||||
* The account must be fully registered before PA05-05 can be called.
|
||||
* Note: PA05-18 must be called before PA02-01, otherwise PA02-01 returns
|
||||
* error 210 "アカウント不在エラー" (Account not found).
|
||||
*/
|
||||
private async activatePhysicalSim(params: {
|
||||
orderId: string;
|
||||
@ -295,7 +298,7 @@ export class SimFulfillmentService {
|
||||
contactIdentity,
|
||||
} = 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,
|
||||
simInventoryId,
|
||||
planSku,
|
||||
@ -328,7 +331,28 @@ export class SimFulfillmentService {
|
||||
});
|
||||
|
||||
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", {
|
||||
orderId,
|
||||
account: accountPhoneNumber,
|
||||
@ -345,7 +369,7 @@ export class SimFulfillmentService {
|
||||
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
|
||||
if (contactIdentity) {
|
||||
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);
|
||||
|
||||
this.logger.log("Physical SIM activated successfully", {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user