Shipping
| Features | CrestApps.OrchardCore.Shipping, CrestApps.OrchardCore.Shipping.EasyPost (EasyPost), CrestApps.OrchardCore.Shipping.Api (Shipping API) |
| Core | CrestApps.OrchardCore.Shipping.Core |
| Category | Commerce |
| Depends on | Commerce, Products, Addresses, OrchardCore.Contents |
The Shipping feature prices shipping. Zones say where; each zone's methods say how and what it costs, through rate providers. Products carry their weight and shipping class in the Shipping part.
Model
ShippingZone(INamedCatalog<ShippingZone>, one document):Name,Position,Countries(ISO 3166-1 alpha-2),Regions(ISO 3166-2, such asUS-AK),RestOfWorldandMethods. A country with no region inRegionsis covered whole.ShippingMethod(anEntity, inside its zone):MethodId,Name,Description,ProviderName,Enabled,Position,Currency(empty for any),TaxCategoryCodeandClassSurcharges(per unit, by class, matched ignoring case). The provider's settings are in the method's properties:FlatRateSettings(Amount,PerItemAmount),TableRateSettings(Basis:Weight,SubtotalorQuantity;RowsofFromandAmount;Maximum) andFreeShippingSettings(MinimumSubtotal).ShippingPart:Weight(one unit, in the site unit),ShippingClass, andVariantWeightsby variant id.GetWeight(variantId)falls back to the product's weight, then to zero.ShippingSettings(groupshipping):WeightUnit(KilogramorPound) andShippingClasses, plus:PickupLocations(up to 20PickupLocation:LocationId,Name,AddressLine1,City,Region,PostalCode,Country,Instructions,Fee, andStockLocationId, the inventory location its orders take units from, ornullfor any).ToAddress()gives the location as anAddress.ShipFrom(anAddress),DefaultParcelLength,DefaultParcelWidth,DefaultParcelHeight(centimeters for a store weighing in kilograms, inches otherwise) andPackagingWeight(added to the items' weight), which carrier quotes and labels use.
- Provider settings of the newer providers:
LocalDeliverySettings(PostalCodes, where981*covers every code starting with981and case, spaces and dashes are ignored, withCovers(postalCode);Amount;FreeFromSubtotal;MinimumSubtotal) andCarrierRateSettings(Carriers, empty for all;MaxRates, 3 by default, 0 for all;MarkupPercent;MarkupAmount;FallbackAmount,nullto offer nothing when the carriers can't be reached).
Rates
IShippingRateServiceFindZoneAsync(address)returns the first zone byPosition(then name) that covers the address, else theRestOfWorldzone, elsenull.GetRatesAsync(ShippingRateRequest)asks the provider of each enabled method of that zone, in order. Methods in another currency, or whose provider is missing (logged), are skipped. A shopper in a market gets the zone's methods in the market's currency, or else the others converted into it throughIShopperCurrency. Class surcharges are added, and amounts are rounded to the currency and never negative.
ShippingRateRequest:Destination(Address),Currency, andLinesofShippingRequestLine(ContentItemId,VariantId,Title,Quantity,UnitPrice,Weightper unit,ShippingClass), withSubtotal,TotalWeightandItemCount.ShippingRate:RateId(the method id, then:optionfor a provider with several),ZoneId,MethodId,ProviderName,Name,Description,Amount,CurrencyandTaxCategoryCode.IShippingRateProvider:Name,DisplayName,Description, andGetRatesAsync(ShippingRateContext)returning none, one or several rates.ShippingRateContexthas theZone,MethodandRequest, andCreateRate(amount, option, name).- Built in:
Flat,Table(the row with the largestFromat or below the value; none below the first row or aboveMaximum),Free(fromMinimumSubtotal),LocalDelivery(LocalDeliveryShippingProvider: one rate when the destination's postal code is covered and the subtotal reachesMinimumSubtotal, free fromFreeFromSubtotal) andCarrier(CarrierShippingRateProvider, see Carriers). - A carrier module registers its own provider, and a
DisplayDriver<ShippingMethod>for its settings that renders only for methods with itsProviderName.
- Built in:
Checkout
ShippingCheckoutHandleradds the Shipping step (ShippingConstants.CheckoutStepKey, order 2, after Contact) to an order's checkout when a line is a good (ProductType.Good). On completion it adds a signed-in buyer's new address to their address book when they asked, as their default shipping address if they had none.ShippingCheckoutFlowDisplayDriverrenders and posts the step.- The address comes from the customer's address book (Customers, optional) or the address editor, with the recipient's name and phone.
ShippingOrderRateServiceprices the order's goods: their weight and class come from the product version bought, and the currency is the order's.- An address posted without a rate chosen for it (or with a rate the address doesn't offer) comes back with its rates and the error Choose how to ship your order, so the step finishes only with a rate priced on the server. One rate is chosen for the buyer.
- The step's billing item is the shipping line (
TaxableKind = Shipping, the method'sTaxCategoryCode), then the invoice is rebuilt. The choice is kept asShippingCheckoutStepinSavedStepsand asCheckoutShippingInfo(address, rate, method, zone, name, amount) in the session.
- Tax destination.
DefaultCheckoutTaxProfileProvidernow setsCheckoutTaxProfile.Destinationto theCheckoutShippingInfoaddress, else theCheckoutContactInfobilling address. The Contact step rebuilds the invoice when it's saved, so an order that doesn't ship is taxed where it's billed. - Orders.
OrderCheckoutHandler.CompletingAsynccopiesCheckoutShippingInfotoOrder.ShippingAddressandOrder.Shipping(OrderShipping: method, rate, zone, provider, name, description, amount).Order.Anonymizekeeps only the country and region of both addresses. The admin order page, the guest order page and the confirmation email show both. - The price is the one shown when the buyer chose the method; editing a method later doesn't change checkouts already past the step.
- Store pickup. With
PickupLocations, the step renders a choice between shipping and each location (ShippingCheckoutStep.PickupLocationId). Pickup needs no address or quote: the location's address becomes the shipping address (so the order is taxed there), and the rate ispickup:{locationId}with providerPickup(OrderShipping.PickupProviderName), the location's fee and its instructions as the description.CheckoutShippingInfo.LocationIdcarries the location'sStockLocationIdtoOrderShipping.LocationId, which Inventory takes the units from first. A location removed since it was chosen sends the buyer back to choose again.
Shipments
Shipment(YesSql,Shippingcollection,ShipmentIndexon order, status and dates):OrderId,OrderNumber,Number(1, 2, … per order),Lines(ShipmentLine: orderLineId,Title,Sku,Quantity),Status(Pending,Shipped,Delivered,Returned),Carrier,TrackingNumber,TrackingUrl,Note, and the created, shipped, delivered, returned and emailed times.IShipmentService(Core), in the caller's unit of work:GetRemainingAsync(order): units of each good not in a shipment, ignoring returned ones.CreateAsync(order, lines, details, ship): only for a paid order, never more than is left; pending, or shipped at once.ShipAsync,DeliverAsync(shipped only),ReturnAsync(shipped or delivered; its units can be shipped again),DeleteAsync(pending only).- Each change sets
Order.FulfillmentStatus(OrderFulfillmentStatus:Unfulfilled,PartiallyFulfilled,Fulfilled, from the units in shipments that left;nullfor an order without goods). - A tracking link must be
httporhttps; an empty one is made from the carrier's template inShippingSettings.Carriers({0}is the escaped number).
IShipmentEventHandler(CreatedAsync,ShippedAsync,DeliveredAsync,ReturnedAsync, withShipmentEventHandlerBase) runs in the same unit of work. Shipping registers:ShippedEmailShipmentHandler: the shipped email (ShipmentEmailBuilder), once per shipment (ShippedEmailSentUtc), whenSendShippedEmailsis on and anIEmailServiceis set up; a guest gets the order link fromIOrderLinkService. For a pickup order (OrderShipping.IsPickup) it says the order is ready for pickup, where to collect it and the location's instructions.ShipmentWorkflowHandler(with Workflows):ShipmentCreatedEvent,ShipmentShippedEvent,ShipmentDeliveredEvent,ShipmentReturnedEvent, with theOrder, theShipment, its number, carrier, tracking number and link and the buyer's email as input, correlated by the shipment id.
- Pickup orders go through the same states, shown as Being got ready, Ready for pickup and Picked up, without a carrier or tracking number.
- Labels and tracking.
ShipmentaddsLabelUrl,LabelCost,LabelCurrency,TrackingStatus(the carrier's latest word) andTrackingEvents(ShipmentTrackingEvent:Utc,Status,Message,Location, newest first), shown on both order pages. - Order pages. Orders builds the order with
IDisplayManager<Order>in the display typesOrderConstants.DisplayTypes.DetailAdmin(the admin page, also My Orders) andDetail(the guest page), and renders the shapes in its pages.OrderShipmentsDisplayDriveraddsOrderShipments_DetailAdmin(the shipments, a new-shipment form and the actions, for Ship orders and update their shipments) orOrderShipments_Detail(what left, with tracking links, for the buyer). - Admin actions (
ShipmentsAdminController):shipping/orders/{orderId}/shipments(create), andshipping/shipments/{id}/ship,/deliver,/return,/delete, each back to the order. Each shipment links its packing slip (the Orders module'sOrdersPackingSlip, with its lines and number).
Carriers
ICarrierProvider(Core) is a shipping account that quotes carriers' rates and buys their labels:Name,DisplayName,IsEnabledAsync(),QuoteAsync(CarrierShipmentRequest)returning aCarrierQuote(QuoteId,RatesofCarrierRate:RateId,Carrier,Service,Amount,Currency,DeliveryDays; or anError), andBuyLabelAsync(quoteId, rateId)returning aCarrierLabel(Carrier,Service,TrackingNumber,TrackingUrl,LabelUrl,Amount,Currency, or anError).CarrierShipmentRequesthasFrom,To, aReferenceand aCarrierParcelin ounces and inches (ToOuncesandToInchesconvert from the store's units). A provider is a feature: it registers itsICarrierProviderand a settings driver for its own tab on Settings > Commerce > Shipping (groupshipping), where it's enabled, as Orchard Core's SMS and email providers are.ICarrierProviderResolver(DefaultCarrierProviderResolver) returns the default provider,CarrierProviderSettings.DefaultProviderNamechosen on the Providers tab (CarrierProviderSettingsDisplayDriver), while it's enabled, and the enabled providers. Everything that quotes or buys labels goes through it.CarrierShippingRateProvider(Carrier) quotes the order's weight plusPackagingWeight, in the default box, fromShipFromto the destination; caches a quote for ten minutes per destination and weight; keeps theCarrierRateSettings.Carriersand the order's currency; and offers the cheapestMaxRateswith the markup, named like USPS Ground Advantage, with the business days as the description. Without a default carrier provider, a ship-from address or a destination, or when the quote fails (logged), it offersFallbackAmountor nothing.- Labels (
ShipmentLabelsAdminController,FulfillOrders):shipping/shipments/{shipmentId}/labelfor a pending shipment that isn't a pickup, prefilled with the items' weight plus the box;/label/ratesquotes the parcel;/label/buybuys the chosen rate, keeps the label on the shipment and ships it with the label's carrier, tracking number and link, which sends the shipped email.OrderShipmentsDisplayDrivershows Buy label when there's a default carrier provider. - EasyPost (feature
CrestApps.OrchardCore.Shipping.EasyPost):EasyPostCarrierProvidercalls EasyPost's API through theEasyPostnamedHttpClient.EasyPostSettings(the EasyPost tab of Settings > Commerce > Shipping, forManageShipping) keepsIsEnabled, andApiKeyandWebhookSecretprotected with data protection (purposeEasyPostSettings). Enabling it while no provider is the default makes it the default; disabling the default clears it. Saving it with a public httpsBaseUrlregisters the webhook through EasyPost's API (RegisterWebhookAsync) with a secret made for it, again when the address or the key changes, and keepsWebhookIdandWebhookUrl; the store never enters a secret.POST shipping/easypost/webhook(routeEasyPostWebhook, a minimal API inEasyPostWebhookEndpoint) accepts only bodies of up to 512 KB signed with the secret (X-Hmac-Signature, HMAC-SHA256);EasyPostTrackingUpdaterappliestracker.createdandtracker.updatedevents to the shipment with that tracking number, and delivers a shipped shipment when the tracker saysdelivered.
API
The Shipping API feature (CrestApps.OrchardCore.Shipping.Api, it needs Shipping and the Checkout API) maps into the commerce JSON API at api/commerce/v1:
- The checkout's Shipping step, through
ShippingCheckoutStepService:checkout/{id}/pickup-locations,checkout/{id}/shipping-rates(a quote for an address) andPUT checkout/{id}/shipping(a pickup location, or an address and its rate). me/orders/{orderId}/shipments, the signed-in buyer's tracking, with View own orders.- The staff fulfillment endpoints, through
IShipmentServicewithFulfillOrders:orders/{orderId}/shipments(what's left to ship, and packing a shipment), andshipments/{shipmentId}/shipand/deliver.
See Commerce API for the endpoints, the conventions they share, and how apps sign in, and Headless commerce API in the User Manual for setting it up in the admin.
Admin
- Commerce > Shipping (
shipping/zones,ShippingZonesController) lists zones in the order they are tried. A zone's page edits it and lists its methods; Add method opens the method editor for a provider (shipping/zones/{zoneId}/methods/create/{providerName}). - The method editor is
IDisplayManager<ShippingMethod>:ShippingMethodDisplayDriverfor the common fields, and one driver per provider. - Try rates (
shipping/rates) runsIShippingRateServicefor an address and an order described by subtotal, weight, items and class.
Permissions
| Permission | Allows |
|---|---|
Manage shipping zones, methods and settings (ManageShipping) | Commerce > Shipping and Settings > Commerce > Shipping (registered for the shipping settings group). |
Ship orders and update their shipments (FulfillOrders) | The Shipments section of an order's page, and its actions. |
Administrators have it by default.
Recipes and deployments
The ShippingZones step creates or updates zones, found by ItemId or Name. Zone names are unique: a new zone named like another is reported as a recipe error. Methods without a MethodId get one. Provider settings go in the method's Properties, keyed by type name:
{
"name": "ShippingZones",
"Zones": [
{
"Name": "United States",
"Countries": ["US"],
"Methods": [
{ "Name": "Standard", "Description": "3 to 5 business days", "ProviderName": "Flat", "Properties": { "FlatRateSettings": { "Amount": 5, "PerItemAmount": 1 } } },
{ "Name": "Free over $100", "ProviderName": "Free", "Properties": { "FreeShippingSettings": { "MinimumSubtotal": 100 } } }
]
},
{ "Name": "Everywhere else", "RestOfWorld": true, "Position": 99, "Methods": [ { "Name": "International", "ProviderName": "Table", "Properties": { "TableRateSettings": { "Basis": 0, "Rows": [ { "From": 0, "Amount": 20 }, { "From": 2, "Amount": 35 } ] } } } ] }
]
}
The Shipping Zones deployment step (Commerce) exports every zone with its methods.
Not included
Return labels; carriers other than through EasyPost (another account implements ICarrierProvider); a parcel other than the default box when quoting at checkout; postal-code zones (local delivery lists postal codes within a zone); dimensional weight; and pick lists. Buyer returns are the Orders module's Returns feature.