Assist_Design/docs/SALESFORCE-PORTAL-SIMPLE-GUIDE.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

301 lines
9.6 KiB
Markdown

# 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<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);
// 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<void> {
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
<!-- For API calls -->
<NamedCredential>
<fullName>Portal_BFF</fullName>
<endpoint>https://your-portal-api.com</endpoint>
<principalType>Anonymous</principalType>
<protocol>HttpsOnly</protocol>
</NamedCredential>
<!-- For webhook secret -->
<NamedCredential>
<fullName>Portal_Webhook</fullName>
<endpoint>https://your-portal-api.com</endpoint>
<principalType>NamedPrincipal</principalType>
<password>your_256_bit_secret_key_here</password>
<username>webhook</username>
</NamedCredential>
```
## 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!