Functional Weave
Code in Rust

Suites · ecommerce

Online shop and retail

Sell goods online or over the counter: basket totals with promotions and VAT, coupons, delivery charges, gift cards, loyalty points, refunds and product codes.

57 capabilities (24 core, 33 optional) and 17 gaps, in build order. Put each line in the file that calls it, then fune build.

Shop VAT is UK first; EU distance selling is covered by retail.oss-vat and US sales tax only at state level.

Calculates tax from published rules; it is not tax advice. Have an accountant check how the app uses it before anyone files a return.

1. Product catalogue

Code and price the products you sell.

retail.sku-generate core
Builds a deterministic SKU code from a prefix and product attributes.
Build a deterministic stock-keeping unit code from a prefix and product attributes.
fune!(retail.sku-generate@^1);  // then call generate_sku(…)
validation.gtin core
Checks an EAN-13, UPC-A or GTIN-14 barcode number before it goes on a product.
Check an EAN-8, UPC-A, EAN-13 or GTIN-14 barcode number against the GS1 mod-10 check digit.
fune!(validation.gtin@^1);  // then call validate_gtin(…)
retail.price-rounding optional
Snaps prices to price points: .99 charm pricing, nearest 5p or 10p.
Snap a price to a price point: charm pricing (.99, .95), nearest 5p or 10p, rounding up, down or to nearest.
fune!(retail.price-rounding@^1);  // then call round_price(…)
retail.price-per-unit optional
Unit price for shelf-edge labels and listings: per kg, per 100 g, per litre.
Unit price for shelf-edge labels: price per kg, per 100 g, per litre, per 100 ml, per metre or per item.
fune!(retail.price-per-unit@^1);  // then call unit_price(…)
finance.markup-price optional
Selling price from cost and a target margin or markup.
Selling price from a cost and a target margin or markup in basis points, with explicit rounding.
fune!(finance.markup-price@^1);  // then call markup_price(…)
finance.margin optional
Margin and markup on a product, so the two are never confused.
Profit on a sale with both the margin and the markup, so the two are never confused.
fune!(finance.margin@^1);  // then call margin(…)
  • product-search Faceted product search, filters and variants (size, colour) are your catalogue's data model.
  • product-images Image upload, resizing and a CDN for product photos.

2. Basket and checkout

Turn a basket into the amount to charge, with offers and VAT applied in the right order.

retail.basket-total core
The checkout total: line prices, promotions, a basket discount, VAT per line and delivery.
Total a basket: line prices, promotions, a basket discount, VAT per line and delivery, into one checked total.
fune!(retail.basket-total@^1);  // then call basket_total(…)
retail.promotion-apply core
Applies BOGOF, 3-for-2, percent off and multibuy deals, best for the customer, shared back to lines.
Apply BOGOF, 3-for-2, percent off, amount off and multibuy deals to a basket, best for the customer, allocated to lines.
fune!(retail.promotion-apply@^1);  // then call apply_promotions(…)
retail.coupon-validate core
Checks a coupon against the basket: dates, usage limits, eligible products, minimum spend.
Check a coupon against a basket: dates, usage limits, eligible products and minimum spend, with reason codes.
fune!(retail.coupon-validate@^1);  // then call validate_coupon(…)
retail.gift-card-balance optional
Pays part or all of an order from a gift card and says what is still due.
Apply a payment from a gift card, redeeming part of the balance or all of it, and say what is still due.
fune!(retail.gift-card-balance@^1);  // then call redeem_gift_card(…)
retail.loyalty-points optional
Points redeemed against the bill and earned on what is paid, with tier multipliers.
Loyalty points for a purchase: redeem in blocks, earn on what is paid, with tier multipliers.
fune!(retail.loyalty-points@^1);  // then call loyalty_points(…)
  • payment-processing Taking card, wallet or BNPL payments through a provider (Stripe, Adyen, PayPal), 3-D Secure and webhooks.
  • basket-storage Persisting baskets across sessions and reserving stock during checkout.

3. Sales tax

Charge the right tax for where the customer is.

finance.tax.vat-rate core Unreviewed
The UK VAT rate in force for a product category on the date of sale.
Look up the VAT rate in force for a jurisdiction, category and date.
fune!(finance.tax.vat-rate@^1);  // then call vat_rate(…)
retail.oss-vat optional
Which EU member state's VAT applies to a distance sale under OSS and the EUR 10,000 threshold.
EU distance-selling VAT: which member state's standard rate applies under OSS and the EUR 10,000 threshold, and the VAT.
fune!(retail.oss-vat@^1);  // then call oss_vat(…)
retail.sales-tax-us optional Unreviewed
US sales tax: the state's base rate from dated data plus a local rate you supply.
US sales tax on a sale: the state's base rate from dated data plus a local rate the caller supplies.
fune!(retail.sales-tax-us@^1);  // then call sales_tax_us(…)
  • us-local-tax-rates US county and city sales tax rates by address: Functional Weave takes the local rate as an input.

4. Delivery

Charge for delivery and promise a date.

retail.shipping-rate core
Delivery charge from a dated rate table by service, zone, weight and size, with free-over thresholds.
Delivery charge from a dated rate table by service, zone, chargeable weight and size, with a free-over threshold.
fune!(retail.shipping-rate@^1);  // then call shipping_rate(…)
retail.volumetric-weight core
Chargeable parcel weight: the greater of actual and volumetric weight.
Chargeable parcel weight: the greater of actual and volumetric (dimensional) weight, rounded up to a billing step.
fune!(retail.volumetric-weight@^1);  // then call chargeable_weight(…)
logistics.delivery-date optional
Estimated dispatch and delivery dates from order time, cut-off and service days.
Estimated dispatch and delivery dates from order time, dispatch cut-off, service days, weekends and UK bank holidays.
fune!(logistics.delivery-date@^1);  // then call delivery_date(…)
validation.uk-postcode optional
Validates the delivery postcode and puts it in canonical form.
Validate a UK postcode and normalise it to canonical upper case with one space before the inward code.
fune!(validation.uk-postcode@^2);  // then call validate_uk_postcode(…)
  • carrier-labels Booking collections and printing labels through a carrier's API (Royal Mail, DPD, Evri).

5. Returns and refunds

Refund returned items exactly.

retail.refund-calculate core
Refund for returned lines, sharing basket discounts back so each refund is exact to the penny.
Refund for returned items, sharing basket discounts back across the sale so every refund is exact to the penny.
fune!(retail.refund-calculate@^1);  // then call calculate_refund(…)

6. Stock

Know when to reorder and what stock is worth.

inventory.reorder-point optional
Reorder point from lead-time demand plus safety stock.
Reorder point: demand over the supplier lead time, rounded up to whole units, plus safety stock.
fune!(inventory.reorder-point@^1);  // then call reorder_point(…)
inventory.valuation-weighted-average optional
Stock value and cost of sales at weighted average cost.
Perpetual weighted average cost: stock value and cost of sales from a movement ledger, rounding once per issue.
fune!(inventory.valuation-weighted-average@^1);  // then call weighted_average_valuation(…)
  • stock-levels Tracking stock on hand per location as orders and deliveries happen.

7. Sales reports

See how the shop is doing.

math.percent-change optional
Sales up or down on last period, in basis points.
Percentage change from one value to another, in basis points, with an explicit rounding mode.
fune!(math.percent-change@^1);  // then call percent_change(…)
charts.bar-chart optional
Bar chart geometry for sales by day or product.
Complete bar chart geometry as data, grouped or stacked: plot area, axes, gridlines, bar rectangles, colours, legend.
fune!(charts.bar-chart@^1);  // then call bar_chart(…)

8. Amounts from money-basics

Hold money as currency-tagged integer minor units and do plain arithmetic on it safely.

money.amount core
The Money type ({ minor, currency }) every other money capability takes and returns.
A currency-tagged monetary amount held in integer minor units.
fune!(money.amount@^1);  // then call money(…)
money.add core
Adds and subtracts amounts, refusing to mix currencies by accident.
Add, subtract and negate monetary amounts, refusing to mix currencies.
fune!(money.add@^1);  // then call add_money(…)
money.sum core
Totals a list of amounts, with an explicit currency for an empty list.
Total a list of monetary amounts, with an explicit currency for the empty case.
fune!(money.sum@^1);  // then call sum_money(…)
money.compare core
Compares, mins, maxes and clamps amounts of one currency without unwrapping them.
Compare two amounts of one currency, refusing to compare across currencies; min, max and clamp too.
fune!(money.compare@^2);  // then call compare_money, min_money, max_money, clamp_money

9. Rates and rounding from money-basics

Apply percentages and divide amounts with the rounding stated, never a float.

money.apply-rate core
Applies a basis-point rate (VAT, discount, commission) to an amount with explicit rounding.
Apply a rate expressed in basis points to a monetary amount, with an explicit rounding mode.
fune!(money.apply-rate@^1);  // then call apply_rate(…)
math.round-div core
Integer division with a named rounding mode, for any money maths the others do not cover.
Integer division with an explicit rounding mode, for money arithmetic that must not drift.
fune!(math.round-div@^1);  // then call round_div(…)
math.basis-points optional
Converts "12.5%" to and from basis points exactly, for rates typed in by users.
Convert a rate between percent, basis points and a plain ratio exactly, as decimal text or integer basis points.
fune!(math.basis-points@^2);  // then call convert_rate, to_basis_points, from_basis_points
math.rational optional
Exact fractions for rates that must not drift across many steps.
Exact fraction arithmetic, always reduced, for rates and ratios that must not drift.
fune!(math.rational@^2);  // then call calculate_rational, rational, add_rational, subtract_rational, multiply_rational, divide_rational, compare_rational, rational_to_integer
math.round-div-big optional
Rounded division on numbers past 2^53, for very large totals.
Divide two integers of any size with an explicit rounding mode, on decimal strings, for exact money sums past 2^53.
fune!(math.round-div-big@^1);  // then call round_div_big(…)

10. Splitting from money-basics

Divide an amount between people, periods or lines so the parts add back to the whole.

money.allocate core
Splits an amount by ratios (60/40, by quantity) without losing or inventing a penny.
Split an amount across ratios without losing or inventing a single minor unit.
fune!(money.allocate@^1);  // then call allocate(…)
money.split-even optional
Splits an amount into n near-equal parts that sum exactly, for instalments or shares.
Split an amount into n near-equal parts that add back up to exactly the whole.
fune!(money.split-even@^1);  // then call split_even(…)

11. Display and input from money-basics

Show amounts to people and read what they type.

money.format core
Renders an amount as text with the currency's real number of decimal places.
Render a monetary amount as text, using the currency's real minor-unit precision.
fune!(money.format@^1);  // then call format_money(…)
money.parse optional
Reads "£1,234.50" or "1.234,50 €" typed by a user into minor units, strictly.
Parse "£1,234.50" or "1.234,50 €" into integer minor units, strictly: no guessing, no rounding.
fune!(money.parse@^1);  // then call parse_money(…)
money.currency-digits optional
How many decimal places each ISO 4217 currency has, for input boxes and validation.
How many decimal places a currency's minor unit has, from the ISO 4217 list.
fune!(money.currency-digits@^1);  // then call currency_digits(…)

12. Other currencies from money-basics

Convert between currencies at a rate you hold.

money.convert optional
Converts an amount at a supplied exact rate with explicit rounding.
Convert an amount to another currency at a supplied exact rate, with an explicit rounding mode.
fune!(money.convert@^1);  // then call convert_money(…)

13. VAT from uk-tax

Work out VAT from the dated rates HMRC publishes.

finance.tax.add-vat core Unreviewed
Net to gross: VAT on a net amount at the rate on the date of supply.
Add VAT to a net amount using the rate in force on the date of supply.
fune!(finance.tax.add-vat@^1);  // then call add_vat(…)
finance.tax.remove-vat core Unreviewed
Gross to net: the VAT already inside a VAT-inclusive price.
Extract the VAT already included in a gross amount, using the rate in force on the date of supply.
fune!(finance.tax.remove-vat@^1);  // then call remove_vat(…)
finance.tax.vat-return optional Unreviewed
The nine-box VAT return totals for a period from categorised sales and purchases.
UK nine-box VAT return totals for a period from categorised sales and purchases (VAT Notice 700/12).
fune!(finance.tax.vat-return@^1);  // then call vat_return(…)
finance.tax.vat-flat-rate optional Unreviewed
VAT due under the Flat Rate Scheme by business sector, for small businesses on it.
UK VAT Flat Rate Scheme: VAT due on gross turnover at the sector's dated rate, limited cost rate and first-year 1% off.
fune!(finance.tax.vat-flat-rate@^1);  // then call flat_rate_vat(…)

14. Tax periods from uk-tax

Put dates into the UK periods tax is worked out over.

dates.uk-tax-year core
Which UK tax year (6 April to 5 April) a date falls in.
The UK personal tax year containing a date: 6 April to 5 April, with HMRC's label.
fune!(dates.uk-tax-year@^1);  // then call uk_tax_year(…)

15. Tax identifiers from uk-tax

Check the numbers HMRC and Companies House issue before storing them.

validation.uk-vat-number core
Checks a UK VAT registration number's checksum.
Check a UK VAT registration number against HMRC's mod-97 checksum, including the post-2010 9755 variant.
fune!(validation.uk-vat-number@^1);  // then call is_uk_vat_number(…)
validation.uk-utr optional
Checks a Unique Taxpayer Reference's check digit.
Check an HMRC Unique Taxpayer Reference's modulus 11 check digit, accepting the K and spacing people type.
fune!(validation.uk-utr@^2);  // then call validate_uk_utr(…)
validation.uk-company-number optional
Checks and pads a Companies House company number.
Check a Companies House company number's format and prefix (SC, NI, OC, LP...) and zero-pad it to eight characters.
fune!(validation.uk-company-number@^2);  // then call validate_uk_company_number(…)

16. Contact details from validation-basics

Check and normalise a person's contact details before storing them.

validation.email core
Is an email address plausible, by a documented subset of RFC 5322.
Is this a plausible email address? A documented, pragmatic subset of RFC 5322, not the full grammar.
fune!(validation.email@^1);  // then call is_email(…)
validation.phone-e164 core
Normalises a phone number to +44… E.164 for a default country.
Normalise a phone number to E.164 (+447700900123) using a default country's trunk and dialling prefixes.
fune!(validation.phone-e164@^2);  // then call validate_phone_e164(…)
text.normalise-name optional
Tidies a typed name: spacing and title case with Mc, O' and hyphen rules.
Tidy a personal name: trim, collapse whitespace and title-case, with Mc, Mac, O', hyphen and van/de rules.
fune!(text.normalise-name@^1);  // then call normalise_name(…)
auth.normalise-email optional
Lower-cases an email's domain for storage and lookup.
Trim an email address and lower-case its domain for storage and lookup, or null if it is not a plausible address.
fune!(auth.normalise-email@^1);  // then call normalise_email(…)

17. Bank details from validation-basics

Check payee and customer bank details before money moves.

validation.iban core
Checks an IBAN's mod-97 checksum and its country's length.
Check an IBAN against the ISO 13616 mod-97-10 checksum and the registered length for its country.
fune!(validation.iban@^1);  // then call is_iban(…)
validation.bic optional
Checks a SWIFT/BIC code's format and splits it.
Check the format of a SWIFT/BIC code (8 or 11 characters) and split it into its parts.
fune!(validation.bic@^1);  // then call validate_bic(…)
validation.uk-sort-code-account optional
Checks a UK sort code and account number by the Vocalink modulus rules, against a table you supply.
Check a UK sort code and account number with the Vocalink/Pay.UK modulus rules, against a table you supply.
fune!(validation.uk-sort-code-account@^3);  // then call validate_uk_sort_code_account(…)
validation.uk-modulus-table optional
Parses Vocalink's VALACDOS and SCSUBTAB files into that table.
Parse Vocalink's VALACDOS.txt and SCSUBTAB.txt into the table UK sort code modulus checking needs.
fune!(validation.uk-modulus-table@^1);  // then call parse_uk_modulus_table(…)

18. Identifiers and check digits from validation-basics

Catch mistyped reference numbers by their check digits.

validation.luhn optional
Luhn mod-10 check, for card numbers and many reference numbers.
Check a digit string against the Luhn mod-10 checksum used by payment cards and many identifiers.
fune!(validation.luhn@^1);  // then call is_luhn(…)
validation.lei optional
Checks a Legal Entity Identifier's check digits.
Check a 20-character Legal Entity Identifier against its ISO 17442 / ISO 7064 MOD 97-10 check digits.
fune!(validation.lei@^1);  // then call validate_lei(…)

19. Displaying sensitive values from validation-basics

Show stored identifiers without exposing them.

text.mask optional
Masks all but the last digits of a card, account or phone number.
Mask all but the last n characters of a card, account or phone number, optionally keeping separators.
fune!(text.mask@^1);  // then call mask(…)

Gaps

What this kind of app usually needs that Functional Weave does not have yet: write these yourself, or use a service.

  • order-emails Order confirmation, dispatch and refund emails.
  • customer-accounts Customer sign-up and login: see the auth suite.
  • fx-rate-source Where exchange rates come from: money.convert takes a rate you supply; fetching and storing daily rates (ECB, a provider's API) is yours.
  • money-storage Storing amounts: the database columns (integer minor units plus a currency code) and their migrations are yours.
  • mtd-submission Making Tax Digital: submitting the return to HMRC's VAT API (OAuth, fraud-prevention headers, obligations) is yours; caps gives the nine boxes.
  • corporation-tax Corporation tax and self-assessment income tax computations are not in caps.
  • vat-number-lookup Checking that a VAT number is registered (HMRC's lookup API) is network work; caps checks the checksum only.
  • address-lookup Turning a postcode into a list of addresses (Royal Mail PAF or a lookup API) is network work and not in caps.
  • email-deliverability Whether a mailbox exists (MX lookups, a confirmation email): validation.email checks the syntax only.
  • vocalink-data The Vocalink modulus tables themselves: download them from Pay.UK under its terms; caps parses them but does not ship them.