SfSkills Domains Docs GitHub

Home / architect / commerce-integration-patterns

commerce-integration-patterns

Use this skill when designing or diagnosing end-to-end integration between Salesforce B2B or D2C Commerce and external systems — including ERP pricing/inventory, PIM product sync, payment gateways, shipping providers, tax engines, and order management systems (OMS).

Open the skill folder on GitHub


Commerce Integration Patterns

This skill activates when an architect or developer needs to design or diagnose integration between Salesforce B2B or D2C Commerce and external systems. It covers the CartExtension Apex namespace that powers real-time pricing, shipping, inventory, and tax lookups during checkout, the payment gateway architecture pattern that keeps raw card data off Salesforce servers, and the Product2 External ID strategy for safe PIM synchronization.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

CartExtension Apex Namespace and the Calculator Pipeline

The CartExtension Apex namespace is the primary integration surface for commerce checkout calculations. It provides abstract base classes that a developer extends to inject external pricing, shipping, inventory, or tax logic into the cart calculation pipeline:

Base ClassExtension Point Name (EPN)Purpose
CartExtension.PricingCartCalculatorCartExtension__PricingOverride or supplement storefront price book pricing with ERP prices
CartExtension.ShippingCartCalculatorCartExtension__ShippingFetch real-time shipping rates from a carrier API
CartExtension.InventoryCartCalculatorCartExtension__InventoryCheck real-time stock availability from an ERP or WMS
CartExtension.TaxCartCalculatorCartExtension__TaxesCalculate tax using an external tax engine (e.g., Avalara, Vertex)

Each calculator class overrides a calculate(CartExtension.CartCalculateCalculatorRequest request) method. The platform invokes registered calculators in sequence during the cart calculation pipeline (triggered on cart page load, quantity changes, and checkout progression).

RegisteredExternalService Custom Metadata

Connecting a CartExtension class to a store requires a RegisteredExternalService custom metadata record. This record ties a specific Apex class to an Extension Point Name (EPN) and a specific store. Critical constraint: only one class may be registered per EPN per store. Attempting to register a second class for the same EPN and store results in the second registration silently not being invoked. Always audit existing registrations before scaffolding a new calculator class.

The metadata record fields are:

Callout Phase Constraint: Before vs After Hooks

CartExtension calculators expose two lifecycle hooks: calculate() (before-phase) and a post-calculation hook. HTTP callouts to external systems are only permitted within the calculate() method (before-phase). If callout code executes in the after-phase hook, Salesforce throws System.CalloutException: You have uncommitted work pending. at runtime. This is one of the most common production incidents in Commerce integrations.

The pattern for safe callout placement:

  1. Perform all external callouts in calculate() before any DML or SOQL that creates uncommitted work.
  2. Store results in local variables within the method.
  3. Write back to cart line items or delivery group records after callouts are complete and all HTTP responses are resolved.

Payment Gateway Architecture

Salesforce Commerce does not accept raw card data. The payment flow is:

  1. The storefront renders a provider-hosted iframe or redirect (owned by the payment gateway) that captures card details client-side.
  2. The provider returns an opaque token or nonce.
  3. The Commerce checkout passes the token to an Apex class — either implementing sfdc_checkout.CartPaymentAuthorize (legacy Aura B2B) or CommercePayments.PaymentGatewayAdapter (modern LWR stores) — which uses the token to call the gateway API.
  4. Raw card data (PAN, CVV, expiry) never transits Salesforce servers at any point in this flow.

For modern LWR-based B2B and D2C stores, the full payment adapter implementation is covered by the apex/commerce-payment-integration skill. This skill covers the gateway selection decision and the SOM integration handoff.

PIM Sync to Product2: No Native Connector

Salesforce Commerce has no out-of-the-box connector to an external Product Information Management (PIM) system. Product catalog data lives in the Product2 standard object. The canonical pattern for safe PIM sync is:

  1. Define External IDs on Product2 — add a custom External ID field (e.g., PIM_Product_Id__c, type Text, marked as External ID and Unique) that holds the PIM system's canonical product identifier.
  2. Upsert using External ID — when syncing from the PIM (via Apex, integration middleware, or Data Loader), use upsert Product2 PIM_Product_Id__c in Apex or the Upsert DML operation with the external ID field specified. This prevents duplicate Product2 records when the same product is synced multiple times.
  3. Sync related objects — ProductCatalog, ProductCategory, ProductCategoryProduct, and PricebookEntry records must be maintained in sync alongside Product2. A partial sync that updates Product2 but not price book entries will result in stale storefront prices.

Common Patterns

Pattern: ERP Real-Time Pricing via PricingCartCalculator

When to use: The store must display ERP contract prices or customer-specific pricing during checkout rather than Salesforce price book prices.

How it works:

public class ErpPricingCalculator extends CartExtension.PricingCartCalculator {

    public override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
        CartExtension.Cart cart = request.getCart();
        CartExtension.CartItemCollection items = cart.getCartItems();

        // Build the list of SKUs to look up
        List<String> skus = new List<String>();
        for (Integer i = 0; i < items.size(); i++) {
            skus.add(items.get(i).getSku());
        }

        // Callout is safe here — before any DML, inside calculate()
        Map<String, Decimal> erpPrices = ErpPricingService.getPrices(
            skus, cart.getAccountId());

        // Write prices back — after callout, before method exit
        for (Integer i = 0; i < items.size(); i++) {
            CartExtension.CartItem item = items.get(i);
            if (erpPrices.containsKey(item.getSku())) {
                item.setSalesPrice(erpPrices.get(item.getSku()));
            }
        }
    }
}

Why not the alternative: Using a Flow-invoked Apex action or a trigger on CartItem to fetch ERP prices introduces uncommitted-work DML before the callout window, causing System.CalloutException in many checkout sequences.

Pattern: Shipping Rate Lookup via ShippingCartCalculator

When to use: The store must display real carrier rates (FedEx, UPS, DHL) rather than flat-rate or free-shipping options configured in Setup.

How it works:

public class CarrierShippingCalculator extends CartExtension.ShippingCartCalculator {

    public override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
        CartExtension.Cart cart = request.getCart();
        CartExtension.CartDeliveryGroupCollection deliveryGroups =
            cart.getCartDeliveryGroups();

        for (Integer i = 0; i < deliveryGroups.size(); i++) {
            CartExtension.CartDeliveryGroup group = deliveryGroups.get(i);

            // Callout is safe here — within calculate(), before DML
            List<CarrierRate> rates = CarrierApiService.getRates(
                group.getDeliverToAddress(),
                cart.getCartItems());

            // Populate shipping options on the delivery group
            CartExtension.CartDeliveryGroupMethodCollection methods =
                group.getCartDeliveryGroupMethods();
            methods.clear();
            for (CarrierRate rate : rates) {
                CartExtension.CartDeliveryGroupMethod method =
                    new CartExtension.CartDeliveryGroupMethod(
                        rate.name, rate.price, rate.carrier);
                methods.add(method);
            }
        }
    }
}

Why not the alternative: Storing shipping rates in a custom object and querying it from checkout bypasses real-time carrier accuracy and requires a separate batch job to keep rates current — unworkable for weight- and zone-based pricing.

Pattern: PIM Upsert Using External ID

When to use: A PIM system is the master for product data and must push updates into Salesforce Commerce without creating duplicates.

How it works:

// In a batch job or Platform Event handler receiving PIM delta feeds
List<Product2> toUpsert = new List<Product2>();
for (PimProductDto dto : incomingProducts) {
    Product2 p = new Product2(
        PIM_Product_Id__c = dto.pimId,   // External ID field
        Name              = dto.name,
        Description       = dto.description,
        StockKeepingUnit  = dto.sku,
        IsActive          = dto.active
    );
    toUpsert.add(p);
}
// Upsert on the External ID — inserts new, updates existing, no duplicates
Database.upsert(toUpsert, Product2.PIM_Product_Id__c, false);

Why not the alternative: Querying by SKU then deciding to insert or update in a trigger context introduces race conditions under high-volume parallel syncs and produces duplicate records when the query misses an in-flight insert.


Decision Guidance

SituationRecommended ApproachReason
Real-time ERP pricing during checkoutCartExtension.PricingCartCalculator with callout in calculate()Only safe callout window in the cart pipeline
Real-time carrier shipping ratesCartExtension.ShippingCartCalculator with callout in calculate()Same pipeline; one class per store per EPN
Real-time inventory during cart/checkoutCartExtension.InventoryCartCalculator with callout in calculate()Prevents oversell; same EPN constraint applies
External tax engine (Avalara, Vertex)CartExtension.TaxCartCalculator or tax provider AppExchange packageCustom calculator if no package available
PIM product syncDatabase.upsert with External ID on Product2No native PIM connector; External ID prevents duplicates
Payment gateway (modern LWR store)CommercePayments.PaymentGatewayAdapter (see apex/commerce-payment-integration)Correct interface for LWR-based B2B/D2C stores
Payment gateway (legacy Aura B2B store)sfdc_checkout.CartPaymentAuthorize (see admin/commerce-checkout-configuration)Legacy interface — different extension model
Post-order capture/refund orchestrationSalesforce Order Management (SOM) if licensed; else custom OMS calloutSOM provides pre-built fulfillment workflow nodes
Batch product/price sync with no real-time requirementScheduled Apex or Data Loader upsert via External IDSimpler and more reliable than event-driven sync for large catalogs

Step-by-step instructions for an AI agent or practitioner working on this task:

  1. Map external systems to extension points — For each external system in scope (ERP, PIM, payment, shipping, tax, OMS), identify the correct Commerce extension point. Confirm which systems require synchronous real-time callouts during checkout vs. batch or async sync. Document the mapping before writing any code.
  2. Audit existing RegisteredExternalService registrations — Query RegisteredExternalService custom metadata for the target store and confirm which EPNs already have registered classes. There is exactly one slot per EPN per store — do not scaffold a new class until you know whether you are replacing or extending existing logic.
  3. Scaffold CartExtension calculator classes — For each EPN requiring a custom class, extend the appropriate base class (PricingCartCalculator, ShippingCartCalculator, InventoryCartCalculator, TaxCartCalculator). Stub the calculate() method to return safely before adding callout logic. Confirm the class compiles and the stub version can be registered without breaking checkout.
  4. Implement callouts within the before-phase only — Add HTTP callout logic exclusively inside the calculate() method, before any DML or platform interactions that create uncommitted work. Use Named Credentials for all external endpoints. Test the callout in isolation with a mock before wiring to checkout.
  5. Create or update RegisteredExternalService records — Deploy a RegisteredExternalService custom metadata record for each new calculator, linking it to the target store's StoreIntegratedService. Validate that the EPN is correctly specified — a typo in the EPN value silently disables the calculator.
  6. Implement PIM sync with External ID upsert — Define External ID fields on Product2 (and related catalog objects as needed). Implement the upsert job using Database.upsert() with the External ID field specified. Test with duplicate inputs to confirm idempotency.
  7. Validate end-to-end through a test order — Walk a test order through the complete checkout flow: add to cart, pricing recalculation, shipping rate fetch, inventory check, tax calculation, payment authorization, and (if SOM is licensed) capture. Confirm each registered calculator is invoked and that no System.CalloutException or silent failures occur in the cart pipeline.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. After-phase callouts throw System.CalloutException — Placing HTTP callouts in any cart pipeline hook after the calculate() method (or after any DML that creates uncommitted work within calculate()) causes the platform to throw System.CalloutException: You have uncommitted work pending. This error can be intermittent depending on execution order, making it hard to reproduce in sandbox. Always perform all callouts before the first DML write in a calculate() invocation.
  2. One class per EPN per store — no exceptions — Only one RegisteredExternalService record per Extension Point Name per store is honored. If a second class is registered for the same EPN and store, only one will be called (behavior is not guaranteed and may vary by release). This means a single calculator class must handle all logic for that EPN, including combining internal price book lookups with ERP callout results.
  3. Raw card data must never transit Salesforce — The payment framework is built on the assumption that card capture happens entirely client-side via a provider-hosted iframe. Any design that routes PANs, CVVs, or expiry dates through Apex — even for logging or validation — violates PCI DSS scope and may trigger Salesforce compliance review. The Apex layer must only ever receive opaque tokens or nonces from the storefront.
  4. No native PIM connector — missing External ID causes duplicate Product2 records — Salesforce Commerce has no built-in PIM integration. If PIM sync jobs use insert instead of upsert with an External ID field, each sync run creates new Product2 records for existing products. Stale duplicates accumulate in the catalog, causing incorrect search results and pricing associations. Always define and populate an External ID on Product2 before the first sync run.
  5. RegisteredExternalService EPN value is case-sensitive and must exactly match the platform constant — A common scaffolding error is setting ExtensionPointName to a human-readable label (e.g., Pricing) instead of the correct platform constant (e.g., CartExtension__Pricing). The calculator silently never fires with no error surface in the UI.

Output Artifacts

ArtifactDescription
Integration architecture decision recordMaps each external system (ERP, PIM, shipping, tax, OMS) to its Commerce extension point with rationale
CartExtension calculator Apex classesOne class per EPN, extending the correct base class, with callout logic in the before-phase only
RegisteredExternalService custom metadata recordsOne record per EPN per store, linking calculator class to the store via StoreIntegratedService
Product2 External ID field definitionCustom field definition and upsert job pattern for safe PIM synchronization
Named Credential configurationsSecure endpoint and credential storage for each external system API
Payment gateway integration recommendationDecision between Salesforce Payments, AppExchange package, or custom CommercePayments adapter

Dependencies

Package files