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"]);
/** Statuses that trigger provisioning */
const PROVISION_TRIGGER_STATUSES = new Set(["Approved", "Reactivate"]);
const PROVISION_TRIGGER_STATUSES = new Set(["Processed", "Reactivate"]);
@Injectable()
export class OrderCdcSubscriber implements OnModuleInit {
@ -134,9 +134,9 @@ export class OrderCdcSubscriber implements OnModuleInit {
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")) {
await this.handleStatusApprovedChange(payload, orderId);
await this.handleStatusProcessedChange(payload, orderId);
}
// 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
* 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
* will be null in the payload even though they have values on the record.
*
* The processor handles:
* - Physical SIM: Status="Approved" + SIM_Type="Physical SIM" + Assigned_Physical_SIM set
* - Standard: Activation_Status__c="Activating"
* - Idempotency via WHMCS_Order_ID__c check
* On API failure, the orchestrator reverts the order Status back to "Approved".
*/
private async handleStatusApprovedChange(
private async handleStatusProcessedChange(
payload: Record<string, unknown>,
orderId: string
): Promise<void> {
const status = extractStringField(payload, ["Status"]);
// Only trigger when status changes to "Approved"
if (status !== "Approved") {
// Only trigger when status changes to "Processed"
if (status !== "Processed") {
return;
}
// Note: We intentionally do NOT check SIM_Type__c or Assign_Physical_SIM__c here
// 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", {
this.logger.log("Enqueuing provisioning job for order status change to Processed", {
orderId,
status,
});
@ -260,15 +253,15 @@ export class OrderCdcSubscriber implements OnModuleInit {
try {
await this.provisioningQueue.enqueue({
sfOrderId: orderId,
idempotencyKey: `cdc-status-approved-${Date.now()}-${orderId}`,
correlationId: `cdc-status-approved-${orderId}`,
idempotencyKey: `cdc-status-processed-${Date.now()}-${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,
});
} 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,
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
// 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 isPhysicalSimApproval =
orderStatus === "Approved" && simType === "Physical SIM" && !!assignedPhysicalSim;
orderStatus === "Processed" && simType === "Physical SIM" && !!assignedPhysicalSim;
if (!isStandardActivation && !isPhysicalSimApproval) {
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,
});
// Update Salesforce with failure status
// Update Salesforce with failure status — revert to "Approved" so it can be retried
const errorShortCode = (
this.orderFulfillmentErrorService.getShortCode(error) || String(errorCode)
)
@ -321,7 +321,7 @@ export class OrderFulfillmentOrchestrator {
try {
await this.salesforceFacade.updateOrder({
Id: sfOrderId,
Status: "Pending Review",
Status: "Approved",
Activation_Status__c: "Failed",
Activation_Error_Code__c: errorShortCode,
Activation_Error_Message__c: userMessage?.slice(0, 255),