feat: trigger Freebit APIs on "Processed" status instead of "Approved"
Some checks failed
Pull Request Checks / Code Quality & Security (push) Has been cancelled
Security Audit / Security Vulnerability Audit (push) Has been cancelled
Security Audit / Dependency Review (push) Has been cancelled
Security Audit / CodeQL Security Analysis (push) Has been cancelled
Security Audit / Check Outdated Dependencies (push) Has been cancelled

- CDC subscriber now listens for Status="Processed" to fire SIM APIs
- On API error, order Status reverts to "Approved" for retry
- Provisioning processor validates "Processed" for Physical SIM flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Temuulen Ankhbayar 2026-03-07 18:19:53 +09:00
parent e7d1371c48
commit afc18988cd
3 changed files with 18 additions and 25 deletions

View File

@ -49,7 +49,7 @@ const INTERNAL_ORDER_FIELDS = new Set([
const INTERNAL_ORDER_ITEM_FIELDS = new Set(["WHMCS_Service_ID__c"]); const INTERNAL_ORDER_ITEM_FIELDS = new Set(["WHMCS_Service_ID__c"]);
/** Statuses that trigger provisioning */ /** Statuses that trigger provisioning */
const PROVISION_TRIGGER_STATUSES = new Set(["Approved", "Reactivate"]); const PROVISION_TRIGGER_STATUSES = new Set(["Processed", "Reactivate"]);
@Injectable() @Injectable()
export class OrderCdcSubscriber implements OnModuleInit { export class OrderCdcSubscriber implements OnModuleInit {
@ -134,9 +134,9 @@ export class OrderCdcSubscriber implements OnModuleInit {
await this.handleActivationStatusChange(payload, orderId); await this.handleActivationStatusChange(payload, orderId);
} }
// Check for provisioning trigger (Status change to "Approved") // Check for provisioning trigger (Status change to "Processed")
if (payload && changedFields.has("Status")) { if (payload && changedFields.has("Status")) {
await this.handleStatusApprovedChange(payload, orderId); await this.handleStatusProcessedChange(payload, orderId);
} }
// Cache invalidation - only for customer-facing field changes // Cache invalidation - only for customer-facing field changes
@ -222,9 +222,9 @@ export class OrderCdcSubscriber implements OnModuleInit {
} }
/** /**
* Handle order status changes to "Approved" * Handle order status changes to "Processed"
* *
* Enqueues a provisioning job when Status changes to "Approved". * Enqueues a provisioning job when Status changes to "Processed".
* The provisioning processor will fetch the full order from Salesforce * The provisioning processor will fetch the full order from Salesforce
* and validate the conditions (SIM_Type__c, Assign_Physical_SIM__c, etc.) * and validate the conditions (SIM_Type__c, Assign_Physical_SIM__c, etc.)
* *
@ -232,27 +232,20 @@ export class OrderCdcSubscriber implements OnModuleInit {
* because CDC only includes CHANGED fields. If only Status was updated, those fields * because CDC only includes CHANGED fields. If only Status was updated, those fields
* will be null in the payload even though they have values on the record. * will be null in the payload even though they have values on the record.
* *
* The processor handles: * On API failure, the orchestrator reverts the order Status back to "Approved".
* - Physical SIM: Status="Approved" + SIM_Type="Physical SIM" + Assigned_Physical_SIM set
* - Standard: Activation_Status__c="Activating"
* - Idempotency via WHMCS_Order_ID__c check
*/ */
private async handleStatusApprovedChange( private async handleStatusProcessedChange(
payload: Record<string, unknown>, payload: Record<string, unknown>,
orderId: string orderId: string
): Promise<void> { ): Promise<void> {
const status = extractStringField(payload, ["Status"]); const status = extractStringField(payload, ["Status"]);
// Only trigger when status changes to "Approved" // Only trigger when status changes to "Processed"
if (status !== "Approved") { if (status !== "Processed") {
return; return;
} }
// Note: We intentionally do NOT check SIM_Type__c or Assign_Physical_SIM__c here this.logger.log("Enqueuing provisioning job for order status change to Processed", {
// because CDC payloads only contain changed fields. The provisioning processor
// will fetch the full order and validate all conditions.
this.logger.log("Enqueuing provisioning job for order status change to Approved", {
orderId, orderId,
status, status,
}); });
@ -260,15 +253,15 @@ export class OrderCdcSubscriber implements OnModuleInit {
try { try {
await this.provisioningQueue.enqueue({ await this.provisioningQueue.enqueue({
sfOrderId: orderId, sfOrderId: orderId,
idempotencyKey: `cdc-status-approved-${Date.now()}-${orderId}`, idempotencyKey: `cdc-status-processed-${Date.now()}-${orderId}`,
correlationId: `cdc-status-approved-${orderId}`, correlationId: `cdc-status-processed-${orderId}`,
}); });
this.logger.log("Successfully enqueued provisioning job for Approved status", { this.logger.log("Successfully enqueued provisioning job for Processed status", {
orderId, orderId,
}); });
} catch (error) { } catch (error) {
this.logger.error("Failed to enqueue provisioning job for Approved status", { this.logger.error("Failed to enqueue provisioning job for Processed status", {
orderId, orderId,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
}); });

View File

@ -34,10 +34,10 @@ export class ProvisioningProcessor extends WorkerHost {
// Guard: Determine if this is a valid provisioning request // Guard: Determine if this is a valid provisioning request
// Case 1: Standard flow - Activation_Status__c = "Activating" // Case 1: Standard flow - Activation_Status__c = "Activating"
// Case 2: Physical SIM flow - Status = "Approved" with SIM_Type__c = "Physical SIM" // Case 2: Physical SIM flow - Status = "Processed" with SIM_Type__c = "Physical SIM"
const isStandardActivation = activationStatus === "Activating"; const isStandardActivation = activationStatus === "Activating";
const isPhysicalSimApproval = const isPhysicalSimApproval =
orderStatus === "Approved" && simType === "Physical SIM" && !!assignedPhysicalSim; orderStatus === "Processed" && simType === "Physical SIM" && !!assignedPhysicalSim;
if (!isStandardActivation && !isPhysicalSimApproval) { if (!isStandardActivation && !isPhysicalSimApproval) {
this.logger.log("Skipping provisioning job: Order not in activatable state", { this.logger.log("Skipping provisioning job: Order not in activatable state", {

View File

@ -312,7 +312,7 @@ export class OrderFulfillmentOrchestrator {
failedStep: context.steps.find(s => s.status === "failed")?.step, failedStep: context.steps.find(s => s.status === "failed")?.step,
}); });
// Update Salesforce with failure status // Update Salesforce with failure status — revert to "Approved" so it can be retried
const errorShortCode = ( const errorShortCode = (
this.orderFulfillmentErrorService.getShortCode(error) || String(errorCode) this.orderFulfillmentErrorService.getShortCode(error) || String(errorCode)
) )
@ -321,7 +321,7 @@ export class OrderFulfillmentOrchestrator {
try { try {
await this.salesforceFacade.updateOrder({ await this.salesforceFacade.updateOrder({
Id: sfOrderId, Id: sfOrderId,
Status: "Pending Review", Status: "Approved",
Activation_Status__c: "Failed", Activation_Status__c: "Failed",
Activation_Error_Code__c: errorShortCode, Activation_Error_Code__c: errorShortCode,
Activation_Error_Message__c: userMessage?.slice(0, 255), Activation_Error_Message__c: userMessage?.slice(0, 255),