Introduction
In the landscape of B2B e-commerce, the integration between a Content Management System (CMS) and an Enterprise Resource Planning (ERP) system is a complex yet vital endeavor. This article shares practical experiences from integrating SAP ERP into the Optimizely Commerce (formerly Episerver) platform for a dental equipment e-commerce project. In this industry, precision in pricing, taxation, and order management is not just a feature—it is a survival requirement.
When dealing with enterprise-scale B2B clients, we often encounter legacy systems that rely on established protocols. This guide explores how to bridge the gap between modern .NET architectures and SAP's robust but rigid backend structures, ensuring a seamless experience for both the end-user and the administrative staff.
1. Architectural Overview
1.1 The High-Level Picture
The integration architecture is designed following a layered architecture model. This ensures a clean separation of concerns, making the system easier to maintain, test, and scale. The flow moves from the Episerver Frontend through a dedicated SAP Service Layer before communicating with the SAP ERP via SOAP/HTTPS.
The primary layers include:
- Frontend Layer: Handles user interactions such as the Shopping Cart and Checkout pages.
- Application/Service Layer: Contains business logic, MediatR handlers, and data transformation services.
- Integration Layer: The direct communication point with SAP using generated SOAP clients.
- External Layer: SAP ERP (ECC or S/4HANA) providing the actual business data.
1.2 Core Components and Responsibilities
To maintain a robust system, we categorized our components into specific roles as shown in the table below:
| Component | Role | Key Files/Namespaces |
|---|---|---|
| SAP Module | Direct communication with SAP via SOAP protocols. | SAP/OrderService.cs, SAP/CustomerService.cs |
| Checkout Services | Mediator layer converting Commerce objects to SAP-ready DTOs. | Features/Checkout/Services/SAPOrderService.cs |
| Event Handlers | Handles post-order placement events using the CQRS pattern. | Features/Checkout/Events/SendOrderToSAP.cs |
| Domain Models | Shared data structures for orders, addresses, and items. | SAP/Domain/SAPLineItem.cs, SAPAddress.cs |
| Error Handling | Managing SAP return codes and admin notifications. | SAPError.cs, SendEmailToSAPAdmins.cs |
2. Communication with SAP via SOAP Web Services
2.1 Why SOAP?
Traditional SAP ERP systems (specifically ECC) utilize BAPI (Business Application Programming Interface). These BAPIs are often exposed as SOAP Web Services using the SAP NetWeaver platform. While REST is the modern standard, SOAP remains the most common and stable integration path for SAP ECC environments due to its strict contract-first approach (WSDL).
In our dental project, we utilized two primary SOAP endpoints:
- SAPOrderService: Handles all order-related operations including Simulation (pricing/tax), Order Creation, and Order History.
- SAPCustomerService: Manages customer master data, including creation, address synchronization, and credit limit verification.
2.2 Connection Configuration
The connection to SAP is established via a BasicHttpBinding. Given the sensitive nature of B2B transactions, we used Transport security (HTTPS) combined with Basic Authentication.
private serviceSoapClient GetSapOrderSoapClient()
{
var binding = new BasicHttpBinding
{
Security =
{
Mode = BasicHttpSecurityMode.Transport,
Transport = { ClientCredentialType = HttpClientCredentialType.Basic }
},
MaxReceivedMessageSize = 20000000, // 20MB for large responses
MaxBufferSize = 20000000,
SendTimeout = TimeSpan.FromSeconds(90),
Name = "serviceSoap"
};
var address = new EndpointAddress(_sapOrderEndpoint);
var orderClient = new serviceSoapClient(binding, address);
var credentials = orderClient.ClientCredentials;
credentials.UserName.UserName = _sapUsername;
credentials.UserName.Password = _sapPassword;
return orderClient;
}
Critical Configuration Notes:
- Buffer Size: We configured the buffer up to 20MB. SAP responses for customer synchronization or large order histories can be surprisingly massive.
- Timeout: A 90-second timeout was necessary. SAP ERP processing, especially when calculating complex tax rules for dental equipment across different regions, can take significant time.
- Secret Management: Credentials should be stored in
appsettings.jsonand injected viaIConfiguration, preferably utilizing Azure Key Vault for production environments.
3. Real-Time Order Management
3.1 Order Simulation: Precision Pricing
In B2B, the price listed in the CMS is often just a "list price." The actual price a customer pays depends on their specific contract, volume discounts, and regional taxes stored in SAP. Order Simulation allows the frontend to send the current cart to SAP to receive "Final" pricing without actually creating a permanent record.
When a customer reaches the checkout summary, the system triggers SimulateOrder(address, lineItems). This ensures the shipmentTotal and taxTotal are 100% accurate according to SAP's internal logic.
// Converting Episerver Line Items to SAP Format
private List<SAPLineItem> ConvertToSAPLineItems(IEnumerable<ILineItem> lineItems)
{
var sapLineItems = new List<SAPLineItem>();
var index = 10; // SAP convention: items start at 10, increment by 10
foreach (var lineItem in lineItems)
{
var variationCode = lineItem.Code;
// Strip prefix if necessary
var sapCode = variationCode.StartsWith("_") ? variationCode.Substring(1) : variationCode;
sapLineItems.Add(new SAPLineItem
{
ItemNum = index.ToString(),
VariantCode = sapCode,
OrderedQty = lineItem.Quantity,
WebPrice = lineItem.PlacedPrice,
IsProduct = !lineItem.Properties[Constants.CommerceFields.IsService].ToBool()
});
index += 10;
}
return sapLineItems;
}
3.2 Order Creation and Resilience
Once the customer clicks "Place Order," the system moves from simulation to creation. We utilize the MediatR library to handle this as a decoupled event. If SAP is temporarily down, the system should not crash; it should gracefully handle the failure.
public async Task Handle(OrderPlaced notification, CancellationToken cancellationToken)
{
var sapConfiguration = _settingsProvider.GetSapConfiguration();
// Check if real-time SAP integration is enabled for this market
if (!sapConfiguration.SapRealTimeOrder)
{
await _mediator.Publish(new StaffOrderNotification(notification.PurchaseOrder));
return;
}
var errors = _sapOrderService.CreateOrder(..., out sapOrderNumber);
if (errors.Any())
{
// Mark order for manual exchange and notify admins
notification.PurchaseOrder.Status = OrderStatus.AwaitingExchange.ToString();
await _emailToSapAdmins.SendEmailToAdmins(errors, ...);
}
else
{
// Success: Store the SAP Reference ID
notification.PurchaseOrder[Constants.CommerceFields.SAPOrderNumber] = sapOrderNumber;
await _mediator.Send(new SaveOrder(notification.PurchaseOrder));
}
}
4. Bidirectional Customer Synchronization
Customer data in a B2B environment usually originates in SAP (the "Source of Truth"). However, users update their profiles in Optimizely. Keeping these in sync is a major challenge.
4.1 Intelligent Address Matching
When syncing addresses from SAP back to Episerver, we use a two-pass matching logic to prevent duplicate address entries:
- Exact Match: Matches based on the
SAPAddressNumber(the unique ID from SAP). - Fuzzy Match: If the SAP number is missing, we match by a combination of
City + PostalCode + StreetAddress.
This ensures that if a user creates an address on the web that already exists in SAP, the system links them rather than creating a duplicate.
4.2 Identity Management with Azure AD B2C
When a new customer registers, we don't just create a record in Episerver and SAP. We also update Azure AD B2C. Storing the SAPCustomerNumber as a custom attribute in B2C allows for seamless authentication and ensures that the user's identity is consistent across all enterprise applications.
5. Strategic Insights & Troubleshooting
5.1 Handling SAP Return Messages
SAP does not use HTTP status codes for business logic errors. Instead, it returns a TYPE field in its response:
- Type "S": Success.
- Type "W": Warning (the order went through, but there is something to note).
- Type "E": Error (the order failed).
One specific hurdle we faced was Warning Message #219 (ID: V4). SAP often returns this warning during simulation, which the standard WCF client might interpret as a failure if not handled specifically in code.
5.2 Troubleshooting SOAP Timeouts
If you experience frequent TaskCanceledException or TimeoutException, consider the following:
- Implement a Retry Policy: Use Polly to implement exponential backoff.
- Circuit Breaker: If SAP is down for maintenance, use a Circuit Breaker to stop sending requests immediately, preventing the web server's thread pool from being exhausted.
- Log the XML: SAP integration issues are nearly impossible to debug without the raw SOAP XML. Use a custom
IClientMessageInspectorto log every request and response during development.
6. Conclusion
Integrating SAP ERP with Optimizely Commerce is a journey that requires deep understanding of both modern web practices and legacy enterprise logic. By implementing a layered architecture, utilizing MediatR for event-driven processing, and handling data synchronization with robust matching logic, you can build a system that is both resilient and scalable.
The key takeaway from our project in the dental industry was: Never trust a response without validation. Whether it's a shipping fee calculation or a tax amount, always implement safety checks to ensure the data flowing into your CMS is logical and consistent.