- Introduced provisioning triggers in OrderCdcSubscriber to handle specific status changes and enqueue provisioning jobs. - Implemented request coalescing in OrdersCacheService and CatalogCacheService to prevent duplicate Salesforce API calls during cache invalidation. - Updated CatalogModule and OrdersModule to export additional services for improved module integration. - Enhanced error handling and logging in various services to provide better insights during operations.
269 lines
7.9 KiB
Markdown
269 lines
7.9 KiB
Markdown
# 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<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("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<string, unknown>,
|
|
orderId: string,
|
|
changedFields: Set<string>
|
|
): Promise<void> {
|
|
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.**
|
|
|