Skip to main content
Version: Latest
Technical Manual

Extending commerce

The commerce modules are built so another module can add to them without changing them. Each extension point is an interface (a seam) that the modules call through every registered implementation. This page says which seam fits which job. The module pages have the details.

Register an implementation in your module's Startup, and depend on the feature that owns the seam in your manifest, so the seam's services are there.

Which seam to use​

Products and prices​

ToImplementLives inSee
Give a buyer their own price, such as a company price listIBuyerPriceProviderProducts.CoreCompanies
Say a product can't be bought here, with the reason shown in place of Add to cartIProductSaleRestrictionProducts.CoreMarkets
Show and convert prices in the shopper's currencyIShopperCurrency (one per site; replace the default)Products.CoreMarkets
Say how many are available, such as from a supplierIProductAvailabilityProviderProducts.CoreInventory
Resolve the price of a product for a quantityIPriceResolverProducts.CoreProducts
Add a button or form to a product's card and pageA display driver placing a shape in the ProductAction display typeCommerce

Carts and checkout​

ToImplementLives inSee
Keep what the buyer chose with a cart line (a date, an amount, a listing) and price, limit or refuse the line by itICartLineHandlerCarts.CoreBookings, Fundraising
Add an action to the cart page, such as Request a quoteICartActionProviderCarts.CoreCompanies
Follow a checkout through its life: started, loaded, completing, completed, failed, canceled or expiredICheckoutHandlerCheckout.AbstractionsCheckout
Hold back the payment step until something is done, such as an approvalICheckoutPaymentGateCheckout.AbstractionsCompanies
Add a step or a panel to the checkout flowA display driver for CheckoutFlowCheckoutBookings
Pay part of an order from a balance (a gift card, credit, terms, a deposit)ICheckoutTenderProviderCheckout.AbstractionsCheckout
Add a payment methodICheckoutPaymentProvider, with ICheckoutPaymentRefundProvider to refund itCheckout.AbstractionsPayments
Offer a payment method only sometimesICheckoutPaymentProviderAvailabilityCheckout.AbstractionsCheckout
Work out tax at checkoutICheckoutTaxServiceCheckout.AbstractionsTaxation

Orders, shipping and tax​

ToImplementLives inSee
React when an order is created, paid, canceled, refunded, returned or its lines changeIOrderEventHandler (derive from OrderEventHandlerBase)Orders.CoreCarts and orders
Email a customer through the store's email templates and layout, in the language they shopped inICommerceEmailSender, with AddCommerceEmailSender(); register the email's kind with AddCommerceEmailKind so the store can write its templateOrders.CoreCarts and orders
Give the buyer what a paid order bought (files, a membership, a gift card) and take it back when the order is canceled or refunded in fullOrderGrantHandlerBase: implement GrantAsync and RevokeAsync, and register it as an IOrderEventHandler. After a partial refund the buyer keeps what they bought.Orders.CoreDownloads, Memberships
Add a shipping rate, such as a carrier'sIShippingRateProviderShipping.CoreShipping
Buy labels and track parcels with a carrierICarrierProviderShipping.CoreShipping
React when a shipment ships or is deliveredIShipmentEventHandlerShipping.CoreShipping
Add tax rules from another sourceITaxRuleProvider, and the other taxation seamsTaxation.AbstractionsTaxation

The rest of the store​

ToImplementLives inSee
Pay marketplace vendors another wayIVendorPayoutProviderMarketplaceMarketplace
Add a step to the store's setup guideIStoreSetupStepAbstractionsStore setup
Send a signed, time-limited link, such as one that opens an orderSignedTokenService to sign and read the token for a purpose, and SiteUrlGenerator for the route's absolute URL from the site's base URL; register both with services.AddSignedLinks()CrestApps.OrchardCore.CoreCarts and orders
Keep store configuration records (such as zones or tax rules) in a named catalog that recipes and deployment plans carryIAuditedModel on the record; AuditedCatalogEntryHandlerBase<T> for its handler, which binds the recipe JSON and stamps CreatedUtc, ModifiedUtc, OwnerId and Author, so the handler only validates (check a unique name with INamedCatalog<T>.IsNameUsedByAnotherEntryAsync); NamedCatalogRecipeStep<T> for the recipe step and NamedCatalogDeploymentSource<TStep, T> for the deployment step, which read and write the same { "name": "...", "<Entries>": [ ... ] } shapeCrestApps.OrchardCore.CoreTaxation, Shipping
Store a secret in your settings, such as a provider's API keyProtectSetting(purpose, value) on IDataProtectionProvider to save it, and UnprotectSettingOrDefault(purpose, value, logger, name) to read it, which logs a warning and treats a value the site's keys can't read as not entered (UnprotectSetting throws instead)CrestApps.OrchardCore.CoreShipping, Taxation
Export and erase your module's records of a personICommercePersonalDataContributorCustomers.AbstractionsCustomers
Add a page to the storefront accountA NamedNavigationProvider created for the CommerceConstants.Account.MenuName menu, and a view rendering its content in the CommerceAccountPage shapeAbstractionsCommerce
Add a screen under CommerceAn AdminNavigationProvider adding under the Commerce nodeCommerce
Change how a storefront page looksOverride its shape in your themeCommerce

A worked example: gift wrapping​

A module that offers gift wrapping on any product needs to do three things:

  • Keep the buyer's choice with the cart line.
  • Charge for it.
  • Tell the warehouse when the order is paid.

The cart line handler reads what the buyer posted with Add to cart, keeps it under a key named after the module, and prices it:

internal sealed class GiftWrapLineHandler : ICartLineHandler
{
private const string Detail = "giftwrap.message";

public int Order => 200;

public Task PrepareAsync(CartLinePrepareContext context, CancellationToken cancellationToken = default)
{
if (context.PostedDetails.TryGetValue(Detail, out var message) && !string.IsNullOrWhiteSpace(message))
{
context.Details[Detail] = message.Trim();
}

return Task.CompletedTask;
}

public async Task EvaluateAsync(CartLineEvaluateContext context, CancellationToken cancellationToken = default)
{
if (context.GetDetail(Detail) is not { } message)
{
return;
}

// 3.00 in the product's currency, converted into the line's.
context.UnitPrice += await context.ConvertAsync(3m, cancellationToken);
context.Details.Add(new CartLineDetail(Detail, "Gift wrapped", message));
}
}

The details go onto the order line, where the order event handler finds them:

internal sealed class GiftWrapOrderHandler : OrderEventHandlerBase
{
private readonly IServiceProvider _serviceProvider;

public GiftWrapOrderHandler(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public override async Task PaidAsync(OrderPaidContext context, CancellationToken cancellationToken = default)
{
// Resolved here, not in the constructor: see the traps below.
var notes = _serviceProvider.GetRequiredService<IGiftWrapNotes>();

foreach (var line in context.Order.Lines.Where(line => line.Details?.ContainsKey("giftwrap.message") == true))
{
await notes.AddAsync(context.Order, line, cancellationToken);
}
}
}

Register both, and add the message field to the product page with a driver for the ProductAction display type. The field must post as details[giftwrap.message] with the form attribute of the product's Add to cart form (add-to-cart-{contentItemId}):

services.AddScoped<ICartLineHandler, GiftWrapLineHandler>();
services.AddScoped<IOrderEventHandler, GiftWrapOrderHandler>();

Traps​

  • Order event handlers and circular dependencies. IOrderService is built with every IOrderEventHandler. A handler that takes a service needing IOrderService (directly or not) in its constructor makes a cycle. Resolve such services from IServiceProvider when the event happens. The same goes for a tender provider that needs the checkout session store.
  • Commits during a request. Checkout and refunds commit the session part-way through a request. A document read before then and saved after is saved twice. Read it again by id after the payment or refund.
  • Amounts typed by buyers. Read amounts that a form posts in the invariant culture. A German admin's culture reads "40.24" as 4024.
  • Line detail keys. Prefix your keys with your module's name, such as giftwrap.message. Other handlers share the same dictionary.
  • Products chosen on their page. A product whose line needs details can't be added from a card. Add your part's name to CartOptions.ChooseOnProductPageParts so cards link to the page instead.
  • Discounts. A line that no discount may touch, such as a donation, sets ExcludeFromDiscounts in EvaluateAsync.
  • Admin lists. List records with Orchard Core's <admin-list> and an AdminListContext<T>, with a display driver for both the SummaryAdmin and SummaryAdminGrid display types, so lists follow the admin's list or grid setting. Put the list's filter selects in its toolbar with IShapeFactory.CreateAdminListFiltersAsync(listName, formId, filters), which builds the shared CommerceAdminListFilters shape; a theme overrides one list's filters with CommerceAdminListFilters-{ListName}.cshtml.