A Core dna store is a set of records and an API that moves a purchase from cart to order: products described in every language and priced in every currency the store sells in, a cart, a checkout that becomes an order, invoices, payments, tax and shipping. The site builds the storefront on top: the cart and checkout pages, their scripts, and the customer emails.
L2–L3
Unit 06 · Tax and shipping configuration
Shipping methods and their templates
Tax and shipping configurationPage 24 of 31Updated
Every shipping method is a template that creates the rates a shopper chooses from. The parts of shipping, what a method's template receives, a complete two-country set-up with weight-banded prices in both template languages, and currencies and languages.
Who this is for. Developers setting up a store's shipping and writing its rate logic. After this page you can configure shipping for the places a store serves and price it by weight, destination and currency, in the site's template language, without framework code.
The parts of shipping
Part
What it is
Shipping category
What a product is for shipping: parcel, oversize, chilled. A product has one; a variant uses its product's. Products without one use the Default shipping category (Shipping.defaultShippingCategory, required).
Group
Every method belongs to a group, which has a name and an identifier ("Unique identifier to define a group even if it has the same name with any other group").
Shipping method
Of type Template ("Specify a custom template to create rates manually"): a Name, the Active switch, its group, the shipping categories it carries, the warehouses it serves for pickup, and its Template code.
Rate
One option the shopper chooses from: a name, a total, an optional service level and courier name, and an expiry, after the Lifetime (Shipping.rates.lifetime, 3,600 seconds).
The categories and warehouses decide which fulfilments a method may carry; the template decides where it ships and what it charges.
What a method's template receives
A method's Template code runs for each fulfilment when shipping is created on a checkout, in the site's template language, Smarty or Twig. What it prints is not shown anywhere: rates exist because the code calls create on the rates helper, once for each option it offers. The code receives:
Variable
What it is
fulfillment
The items being shipped. getFulfillmentItems() gives each item, with getItem() (the line, whose getProduct() is the product or variant added) and getQuantity(); getDeliveryAddress() is the delivery address, empty for a pickup; getWarehouse() is the pickup warehouse; getOrder() is the checkout.
rates
The rates helper, with one method: create(total, name, serviceLevel, courierName); the last two are optional.
site, user
The current site and user.
siteData
The site-wide key-value store: get() and set().
query
The query helper, as $_query is in page templates: findBy(), findOneBy() and the rest.
The admin's own examples are $rates->create('100', 'Standard') and $rates->create('200', 'Express', 'exp', 'Bob Smith') in Smarty, and rates.create('100', 'Standard') in Twig. For each rate:
total is a string holding a number of zero or more, in the checkout's currency, entered the same way as product prices: with tax or without, as Prices that include tax sets. A value that is not a number, or is negative, creates no rate.
name is what the shopper sees. serviceLevel and courierName are stored on the rate and returned with it.
Code that stops with an error creates no rates for that fulfilment, so guard every value that can be empty, above all the delivery address, which a pickup does not have.
Weight is each product's and variant's getWeight(): a decimal string, or null when no weight was entered. It carries no unit, so the store picks one (kilograms below) and enters every weight in it.
A complete two-country set-up
An Australian store ships parcels in Australia and to New Zealand, prices in Australian dollars, and keeps weights in kilograms. Its shipping takes five steps:
Create a shipping category Parcel and make it the Default shipping category, so every product without a category of its own is carried too.
Create a group Delivery, identifier delivery.
Enter a weight in kilograms on every product and variant.
Create a shipping method of type Template: name Post, group Delivery, category Parcel, Active on.
Give it the template below. It reads the delivery country and creates rates only for the two countries the store serves, priced by the parcel's weight.
{% set address = fulfillment.getDeliveryAddress() %}
{% set country = address ? address.getCountry() : null %}
{% set code = country ? country.getAlpha2Code() : '' %}
{% set weight = 0 %}
{% for fulfillmentItem in fulfillment.getFulfillmentItems() %}
{% set product = fulfillmentItem.getItem().getProduct() %}
{% if product and product.getWeight() is not null %}
{% set weight = weight + product.getWeight() * fulfillmentItem.getQuantity() %}
{% endif %}
{% endfor %}
{% if code == 'AU' %}
{% if weight <= 1 %}
{{ rates.create('9.95', 'Standard post', 'standard') }}
{% elseif weight <= 5 %}
{{ rates.create('14.95', 'Standard post', 'standard') }}
{% elseif weight <= 20 %}
{{ rates.create('24.95', 'Standard post', 'standard') }}
{% endif %}
{% if weight <= 5 %}
{{ rates.create('19.95', 'Express post', 'express') }}
{% endif %}
{% elseif code == 'NZ' %}
{% if weight <= 2 %}
{{ rates.create('25.00', 'International post', 'international') }}
{% elseif weight <= 10 %}
{{ rates.create('45.00', 'International post', 'international') }}
{% endif %}
{% endif %}
The country is read from the address's country record, which a checkout address has when its countryName matches the country list (Writing the email and addresses), and compared by its two-letter code. An address with no country record, or in another country, gets no rate from this method.
With this configuration, creating shipping on a checkout gives these rates:
A rate is created in the checkout's currency, so on a multi-currency store each total has to be the price in that currency. Read the checkout's currency code from the fulfilment and keep one price per currency:
{% set standard = {'AUD': '9.95', 'NZD': '10.95'} %}
{% set currencyCode = fulfillment.getOrder().getCurrencyCode() %}
{% if standard[currencyCode] is defined %}
{{ rates.create(standard[currencyCode], 'Standard post', 'standard') }}
{% endif %}
A currency with no price in the list gets no rate, which keeps a method from charging an amount meant for another currency. How a cart's currency is chosen is on Prices and currencies.
A rate's name is stored as the template wrote it. On a multilingual store, give every rate a serviceLevel (standard, express, international) and let the checkout page print the label for that service level in the page's language, falling back to the rate's name.
One method or several
A method's categories and warehouses apply to every rate it creates, so add a method wherever those differ. A store with bulky goods adds a category Oversize on those products and a second method, "Courier", that carries Parcel and Oversize; a fulfilment holding an oversize item then gets its rates from Courier alone. A store that offers collection adds a method with its pickup warehouses whose template creates a free rate when the fulfilment has a warehouse: {if $fulfillment->getWarehouse()}{$rates->create('0', 'Collect in store', 'pickup')}{/if} in Smarty, {% if fulfillment.getWarehouse() %}{{ rates.create('0', 'Collect in store', 'pickup') }}{% endif %} in Twig.
The rates at checkout
Creating shipping on a checkout runs every active method that may carry the fulfilment and gathers the rates they create into one list, which the shopper chooses from (Creating shipping and showing the rates). A rate lasts for the Lifetime, an hour by default, and a checkout whose selected rate has expired creates shipping again (A rate lasts an hour). Tax on the shipping is the tax provider's, from its Shipping Tax Code (Tax categories and provider codes).