# Simple Salesforce-to-Portal Communication Guide ## The Simple Flow (No Reverse Webhooks Needed!) ``` 1. Customer places order → Portal creates Salesforce Order (Pending Review) 2. Salesforce operator → Clicks "Provision in WHMCS" Quick Action 3. Salesforce → Calls Portal BFF → POST /orders/{sfOrderId}/provision 4. Portal BFF → Provisions in WHMCS → DIRECTLY updates Salesforce Order (via existing SF API) 5. Customer → Polls Portal for status updates ``` **Key insight**: You already have Salesforce API access in your Portal BFF, so you can directly update the Order status. No reverse webhooks needed! ## 1. Salesforce Quick Action Security ### Salesforce Apex (Secure Call to Portal) ```apex public class OrderProvisioningService { private static final String WEBHOOK_SECRET = '{!$Credential.Portal_Webhook.Password}'; @future(callout=true) public static void provisionOrder(String orderId) { try { // Simple 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); // Call Portal BFF 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); // Simple response handling if (res.getStatusCode() != 200) { throw new Exception('Portal returned: ' + res.getStatusCode()); } } catch (Exception e) { // Update order status on failure 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 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); } update ord; } } ``` ## 2. Portal BFF Implementation (Simple!) ### Enhanced Security for Provisioning Endpoint ```typescript // apps/bff/src/orders/orders.controller.ts @Post(':sfOrderId/provision') @UseGuards(EnhancedWebhookSignatureGuard) // Your existing guard @ApiOperation({ summary: "Provision order from Salesforce" }) async provisionOrder( @Param('sfOrderId') sfOrderId: string, @Body() payload: { orderId: string; timestamp: string; nonce: string }, @Headers('idempotency-key') idempotencyKey: string ) { return await this.orderOrchestrator.provisionOrder(sfOrderId, payload, idempotencyKey); } ``` ### Order Orchestrator (Direct Salesforce Updates) ```typescript // apps/bff/src/orders/services/order-orchestrator.service.ts @Injectable() export class OrderOrchestrator { constructor( private salesforceService: SalesforceService, // Your existing service private whmcsService: WhmcsService, @Inject(Logger) private logger: Logger ) {} async provisionOrder(sfOrderId: string, payload: any, idempotencyKey: string) { try { // 1. Update SF status to "Activating" await this.updateSalesforceOrderStatus(sfOrderId, 'Activating'); // 2. Your existing provisioning logic const result = await this.provisionInWhmcs(sfOrderId); // 3. Update SF status to "Provisioned" with WHMCS ID await this.updateSalesforceOrderStatus(sfOrderId, 'Provisioned', { whmcsOrderId: result.whmcsOrderId, }); this.logger.log('Order provisioned successfully', { sfOrderId, whmcsOrderId: result.whmcsOrderId, }); return { success: true, status: 'Provisioned', whmcsOrderId: result.whmcsOrderId, }; } catch (error) { // Update SF status to "Failed" await this.updateSalesforceOrderStatus(sfOrderId, 'Failed', { errorCode: 'PROVISIONING_ERROR', errorMessage: error instanceof Error ? error.message : String(error), }); this.logger.error('Order provisioning failed', { sfOrderId, error: error instanceof Error ? error.message : String(error), }); throw error; } } // Simple direct Salesforce update (using your existing SF service) private async updateSalesforceOrderStatus( 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); } // Use your existing Salesforce service to update await this.salesforceService.updateOrder(updateData); this.logger.log('Salesforce order status updated', { sfOrderId, status, }); } catch (error) { this.logger.error('Failed to update Salesforce order status', { sfOrderId, status, error: error instanceof Error ? error.message : String(error), }); // Don't throw - provisioning succeeded, this is just a status update } } } ``` ### Add Update Method to Salesforce Service ```typescript // apps/bff/src/vendors/salesforce/salesforce.service.ts // Add this method to your existing SalesforceService async updateOrder(orderData: { Id: string; [key: string]: any }): Promise { try { const sobject = this.connection.sobject('Order'); await sobject.update(orderData); this.logger.log('Order updated in Salesforce', { orderId: orderData.Id, fields: Object.keys(orderData).filter(k => k !== 'Id'), }); } catch (error) { this.logger.error('Failed to update order in Salesforce', { orderId: orderData.Id, error: error instanceof Error ? error.message : String(error), }); throw error; } } ``` ## 3. Customer UI (Simple Polling) ```typescript // Portal UI - simple polling for order status export function useOrderStatus(sfOrderId: string) { const [orderStatus, setOrderStatus] = useState<{ status: string; whmcsOrderId?: string; error?: string; }>({ status: 'Pending Review' }); useEffect(() => { const pollStatus = async () => { try { const response = await fetch(`/api/orders/${sfOrderId}`); const data = await response.json(); setOrderStatus(data); // Stop polling when 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 orderStatus; } ``` ## 4. Security Configuration ### Environment Variables (Simple) ```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 ``` ### Salesforce Named Credentials ```xml Portal_BFF https://your-portal-api.com Anonymous HttpsOnly Portal_Webhook https://your-portal-api.com NamedPrincipal your_256_bit_secret_key_here webhook ``` ## Summary: Why This is Simple ✅ **No reverse webhooks** - Portal directly updates Salesforce via existing API ✅ **One-way communication** - Salesforce → Portal → Direct SF update ✅ **Uses existing infrastructure** - Your SF service, webhook guards, etc. ✅ **Simple customer experience** - Portal polls for status updates ✅ **Production ready** - HMAC security, idempotency, error handling This follows exactly what your docs specify: Salesforce calls Portal, Portal provisions and updates Salesforce directly. Much cleaner!