# 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: ```typescript // 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 ```apex 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 payload = new Map{ '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 ```typescript // 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 ```typescript // 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 ```bash # 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 ```xml Portal_BFF https://your-portal-api.com Anonymous HttpsOnly false Portal_Webhook https://your-portal-api.com NamedPrincipal Legacy your_256_bit_secret_key_here webhook ``` ## 4. Customer Experience ### Portal UI Polling The portal should poll for order status updates: ```typescript // In your Portal UI export function useOrderStatus(sfOrderId: string) { const [status, setStatus] = useState('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 ```typescript // 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 ```typescript 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]].