2025-09-02 16:09:17 +09:00
# 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
2025-09-04 14:17:54 +09:00
3. Salesforce calls Portal BFF → POST /orders/{sfOrderId}/fulfill
2025-09-02 16:09:17 +09:00
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:
2025-09-04 14:17:54 +09:00
- **Endpoint**: `POST /orders/{sfOrderId}/fulfill`
2025-09-02 16:09:17 +09:00
- **Authentication**: Named Credentials + HMAC signature
- **Security**: IP allowlisting, idempotency keys, signed headers
### Enhanced Security Implementation
Use your existing `EnhancedWebhookSignatureGuard` for the provisioning endpoint:
```typescript
2025-09-04 14:17:54 +09:00
// apps/bff/src/orders/controllers/order-fulfillment.controller.ts
@Post (':sfOrderId/fulfill')
2025-09-02 16:09:17 +09:00
@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" })
2025-09-04 14:17:54 +09:00
async fulfillOrder(
2025-09-02 16:09:17 +09:00
@Param ('sfOrderId') sfOrderId: string,
2025-09-04 14:17:54 +09:00
@Body () payload: { orderId: string; timestamp: string; nonce: string },
2025-09-02 16:09:17 +09:00
@Headers ('idempotency-key') idempotencyKey: string
) {
2025-09-04 14:17:54 +09:00
return await this.orderFulfillmentService.fulfillOrder(sfOrderId, payload, idempotencyKey);
2025-09-02 16:09:17 +09:00
}
```
### 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< 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();
2025-09-04 14:17:54 +09:00
req.setEndpoint('callout:Portal_BFF/orders/' + orderId + '/fulfill');
2025-09-02 16:09:17 +09:00
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
2025-09-04 14:17:54 +09:00
async fulfillOrder(sfOrderId: string, payload: any, idempotencyKey: string) {
2025-09-02 16:09:17 +09:00
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
<!-- 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:
```typescript
// 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
```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)
2025-09-04 14:17:54 +09:00
.post('/orders/test-order-id/fulfill')
2025-09-02 16:09:17 +09:00
.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)
2025-09-04 14:17:54 +09:00
.post('/orders/test-order-id/fulfill')
2025-09-02 16:09:17 +09:00
.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]].