Shipping System Overview¶
Merchello's shipping system supports both flat-rate configured shipping and dynamic real-time carrier rates. This guide covers the architecture, how the pieces fit together, and the key concepts you need to understand.
Key Concepts¶
Flat-Rate vs Dynamic Providers¶
Merchello has two types of shipping providers:
Flat-rate providers use pre-configured destination-based costs: - You set up shipping options with costs per country/state - Costs are looked up from the database at checkout - No external API calls needed - Example: "Standard Shipping: $5.99 to US, $12.99 to UK"
Dynamic providers fetch live rates from carrier APIs: - Real-time quotes from FedEx, UPS, etc. - Rates vary by package weight, dimensions, and destination - Requires API credentials and network access - Example: "FedEx Ground: $8.47 (calculated by FedEx)"
Shipping Options¶
A shipping option represents a service level (e.g., "Standard Shipping", "Express Delivery"). For flat-rate providers, each option has associated costs by destination. For dynamic providers, shipping options are created per carrier service type.
Shipping Groups¶
At checkout, basket items are grouped by warehouse. Each group has its own set of available shipping options. This handles the common scenario where items ship from different warehouses.
Architecture¶
Shipping functionality is split across three services:
| Service | Purpose | Source |
|---|---|---|
IShippingService |
Business logic and orchestration for basket/product shipping | IShippingService.cs |
IShippingQuoteService |
Fetches quotes from shipping providers (FedEx, UPS, flat-rate) | IShippingQuoteService.cs |
IShippingCostResolver |
Resolves costs from flat-rate shipping option configurations | ShippingCostResolver.cs |
Invariant (CLAUDE.md):
IShippingService.GetShippingOptionsForBasket()is the basket-level entry point; it uses the activeIOrderGroupingStrategyinternally.IShippingQuoteService.GetQuotes*()is the source of truth for quote retrieval.ShippingCostResolver.ResolveBaseCost()is the single source of truth for the flat-rate fallback chain -- do not reimplement the match logic anywhere else.
Cost Resolution Priority¶
For flat-rate shipping, costs are resolved in this strict priority order by ShippingCostResolver.ResolveBaseCost():
- State/Region -- A cost entry matching the exact country + state/province code
- Country -- A cost entry matching the country code with no region
- Universal -- A cost entry with country code
*(wildcard, no region) - Fixed Cost -- The shipping option's fallback
FixedCost(normalized to0for flat-rate when null)
// IShippingCostResolver resolves in priority order
decimal? cost = shippingCostResolver.ResolveBaseCost(
costs: shippingOption.ShippingCosts,
countryCode: "US",
regionCode: "CA",
fixedCostFallback: shippingOption.FixedCost);
Dynamic providers (
UsesLiveRates = true) must NOT rely on fixed-cost entries. Carrier rates are fetched live from the provider API at checkout. If you need a flat-rate option named after a carrier (e.g., "FedEx Ground" at a fixed $8.99), create it withProviderKey = "flat-rate"rather thanProviderKey = "fedex".
Warehouse-Based Shipping¶
Shipping is warehouse-centric. Each warehouse:
- Has service regions (countries/states it can ship to)
- Has shipping options assigned to it
- Can have dynamic provider configurations (FedEx, UPS)
- Has priority ordering for stock allocation
At checkout, items are allocated to warehouses based on:
1. ProductRootWarehouse priority ordering
2. Service region eligibility (can this warehouse ship there?)
3. Stock availability (Stock - Reserved >= quantity)
Shipping vs Fulfilment¶
Merchello keeps shipping (customer-facing rate quoting) strictly separate from fulfilment (back-of-house 3PL workflow). This is a CLAUDE.md invariant:
| Concern | Shipping | Fulfilment |
|---|---|---|
| Purpose | Customer-facing rates and delivery options | 3PL submission, status tracking, inventory sync |
| When | At checkout (before payment) | After payment (order processing) |
| Who sees it | The customer | The warehouse/3PL |
| Built-in providers | Flat Rate, FedEx, UPS | ShipBob, Supplier Direct |
| Interfaces | IShippingProvider |
IFulfilmentProvider |
Never mix carrier quoting logic (shipping) with warehouse submission logic (fulfilment). The shipping service category inferred at checkout (Standard, Express, Overnight, Economy) is passed to fulfilment providers, which map it to their own method names -- that is the only bridge. See Fulfilment Overview.
Checkout Flow¶
Here's how shipping works during checkout:
- Customer enters address -- the checkout initializes with their country/state
- Order grouping -- items are grouped by warehouse based on stock and service regions
- Quote fetching -- each group gets shipping options:
- Flat-rate: costs looked up from
ShippingCosttable - Dynamic: live rates fetched from carrier APIs
- Customer selects shipping -- one option per group
- Totals calculated -- shipping costs added to the basket total
- Selection stored -- shipping selections saved in the checkout session
Selection Key Contract¶
Shipping selections use a stable key format that must remain unchanged:
| Type | Format | Example |
|---|---|---|
| Flat-rate | so:{guid} |
so:abc12345-... |
| Dynamic | dyn:{provider}:{serviceCode} |
dyn:fedex:FEDEX_GROUND |
These keys are parsed by SelectionKeyExtensions.TryParse() into order fields (ShippingProviderKey, ShippingServiceCode, ShippingServiceName) and must remain stable across the checkout flow and any custom grouping strategies.
Invariant (CLAUDE.md): Treat this contract as wire-protocol. Checkout, order edit, notifications, and fulfilment all rely on it. Keep the parsed rate in
QuotedCostsso dynamic rates seen at selection time are what the customer pays.
Product Shipping Restrictions¶
Products can be restricted from certain shipping methods:
ProductRoot.AllowExternalCarrierShipping = falseblocks dynamic carrier options for those products (only flat-rate options are shown)- Non-shippable products (digital, services) are excluded from shipping groups entirely
- Weight and dimensions from the product's package configuration are sent to carrier APIs
Built-in Providers¶
| Provider | Type | Live Rates | Tracking | International |
|---|---|---|---|---|
| Flat Rate | Static | No | No | Yes |
| FedEx | Dynamic | Yes | Yes | Yes |
| UPS | Dynamic | Yes | Yes | Yes |
See Flat Rate Shipping and Dynamic Shipping Providers for detailed setup guides.
IShippingService Reference¶
Defined in IShippingService.cs:
| Method | Purpose |
|---|---|
GetShippingOptionsForBasket(parameters, ct) |
Get grouped shipping options for the basket (uses the active IOrderGroupingStrategy internally) |
GetShippingSummaryForReview(basket, address, selections, ct) |
Get shipping summary for order review |
GetRequiredWarehouses(basket, address, ct) |
Determine which warehouses are needed to fulfill the basket |
GetAllShippingOptions(ct) |
List all shipping options in the system |
GetShippingOptionsForProductAsync(productId, country, region, ct) |
Get options for a product detail page |
GetShippingOptionByIdAsync(id, ct) |
Get a specific shipping option |
GetShippingOptionsForWarehouseAsync(warehouseId, country, state, ct) |
Get options for a warehouse to a destination |
GetFulfillmentOptionsForProductAsync(productId, country, state, ct) |
Get best warehouse for a product at a destination |
GetDefaultFulfillingWarehouseAsync(productId, ct) |
Get default warehouse for a product when no address is known |
Shipping Tax¶
Shipping can be taxable depending on jurisdiction. Merchello supports four shipping tax modes:
| Mode | Behavior |
|---|---|
NotTaxed |
Shipping is not taxable |
FixedRate |
Apply the returned fixed tax rate to shipping |
Proportional |
Tax rate is a weighted average of basket item tax rates (EU/UK VAT) |
ProviderCalculated |
Tax provider determines from full order context (e.g., Avalara) |
The shipping tax configuration is queried via ITaxProviderManager.GetShippingTaxConfigurationAsync(countryCode, stateCode) and returned as a ShippingTaxConfigurationResult. Proportional math is centralized in ITaxCalculationService.CalculateProportionalShippingTax(). See Shipping Tax for the full model.
Invariant (CLAUDE.md): Never hardcode shipping tax rates. Always query the tax provider. Never reimplement the proportional formula outside
ITaxCalculationService.