Assist_Design/docs/CDC_ONLY_PROVISIONING_ALTERNATIVE.md
barsa 309dac630f Enhance order processing and caching mechanisms
- 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.
2025-11-06 17:01:34 +09:00

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.**