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

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

  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

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