Assist_Design/docs/CDC_ONLY_PROVISIONING_ALTERNATIVE.md
barsa d943d04754 Refactor environment configuration and enhance order processing logic
- 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.
2025-11-17 10:31:33 +09:00

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