- 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.
266 lines
7.9 KiB
Markdown
266 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_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
|
|
|
|
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 Flow that sets `Activation_Status__c = "Activating"` 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 `handleActivationStatusChange()` 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: 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
|
|
|
|
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.**
|
|
|