# CDC-Only Order Provisioning (Alternative Approach) ## Overview This document shows how to use ONLY CDC (OrderCdcSubscriber) for both cache invalidation AND order provisioning, eliminating the need for Platform Events. ## Modified OrderCdcSubscriber ```typescript import { Injectable, Inject, OnModuleInit, OnModuleDestroy } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import PubSubApiClientPkg from "salesforce-pubsub-api-client"; import { SalesforceConnection } from "../services/salesforce-connection.service"; import { OrdersCacheService } from "@bff/modules/orders/services/orders-cache.service"; import { ProvisioningQueueService } from "@bff/modules/orders/queue/provisioning.queue"; @Injectable() export class OrderCdcSubscriber implements OnModuleInit, OnModuleDestroy { // ... (same client setup as before) private readonly PROVISION_STATUSES = new Set(["Approved", "Reactivate"]); constructor( private readonly config: ConfigService, private readonly sfConnection: SalesforceConnection, private readonly ordersCache: OrdersCacheService, private readonly provisioningQueue: ProvisioningQueueService, // ← Add this @Inject(Logger) private readonly logger: Logger ) {} private async handleOrderEvent(data: unknown): Promise { const payload = this.extractPayload(data); const changedFields = this.extractChangedFields(payload); const orderId = this.extractStringField(payload, ["Id"]); const accountId = this.extractStringField(payload, ["AccountId"]); if (!orderId) { this.logger.warn("Order CDC event missing Order ID; skipping"); return; } // 1. CHECK FOR PROVISIONING TRIGGER if (changedFields.has("Status")) { await this.handleStatusChange(payload, orderId, changedFields); } // 2. CACHE INVALIDATION (existing logic) const hasCustomerFacingChange = this.hasCustomerFacingChanges(changedFields); if (hasCustomerFacingChange) { this.logger.log("Order CDC event with customer-facing changes, invalidating cache", { orderId, accountId, changedFields: Array.from(changedFields), }); await this.ordersCache.invalidateOrder(orderId); if (accountId) { await this.ordersCache.invalidateAccountOrders(accountId); } } else { this.logger.debug("Order CDC event contains only internal field changes; skipping invalidation", { orderId, changedFields: Array.from(changedFields), }); } } /** * Handle Status field changes and trigger provisioning if needed */ private async handleStatusChange( payload: Record, orderId: string, changedFields: Set ): Promise { const newStatus = this.extractStringField(payload, ["Status"]); const activationStatus = this.extractStringField(payload, ["Activation_Status__c"]); // Guard: Only provision for specific statuses if (!newStatus || !this.PROVISION_STATUSES.has(newStatus)) { this.logger.debug("Status changed but not a provision trigger", { orderId, newStatus, }); return; } // Guard: Don't trigger if already provisioning/provisioned if (activationStatus === "Activating" || activationStatus === "Activated") { this.logger.debug("Order already provisioning/provisioned, skipping", { orderId, activationStatus, }); return; } // Guard: Check if WHMCS Order ID already exists (idempotency) const whmcsOrderId = this.extractStringField(payload, ["WHMCS_Order_ID__c"]); if (whmcsOrderId) { this.logger.log("Order already has WHMCS Order ID, skipping provisioning", { orderId, whmcsOrderId, }); return; } // Trigger provisioning this.logger.log("Order status changed to provision trigger, enqueuing fulfillment", { orderId, status: newStatus, activationStatus, }); try { await this.provisioningQueue.enqueue({ sfOrderId: orderId, idempotencyKey: `cdc-status-change-${Date.now()}-${orderId}`, correlationId: `cdc-${orderId}`, }); this.logger.log("Successfully enqueued provisioning job from CDC", { orderId, trigger: "Status change to " + newStatus, }); } catch (error) { this.logger.error("Failed to enqueue provisioning job from CDC", { orderId, error: error instanceof Error ? error.message : String(error), }); } } // ... (rest of the existing methods) } ``` ## Comparison ### Before (Platform Event + CDC): ``` Salesforce Flow: - Order Status = "Approved" - Publish Order_Fulfilment_Requested__e ↓ Portal (SalesforcePubSubSubscriber): - Receives Platform Event - Enqueues provisioning job Portal (OrderCdcSubscriber): - Receives OrderChangeEvent - Invalidates cache ``` ### After (CDC Only): ``` Salesforce: - Order Status = "Approved" ↓ Portal (OrderCdcSubscriber): - Receives OrderChangeEvent - Checks: Status changed to "Approved"? - Yes → Enqueues provisioning job - Also → Invalidates cache (if customer-facing) ``` ## Benefits of CDC-Only 1. ✅ **Simpler Salesforce Setup** - No Platform Event definition needed - No Flow to publish event - Just enable CDC on Order object 2. ✅ **Single Mechanism** - One subscriber handles everything - Fewer moving parts - Easier to understand 3. ✅ **Automatic** - Any Status change to "Approved" triggers provisioning - No manual Flow maintenance ## Drawbacks of CDC-Only 1. ❌ **Less Explicit Control** - No clear "provision this order" signal - Inferred from field changes - Harder to add custom context 2. ❌ **More Guards Needed** - Must check: Not already provisioning - Must check: WHMCS Order ID doesn't exist - Must check: Activation Status - More defensive coding 3. ❌ **Business Logic in Portal** - Portal must know when to provision - Salesforce can't pre-validate - Logic duplicated 4. ❌ **No Custom Metadata** - Can't include: RequestedBy, Priority, etc. - Limited to Order fields - Less context for debugging ## When to Use CDC-Only ✅ Use CDC-Only if: - Simple approval workflow (Draft → Approved → Completed) - No complex business validation needed - Want simplest possible setup - Okay with less control ❌ Stick with Platform Events if: - Complex approval workflows - Need custom context/metadata - Multiple trigger conditions - Business logic should stay in Salesforce ## Migration Steps If you decide to switch to CDC-Only: 1. **Update OrderCdcSubscriber** - Add `ProvisioningQueueService` dependency - Add `handleStatusChange()` method - Add guards for idempotency 2. **Remove SalesforcePubSubSubscriber** (optional) - Or keep it for other Platform Events - Just don't subscribe to Order_Fulfilment_Requested__e 3. **Remove Salesforce Flow** - Delete Flow that publishes Order_Fulfilment_Requested__e - Or disable it 4. **Test Thoroughly** - Test: Status → "Approved" triggers provisioning - Test: Already provisioned orders don't re-trigger - Test: Cancelled orders don't trigger - Test: Cache invalidation still works 5. **Update .env** ```bash # Can remove (if not using other Platform Events) # SF_PROVISION_EVENT_CHANNEL=/event/Order_Fulfilment_Requested__e # Keep these (still needed for CDC) SF_ORDER_CDC_CHANNEL=/data/OrderChangeEvent SF_ORDER_ITEM_CDC_CHANNEL=/data/OrderItemChangeEvent ``` ## Recommendation **For your use case, I recommend keeping the Platform Event approach:** Reasons: 1. You have complex approval workflows 2. You want explicit control over provisioning 3. Salesforce handles business validation 4. Clear separation of concerns: - Platform Event = "Do something" (provisioning) - CDC = "Something changed" (cache invalidation) **The dual-mechanism approach is more robust for production systems.**