2025-09-02 16:09:17 +09:00
|
|
|
|
# Order Fulfillment - Complete Implementation Guide
|
|
|
|
|
|
|
|
|
|
|
|
*This document provides the complete, up-to-date specification for order creation and fulfillment workflow.*
|
|
|
|
|
|
|
|
|
|
|
|
## 🏗️ Architecture Overview
|
|
|
|
|
|
|
|
|
|
|
|
### System Components
|
|
|
|
|
|
- **Portal Frontend**: Next.js customer interface
|
|
|
|
|
|
- **Portal BFF**: NestJS backend orchestrating all integrations
|
|
|
|
|
|
- **Salesforce**: Order management, catalog, CS review/approval
|
|
|
|
|
|
- **WHMCS**: Billing, payment methods, service provisioning
|
|
|
|
|
|
|
|
|
|
|
|
### Data Flow
|
|
|
|
|
|
```
|
|
|
|
|
|
Customer → Portal → BFF → Salesforce (Order Creation)
|
2025-09-06 10:01:44 +09:00
|
|
|
|
CS Team → Salesforce (Platform Event) → BFF (Subscriber) → WHMCS (Order Fulfillment)
|
2025-09-02 16:09:17 +09:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🛍️ Complete Customer Journey
|
|
|
|
|
|
|
|
|
|
|
|
### Phase 1: Order Creation
|
|
|
|
|
|
|
|
|
|
|
|
#### 1. Customer Signup
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Required fields
|
|
|
|
|
|
{
|
|
|
|
|
|
email: "customer@example.com",
|
|
|
|
|
|
password: "secure_password",
|
|
|
|
|
|
firstName: "John",
|
|
|
|
|
|
lastName: "Doe",
|
|
|
|
|
|
customerNumber: "SF123456" // Salesforce Account Number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Portal creates:
|
|
|
|
|
|
├── WHMCS Client (with Customer Number in custom field)
|
|
|
|
|
|
├── Portal User account
|
|
|
|
|
|
└── Mapping: userId ↔ whmcsClientId ↔ sfAccountId
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 2. Payment Method Setup (Required Gate)
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Portal checks payment method before checkout
|
|
|
|
|
|
GET /billing/payment-methods/summary
|
|
|
|
|
|
Response: { hasPaymentMethod: true/false }
|
|
|
|
|
|
|
|
|
|
|
|
// If false, redirect to WHMCS SSO
|
|
|
|
|
|
POST /auth/sso-link
|
|
|
|
|
|
Response: { ssoUrl: "https://whmcs.com/index.php?rp=/account/paymentmethods&token=..." }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 3. Browse Catalog
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Personalized catalog based on eligibility
|
|
|
|
|
|
GET /catalog/personalized
|
|
|
|
|
|
Headers: { Authorization: "Bearer jwt_token" }
|
|
|
|
|
|
|
|
|
|
|
|
// BFF queries Salesforce
|
|
|
|
|
|
SELECT Id, Name, StockKeepingUnit, WH_Product_ID__c, Billing_Cycle__c
|
|
|
|
|
|
FROM Product2
|
|
|
|
|
|
WHERE Portal_Catalog__c = true
|
|
|
|
|
|
AND Internet_Offering_Type__c = :accountEligibility
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 4. Place Order
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Customer checkout
|
|
|
|
|
|
POST /orders
|
|
|
|
|
|
{
|
|
|
|
|
|
"items": [
|
|
|
|
|
|
{ "sku": "INTERNET-GOLD-APT-1G", "quantity": 1 },
|
|
|
|
|
|
{ "sku": "INTERNET-INSTALL-SINGLE", "quantity": 1 },
|
|
|
|
|
|
{ "sku": "INTERNET-ADDON-HOME-PHONE", "quantity": 1 }
|
|
|
|
|
|
],
|
|
|
|
|
|
"activationType": "Scheduled",
|
|
|
|
|
|
"activationScheduledAt": "2024-01-20T09:00:00Z"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BFF creates in Salesforce:
|
|
|
|
|
|
Order {
|
|
|
|
|
|
AccountId: "001xx000004TmiQAAS",
|
|
|
|
|
|
Status: "Pending Review",
|
|
|
|
|
|
Order_Type__c: "Internet",
|
|
|
|
|
|
Activation_Type__c: "Scheduled",
|
|
|
|
|
|
Activation_Scheduled_At__c: "2024-01-20T09:00:00Z"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
OrderItems [
|
|
|
|
|
|
{ Product2.SKU: "INTERNET-GOLD-APT-1G", Quantity: 1 },
|
|
|
|
|
|
{ Product2.SKU: "INTERNET-INSTALL-SINGLE", Quantity: 1 },
|
|
|
|
|
|
{ Product2.SKU: "INTERNET-ADDON-HOME-PHONE", Quantity: 1 }
|
|
|
|
|
|
]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Phase 2: CS Review & Approval
|
|
|
|
|
|
|
|
|
|
|
|
#### 5. Order Review (Salesforce)
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- CS Team views order with all details
|
|
|
|
|
|
SELECT Id, OrderNumber, Status, TotalAmount, Account.Name,
|
|
|
|
|
|
Order_Type__c, Activation_Type__c, Activation_Scheduled_At__c,
|
|
|
|
|
|
(SELECT Product2.Name, Product2.WH_Product_ID__c, Quantity, UnitPrice
|
|
|
|
|
|
FROM OrderItems)
|
|
|
|
|
|
FROM Order
|
|
|
|
|
|
WHERE Id = '8014x000000ABCDXYZ'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2025-09-06 10:01:44 +09:00
|
|
|
|
#### 6. Provision Trigger (Platform Events)
|
|
|
|
|
|
```text
|
|
|
|
|
|
Salesforce Record‑Triggered Flow publishes Platform Event: OrderProvisionRequested__e
|
|
|
|
|
|
Fields:
|
|
|
|
|
|
- OrderId__c (Text 18)
|
|
|
|
|
|
- IdemKey__c (Text 80, optional)
|
2025-09-02 16:09:17 +09:00
|
|
|
|
```
|
2025-09-06 10:01:44 +09:00
|
|
|
|
The portal subscribes to this event, enqueues a job, and performs provisioning.
|
2025-09-02 16:09:17 +09:00
|
|
|
|
|
|
|
|
|
|
### Phase 3: Order Fulfillment
|
|
|
|
|
|
|
|
|
|
|
|
#### 7. Order Fulfillment Service (Modular Architecture)
|
|
|
|
|
|
|
|
|
|
|
|
##### OrderFulfillmentValidator
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
class OrderFulfillmentValidator {
|
|
|
|
|
|
async validateFulfillmentRequest(sfOrderId: string, idempotencyKey: string) {
|
|
|
|
|
|
// 1. Validate Salesforce order exists
|
|
|
|
|
|
const sfOrder = await this.salesforceService.getOrder(sfOrderId);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Check idempotency (already provisioned?)
|
|
|
|
|
|
if (sfOrder.WHMCS_Order_ID__c) {
|
|
|
|
|
|
return { isAlreadyProvisioned: true, whmcsOrderId: sfOrder.WHMCS_Order_ID__c };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Get WHMCS client ID from mapping
|
|
|
|
|
|
const clientId = await this.mappingsService.findBySfAccountId(sfOrder.Account.Id);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Validate payment method exists
|
|
|
|
|
|
const hasPaymentMethod = await this.whmcsOrderService.hasPaymentMethod(clientId);
|
|
|
|
|
|
if (!hasPaymentMethod) {
|
|
|
|
|
|
throw new ConflictException('Payment method missing - client must add payment method before fulfillment');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { sfOrder, clientId, isAlreadyProvisioned: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
##### OrderWhmcsMapper
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
class OrderWhmcsMapper {
|
|
|
|
|
|
mapOrderItemsToWhmcs(orderItems: any[]): WhmcsOrderItem[] {
|
|
|
|
|
|
return orderItems.map(item => ({
|
|
|
|
|
|
productId: item.product.whmcsProductId, // From WH_Product_ID__c
|
|
|
|
|
|
billingCycle: item.product.billingCycle.toLowerCase(), // From Billing_Cycle__c
|
|
|
|
|
|
quantity: item.quantity
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
##### OrderFulfillmentOrchestrator
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
class OrderFulfillmentOrchestrator {
|
|
|
|
|
|
async executeFulfillment(sfOrderId: string, payload: any, idempotencyKey: string) {
|
|
|
|
|
|
const context = { sfOrderId, idempotencyKey, steps: [] };
|
|
|
|
|
|
|
|
|
|
|
|
// Step 1: Validate request
|
|
|
|
|
|
context.validation = await this.validator.validateFulfillmentRequest(sfOrderId, idempotencyKey);
|
|
|
|
|
|
|
|
|
|
|
|
if (context.validation.isAlreadyProvisioned) {
|
|
|
|
|
|
return { success: true, status: 'Already Fulfilled' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Step 2: Update SF status to "Activating"
|
|
|
|
|
|
await this.salesforceService.updateOrder({
|
|
|
|
|
|
Id: sfOrderId,
|
|
|
|
|
|
Status: 'Activating',
|
|
|
|
|
|
Provisioning_Status__c: 'In Progress'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Step 3: Get full order details
|
|
|
|
|
|
context.orderDetails = await this.orderOrchestrator.getOrder(sfOrderId);
|
|
|
|
|
|
|
|
|
|
|
|
// Step 4: Map to WHMCS format
|
|
|
|
|
|
context.mappingResult = await this.mapper.mapOrderItemsToWhmcs(context.orderDetails.items);
|
|
|
|
|
|
|
|
|
|
|
|
// Step 5: Create WHMCS order
|
|
|
|
|
|
context.whmcsResult = await this.whmcsOrderService.addOrder({
|
|
|
|
|
|
clientId: context.validation.clientId,
|
|
|
|
|
|
items: context.mappingResult.whmcsItems,
|
|
|
|
|
|
paymentMethod: "mailin",
|
|
|
|
|
|
noinvoice: true,
|
|
|
|
|
|
noemail: true
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Step 6: Accept/provision WHMCS order
|
|
|
|
|
|
await this.whmcsOrderService.acceptOrder(context.whmcsResult.orderId);
|
|
|
|
|
|
|
|
|
|
|
|
// Step 7: Update SF with success
|
|
|
|
|
|
await this.salesforceService.updateOrder({
|
|
|
|
|
|
Id: sfOrderId,
|
|
|
|
|
|
Status: 'Activated',
|
|
|
|
|
|
Provisioning_Status__c: 'Fulfilled',
|
|
|
|
|
|
WHMCS_Order_ID__c: context.whmcsResult.orderId
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return { success: true, status: 'Fulfilled', whmcsOrderId: context.whmcsResult.orderId };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📊 Complete Data Mapping Reference
|
|
|
|
|
|
|
|
|
|
|
|
### Salesforce to WHMCS Mapping
|
|
|
|
|
|
|
|
|
|
|
|
#### Order Header Mapping
|
|
|
|
|
|
| Source | Target | Example | Notes |
|
|
|
|
|
|
|--------|--------|---------|-------|
|
|
|
|
|
|
| `Order.AccountId` | Resolved to `clientid` | `1` | Via portal mapping table |
|
|
|
|
|
|
| `Order.Id` | Added to order notes | `sfOrderId=8014x000000ABCDXYZ` | For tracking |
|
|
|
|
|
|
| N/A | `paymentmethod` | `"mailin"` | Required by WHMCS API |
|
|
|
|
|
|
| N/A | `noinvoice` | `true` | Don't create invoice during provisioning |
|
|
|
|
|
|
| N/A | `noemail` | `true` | Don't send emails during provisioning |
|
|
|
|
|
|
|
|
|
|
|
|
#### OrderItem Array Mapping
|
|
|
|
|
|
| Salesforce Field | WHMCS Parameter | Example Value | Format |
|
|
|
|
|
|
|------------------|-----------------|---------------|--------|
|
|
|
|
|
|
| `Product2.WH_Product_ID__c` | `pid[]` | `["185", "242", "246"]` | String array |
|
|
|
|
|
|
| `Product2.Billing_Cycle__c` | `billingcycle[]` | `["monthly", "onetime", "monthly"]` | String array |
|
|
|
|
|
|
| `OrderItem.Quantity` | `qty[]` | `[1, 1, 1]` | Number array |
|
|
|
|
|
|
|
|
|
|
|
|
#### Product ID Mapping Examples
|
|
|
|
|
|
| Product Name | Salesforce SKU | WH_Product_ID__c | WHMCS pid | Billing Cycle |
|
|
|
|
|
|
|--------------|----------------|------------------|-----------|---------------|
|
|
|
|
|
|
| Internet Gold (Apartment 1G) | `INTERNET-GOLD-APT-1G` | 185 | "185" | "monthly" |
|
|
|
|
|
|
| Single Installation | `INTERNET-INSTALL-SINGLE` | 242 | "242" | "onetime" |
|
|
|
|
|
|
| Hikari Denwa Service | `INTERNET-ADDON-HOME-PHONE` | 246 | "246" | "monthly" |
|
|
|
|
|
|
| Hikari Denwa Installation | `INTERNET-ADDON-DENWA-INSTALL` | 247 | "247" | "onetime" |
|
|
|
|
|
|
| Weekend Installation Fee | `INTERNET-INSTALL-WEEKEND` | 245 | "245" | "onetime" |
|
|
|
|
|
|
|
|
|
|
|
|
### WHMCS API Request/Response Format
|
|
|
|
|
|
|
|
|
|
|
|
#### AddOrder Request
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"action": "AddOrder",
|
|
|
|
|
|
"clientid": 1,
|
|
|
|
|
|
"paymentmethod": "mailin",
|
|
|
|
|
|
"pid": ["185", "242", "246", "247"],
|
|
|
|
|
|
"billingcycle": ["monthly", "onetime", "monthly", "onetime"],
|
|
|
|
|
|
"qty": [1, 1, 1, 1],
|
|
|
|
|
|
"noinvoice": true,
|
|
|
|
|
|
"noemail": true,
|
|
|
|
|
|
"promocode": "",
|
|
|
|
|
|
"configoptions": ["", "", "", ""],
|
|
|
|
|
|
"customfields": ["", "", "", ""]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### AddOrder Response
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"result": "success",
|
|
|
|
|
|
"orderid": 12345,
|
|
|
|
|
|
"serviceids": "67890,67891,67892,67893",
|
|
|
|
|
|
"addonids": "",
|
|
|
|
|
|
"domainids": "",
|
|
|
|
|
|
"invoiceid": 0
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### AcceptOrder Request
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"action": "AcceptOrder",
|
|
|
|
|
|
"orderid": 12345
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### AcceptOrder Response
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"result": "success"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Status Update Mapping
|
|
|
|
|
|
|
|
|
|
|
|
#### Success Flow
|
|
|
|
|
|
| Step | Salesforce Order.Status | Provisioning_Status__c | WHMCS_Order_ID__c |
|
|
|
|
|
|
|------|------------------------|------------------------|-------------------|
|
|
|
|
|
|
| Initial | "Pending Review" | null | null |
|
|
|
|
|
|
| CS Approval | "Activating" | "In Progress" | null |
|
|
|
|
|
|
| WHMCS Created | "Activating" | "In Progress" | "12345" |
|
|
|
|
|
|
| Services Provisioned | "Activated" | "Fulfilled" | "12345" |
|
|
|
|
|
|
|
|
|
|
|
|
#### Failure Flow
|
|
|
|
|
|
| Step | Salesforce Order.Status | Provisioning_Status__c | Error Fields |
|
|
|
|
|
|
|------|------------------------|------------------------|--------------|
|
|
|
|
|
|
| Initial | "Pending Review" | null | null |
|
|
|
|
|
|
| CS Approval | "Activating" | "In Progress" | null |
|
|
|
|
|
|
| Failure | "Draft" | "Failed" | Error_Code__c, Error_Message__c |
|
|
|
|
|
|
|
|
|
|
|
|
## 🔒 Security Implementation
|
|
|
|
|
|
|
2025-09-06 10:01:44 +09:00
|
|
|
|
- No inbound Salesforce webhooks are used; provisioning is triggered via Platform Events.
|
|
|
|
|
|
- Portal authenticates to Salesforce via JWT (Connected App) and requires Platform Event permissions.
|
2025-09-02 16:09:17 +09:00
|
|
|
|
|
|
|
|
|
|
### Error Codes
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
enum FulfillmentErrorCode {
|
|
|
|
|
|
PAYMENT_METHOD_MISSING = "PAYMENT_METHOD_MISSING",
|
|
|
|
|
|
ORDER_NOT_FOUND = "ORDER_NOT_FOUND",
|
|
|
|
|
|
WHMCS_ERROR = "WHMCS_ERROR",
|
|
|
|
|
|
MAPPING_ERROR = "MAPPING_ERROR",
|
|
|
|
|
|
FULFILLMENT_ERROR = "FULFILLMENT_ERROR"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## ⚡ Performance Metrics
|
|
|
|
|
|
|
|
|
|
|
|
### Typical Timeline
|
|
|
|
|
|
```
|
2025-09-06 10:01:44 +09:00
|
|
|
|
10:30:00.000 - CS approves Order
|
|
|
|
|
|
10:30:00.050 - Platform Event published (OrderProvisionRequested__e)
|
|
|
|
|
|
10:30:00.080 - BFF subscriber enqueues provisioning job
|
2025-09-02 16:09:17 +09:00
|
|
|
|
10:30:00.200 - Salesforce order updated to "Activating"
|
|
|
|
|
|
10:30:00.500 - Order details retrieved and mapped
|
|
|
|
|
|
10:30:01.000 - WHMCS AddOrder API call
|
|
|
|
|
|
10:30:01.500 - WHMCS AcceptOrder API call
|
|
|
|
|
|
10:30:02.000 - Services provisioned in WHMCS
|
|
|
|
|
|
10:30:02.200 - Salesforce updated to "Activated"
|
|
|
|
|
|
|
2025-09-06 10:01:44 +09:00
|
|
|
|
Total fulfillment time: ~2.2 seconds (asynchronous trigger) ⚡
|
2025-09-02 16:09:17 +09:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### API Call Performance
|
|
|
|
|
|
- **Salesforce getOrder**: ~200ms
|
|
|
|
|
|
- **WHMCS AddOrder**: ~400ms
|
|
|
|
|
|
- **WHMCS AcceptOrder**: ~300ms
|
|
|
|
|
|
- **Salesforce updateOrder**: ~150ms
|
|
|
|
|
|
|
|
|
|
|
|
## 🔧 Configuration Requirements
|
|
|
|
|
|
|
|
|
|
|
|
### Salesforce Setup
|
|
|
|
|
|
```
|
2025-09-06 10:01:44 +09:00
|
|
|
|
1) Platform Event: OrderProvisionRequested__e (fields: OrderId__c [Text 18], IdemKey__c [Text 80, optional])
|
|
|
|
|
|
2) Permission Set: grant Platform Event permissions and PE object read to the portal integration user
|
|
|
|
|
|
3) Flow (Record‑Triggered): On Order Status = Approved → Create OrderProvisionRequested__e with OrderId__c
|
2025-09-02 16:09:17 +09:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Environment Variables
|
|
|
|
|
|
```bash
|
2025-09-06 10:01:44 +09:00
|
|
|
|
# BFF Configuration (Salesforce Platform Events)
|
|
|
|
|
|
SF_EVENTS_ENABLED=true
|
|
|
|
|
|
SF_PROVISION_EVENT_CHANNEL=/event/OrderProvisionRequested__e
|
|
|
|
|
|
SF_EVENTS_REPLAY=LATEST
|
|
|
|
|
|
|
|
|
|
|
|
# WHMCS API
|
2025-09-02 16:09:17 +09:00
|
|
|
|
WHMCS_API_IDENTIFIER=your_whmcs_api_id
|
|
|
|
|
|
WHMCS_API_SECRET=your_whmcs_api_secret
|
|
|
|
|
|
WHMCS_API_URL=https://your-whmcs.com/includes/api.php
|
|
|
|
|
|
|
|
|
|
|
|
# Database
|
|
|
|
|
|
DATABASE_URL=postgresql://user:pass@host:5432/portal
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
This comprehensive guide ensures consistent implementation across all teams and provides the complete picture of the order fulfillment workflow.
|