fix: resolve Order Activation Flow issues
- Fix SF Order locking by deferring Status change to final step
- executeSfActivatedUpdate now only sets Activation_Status__c
- executeSfRegistrationComplete sets Status: Processed atomically with WHMCS info
- Add WHMCS custom fields update step (whmcs_custom_fields)
- AddOrder API expects field IDs, UpdateClientProduct accepts field names
- New step updates SIM Number, Serial Number, EID after order acceptance
- Add Opportunity WH_Registeration__c field update
- Sets productselect={serviceId} for WHMCS linking
- Add SIM Inventory assignment fields
- Assigned_Account__c, Assigned_Order__c, SIM_Type__c now populated
- Remove PA05-18 Semi-Black SIM registration (only Black SIMs used)
- Changed to direct PA02-01 call with createType=new
- Fix me-status to check for Status: Processed instead of Activated
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
df742e50bc
commit
5c67fc34ea
@ -12,7 +12,12 @@ export interface DevAuthConfig {
|
||||
skipOtp: boolean;
|
||||
}
|
||||
|
||||
export const createDevAuthConfig = (): DevAuthConfig => {
|
||||
/**
|
||||
* Get the current dev auth configuration.
|
||||
* This function reads environment variables at call time, not at module load time.
|
||||
* This is important because .env files are loaded by NestJS ConfigModule after modules are imported.
|
||||
*/
|
||||
export const getDevAuthConfig = (): DevAuthConfig => {
|
||||
const isDevelopment = process.env["NODE_ENV"] !== "production";
|
||||
|
||||
return {
|
||||
@ -36,4 +41,7 @@ export const createDevAuthConfig = (): DevAuthConfig => {
|
||||
};
|
||||
};
|
||||
|
||||
export const devAuthConfig = createDevAuthConfig();
|
||||
/**
|
||||
* @deprecated Use getDevAuthConfig() instead to ensure env vars are read after ConfigModule loads
|
||||
*/
|
||||
export const devAuthConfig = getDevAuthConfig();
|
||||
|
||||
@ -4,7 +4,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { CsrfService } from "../services/csrf.service.js";
|
||||
import { devAuthConfig } from "../../config/auth-dev.config.js";
|
||||
import { getDevAuthConfig } from "../../config/auth-dev.config.js";
|
||||
|
||||
interface CsrfRequestBody {
|
||||
_csrf?: string | string[];
|
||||
@ -61,8 +61,9 @@ export class CsrfMiddleware implements NestMiddleware {
|
||||
|
||||
use(req: CsrfRequest, res: Response, next: NextFunction): void {
|
||||
// Skip CSRF protection entirely in development if disabled
|
||||
if (devAuthConfig.disableCsrf) {
|
||||
if (devAuthConfig.enableDebugLogs) {
|
||||
const devConfig = getDevAuthConfig();
|
||||
if (devConfig.disableCsrf) {
|
||||
if (devConfig.enableDebugLogs) {
|
||||
this.logger.debug("CSRF protection disabled in development", {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
|
||||
@ -180,6 +180,7 @@ export class OpportunityMutationService {
|
||||
const payload: Record<string, unknown> = {
|
||||
Id: safeOppId,
|
||||
[OPPORTUNITY_FIELD_MAP.whmcsServiceId]: whmcsServiceId,
|
||||
[OPPORTUNITY_FIELD_MAP.whmcsRegistrationUrl]: `productselect=${whmcsServiceId}`,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@ -28,6 +28,18 @@ export const SIM_INVENTORY_STATUS = {
|
||||
|
||||
export type SimInventoryStatus = (typeof SIM_INVENTORY_STATUS)[keyof typeof SIM_INVENTORY_STATUS];
|
||||
|
||||
/**
|
||||
* Optional assignment details when marking a SIM as assigned
|
||||
*/
|
||||
export interface SimAssignmentDetails {
|
||||
/** Salesforce Account ID to assign the SIM to */
|
||||
accountId?: string;
|
||||
/** Salesforce Order ID that assigned the SIM */
|
||||
orderId?: string;
|
||||
/** SIM Type (eSIM or Physical SIM) */
|
||||
simType?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SIM Inventory record from Salesforce
|
||||
*/
|
||||
@ -165,19 +177,46 @@ export class SalesforceSIMInventoryService {
|
||||
|
||||
/**
|
||||
* Update SIM Inventory status to "Assigned" after successful activation
|
||||
*
|
||||
* @param simInventoryId - Salesforce ID of the SIM_Inventory__c record
|
||||
* @param details - Optional assignment details (account, order, SIM type)
|
||||
*/
|
||||
async markAsAssigned(simInventoryId: string): Promise<void> {
|
||||
async markAsAssigned(simInventoryId: string, details?: SimAssignmentDetails): Promise<void> {
|
||||
const safeId = assertSalesforceId(simInventoryId, "simInventoryId");
|
||||
|
||||
this.logger.log("Marking SIM Inventory as Assigned", { simInventoryId: safeId });
|
||||
|
||||
try {
|
||||
await this.sf.sobject("SIM_Inventory__c").update?.({
|
||||
Id: safeId,
|
||||
Status__c: SIM_INVENTORY_STATUS.ASSIGNED,
|
||||
this.logger.log("Marking SIM Inventory as Assigned", {
|
||||
simInventoryId: safeId,
|
||||
hasAssignmentDetails: !!details,
|
||||
accountId: details?.accountId,
|
||||
orderId: details?.orderId,
|
||||
simType: details?.simType,
|
||||
});
|
||||
|
||||
this.logger.log("SIM Inventory marked as Assigned", { simInventoryId: safeId });
|
||||
try {
|
||||
const updatePayload: Record<string, unknown> = {
|
||||
Id: safeId,
|
||||
Status__c: SIM_INVENTORY_STATUS.ASSIGNED,
|
||||
};
|
||||
|
||||
// Add optional assignment fields if provided
|
||||
if (details?.accountId) {
|
||||
updatePayload["Assigned_Account__c"] = details.accountId;
|
||||
}
|
||||
if (details?.orderId) {
|
||||
updatePayload["Assigned_Order__c"] = details.orderId;
|
||||
}
|
||||
if (details?.simType) {
|
||||
updatePayload["SIM_Type__c"] = details.simType;
|
||||
}
|
||||
|
||||
await this.sf.sobject("SIM_Inventory__c").update?.(updatePayload as { Id: string });
|
||||
|
||||
this.logger.log("SIM Inventory marked as Assigned", {
|
||||
simInventoryId: safeId,
|
||||
assignedAccount: details?.accountId,
|
||||
assignedOrder: details?.orderId,
|
||||
simType: details?.simType,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
this.logger.error("Failed to update SIM Inventory status", {
|
||||
simInventoryId: safeId,
|
||||
|
||||
@ -184,6 +184,67 @@ export class WhmcsOrderService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update service custom fields after order is accepted
|
||||
* Uses UpdateClientProduct API which supports field names (not just IDs)
|
||||
*
|
||||
* @param serviceId - The WHMCS service ID to update
|
||||
* @param customFields - Key-value map of custom field names to values
|
||||
*/
|
||||
async updateServiceCustomFields(
|
||||
serviceId: number,
|
||||
customFields: Record<string, string>
|
||||
): Promise<void> {
|
||||
if (!customFields || Object.keys(customFields).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log("Updating WHMCS service custom fields", {
|
||||
serviceId,
|
||||
fieldCount: Object.keys(customFields).length,
|
||||
fieldNames: Object.keys(customFields),
|
||||
});
|
||||
|
||||
try {
|
||||
// Serialize custom fields to WHMCS format (base64 encoded PHP serialized array)
|
||||
const serialized = this.serializeCustomFields(customFields);
|
||||
|
||||
await this.connection.makeRequest("UpdateClientProduct", {
|
||||
serviceid: serviceId,
|
||||
customfields: serialized,
|
||||
});
|
||||
|
||||
this.logger.log("WHMCS service custom fields updated", {
|
||||
serviceId,
|
||||
fields: Object.keys(customFields),
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to update WHMCS service custom fields", {
|
||||
error: extractErrorMessage(error),
|
||||
serviceId,
|
||||
fieldNames: Object.keys(customFields),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize custom fields to WHMCS format (base64 encoded PHP serialized array)
|
||||
*/
|
||||
private serializeCustomFields(data: Record<string, string>): string {
|
||||
const entries = Object.entries(data).filter(([k, v]) => k && v);
|
||||
if (entries.length === 0) return "";
|
||||
|
||||
const parts = entries.map(([key, value]) => {
|
||||
const keyBytes = Buffer.byteLength(key, "utf8");
|
||||
const valueBytes = Buffer.byteLength(value, "utf8");
|
||||
return `s:${keyBytes}:"${key}";s:${valueBytes}:"${value}";`;
|
||||
});
|
||||
|
||||
const serialized = `a:${entries.length}:{${parts.join("")}}`;
|
||||
return Buffer.from(serialized).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order details from WHMCS
|
||||
*/
|
||||
|
||||
@ -41,7 +41,7 @@ import {
|
||||
clearTrustedDeviceCookie,
|
||||
getTrustedDeviceToken,
|
||||
} from "./utils/trusted-device-cookie.util.js";
|
||||
import { devAuthConfig } from "@bff/core/config/auth-dev.config.js";
|
||||
import { getDevAuthConfig } from "@bff/core/config/auth-dev.config.js";
|
||||
import { TrustedDeviceService } from "../../infra/trusted-device/trusted-device.service.js";
|
||||
|
||||
// Import Zod schemas from domain
|
||||
@ -143,7 +143,7 @@ export class AuthController {
|
||||
this.applyAuthRateLimitHeaders(req, res);
|
||||
|
||||
// In dev mode with SKIP_OTP=true, skip OTP and complete login directly
|
||||
if (devAuthConfig.skipOtp) {
|
||||
if (getDevAuthConfig().skipOtp) {
|
||||
const loginResult = await this.authOrchestrator.completeLogin(
|
||||
{ id: req.user.id, email: req.user.email, role: req.user.role ?? "USER" },
|
||||
req
|
||||
|
||||
@ -217,7 +217,7 @@ export class MeStatusAggregator {
|
||||
o =>
|
||||
o.status === "Draft" ||
|
||||
o.status === "Pending" ||
|
||||
(o.status === "Activated" && o.activationStatus !== "Completed")
|
||||
(o.status === "Processed" && o.activationStatus !== "Completed")
|
||||
);
|
||||
|
||||
if (!pendingOrder) {
|
||||
|
||||
@ -100,12 +100,28 @@ export class FulfillmentStepExecutors {
|
||||
hasContactIdentity: !!contactIdentity,
|
||||
});
|
||||
|
||||
// Build assignment details for SIM Inventory record (only include defined properties)
|
||||
const assignmentDetails: {
|
||||
accountId?: string;
|
||||
orderId?: string;
|
||||
simType?: string;
|
||||
} = {
|
||||
orderId: ctx.sfOrderId,
|
||||
};
|
||||
if (ctx.validation?.sfOrder?.AccountId) {
|
||||
assignmentDetails.accountId = ctx.validation.sfOrder.AccountId;
|
||||
}
|
||||
if (ctx.validation?.sfOrder?.SIM_Type__c) {
|
||||
assignmentDetails.simType = ctx.validation.sfOrder.SIM_Type__c;
|
||||
}
|
||||
|
||||
// Build request with only defined optional properties
|
||||
const request: Parameters<typeof this.simFulfillmentService.fulfillSimOrder>[0] = {
|
||||
orderDetails: ctx.orderDetails,
|
||||
configurations,
|
||||
voiceMailEnabled,
|
||||
callWaitingEnabled,
|
||||
assignmentDetails,
|
||||
};
|
||||
if (assignedPhysicalSimId) {
|
||||
request.assignedPhysicalSimId = assignedPhysicalSimId;
|
||||
@ -120,7 +136,8 @@ export class FulfillmentStepExecutors {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Salesforce order status to "Activated" after SIM fulfillment
|
||||
* Update Salesforce Activation_Status__c after SIM fulfillment
|
||||
* Note: Status is NOT changed here to avoid locking the Order before WHMCS setup
|
||||
*/
|
||||
async executeSfActivatedUpdate(
|
||||
ctx: OrderFulfillmentContext,
|
||||
@ -130,13 +147,14 @@ export class FulfillmentStepExecutors {
|
||||
return { skipped: true };
|
||||
}
|
||||
|
||||
// Only update Activation_Status__c - Status will be set in sf_registration_complete
|
||||
// to avoid locking the Order before WHMCS info can be written
|
||||
const result = await this.salesforceFacade.updateOrder({
|
||||
Id: ctx.sfOrderId,
|
||||
Status: "Activated",
|
||||
Activation_Status__c: "Activated",
|
||||
});
|
||||
this.sideEffects.publishStatusUpdate(ctx.sfOrderId, {
|
||||
status: "Activated",
|
||||
status: ctx.validation?.sfOrder?.Status ?? "Pending",
|
||||
activationStatus: "Activated",
|
||||
stage: "in_progress",
|
||||
source: "fulfillment",
|
||||
@ -166,11 +184,15 @@ export class FulfillmentStepExecutors {
|
||||
// Set phone number as domain (shows in WHMCS Domain field)
|
||||
item.domain = simFulfillmentResult.phoneNumber!;
|
||||
// Also add to custom fields for SIM Number field
|
||||
// Note: Field names must match exactly as configured in WHMCS (with spaces)
|
||||
item.customFields = {
|
||||
...item.customFields,
|
||||
SimNumber: simFulfillmentResult.phoneNumber,
|
||||
"SIM Number": simFulfillmentResult.phoneNumber,
|
||||
...(simFulfillmentResult.serialNumber && {
|
||||
SerialNumber: simFulfillmentResult.serialNumber,
|
||||
"Serial Number": simFulfillmentResult.serialNumber,
|
||||
}),
|
||||
...(simFulfillmentResult.eid && {
|
||||
EID: simFulfillmentResult.eid,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -256,33 +278,78 @@ export class FulfillmentStepExecutors {
|
||||
return { orderId, serviceIds };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update WHMCS service custom fields after order is accepted
|
||||
* This is necessary because AddOrder expects field IDs, but we use field names
|
||||
*/
|
||||
async executeWhmcsCustomFieldsUpdate(
|
||||
ctx: OrderFulfillmentContext,
|
||||
whmcsCreateResult?: WhmcsOrderResult,
|
||||
simFulfillmentResult?: SimFulfillmentResult
|
||||
): Promise<{ updated: boolean; serviceId?: number }> {
|
||||
const serviceId = whmcsCreateResult?.serviceIds?.[0];
|
||||
|
||||
if (!serviceId) {
|
||||
this.logger.debug("No WHMCS service ID available for custom fields update", {
|
||||
sfOrderId: ctx.sfOrderId,
|
||||
});
|
||||
return { updated: false };
|
||||
}
|
||||
|
||||
// Build custom fields from SIM fulfillment result
|
||||
const customFields: Record<string, string> = {};
|
||||
|
||||
if (simFulfillmentResult?.phoneNumber) {
|
||||
customFields["SIM Number"] = simFulfillmentResult.phoneNumber;
|
||||
}
|
||||
if (simFulfillmentResult?.serialNumber) {
|
||||
customFields["Serial Number"] = simFulfillmentResult.serialNumber;
|
||||
}
|
||||
if (simFulfillmentResult?.eid) {
|
||||
customFields["EID"] = simFulfillmentResult.eid;
|
||||
}
|
||||
|
||||
if (Object.keys(customFields).length === 0) {
|
||||
this.logger.debug("No custom fields to update for WHMCS service", {
|
||||
sfOrderId: ctx.sfOrderId,
|
||||
serviceId,
|
||||
});
|
||||
return { updated: false, serviceId };
|
||||
}
|
||||
|
||||
this.logger.log("Updating WHMCS service custom fields", {
|
||||
sfOrderId: ctx.sfOrderId,
|
||||
serviceId,
|
||||
fieldNames: Object.keys(customFields),
|
||||
});
|
||||
|
||||
await this.whmcsOrderService.updateServiceCustomFields(serviceId, customFields);
|
||||
|
||||
return { updated: true, serviceId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Salesforce with WHMCS registration info
|
||||
* This is the final step that sets Status to "Processed"
|
||||
*/
|
||||
async executeSfRegistrationComplete(
|
||||
ctx: OrderFulfillmentContext,
|
||||
whmcsCreateResult?: WhmcsOrderResult,
|
||||
simFulfillmentResult?: SimFulfillmentResult
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Parameter kept for interface consistency
|
||||
_simFulfillmentResult?: SimFulfillmentResult
|
||||
): Promise<void> {
|
||||
// For SIM orders that are already "Activated", don't change Status
|
||||
// Only update WHMCS info. For non-SIM orders, set Status to "Activated"
|
||||
const isSIMOrder = ctx.orderDetails?.orderType === "SIM";
|
||||
const isAlreadyActivated = simFulfillmentResult?.activated === true;
|
||||
|
||||
// Set Status to "Processed" and update WHMCS info
|
||||
// Status is set here (not earlier) to avoid locking the Order before WHMCS data is written
|
||||
const updatePayload: { Id: string; [key: string]: unknown } = {
|
||||
Id: ctx.sfOrderId,
|
||||
Status: "Processed",
|
||||
Activation_Status__c: "Activated",
|
||||
WHMCS_Order_ID__c: whmcsCreateResult?.orderId?.toString(),
|
||||
};
|
||||
|
||||
// Only set Status if not already activated (non-SIM orders)
|
||||
if (!isSIMOrder || !isAlreadyActivated) {
|
||||
updatePayload["Status"] = "Activated";
|
||||
}
|
||||
|
||||
const result = await this.salesforceFacade.updateOrder(updatePayload);
|
||||
this.sideEffects.publishStatusUpdate(ctx.sfOrderId, {
|
||||
status: "Activated",
|
||||
status: "Processed",
|
||||
activationStatus: "Activated",
|
||||
stage: "completed",
|
||||
source: "fulfillment",
|
||||
@ -290,7 +357,7 @@ export class FulfillmentStepExecutors {
|
||||
payload: {
|
||||
whmcsOrderId: whmcsCreateResult?.orderId,
|
||||
whmcsServiceIds: whmcsCreateResult?.serviceIds,
|
||||
simPhoneNumber: simFulfillmentResult?.phoneNumber,
|
||||
simPhoneNumber: ctx.simFulfillmentResult?.phoneNumber,
|
||||
},
|
||||
});
|
||||
await this.sideEffects.notifyOrderActivated(ctx.sfOrderId, ctx.validation?.sfOrder?.AccountId);
|
||||
|
||||
@ -68,10 +68,11 @@ export class FulfillmentStepFactory {
|
||||
steps.push(this.createSfActivatedUpdateStep(context, state));
|
||||
}
|
||||
|
||||
// Steps 5-9: WHMCS and completion
|
||||
// Steps 5-10: WHMCS and completion
|
||||
steps.push(this.createMappingStep(context, state));
|
||||
steps.push(this.createWhmcsCreateStep(context, state));
|
||||
steps.push(this.createWhmcsAcceptStep(context, state));
|
||||
steps.push(this.createWhmcsCustomFieldsUpdateStep(context, state));
|
||||
steps.push(this.createSfRegistrationCompleteStep(context, state));
|
||||
steps.push(this.createOpportunityUpdateStep(context, state));
|
||||
|
||||
@ -130,7 +131,7 @@ export class FulfillmentStepFactory {
|
||||
): DistributedStep {
|
||||
return {
|
||||
id: "sf_activated_update",
|
||||
description: "Update Salesforce order status to Activated",
|
||||
description: "Update Salesforce Activation_Status to Activated",
|
||||
execute: this.createTrackedStep(ctx, "sf_activated_update", async () => {
|
||||
return this.executors.executeSfActivatedUpdate(ctx, state.simFulfillmentResult);
|
||||
}),
|
||||
@ -201,6 +202,24 @@ export class FulfillmentStepFactory {
|
||||
};
|
||||
}
|
||||
|
||||
private createWhmcsCustomFieldsUpdateStep(
|
||||
ctx: OrderFulfillmentContext,
|
||||
state: StepState
|
||||
): DistributedStep {
|
||||
return {
|
||||
id: "whmcs_custom_fields",
|
||||
description: "Update WHMCS service custom fields (SIM Number, Serial Number, EID)",
|
||||
execute: this.createTrackedStep(ctx, "whmcs_custom_fields", async () => {
|
||||
return this.executors.executeWhmcsCustomFieldsUpdate(
|
||||
ctx,
|
||||
state.whmcsCreateResult,
|
||||
state.simFulfillmentResult
|
||||
);
|
||||
}),
|
||||
critical: false, // Custom fields update failure shouldn't rollback fulfillment
|
||||
};
|
||||
}
|
||||
|
||||
private createSfRegistrationCompleteStep(
|
||||
ctx: OrderFulfillmentContext,
|
||||
state: StepState
|
||||
|
||||
@ -230,6 +230,7 @@ export class OrderFulfillmentOrchestrator {
|
||||
{ step: "mapping", status: "pending" },
|
||||
{ step: "whmcs_create", status: "pending" },
|
||||
{ step: "whmcs_accept", status: "pending" },
|
||||
{ step: "whmcs_custom_fields", status: "pending" },
|
||||
{ step: "sf_registration_complete", status: "pending" },
|
||||
{ step: "opportunity_update", status: "pending" }
|
||||
);
|
||||
|
||||
@ -22,6 +22,18 @@ export interface ContactIdentityData {
|
||||
birthday: string; // YYYYMMDD format
|
||||
}
|
||||
|
||||
/**
|
||||
* Assignment details for Physical SIM inventory
|
||||
*/
|
||||
export interface SimAssignmentDetails {
|
||||
/** Salesforce Account ID to assign the SIM to */
|
||||
accountId?: string;
|
||||
/** Salesforce Order ID that assigned the SIM */
|
||||
orderId?: string;
|
||||
/** SIM Type (eSIM or Physical SIM) */
|
||||
simType?: string;
|
||||
}
|
||||
|
||||
export interface SimFulfillmentRequest {
|
||||
orderDetails: OrderDetails;
|
||||
configurations: Record<string, unknown>;
|
||||
@ -33,6 +45,8 @@ export interface SimFulfillmentRequest {
|
||||
callWaitingEnabled?: boolean;
|
||||
/** Contact identity data for PA05-05 */
|
||||
contactIdentity?: ContactIdentityData;
|
||||
/** Assignment details for SIM Inventory record (Physical SIM only) */
|
||||
assignmentDetails?: SimAssignmentDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,6 +63,8 @@ export interface SimFulfillmentResult {
|
||||
serialNumber?: string;
|
||||
/** Salesforce SIM Inventory ID */
|
||||
simInventoryId?: string;
|
||||
/** EID for eSIM (for WHMCS custom fields) */
|
||||
eid?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -67,6 +83,7 @@ export class SimFulfillmentService {
|
||||
voiceMailEnabled = false,
|
||||
callWaitingEnabled = false,
|
||||
contactIdentity,
|
||||
assignmentDetails,
|
||||
} = request;
|
||||
|
||||
const simType = this.readEnum(configurations["simType"], ["eSIM", "Physical SIM"]);
|
||||
@ -154,6 +171,7 @@ export class SimFulfillmentService {
|
||||
activated: true,
|
||||
simType: "eSIM",
|
||||
phoneNumber,
|
||||
eid,
|
||||
};
|
||||
} else {
|
||||
// Physical SIM activation flow (PA05-18 + PA02-01 + PA05-05)
|
||||
@ -172,6 +190,7 @@ export class SimFulfillmentService {
|
||||
voiceMailEnabled,
|
||||
callWaitingEnabled,
|
||||
contactIdentity,
|
||||
assignmentDetails,
|
||||
});
|
||||
|
||||
this.logger.log("Physical SIM fulfillment completed successfully", {
|
||||
@ -259,19 +278,15 @@ export class SimFulfillmentService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate Physical SIM via Freebit PA05-18 + PA02-01 + PA05-05 APIs
|
||||
* Activate Physical SIM (Black SIM) via Freebit PA02-01 + PA05-05 APIs
|
||||
*
|
||||
* Flow for Physical SIMs:
|
||||
* Flow for Physical SIMs (Black 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 PA05-18 (Semi-Black Registration) - MUST be called first!
|
||||
* 5. Call Freebit PA02-01 (Account Registration) with createType="add"
|
||||
* 6. Call Freebit PA05-05 (Voice Options) to configure voice features
|
||||
* 7. Update SIM Inventory status to "Assigned"
|
||||
*
|
||||
* Note: PA05-18 must be called before PA02-01, otherwise PA02-01 will fail
|
||||
* with error 210 "アカウント不在エラー" (Account not found error).
|
||||
* 4. Call Freebit PA02-01 (Account Registration) with createType="new"
|
||||
* 5. Call Freebit PA05-05 (Voice Options) to configure voice features
|
||||
* 6. Update SIM Inventory status to "Assigned"
|
||||
*/
|
||||
private async activatePhysicalSim(params: {
|
||||
orderId: string;
|
||||
@ -281,6 +296,7 @@ export class SimFulfillmentService {
|
||||
voiceMailEnabled: boolean;
|
||||
callWaitingEnabled: boolean;
|
||||
contactIdentity?: ContactIdentityData | undefined;
|
||||
assignmentDetails?: SimAssignmentDetails | undefined;
|
||||
}): Promise<{ phoneNumber: string; serialNumber: string }> {
|
||||
const {
|
||||
orderId,
|
||||
@ -290,9 +306,10 @@ export class SimFulfillmentService {
|
||||
voiceMailEnabled,
|
||||
callWaitingEnabled,
|
||||
contactIdentity,
|
||||
assignmentDetails,
|
||||
} = params;
|
||||
|
||||
this.logger.log("Starting Physical SIM activation (PA05-18 + PA02-01 + PA05-05)", {
|
||||
this.logger.log("Starting Physical SIM activation (PA02-01 + PA05-05)", {
|
||||
orderId,
|
||||
simInventoryId,
|
||||
planSku,
|
||||
@ -315,50 +332,27 @@ export class SimFulfillmentService {
|
||||
|
||||
// Use phone number from SIM inventory
|
||||
const accountPhoneNumber = simRecord.phoneNumber;
|
||||
// PT Number is the productNumber required for PA05-18
|
||||
const productNumber = simRecord.ptNumber;
|
||||
|
||||
this.logger.log("Physical SIM inventory validated", {
|
||||
orderId,
|
||||
simInventoryId,
|
||||
accountPhoneNumber,
|
||||
productNumber,
|
||||
planCode,
|
||||
});
|
||||
|
||||
try {
|
||||
// Step 4: Call Freebit PA05-18 (Semi-Black Registration) - MUST be first!
|
||||
this.logger.log("Calling PA05-18 Semi-Black Registration", {
|
||||
orderId,
|
||||
account: accountPhoneNumber,
|
||||
productNumber,
|
||||
planCode,
|
||||
});
|
||||
|
||||
await this.freebitFacade.registerSemiBlackAccount({
|
||||
account: accountPhoneNumber,
|
||||
productNumber,
|
||||
planCode,
|
||||
});
|
||||
|
||||
this.logger.log("PA05-18 Semi-Black Registration successful", {
|
||||
orderId,
|
||||
account: accountPhoneNumber,
|
||||
});
|
||||
|
||||
// Step 5: Call Freebit PA02-01 (Account Registration) with createType="add"
|
||||
// Note: After PA05-18, we use createType="add" (not "new")
|
||||
// Step 4: Call Freebit PA02-01 (Account Registration) for Black SIM
|
||||
this.logger.log("Calling PA02-01 Account Registration", {
|
||||
orderId,
|
||||
account: accountPhoneNumber,
|
||||
planCode,
|
||||
createType: "add",
|
||||
createType: "new",
|
||||
});
|
||||
|
||||
await this.freebitFacade.registerAccount({
|
||||
account: accountPhoneNumber,
|
||||
planCode,
|
||||
createType: "add",
|
||||
createType: "new",
|
||||
});
|
||||
|
||||
this.logger.log("PA02-01 Account Registration successful", {
|
||||
@ -366,7 +360,7 @@ export class SimFulfillmentService {
|
||||
account: accountPhoneNumber,
|
||||
});
|
||||
|
||||
// Step 6: Call Freebit PA05-05 (Voice Options Registration)
|
||||
// Step 5: 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", {
|
||||
@ -401,8 +395,8 @@ export class SimFulfillmentService {
|
||||
});
|
||||
}
|
||||
|
||||
// Step 7: Update SIM Inventory status to "Assigned"
|
||||
await this.simInventory.markAsAssigned(simInventoryId);
|
||||
// Step 6: Update SIM Inventory status to "Assigned" with assignment details
|
||||
await this.simInventory.markAsAssigned(simInventoryId, assignmentDetails);
|
||||
|
||||
this.logger.log("Physical SIM activated successfully", {
|
||||
orderId,
|
||||
|
||||
@ -6,3 +6,14 @@ catalog:
|
||||
"@types/node": ^25.2.0
|
||||
typescript: ^5.9.3
|
||||
zod: ^4.3.6
|
||||
onlyBuiltDependencies:
|
||||
- "@nestjs/core"
|
||||
- "@prisma/engines"
|
||||
- "@swc/core"
|
||||
- argon2
|
||||
- esbuild
|
||||
- prisma
|
||||
- protobufjs
|
||||
- sharp
|
||||
- ssh2
|
||||
- unrs-resolver
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user