Functional Weave
Code in Python

retail.shipping-rate

Delivery charge from a dated rate table by service, zone, chargeable weight and size, with a free-over threshold.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 20 tests, run in TypeScript, Python and Rust.

What it does

Looks up a delivery charge on a rate card: pick the service and zone, find the cheapest weight band the parcel fits on the order date, and waive the charge when the basket reaches the free-delivery threshold.

## The rate card is an argument

For example

  • shipping_rate(bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £20.00, 2026-03-01) → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.95, price £3.95, free false a small parcel in the first band
  • shipping_rate(bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £50.00, 2026-03-01) → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.95, price £0.00, free true free delivery at exactly the threshold
  • shipping_rate(bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £49.99, 2026-03-01) → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.95, price £3.95, free false one penny under the threshold pays

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def shipping_rate(bands: Sequence[ShippingBand], service: str, zone: str, parcel: Parcel, basket_total: Money, on_date: str) -> ShippingQuote
bandsShippingBand[]the merchant's rate card
servicestring
zonestring
parcelParcel
basket_totalMoneywhat counts towards free delivery, usually goods after discounts
on_datedatethe order date, which decides the rate card in force
returnsShippingQuote

The types it declares, generated into your project

@dataclass(frozen=True)
class ShippingBand:
    """One row of a rate card: a weight band for one service and zone."""

    service: str
    zone: str
    #: heaviest chargeable weight the band takes
    max_grams: int
    #: longest side allowed; null for no limit
    max_length_mm: Optional[int]
    #: cm3 per kg, e.g. 5000; null to charge on actual weight only
    volumetric_divisor: Optional[int]
    #: the band's billing step, 1 for none
    round_up_to_grams: int
    price: Money
    #: delivery is free when basketTotal is at least this; null for never
    free_over: Optional[Money]
    valid_from: str
    #: last day in force, inclusive; null while current
    valid_to: Optional[str]

@dataclass(frozen=True)
class Parcel:
    """The packed parcel."""

    length_mm: int
    width_mm: int
    height_mm: int
    actual_grams: int

@dataclass(frozen=True)
class ShippingQuote:
    """The band that applies and what it costs."""

    service: str
    zone: str
    #: the weight the band charged on
    chargeable_grams: int
    #: the band's upper limit, to show "up to 2 kg"
    band_max_grams: int
    #: the band's price before any free-delivery threshold
    standard_price: Money
    #: what the customer pays
    price: Money
    #: true when the free-over threshold was met
    free: bool

Your code names it in one line, in the file that uses it

from fune.retail.shipping_rate import shipping_rate  # retail.shipping-rate@^1
impl/python.py · 84 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re
from typing import Optional, Sequence

from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_compare import compare_money  ← from money.compare ^1.0.0 · built alongside by fune
from .retail_volumetric_weight import chargeable_weight  ← from retail.volumetric-weight ^1.0.0 · built alongside by fune
from .retail_shipping_rate_types import ShippingBand, Parcel, ShippingQuote

_ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")


def _whole(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def _chargeable_in(band: ShippingBand, parcel: Parcel) -> int:
    if band.volumetric_divisor is None:
        return round_div(parcel.actual_grams, band.round_up_to_grams, "up") * band.round_up_to_grams
    return chargeable_weight(
        parcel.length_mm,
        parcel.width_mm,
        parcel.height_mm,
        parcel.actual_grams,
        band.volumetric_divisor,
        band.round_up_to_grams,
    ).chargeable_grams


def shipping_rate(
    bands: Sequence[ShippingBand],
    service: str,
    zone: str,
    parcel: Parcel,
    basket_total: Money,
    on_date: str,
) -> ShippingQuote:
    """The delivery charge for a parcel: the smallest band on the rate card in
    force on the order date that takes it, free when the basket reaches the
    band's threshold.
    """
    if not isinstance(on_date, str) or not _ISO_DATE.fullmatch(on_date):
        raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,))
    for d in (parcel.length_mm, parcel.width_mm, parcel.height_mm):
        if not _whole(d) or d < 1:
            raise ValueError("dimensions must be 1 mm or more, received %r" % (d,))
    if not _whole(parcel.actual_grams) or parcel.actual_grams < 0:
        raise ValueError("actualGrams must not be negative, received %r" % (parcel.actual_grams,))
    longest = max(parcel.length_mm, parcel.width_mm, parcel.height_mm)

    best: Optional[ShippingBand] = None
    best_grams = 0
    for band in bands:
        if band.service != service or band.zone != zone:
            continue
        if on_date < band.valid_from or (band.valid_to is not None and on_date > band.valid_to):
            continue
        if band.max_length_mm is not None and longest > band.max_length_mm:
            continue
        grams = _chargeable_in(band, parcel)
        if grams > band.max_grams:
            continue
        if (
            best is None
            or band.max_grams < best.max_grams
            or (band.max_grams == best.max_grams and compare_money(band.price, best.price) < 0)
        ):
            best = band
            best_grams = grams
    if best is None:
        raise ValueError(
            'no shipping band for service "%s" to zone "%s" on %s fits this parcel' % (service, zone, on_date)
        )

    free = best.free_over is not None and compare_money(basket_total, best.free_over) >= 0
    return ShippingQuote(
        service=service,
        zone=zone,
        chargeable_grams=best_grams,
        band_max_grams=best.max_grams,
        standard_price=best.price,
        price=money(0, best.price.currency) if free else best.price,
        free=free,
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add retail.shipping-rate
Download for Python retail.shipping-rate-1.0.1-python.fune · 53,210 bytes sha256 c99873caa68799749624aba37350805e7f8f76b00c181b6c86b135ab4a96098e

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./retail.shipping-rate-1.0.1-python.fune, or fetch it from a terminal with fune pull retail.shipping-rate@1.0.1:python.

The whole function, every language, is one file too: retail.shipping-rate-1.0.1.fune, 62,057 bytes, sha256 39fd330b80771b492b4376d7ecbcaef5ad74059b5172124f042f30150bef46c3. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before retail.shipping-rate

after — your function gets the result and the arguments, and returns the final result.

# fune: after retail.shipping-rate

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

# fune: replace math.round-div in retail.shipping-rate
# fune: replace money.amount in retail.shipping-rate
# fune: replace money.compare in retail.shipping-rate
# fune: replace retail.volumetric-weight in retail.shipping-rate

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show retail.shipping-rate --steps.

# fune: step retail.shipping-rate after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a small parcel in the first band bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £20.00, 2026-03-01 → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.95, price £3.95, free false
free delivery at exactly the threshold bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £50.00, 2026-03-01 → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.95, price £0.00, free true
one penny under the threshold pays bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £49.99, 2026-03-01 → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.95, price £3.95, free false
exactly the band's maximum weight fits bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 2,000, £0.00, 2026-03-01 → service standard, zone UK, chargeable grams 2,000, band max grams 2,000, standard price £3.95, price £3.95, free false
too heavy for the first band moves up, rounded to the half kilo bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 2,300, £0.00, 2026-03-01 → service standard, zone UK, chargeable grams 2,500, band max grams 10,000, standard price £6.95, price £6.95, free false
too long for the first band: charged on volume in the next bands ×6, standard, UK, length mm 600, width mm 100, height mm 100, actual grams 800, £0.00, 2026-03-01 → service standard, zone UK, chargeable grams 1,500, band max grams 10,000, standard price £6.95, price £6.95, free false
a bulky light parcel is charged on 24 kg volumetric and is never free bands ×6, standard, UK, length mm 800, width mm 500, height mm 300, actual grams 3,000, £100.00, 2026-03-01 → service standard, zone UK, chargeable grams 24,000, band max grams 30,000, standard price £12.95, price £12.95, free false
express has its own band bands ×6, express, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £100.00, 2026-03-01 → service express, zone UK, chargeable grams 1,500, band max grams 10,000, standard price £9.95, price £9.95, free false
another zone has its own price bands ×6, standard, EU, length mm 300, width mm 200, height mm 100, actual grams 800, £100.00, 2026-03-01 → service standard, zone EU, chargeable grams 800, band max grams 2,000, standard price £9.95, price £9.95, free false
last year's rate card, with its lower threshold bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £40.00, 2025-06-01 → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.50, price £0.00, free true
Show the other 10 tests
CaseArgumentsExpected
the last day of a rate card is inclusive bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £20.00, 2025-12-31 → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.50, price £3.50, free false
the next day the new card applies bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £20.00, 2026-01-01 → service standard, zone UK, chargeable grams 800, band max grams 2,000, standard price £3.95, price £3.95, free false
the orientation of the parcel does not matter bands ×6, standard, UK, length mm 100, width mm 600, height mm 100, actual grams 800, £0.00, 2026-03-01 → service standard, zone UK, chargeable grams 1,500, band max grams 10,000, standard price £6.95, price £6.95, free false
a zone with no bands is an error bands ×6, standard, US, length mm 300, width mm 200, height mm 100, actual grams 800, £0.00, 2026-03-01 → error: no shipping band for service "standard" to zone "US" on 2026-03-01 fits this parcel
a parcel too heavy for every band is an error bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 40,000, £0.00, 2026-03-01 → error: no shipping band for service "standard" to zone "UK"
a date before any rate card is an error bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £0.00, 2024-12-31 → error: no shipping band
a basket in another currency cannot meet a sterling threshold bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, €60.00, 2026-03-01 → error: currency mismatch
a malformed date is an error bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £0.00, 1 March 2026 → error: onDate must be an ISO date (YYYY-MM-DD)
a trailing newline is not part of an ISO date (onDate) bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £0.00, 2026-09-16 → error: onDate must be an ISO date (YYYY-MM-DD)
non-ASCII digits are not an ISO date (onDate) bands ×6, standard, UK, length mm 300, width mm 200, height mm 100, actual grams 800, £0.00, ٢٠٢٦-09-16 → error: onDate must be an ISO date (YYYY-MM-DD)

More from the author

Delivery prices are the merchant's own commercial terms, negotiated with a carrier and changed whenever the merchant likes, not published rules. So the table is passed in as `bands`, not shipped as registry data; each row still carries `validFrom` and `validTo`, so one card can hold last year's prices and this year's and the order date picks between them. The table below, used by the vectors, is **illustrative only**: it is not any carrier's price list.

| service | zone | up to | longest side | divisor | step | price | free over | from | to | |---|---|---|---|---|---|---|---|---|---| | standard | UK | 2 kg | 450 mm | none | 1 g | £3.95 | £50 | 2026-01-01 | | | standard | UK | 10 kg | 1000 mm | 5000 | 500 g | £6.95 | £50 | 2026-01-01 | | | standard | UK | 30 kg | 1500 mm | 5000 | 1 kg | £12.95 | never | 2026-01-01 | | | express | UK | 10 kg | 1000 mm | 5000 | 500 g | £9.95 | never | 2026-01-01 | | | standard | EU | 2 kg | 450 mm | none | 1 g | £9.95 | never | 2026-01-01 | | | standard | UK | 2 kg | 450 mm | none | 1 g | £3.50 | £40 | 2025-01-01 | 2025-12-31 |

## How a band is chosen

A band matches when its service and zone are the ones asked for, the order date is within its dates (both ends inclusive), the parcel's longest side is within `maxLengthMm`, and the parcel's chargeable weight in that band is within `maxGrams`. Chargeable weight depends on the band: with a `volumetricDivisor` it is the greater of actual and volumetric weight (`retail.volumetric-weight`), rounded up to the band's step; without one it is the actual weight rounded up to the step. Of the matching bands the one with the smallest `maxGrams` wins, then the cheaper, then the first listed.

No matching band is an error that names the service, zone and date. That is deliberate: a checkout that quietly charges nothing for a parcel it cannot ship is worse than one that refuses it.

## Free delivery

When the band has `freeOver` and `basketTotal` is at least that amount, the price is zero and `free` is true; `standardPrice` still says what it would have cost, for "you saved £3.95" messages. The threshold is compared exactly, so £49.99 is not £50. Which total counts (before or after coupons, with or without VAT) is the merchant's rule: pass that total.

1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.

Files

PathBytes
README.md2,629
impl/python.py3,096
impl/rust.rs5,728
impl/typescript.ts2,741
vectors.json37,254