Extend add-to-cart handlers by product type

Prev Next

This article helps Apex developers, implementation partners, and solution architects extend OrderCentral add-to-cart behavior for specific web product types without replacing the full shopping cart flow. Use this extension point when different product record types need different add-to-cart rules, validation, or cart-line construction behavior.

This article documents the supported implementation contract. It focuses on the public extension seam, the plugin registration model, and the upgrade-safe way to customize add-to-cart handling.

When to customize add-to-cart behavior

Create a custom add-to-cart handler when a specific web product type needs behavior that the default handler does not provide.

Typical examples include:

  • validating product-type-specific fields before the item is added
  • disabling cart-line merging for a specific product type
  • adding product-type-specific logic before or after the standard add-to-cart flow runs
  • building grouped or configurable cart lines in a custom way for one product type

Do not build a custom handler when all product types can follow the standard AddProductToCartHandler behavior.

How handler resolution works

OrderCentral resolves the add-to-cart handler by the RecordType.DeveloperName of the target Web_Product__c record.

At runtime, ShoppingCartService follows this model:

  1. It reads the web product's record type developer name.
  2. It resolves the plugin type AddProductToCartHandler.
  3. It looks for a Plugin_Registration__mdt child record whose Record_Type_Name__c matches that record type developer name.
  4. If a matching registration contains Class_Name__c, OrderCentral instantiates that class.
  5. If no matching registration is found, OrderCentral falls back to the default class configured on the Plugin.AddProductToCartHandler plugin record.
  6. The shipped default class is AddProductToCartHandler.

That means handler selection is driven by product type, not by storefront page, request origin, or custom logic in the cart API caller.

Default plugin behavior

The shipped plugin metadata defines:

  • plugin type: AddProductToCartHandler
  • default class: AddProductToCartHandler

If you do nothing, simple, configurable, and grouped products all continue to use the standard handler.

Extend the base class or implement the interface

OrderCentral exposes two supported entry points:

  • implement IAddProductToCartHandler
  • extend AddProductToCartHandler

In most implementations, extending AddProductToCartHandler is the safer default.

Approach Use it when What you keep What you must own
Extend AddProductToCartHandler You want to add product-type-specific behavior while preserving the standard cart flow. Standard merge behavior, child-item handling, cart result creation, legacy dependency wiring, and cart item registration behavior. Only the custom logic you add in your override.
Implement IAddProductToCartHandler directly You are intentionally replacing the full add-to-cart implementation for that product type. Only the handle(AddShoppingCartItemDto request) contract. Cart merge behavior, child-item creation, unit-of-work registration, add-to-cart result construction, and compatibility with pricing and recalculation.

Why the base class is usually safer

ShoppingCartService injects the default cart dependencies only when the resolved handler instance is an AddProductToCartHandler.

That matters because the base class already knows how to work with:

  • existing cart items for merge checks
  • the parent and child unit-of-work instances
  • result collection for newly added cart lines
  • grouped-product child item registration
  • add-on child item registration
  • parent-child cart relationships

If you implement IAddProductToCartHandler from scratch, that wiring is not provided automatically. Use interface-only implementations only when you fully understand and intentionally need to replace the complete flow.

Supported override points in AddProductToCartHandler

For most customizations, treat these as the practical extension contract:

  • doHandle(HandleAddProductRequest request)
  • registerNew(CartItem newShoppingCartItem)

Override doHandle(...) for flow-level customization

Override doHandle(...) when you want to:

  • apply product-type-specific validation
  • adjust merge behavior
  • add pre-processing before the standard cart item registration runs
  • add post-processing after the standard logic completes

When possible, call super.doHandle(request) so the standard registration behavior stays intact.

Example:

global with sharing class ExampleAddProductToCartHandler extends AddProductToCartHandler {
    global override void doHandle(HandleAddProductRequest request) {
        if (request == null || request.webProductId == null) {
            throw new Application.InvalidArgumentException('Web product Id is required');
        }

        // Example: force separate cart lines for this product type.
        request.mergeIfProductIsInCart = false;

        super.doHandle(request);
    }
}

Override registerNew(CartItem) for typed cart-item registration

Override registerNew(CartItem) when your custom handler needs to control how new cart lines are built from the base class helper models.

The base class exposes these helper types:

  • SimpleCartItem
  • ConfigurableCartItem
  • GroupedCartItem
  • GroupOptionItem

Treat these helper classes as part of the supported base-class extension story only.

Important boundary:

  • they are not the public storefront request payload
  • they are helper models used by the base class when registering new cart lines
  • they matter mainly when you override registerNew(CartItem) or construct your own typed cart-item instances inside a subclass

Example:

global with sharing class ExampleGroupedAddToCartHandler extends AddProductToCartHandler {
    global override void registerNew(CartItem newShoppingCartItem) {
        if (newShoppingCartItem instanceof GroupedCartItem) {
            GroupedCartItem groupedCartItem = (GroupedCartItem) newShoppingCartItem;

            if (groupedCartItem.optionItems == null || groupedCartItem.optionItems.isEmpty()) {
                throw new Application.InvalidArgumentException(
                    'Grouped products must include option items'
                );
            }
        }

        super.registerNew(newShoppingCartItem);
    }
}

When interface-only implementations are appropriate

Implement IAddProductToCartHandler directly only when you need to own the entire handler lifecycle.

That usually means all of the following are true:

  • the standard merge and registration model is not suitable
  • the base class helper types are not sufficient for your product structure
  • you are prepared to own parent and child cart-line registration logic
  • you are prepared to maintain compatibility with cart calculation and result handling across upgrades

If your goal is only to add validation, enrich fields, or change limited registration behavior, extending AddProductToCartHandler is the better option.

Register a handler for a product type

Register add-to-cart handlers through the plugin framework.

For this extension point:

  • the plugin record is Plugin.AddProductToCartHandler
  • the override registration is stored in Plugin_Registration__mdt
  • the matching field is Record_Type_Name__c
  • the target Apex class is stored in Class_Name__c

Registration steps

  1. Create and deploy the Apex handler class.
  2. Make sure the class is visible to the runtime you are targeting.
  3. Create or update a Plugin_Registration__mdt record under the AddProductToCartHandler plugin.
  4. Set Record_Type_Name__c to the exact RecordType.DeveloperName of the target web product type.
  5. Set Class_Name__c to the Apex class name that should handle that product type.
  6. Leave Component_Bundle_Name__c empty for this class-based extension point.
  7. Deploy the metadata and validate the behavior with products that use the mapped record type.

Registration rules that matter

  • Record_Type_Name__c must match the developer name exactly, not the translated label.
  • The class named in Class_Name__c must implement IAddProductToCartHandler.
  • If your implementation extends AddProductToCartHandler, it still satisfies the interface requirement.
  • If the class name cannot be resolved at runtime, plugin resolution fails.
  • If no matching registration exists, the plugin falls back to the default AddProductToCartHandler class.

Namespace guidance

The plugin resolver loads the handler with Apex Type.forName(...). Set Class_Name__c to the class name exactly as Apex should resolve it in the target org.

In practice:

  • use the class name that exists in the org where the registration is deployed
  • if your implementation lives in a namespace, use the namespaced class name used in that org
  • validate the registration in a real target environment after deployment instead of assuming a sandbox class name will always resolve the same way everywhere

Validation guidance for custom handlers

Validate custom handlers at both the Apex and storefront levels.

Apex validation

At minimum, test these scenarios:

  • an unmapped product type uses the default AddProductToCartHandler
  • a mapped record type resolves your custom handler
  • a simple product still creates the expected parent cart item
  • a configurable product still preserves the selected webProductOptionId
  • a grouped product still creates the expected parent and child cart items
  • add-ons are still registered correctly when the request includes them
  • merge behavior works as intended when mergeIfProductIsInCart is true or false
  • invalid quantity or missing required identifiers still fail with a clear exception

Storefront validation

After deploying the handler and registration, validate with real products for each affected flow:

  1. Add a simple product of the mapped type to the cart and confirm the expected line is created.
  2. Add the same simple product again and confirm merge behavior matches your design.
  3. Add a configurable product of the mapped type and confirm the selected option is preserved.
  4. Add a grouped product of the mapped type and confirm child lines are created correctly.
  5. Add a product with add-ons and confirm child add-on lines still register correctly.
  6. Test an unmapped record type and confirm it still uses the standard handler.
  7. Recalculate the cart and confirm totals update without orphaned or duplicate lines.

Compatibility and upgrade guidance

Keep custom handlers close to the supported extension seam.

Recommended practices:

  • prefer extending AddProductToCartHandler over implementing the interface from scratch
  • keep overrides narrow and delegate to super whenever the standard behavior is still correct
  • use doHandle(...) for request-level behavior and registerNew(CartItem) only when you truly need typed cart-item control
  • treat SimpleCartItem, ConfigurableCartItem, GroupedCartItem, and GroupOptionItem as helper classes for subclassing, not as a standalone API contract

Avoid these patterns unless you have a strong reason:

  • copying the full default handler implementation into a custom class
  • reimplementing cart merge logic without an explicit business requirement
  • bypassing the standard child-item registration logic for grouped products or add-ons
  • rebuilding unit-of-work wiring or add-to-cart result collection in a subclass that could have delegated to the base class

After each upgrade, revalidate simple, configurable, grouped, and add-on product flows for every record type that uses a custom handler.

Summary

OrderCentral resolves add-to-cart handlers by web product record type through the AddProductToCartHandler plugin. The supported and upgrade-safe pattern is to register a handler per record type and, in most cases, extend AddProductToCartHandler rather than implement IAddProductToCartHandler from scratch. Use doHandle(...) for focused flow changes, use registerNew(CartItem) when you need typed cart-item control, and validate custom behavior across simple, configurable, grouped, and add-on product flows.