SfSkills Domains Docs GitHub

Home / apex / commerce-extension-points

commerce-extension-points

Use this skill when extending Salesforce B2B/B2C Commerce Cloud behavior through Apex: building custom cart calculators, implementing pricing hooks, integrating inventory checks, or registering custom checkout extensions via the CartExtension Apex namespace and RegisteredExternalService metadata. NOT for standard LWC Commerce components, declarative store configuration, or Experience Builder page layouts — use admin/commerce-checkout-configuration for those.

Open the skill folder on GitHub


Commerce Extension Points

Use this skill when you need to write Apex that customizes Salesforce Commerce Cloud cart calculation, pricing, or checkout behavior by implementing a class in the CartExtension namespace and registering it as a RegisteredExternalService metadata record. This skill covers the full lifecycle: selecting the correct extension point, implementing the synchronous Apex class, registering it, and validating it without triggering governor-limit failures in tests.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

CartExtension Apex Namespace

All B2B/B2C Commerce extension point classes live in the CartExtension Apex namespace, which is a platform-provided namespace — not a managed package. The core base class for pricing customization is CartExtension.PricingCartCalculator. To implement a custom pricing calculator, extend this class and override the calculate method:

public class MyPricingCalculator extends CartExtension.PricingCartCalculator {
    public override void calculate(CartExtension.CartCalculateCalculatorRequest request) {
        // custom pricing logic here — synchronous only, no DML, no @future
    }
}

Other extension points follow the same pattern: extend the appropriate CartExtension base class and override the lifecycle method. The namespace provides base classes for inventory, promotions, shipping, and taxes.

Extension Registration via RegisteredExternalService Metadata

The link between an Apex class and the Commerce engine is established through RegisteredExternalService custom metadata. Each record specifies:

The metadata record is deployed as part of the org metadata, not configured through Admin UI. If the class name in ExternalServiceProvider does not match an existing Apex class that extends the correct base, the extension is silently skipped during cart recalculation.

Synchronous Execution and Prohibited Operations

Cart extensions run synchronously and inline during the platform-managed cart recalculation pipeline. Two operations are categorically prohibited:

  1. DML inside extension hooks — Any insert, update, delete, or upsert inside the calculate method or equivalent hook method throws System.DmlException immediately. This prohibition includes Database.insert() with allOrNone=false. Data writes must happen outside the extension, in a separate transaction.
  2. Asynchronous Apex — Any call to @future methods, System.enqueueJob(), Database.executeBatch(), or Messaging.sendEmail() inside an extension hook triggers a System.AsyncException at runtime. The async call cannot be deferred to complete after the synchronous extension returns; the platform detects it immediately.

Before Hooks vs. After Hooks

Extension points have lifecycle phases. Callouts to external HTTP endpoints are only permitted in before hooks — those that run before the platform writes the calculated values to the cart. After hooks execute in a context where the HTTP callout stack is closed. The distinction is enforced at runtime, not at compile time: code that calls Http.send() inside an after hook compiles and deploys successfully but throws System.CalloutException when the cart recalculates.


Common Patterns

Pattern: Custom Pricing Calculator

When to use: A business rule requires overriding or supplementing the default Commerce pricing logic — for example, applying customer-segment-specific prices from an external price list not managed in Salesforce price books.

How it works:

  1. Create an Apex class extending CartExtension.PricingCartCalculator.
  2. Override the calculate(CartExtension.CartCalculateCalculatorRequest request) method.
  3. Access cart items via request.getCart().getCartItems().
  4. Set adjusted prices directly on each CartExtension.CartItem object using the provided setter methods — do not attempt DML.
  5. Create a RegisteredExternalService metadata record pointing to the class with EPN Commerce_Domain_Pricing_CartCalculator.
  6. Deploy both the class and the metadata record together.

Why not the alternative: Price book rules alone cannot model complex B2B pricing tiers or external price feed lookups. Standard Commerce pricing is a good fallback but cannot call an external system or apply conditional logic that depends on runtime cart state.

Pattern: Inventory Check Before Checkout

When to use: Real-time inventory validation against an external warehouse management system is required before allowing the buyer to proceed to checkout.

How it works:

  1. Create an Apex class extending the appropriate CartExtension inventory base class.
  2. In the before hook method, perform an HTTP callout to the external WMS. Callouts are permitted in before hooks.
  3. For each cart item, evaluate the inventory response and update the item's CartValidationOutput collection to report insufficient stock as a validation failure.
  4. The Commerce engine reads the validation outputs and prevents checkout progress if failures are present. No DML is needed; all state changes go through the provided request/response objects.
  5. Register via RegisteredExternalService with the inventory EPN.

Why not the alternative: Platform-side inventory (Salesforce Order Management stock) does not always reflect real-time WMS state, especially in high-velocity or multi-channel scenarios. The extension point is the only supported path for blocking checkout based on live external data.


Decision Guidance

SituationRecommended ApproachReason
Custom pricing from external systemExtend CartExtension.PricingCartCalculator, callout in before hookOnly supported pattern; callout permitted in before phase
Need to write records when cart recalculatesDefer DML to a separate transaction outside the extensionDML inside extension hooks throws System.DmlException
Need async processing during cart calculationNot possible; all extension logic must be synchronous@future and enqueueJob throw System.AsyncException inside hooks
Inventory check against external WMSUse before hook with HTTP callout; report failures via CartValidationOutputAfter hooks prohibit callouts; before hook is the correct phase
Multiple extensions needed for same EPNOnly one class per EPN per store is supportedLast deployed record wins; chain logic within a single class
Testing cart extension logicInstantiate the class and call calculate() directly in test with a mock CartCalculateCalculatorRequestNever trigger live cart recalculation in test context

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

  1. Confirm store type and EPN — Verify the store is LWR Managed Checkout. Identify the exact EPN string for the required extension (pricing, inventory, promotions, shipping, or tax). Check whether a RegisteredExternalService record already exists for this EPN and store.
  2. Design for synchronous constraints — Map out every operation the extension will perform. Flag any DML, async calls, or after-hook callouts. Redesign the flow to eliminate them: push DML to a separate process, use before-hook phase for any HTTP callouts.
  3. Implement the Apex class — Extend the correct CartExtension base class. Override the required lifecycle method. Use only the provided CartExtension API objects to read cart state and write results. Do not use SOQL queries inside the hot path if performance matters; cache lookups via static maps if the cart has many items.
  4. Create the RegisteredExternalService metadata — Author the .md-meta.xml file specifying ExternalServiceProviderType, ExtensionPointName, and ExternalServiceProvider (the class name). Double-check spelling — EPN mismatches are silent at deploy time.
  5. Write test classes — Instantiate the extension class in a test and invoke calculate() directly with a mocked request. Use @TestVisible private constructors on any helper if needed. Do not rely on a live cart to trigger the extension in tests.
  6. Deploy both artifacts together — Include the Apex class and the RegisteredExternalService metadata in the same deployment. Deploying the metadata without the class results in a silent skip; deploying the class without the metadata means the extension is never invoked.
  7. Validate post-deployment — Add a test item to a cart in the target store and confirm the extension fires. Check debug logs for the class name. Confirm no unhandled exceptions occur during the first real cart recalculation.

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. DML Inside Extension Causes Immediate Runtime Exception — Any DML statement inside a cart calculator hook throws System.DmlException at runtime with the message "DML not allowed during cart recalculation." This does not appear at compile time or deploy time. Tests that bypass the live cart also bypass this check. The first indication of the problem is a failed cart recalculation in a live store or integration test that actually triggers the pipeline.
  2. EPN String Typo Is Silent — If the ExtensionPointName field in the RegisteredExternalService record contains a typo or uses incorrect casing, the platform does not throw an error at deploy time and does not log a warning at runtime. The extension is simply never called. The correct EPN for pricing is exactly Commerce_Domain_Pricing_CartCalculator. Always copy the EPN from the official documentation rather than typing it manually.
  3. After-Hook Callout Exception — HTTP callouts are permitted in before hooks but prohibited in after hooks. Code that calls new Http().send() in an after hook compiles and deploys successfully. The System.CalloutException only surfaces at runtime during cart recalculation. Moving the callout to the before hook phase resolves it.
  4. One Extension Per EPN Per Store — Only one RegisteredExternalService record can be active per EPN per store. When a second record is deployed for the same EPN, the last deployed record silently wins. No error is raised, and the previously active extension stops firing without any notification. This is particularly dangerous during incremental deployments where a new version of the extension is deployed as a separate record rather than updating the existing one.

Output Artifacts

ArtifactDescription
Custom CartExtension Apex classSynchronous implementation of the target extension point with no DML and no async calls
RegisteredExternalService metadataCustom metadata record linking the Apex class to the correct EPN and store
Extension test classUnit test that invokes calculate() directly with mocked CartExtension API objects

Package files