I’ve implemented this exact scenario - D365 intercompany integration with legacy SAP via REST API. Here’s the complete solution addressing all three focus areas:
Root Cause - Multi-Factor Issue:
Your duplicate orders stem from three interconnected problems: legacy ERP retry logic, missing idempotency in your REST API implementation, and race conditions in intercompany order processing.
1. Legacy ERP Integration Patterns:
SAP and other legacy systems typically implement retry mechanisms that D365 integrations must handle gracefully:
- SAP sends orders via RFC/HTTP with 30-second default timeout
- If D365 doesn’t respond within timeout, SAP marks the call as failed and retries
- Your D365 logs show one call because SAP’s retry appears as a new request with identical payload
- The first request might still be processing when the retry arrives
Solution for Legacy System:
- Implement message queue pattern: Have legacy ERP send to Azure Service Bus first
- D365 pulls from queue, processes, and acknowledges (prevents retry)
- Add correlation ID from legacy system to track message lifecycle
- Configure SAP timeout to 60+ seconds to account for D365 processing time
2. REST API Idempotency Implementation:
Your custom X++ service must implement proper idempotency checks:
// Add idempotency check in your X++ service:
public SalesOrder createIntercompanyOrder(OrderPayload _payload)
{
// Check for existing order using legacy reference
SalesTable existingOrder = SalesTable::findByExternalRef(_payload.legacyOrderId);
if (existingOrder.RecId != 0)
return existingOrder; // Return existing, don't create duplicate
}
Key API Implementation Points:
- Add ExternalOrderReference field to SalesTable (if not exists)
- Create unique index on ExternalOrderReference + DataAreaId
- Always check this field before creating new intercompany orders
- Return HTTP 200 with existing order details if duplicate detected (not 409 Conflict)
- Use ttsbegin/ttscommit properly to ensure atomic operations
3. Duplicate Order Prevention in Intercompany Flow:
Intercompany orders in D365 have specific mechanisms to prevent duplicates:
Implement Pessimistic Locking:
ttsbegin;
select forupdate salesTable
where salesTable.ExternalOrderRef == _payload.legacyOrderId;
if (!salesTable.RecId)
{
// Safe to create - we have exclusive lock
salesTable.clear();
salesTable.initValue();
// ... create order
}
ttscommit;
Set Proper Intercompany Fields:
- IntercompanyOriginalSalesId: Legacy system’s order ID
- IntercompanyChain: Link to purchase order in buying legal entity
- IntercompanyAutoCreateOrders: Set to ‘No’ to prevent cascading duplicates
- ExternalItemNumber: Map legacy item codes correctly
Complete Solution Architecture:
-
Message Deduplication Layer:
- Add Azure API Management in front of your D365 REST endpoint
- Configure caching policy with 60-second window using legacy order ID as key
- Returns cached response for duplicate requests within window
-
Database-Level Protection:
- Create unique index: `CREATE UNIQUE INDEX idx_ExtOrderRef ON SalesTable(ExternalOrderReference, DataAreaId)
- This prevents duplicates even if application logic fails
-
Enhanced Logging:
- Log correlation ID from legacy system in every API call
- Track: Request received → Duplicate check → Order creation → Response sent
- Monitor for requests with same correlation ID arriving multiple times
-
Order Reconciliation Fix:
- Build reconciliation report comparing D365 intercompany orders to legacy system
- Use ExternalOrderReference as matching key
- Flag any D365 orders without legacy reference (manual cleanup needed)
- Schedule daily reconciliation batch job to detect discrepancies early
Testing the Solution:
- Send same order payload twice within 10 seconds - should return same order
- Simulate SAP timeout by delaying D365 response - verify retry handling
- Test concurrent API calls with identical payload - verify locking prevents duplicates
- Validate intercompany chain creation doesn’t trigger duplicate purchase orders
After implementing these changes, your duplicate rate should drop to zero. The key is defense in depth: prevent at API layer, database layer, and application logic layer.