Assist_Design/docs/SALESFORCE-ORDER-COMMUNICATION.md
T. Narantuya 98f998db51 Refactor code for improved readability and maintainability
- Simplified import statements in auth.controller.ts and consolidated DTO imports.
- Streamlined accountStatus method in AuthController for better clarity.
- Refactored error handling in AuthService for existing mapping checks and password validation.
- Cleaned up whitespace and formatting across various files for consistency.
- Enhanced logging configuration in logging.module.ts to reduce noise and improve clarity.
- Updated frontend components for better formatting and readability in ProfilePage and SignupPage.
2025-09-02 16:09:17 +09:00

11 KiB

Salesforce-to-Portal Order Communication Guide

Overview

This guide focuses specifically on secure communication between Salesforce and your Portal for order provisioning. This is NOT about invoices or billing - it's about the order approval and provisioning workflow.

The Order Flow

1. Customer places order → Portal creates Salesforce Order (Status: "Pending Review")
2. Salesforce operator reviews → Clicks "Provision in WHMCS" Quick Action  
3. Salesforce calls Portal BFF → POST /orders/{sfOrderId}/provision
4. Portal BFF provisions in WHMCS → Updates Salesforce Order status
5. Customer sees updated status in Portal

1. Salesforce → Portal (Order Provisioning)

Current Implementation

Your existing architecture already handles this securely via the Quick Action that calls your BFF endpoint:

  • Endpoint: POST /orders/{sfOrderId}/provision
  • Authentication: Named Credentials + HMAC signature
  • Security: IP allowlisting, idempotency keys, signed headers

Enhanced Security Implementation

Use your existing EnhancedWebhookSignatureGuard for the provisioning endpoint:

// apps/bff/src/orders/orders.controller.ts
@Post(':sfOrderId/provision')
@UseGuards(EnhancedWebhookSignatureGuard)
@ApiHeader({ name: "X-SF-Signature", description: "Salesforce HMAC signature" })
@ApiHeader({ name: "X-SF-Timestamp", description: "Request timestamp" })
@ApiHeader({ name: "X-SF-Nonce", description: "Unique nonce" })
@ApiHeader({ name: "Idempotency-Key", description: "Idempotency key" })
async provisionOrder(
  @Param('sfOrderId') sfOrderId: string,
  @Body() payload: ProvisionOrderRequest,
  @Headers('idempotency-key') idempotencyKey: string
) {
  return await this.orderOrchestrator.provisionOrder(sfOrderId, payload, idempotencyKey);
}

Salesforce Apex Implementation

public class OrderProvisioningService {
    private static final String WEBHOOK_SECRET = '{!$Credential.Portal_Webhook.Password}';
    
    @future(callout=true)
    public static void provisionOrder(String orderId) {
        try {
            // Create secure payload
            Map<String, Object> payload = new Map<String, Object>{
                'orderId' => orderId,
                'timestamp' => System.now().format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''),
                'nonce' => generateNonce()
            };
            
            String jsonPayload = JSON.serialize(payload);
            String signature = generateHMACSignature(jsonPayload, WEBHOOK_SECRET);
            
            // Make secure HTTP call
            HttpRequest req = new HttpRequest();
            req.setEndpoint('callout:Portal_BFF/orders/' + orderId + '/provision');
            req.setMethod('POST');
            req.setHeader('Content-Type', 'application/json');
            req.setHeader('X-SF-Signature', signature);
            req.setHeader('X-SF-Timestamp', payload.get('timestamp').toString());
            req.setHeader('X-SF-Nonce', payload.get('nonce').toString());
            req.setHeader('Idempotency-Key', 'provision_' + orderId + '_' + System.now().getTime());
            req.setBody(jsonPayload);
            req.setTimeout(30000);
            
            Http http = new Http();
            HttpResponse res = http.send(req);
            
            handleProvisioningResponse(orderId, res);
            
        } catch (Exception e) {
            updateOrderStatus(orderId, 'Failed', e.getMessage());
        }
    }
    
    private static String generateHMACSignature(String data, String key) {
        Blob hmacData = Crypto.generateMac('HmacSHA256', Blob.valueOf(data), Blob.valueOf(key));
        return EncodingUtil.convertToHex(hmacData);
    }
    
    private static String generateNonce() {
        return EncodingUtil.convertToHex(Crypto.generateAesKey(128)).substring(0, 16);
    }
    
    private static void handleProvisioningResponse(String orderId, HttpResponse res) {
        if (res.getStatusCode() == 200) {
            updateOrderStatus(orderId, 'Provisioned', null);
        } else {
            updateOrderStatus(orderId, 'Failed', 'HTTP ' + res.getStatusCode());
        }
    }
    
    private static void updateOrderStatus(String orderId, String status, String errorMessage) {
        Order ord = [SELECT Id FROM Order WHERE Id = :orderId LIMIT 1];
        ord.Provisioning_Status__c = status;
        if (errorMessage != null) {
            ord.Provisioning_Error_Message__c = errorMessage.left(255); // Truncate if needed
        }
        update ord;
    }
}

2. Optional: Portal → Salesforce (Status Updates)

If you want to send status updates back to Salesforce during provisioning, you can implement a reverse webhook:

Portal BFF Implementation

// apps/bff/src/vendors/salesforce/services/order-status-update.service.ts
@Injectable()
export class OrderStatusUpdateService {
  constructor(
    private salesforceConnection: SalesforceConnection,
    @Inject(Logger) private logger: Logger
  ) {}

  async updateOrderStatus(
    sfOrderId: string, 
    status: 'Activating' | 'Provisioned' | 'Failed',
    details?: {
      whmcsOrderId?: string;
      errorCode?: string;
      errorMessage?: string;
    }
  ) {
    try {
      const updateData: any = {
        Id: sfOrderId,
        Provisioning_Status__c: status,
        Last_Provisioning_At__c: new Date().toISOString(),
      };

      if (details?.whmcsOrderId) {
        updateData.WHMCS_Order_ID__c = details.whmcsOrderId;
      }

      if (status === 'Failed' && details?.errorCode) {
        updateData.Provisioning_Error_Code__c = details.errorCode;
        updateData.Provisioning_Error_Message__c = details.errorMessage?.substring(0, 255);
      }

      await this.salesforceConnection.sobject('Order').update(updateData);

      this.logger.log('Order status updated in Salesforce', {
        sfOrderId,
        status,
        whmcsOrderId: details?.whmcsOrderId,
      });
    } catch (error) {
      this.logger.error('Failed to update order status in Salesforce', {
        sfOrderId,
        status,
        error: error instanceof Error ? error.message : String(error),
      });
      // Don't throw - this is a non-critical update
    }
  }
}

Usage in Order Orchestrator

// In your existing OrderOrchestrator service
async provisionOrder(sfOrderId: string, payload: any, idempotencyKey: string) {
  try {
    // Update status to "Activating"
    await this.orderStatusUpdateService.updateOrderStatus(sfOrderId, 'Activating');

    // Your existing provisioning logic...
    const whmcsOrderId = await this.provisionInWhmcs(sfOrderId, payload);

    // Update status to "Provisioned" with WHMCS order ID
    await this.orderStatusUpdateService.updateOrderStatus(sfOrderId, 'Provisioned', {
      whmcsOrderId: whmcsOrderId.toString(),
    });

    return { success: true, whmcsOrderId };
  } catch (error) {
    // Update status to "Failed" with error details
    await this.orderStatusUpdateService.updateOrderStatus(sfOrderId, 'Failed', {
      errorCode: 'PROVISIONING_ERROR',
      errorMessage: error instanceof Error ? error.message : String(error),
    });
    
    throw error;
  }
}

3. Security Configuration

Environment Variables

# Salesforce webhook security
SF_WEBHOOK_SECRET=your_256_bit_secret_key_here
SF_WEBHOOK_IP_ALLOWLIST=13.108.0.0/14,204.14.232.0/23
WEBHOOK_TIMESTAMP_TOLERANCE=300000  # 5 minutes

# Monitoring
SECURITY_ALERT_WEBHOOK=https://your-monitoring-service.com/alerts

Salesforce Named Credential

<!-- Named Credential: Portal_BFF -->
<NamedCredential>
    <fullName>Portal_BFF</fullName>
    <endpoint>https://your-portal-api.com</endpoint>
    <principalType>Anonymous</principalType>
    <protocol>HttpsOnly</protocol>
    <generateAuthorizationHeader>false</generateAuthorizationHeader>
</NamedCredential>

<!-- Named Credential: Portal_Webhook (for the secret) -->
<NamedCredential>
    <fullName>Portal_Webhook</fullName>
    <endpoint>https://your-portal-api.com</endpoint>
    <principalType>NamedPrincipal</principalType>
    <namedCredentialType>Legacy</namedCredentialType>
    <password>your_256_bit_secret_key_here</password>
    <username>webhook</username>
</NamedCredential>

4. Customer Experience

Portal UI Polling

The portal should poll for order status updates:

// In your Portal UI
export function useOrderStatus(sfOrderId: string) {
  const [status, setStatus] = useState<OrderStatus>('Pending Review');
  
  useEffect(() => {
    const pollStatus = async () => {
      try {
        const response = await fetch(`/api/orders/${sfOrderId}`);
        const data = await response.json();
        setStatus(data.status);
        
        // Stop polling when order is complete
        if (['Provisioned', 'Failed'].includes(data.status)) {
          clearInterval(interval);
        }
      } catch (error) {
        console.error('Failed to fetch order status:', error);
      }
    };
    
    const interval = setInterval(pollStatus, 5000); // Poll every 5 seconds
    pollStatus(); // Initial fetch
    
    return () => clearInterval(interval);
  }, [sfOrderId]);
  
  return status;
}

5. Monitoring and Alerting

Key Metrics to Monitor

  • Provisioning Success Rate: Track successful vs failed provisioning attempts
  • Provisioning Latency: Time from Quick Action to completion
  • WHMCS API Errors: Monitor WHMCS integration health
  • Webhook Security Events: Failed signature validations, old timestamps

Alert Conditions

// Example monitoring service
@Injectable()
export class OrderProvisioningMonitoringService {
  async recordProvisioningAttempt(sfOrderId: string, success: boolean, duration: number) {
    // Record metrics
    this.metricsService.increment('order.provisioning.attempts', {
      success: success.toString(),
    });
    
    this.metricsService.histogram('order.provisioning.duration', duration);
    
    // Alert on high failure rate
    const recentFailureRate = await this.getRecentFailureRate();
    if (recentFailureRate > 0.1) { // 10% failure rate
      await this.alertingService.sendAlert('High order provisioning failure rate');
    }
  }
}

6. Testing

Security Testing

describe('Order Provisioning Security', () => {
  it('should reject requests without valid HMAC signature', async () => {
    const response = await request(app)
      .post('/orders/test-order-id/provision')
      .send({ orderId: 'test-order-id' })
      .expect(401);
  });

  it('should reject requests with old timestamps', async () => {
    const oldTimestamp = new Date(Date.now() - 10 * 60 * 1000).toISOString();
    const payload = { orderId: 'test-order-id', timestamp: oldTimestamp };
    const signature = generateHmacSignature(JSON.stringify(payload));
    
    const response = await request(app)
      .post('/orders/test-order-id/provision')
      .set('X-SF-Signature', signature)
      .set('X-SF-Timestamp', oldTimestamp)
      .send(payload)
      .expect(401);
  });
});

Summary

This focused approach ensures secure communication specifically for your order provisioning workflow:

  1. Salesforce Quick Action → Secure HTTPS call to Portal BFF
  2. Portal BFF → Processes order, provisions in WHMCS
  3. Optional: Portal sends status updates back to Salesforce
  4. Customer → Sees real-time order status in Portal UI

The security is handled by your existing infrastructure with enhanced webhook signature validation, making it production-ready and secure memory:6689308.