- Updated SF_PUBSUB_NUM_REQUESTED in environment configuration to improve flow control. - Enhanced CatalogCdcSubscriber and OrderCdcSubscriber to utilize a dynamic numRequested value for subscriptions, improving event handling. - Removed deprecated WHMCS API access key configurations from WhmcsConfigService to streamline integration. - Improved error handling and logging in various services for better operational insights. - Refactored currency service to centralize fallback currency logic, ensuring consistent currency handling across the application.
7.9 KiB
7.9 KiB
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
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_TRIGGER_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<void> {
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("Activation_Status__c")) {
await this.handleActivationStatusChange(payload, orderId);
}
// 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 handleActivationStatusChange(
payload: Record<string, unknown>,
orderId: string
): Promise<void> {
const activationStatus = this.extractStringField(payload, ["Activation_Status__c"]);
const status = this.extractStringField(payload, ["Status"]);
if (activationStatus !== "Activating") {
this.logger.debug("Activation status changed but not to Activating", {
orderId,
activationStatus,
});
return;
}
if (status && !this.PROVISION_TRIGGER_STATUSES.has(status)) {
this.logger.debug("Activation set to Activating but order status isn't Approved/Reactivate", {
orderId,
activationStatus,
status,
});
return;
}
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;
}
this.logger.log("Activation status moved to Activating, enqueuing fulfillment", {
orderId,
activationStatus,
status,
});
try {
await this.provisioningQueue.enqueue({
sfOrderId: orderId,
idempotencyKey: `cdc-activation-${Date.now()}-${orderId}`,
correlationId: `cdc-${orderId}`,
});
this.logger.log("Successfully enqueued provisioning job from activation change", {
orderId,
activationStatus,
status,
});
} catch (error) {
this.logger.error("Failed to enqueue provisioning job from activation change", {
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" (Flow sets Activation_Status__c = "Activating")
↓
Portal (OrderCdcSubscriber):
- Receives OrderChangeEvent
- Checks: Activation_Status__c changed to "Activating"?
- Yes → Enqueues provisioning job
- Also → Invalidates cache (if customer-facing)
Benefits of CDC-Only
-
✅ Simpler Salesforce Setup
- No Platform Event definition needed
- No Flow to publish event
- Just enable CDC on Order object
-
✅ Single Mechanism
- One subscriber handles everything
- Fewer moving parts
- Easier to understand
-
✅ Automatic
- Any Flow that sets
Activation_Status__c = "Activating"triggers provisioning - No manual Flow maintenance
- Any Flow that sets
Drawbacks of CDC-Only
-
❌ Less Explicit Control
- No clear "provision this order" signal
- Inferred from field changes
- Harder to add custom context
-
❌ More Guards Needed
- Must check: Not already provisioning
- Must check: WHMCS Order ID doesn't exist
- Must check: Activation Status
- More defensive coding
-
❌ Business Logic in Portal
- Portal must know when to provision
- Salesforce can't pre-validate
- Logic duplicated
-
❌ 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:
-
Update OrderCdcSubscriber
- Add
ProvisioningQueueServicedependency - Add
handleActivationStatusChange()method - Add guards for idempotency
- Add
-
Remove SalesforcePubSubSubscriber (optional)
- Or keep it for other Platform Events
- Just don't subscribe to Order_Fulfilment_Requested__e
-
Remove Salesforce Flow
- Delete Flow that publishes Order_Fulfilment_Requested__e
- Or disable it
-
Test Thoroughly
- Test: Activation_Status__c → "Activating" triggers provisioning
- Test: Already provisioned orders don't re-trigger
- Test: Cancelled orders don't trigger
- Test: Cache invalidation still works
-
Update .env
# 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:
- You have complex approval workflows
- You want explicit control over provisioning
- Salesforce handles business validation
- Clear separation of concerns:
- Platform Event = "Do something" (provisioning)
- CDC = "Something changed" (cache invalidation)
The dual-mechanism approach is more robust for production systems.