{
  "id": "ecommerce",
  "title": "Online shop and retail",
  "summary": "Sell goods online or over the counter: basket totals with promotions and VAT, coupons, delivery charges, gift cards, loyalty points, refunds and product codes.",
  "synonyms": [
    "ecommerce",
    "e-commerce",
    "online shop",
    "online store",
    "web shop",
    "webshop",
    "shop",
    "store",
    "retail",
    "checkout",
    "shopping cart",
    "basket",
    "point of sale",
    "pos",
    "till",
    "shopify",
    "woocommerce",
    "sell products online",
    "merchandise"
  ],
  "apps": [
    "online shop",
    "ecommerce store",
    "point of sale system",
    "retail till",
    "shopping cart and checkout",
    "marketplace seller tool",
    "store front for a small brand"
  ],
  "language": "rust",
  "includes": [
    "money-basics",
    "uk-tax",
    "validation-basics"
  ],
  "includedBy": [],
  "caveats": [
    "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."
  ],
  "sections": [
    {
      "key": "catalogue",
      "title": "Product catalogue",
      "purpose": "Code and price the products you sell.",
      "from": "ecommerce",
      "members": [
        {
          "id": "retail.sku-generate",
          "tier": "core",
          "why": "Builds a deterministic SKU code from a prefix and product attributes.",
          "section": "catalogue",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Build a deterministic stock-keeping unit code from a prefix and product attributes.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "generateSku"
          ],
          "signatures": [
            "pub fn generate_sku(prefix: &str, attributes: &[String], segment_length: i64) -> String"
          ],
          "reference": "fune!(retail.sku-generate@^1);  // then call generate_sku(…)",
          "url": "http://www.functionalweave.com/functions/retail.sku-generate"
        },
        {
          "id": "validation.gtin",
          "tier": "core",
          "why": "Checks an EAN-13, UPC-A or GTIN-14 barcode number before it goes on a product.",
          "section": "catalogue",
          "from": "ecommerce",
          "version": "1.1.0",
          "summary": "Check an EAN-8, UPC-A, EAN-13 or GTIN-14 barcode number against the GS1 mod-10 check digit.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateGtin"
          ],
          "signatures": [
            "pub fn validate_gtin(value: &str) -> GtinCheck"
          ],
          "reference": "fune!(validation.gtin@^1);  // then call validate_gtin(…)",
          "url": "http://www.functionalweave.com/functions/validation.gtin"
        },
        {
          "id": "retail.price-rounding",
          "tier": "optional",
          "why": "Snaps prices to price points: .99 charm pricing, nearest 5p or 10p.",
          "section": "catalogue",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Snap a price to a price point: charm pricing (.99, .95), nearest 5p or 10p, rounding up, down or to nearest.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "roundPrice"
          ],
          "signatures": [
            "pub fn round_price(price: &Money, step: i64, ending: i64, direction: &str) -> Money"
          ],
          "reference": "fune!(retail.price-rounding@^1);  // then call round_price(…)",
          "url": "http://www.functionalweave.com/functions/retail.price-rounding"
        },
        {
          "id": "retail.price-per-unit",
          "tier": "optional",
          "why": "Unit price for shelf-edge labels and listings: per kg, per 100 g, per litre.",
          "section": "catalogue",
          "from": "ecommerce",
          "version": "1.0.1",
          "summary": "Unit price for shelf-edge labels: price per kg, per 100 g, per litre, per 100 ml, per metre or per item.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "unitPrice"
          ],
          "signatures": [
            "pub fn unit_price(price: &Money, quantity: &str, unit: &str, per: &str) -> Money"
          ],
          "reference": "fune!(retail.price-per-unit@^1);  // then call unit_price(…)",
          "url": "http://www.functionalweave.com/functions/retail.price-per-unit"
        },
        {
          "id": "finance.markup-price",
          "tier": "optional",
          "why": "Selling price from cost and a target margin or markup.",
          "section": "catalogue",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Selling price from a cost and a target margin or markup in basis points, with explicit rounding.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "markupPrice"
          ],
          "signatures": [
            "pub fn markup_price(cost: &Money, basis_points: i64, basis: &str, mode: &RoundingMode) -> Money"
          ],
          "reference": "fune!(finance.markup-price@^1);  // then call markup_price(…)",
          "url": "http://www.functionalweave.com/functions/finance.markup-price"
        },
        {
          "id": "finance.margin",
          "tier": "optional",
          "why": "Margin and markup on a product, so the two are never confused.",
          "section": "catalogue",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Profit on a sale with both the margin and the markup, so the two are never confused.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "margin"
          ],
          "signatures": [
            "pub fn margin(cost: &Money, price: &Money) -> MarginBreakdown"
          ],
          "reference": "fune!(finance.margin@^1);  // then call margin(…)",
          "url": "http://www.functionalweave.com/functions/finance.margin"
        }
      ],
      "gaps": [
        {
          "key": "product-search",
          "text": "Faceted product search, filters and variants (size, colour) are your catalogue's data model.",
          "section": "catalogue",
          "from": "ecommerce"
        },
        {
          "key": "product-images",
          "text": "Image upload, resizing and a CDN for product photos.",
          "section": "catalogue",
          "from": "ecommerce"
        }
      ]
    },
    {
      "key": "basket",
      "title": "Basket and checkout",
      "purpose": "Turn a basket into the amount to charge, with offers and VAT applied in the right order.",
      "from": "ecommerce",
      "members": [
        {
          "id": "retail.basket-total",
          "tier": "core",
          "why": "The checkout total: line prices, promotions, a basket discount, VAT per line and delivery.",
          "section": "basket",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Total a basket: line prices, promotions, a basket discount, VAT per line and delivery, into one checked total.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "basketTotal"
          ],
          "signatures": [
            "pub fn basket_total(lines: &[BasketLine], promotions: &[Promotion], basket_discount: &Money, delivery: &Money, delivery_tax_category: &str, prices_include_vat: bool, jurisdiction: &str, on_date: &str) -> Basket"
          ],
          "reference": "fune!(retail.basket-total@^1);  // then call basket_total(…)",
          "url": "http://www.functionalweave.com/functions/retail.basket-total"
        },
        {
          "id": "retail.promotion-apply",
          "tier": "core",
          "why": "Applies BOGOF, 3-for-2, percent off and multibuy deals, best for the customer, shared back to lines.",
          "section": "basket",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Apply BOGOF, 3-for-2, percent off, amount off and multibuy deals to a basket, best for the customer, allocated to lines.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "applyPromotions"
          ],
          "signatures": [
            "pub fn apply_promotions(lines: &[PromoLine], promotions: &[Promotion]) -> PromotionResult"
          ],
          "reference": "fune!(retail.promotion-apply@^1);  // then call apply_promotions(…)",
          "url": "http://www.functionalweave.com/functions/retail.promotion-apply"
        },
        {
          "id": "retail.coupon-validate",
          "tier": "core",
          "why": "Checks a coupon against the basket: dates, usage limits, eligible products, minimum spend.",
          "section": "basket",
          "from": "ecommerce",
          "version": "1.0.1",
          "summary": "Check a coupon against a basket: dates, usage limits, eligible products and minimum spend, with reason codes.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateCoupon"
          ],
          "signatures": [
            "pub fn validate_coupon(coupon: &Coupon, lines: &[CouponLine], customer_uses: i64, on_date: &str) -> CouponCheck"
          ],
          "reference": "fune!(retail.coupon-validate@^1);  // then call validate_coupon(…)",
          "url": "http://www.functionalweave.com/functions/retail.coupon-validate"
        },
        {
          "id": "retail.gift-card-balance",
          "tier": "optional",
          "why": "Pays part or all of an order from a gift card and says what is still due.",
          "section": "basket",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Apply a payment from a gift card, redeeming part of the balance or all of it, and say what is still due.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "redeemGiftCard"
          ],
          "signatures": [
            "pub fn redeem_gift_card(balance: &Money, amount_due: &Money) -> GiftCardRedemption"
          ],
          "reference": "fune!(retail.gift-card-balance@^1);  // then call redeem_gift_card(…)",
          "url": "http://www.functionalweave.com/functions/retail.gift-card-balance"
        },
        {
          "id": "retail.loyalty-points",
          "tier": "optional",
          "why": "Points redeemed against the bill and earned on what is paid, with tier multipliers.",
          "section": "basket",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Loyalty points for a purchase: redeem in blocks, earn on what is paid, with tier multipliers.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "loyaltyPoints"
          ],
          "signatures": [
            "pub fn loyalty_points(scheme: &LoyaltyScheme, spend: &Money, qualifying_spend: &Money, balance: i64, redeem_points: i64) -> LoyaltyOutcome"
          ],
          "reference": "fune!(retail.loyalty-points@^1);  // then call loyalty_points(…)",
          "url": "http://www.functionalweave.com/functions/retail.loyalty-points"
        }
      ],
      "gaps": [
        {
          "key": "payment-processing",
          "text": "Taking card, wallet or BNPL payments through a provider (Stripe, Adyen, PayPal), 3-D Secure and webhooks.",
          "section": "basket",
          "from": "ecommerce"
        },
        {
          "key": "basket-storage",
          "text": "Persisting baskets across sessions and reserving stock during checkout.",
          "section": "basket",
          "from": "ecommerce"
        }
      ]
    },
    {
      "key": "tax",
      "title": "Sales tax",
      "purpose": "Charge the right tax for where the customer is.",
      "from": "ecommerce",
      "members": [
        {
          "id": "finance.tax.vat-rate",
          "tier": "core",
          "why": "The UK VAT rate in force for a product category on the date of sale.",
          "section": "tax",
          "from": "ecommerce",
          "version": "1.2.2",
          "summary": "Look up the VAT rate in force for a jurisdiction, category and date.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "vatRate"
          ],
          "signatures": [
            "pub fn vat_rate(jurisdiction: &str, category: &str, on_date: &str) -> VatRate"
          ],
          "reference": "fune!(finance.tax.vat-rate@^1);  // then call vat_rate(…)",
          "url": "http://www.functionalweave.com/functions/finance.tax.vat-rate"
        },
        {
          "id": "retail.oss-vat",
          "tier": "optional",
          "why": "Which EU member state's VAT applies to a distance sale under OSS and the EUR 10,000 threshold.",
          "section": "tax",
          "from": "ecommerce",
          "version": "1.0.1",
          "summary": "EU distance-selling VAT: which member state's standard rate applies under OSS and the EUR 10,000 threshold, and the VAT.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "ossVat"
          ],
          "signatures": [
            "pub fn oss_vat(net: &Money, seller_country: &str, customer_country: &str, eu_sales_previous_year: &Money, eu_sales_this_year: &Money, opted_in: bool, on_date: &str) -> OssVat"
          ],
          "reference": "fune!(retail.oss-vat@^1);  // then call oss_vat(…)",
          "url": "http://www.functionalweave.com/functions/retail.oss-vat"
        },
        {
          "id": "retail.sales-tax-us",
          "tier": "optional",
          "why": "US sales tax: the state's base rate from dated data plus a local rate you supply.",
          "section": "tax",
          "from": "ecommerce",
          "version": "1.1.1",
          "summary": "US sales tax on a sale: the state's base rate from dated data plus a local rate the caller supplies.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "salesTaxUs"
          ],
          "signatures": [
            "pub fn sales_tax_us(amount: &Money, state: &str, local_rate_per_million: i64, on_date: &str) -> UsSalesTax"
          ],
          "reference": "fune!(retail.sales-tax-us@^1);  // then call sales_tax_us(…)",
          "url": "http://www.functionalweave.com/functions/retail.sales-tax-us"
        }
      ],
      "gaps": [
        {
          "key": "us-local-tax-rates",
          "text": "US county and city sales tax rates by address: Functional Weave takes the local rate as an input.",
          "section": "tax",
          "from": "ecommerce"
        }
      ]
    },
    {
      "key": "delivery",
      "title": "Delivery",
      "purpose": "Charge for delivery and promise a date.",
      "from": "ecommerce",
      "members": [
        {
          "id": "retail.shipping-rate",
          "tier": "core",
          "why": "Delivery charge from a dated rate table by service, zone, weight and size, with free-over thresholds.",
          "section": "delivery",
          "from": "ecommerce",
          "version": "1.0.1",
          "summary": "Delivery charge from a dated rate table by service, zone, chargeable weight and size, with a free-over threshold.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "shippingRate"
          ],
          "signatures": [
            "pub fn shipping_rate(bands: &[ShippingBand], service: &str, zone: &str, parcel: &Parcel, basket_total: &Money, on_date: &str) -> ShippingQuote"
          ],
          "reference": "fune!(retail.shipping-rate@^1);  // then call shipping_rate(…)",
          "url": "http://www.functionalweave.com/functions/retail.shipping-rate"
        },
        {
          "id": "retail.volumetric-weight",
          "tier": "core",
          "why": "Chargeable parcel weight: the greater of actual and volumetric weight.",
          "section": "delivery",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Chargeable parcel weight: the greater of actual and volumetric (dimensional) weight, rounded up to a billing step.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "chargeableWeight"
          ],
          "signatures": [
            "pub fn chargeable_weight(length_mm: i64, width_mm: i64, height_mm: i64, actual_grams: i64, divisor: i64, round_up_to_grams: i64) -> ChargeableWeight"
          ],
          "reference": "fune!(retail.volumetric-weight@^1);  // then call chargeable_weight(…)",
          "url": "http://www.functionalweave.com/functions/retail.volumetric-weight"
        },
        {
          "id": "logistics.delivery-date",
          "tier": "optional",
          "why": "Estimated dispatch and delivery dates from order time, cut-off and service days.",
          "section": "delivery",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Estimated dispatch and delivery dates from order time, dispatch cut-off, service days, weekends and UK bank holidays.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "deliveryDate"
          ],
          "signatures": [
            "pub fn delivery_date(order_date: &str, order_time: &str, cut_off: &str, service_days: i64, region: Option<&UkRegion>, closures: &[String]) -> DeliveryEstimate"
          ],
          "reference": "fune!(logistics.delivery-date@^1);  // then call delivery_date(…)",
          "url": "http://www.functionalweave.com/functions/logistics.delivery-date"
        },
        {
          "id": "validation.uk-postcode",
          "tier": "optional",
          "why": "Validates the delivery postcode and puts it in canonical form.",
          "section": "delivery",
          "from": "ecommerce",
          "version": "2.0.0",
          "summary": "Validate a UK postcode and normalise it to canonical upper case with one space before the inward code.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkPostcode"
          ],
          "signatures": [
            "pub fn validate_uk_postcode(value: &str) -> UkPostcode"
          ],
          "reference": "fune!(validation.uk-postcode@^2);  // then call validate_uk_postcode(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-postcode"
        }
      ],
      "gaps": [
        {
          "key": "carrier-labels",
          "text": "Booking collections and printing labels through a carrier's API (Royal Mail, DPD, Evri).",
          "section": "delivery",
          "from": "ecommerce"
        }
      ]
    },
    {
      "key": "returns",
      "title": "Returns and refunds",
      "purpose": "Refund returned items exactly.",
      "from": "ecommerce",
      "members": [
        {
          "id": "retail.refund-calculate",
          "tier": "core",
          "why": "Refund for returned lines, sharing basket discounts back so each refund is exact to the penny.",
          "section": "returns",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Refund for returned items, sharing basket discounts back across the sale so every refund is exact to the penny.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "calculateRefund"
          ],
          "signatures": [
            "pub fn calculate_refund(lines: &[SaleLine], basket_discount: &Money, returns: &[ReturnLine]) -> Refund"
          ],
          "reference": "fune!(retail.refund-calculate@^1);  // then call calculate_refund(…)",
          "url": "http://www.functionalweave.com/functions/retail.refund-calculate"
        }
      ],
      "gaps": []
    },
    {
      "key": "stock",
      "title": "Stock",
      "purpose": "Know when to reorder and what stock is worth.",
      "from": "ecommerce",
      "members": [
        {
          "id": "inventory.reorder-point",
          "tier": "optional",
          "why": "Reorder point from lead-time demand plus safety stock.",
          "section": "stock",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Reorder point: demand over the supplier lead time, rounded up to whole units, plus safety stock.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "reorderPoint"
          ],
          "signatures": [
            "pub fn reorder_point(demand: i64, demand_period_days: i64, lead_time_days: i64, safety_stock: i64) -> i64"
          ],
          "reference": "fune!(inventory.reorder-point@^1);  // then call reorder_point(…)",
          "url": "http://www.functionalweave.com/functions/inventory.reorder-point"
        },
        {
          "id": "inventory.valuation-weighted-average",
          "tier": "optional",
          "why": "Stock value and cost of sales at weighted average cost.",
          "section": "stock",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Perpetual weighted average cost: stock value and cost of sales from a movement ledger, rounding once per issue.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "weightedAverageValuation"
          ],
          "signatures": [
            "pub fn weighted_average_valuation(movements: &[StockMovement], currency: &str, mode: &RoundingMode) -> AverageCostValuation"
          ],
          "reference": "fune!(inventory.valuation-weighted-average@^1);  // then call weighted_average_valuation(…)",
          "url": "http://www.functionalweave.com/functions/inventory.valuation-weighted-average"
        }
      ],
      "gaps": [
        {
          "key": "stock-levels",
          "text": "Tracking stock on hand per location as orders and deliveries happen.",
          "section": "stock",
          "from": "ecommerce"
        }
      ]
    },
    {
      "key": "reports",
      "title": "Sales reports",
      "purpose": "See how the shop is doing.",
      "from": "ecommerce",
      "members": [
        {
          "id": "math.percent-change",
          "tier": "optional",
          "why": "Sales up or down on last period, in basis points.",
          "section": "reports",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Percentage change from one value to another, in basis points, with an explicit rounding mode.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "percentChange"
          ],
          "signatures": [
            "pub fn percent_change(from_value: i64, to_value: i64, mode: &RoundingMode) -> Option<i64>"
          ],
          "reference": "fune!(math.percent-change@^1);  // then call percent_change(…)",
          "url": "http://www.functionalweave.com/functions/math.percent-change"
        },
        {
          "id": "charts.bar-chart",
          "tier": "optional",
          "why": "Bar chart geometry for sales by day or product.",
          "section": "reports",
          "from": "ecommerce",
          "version": "1.0.0",
          "summary": "Complete bar chart geometry as data, grouped or stacked: plot area, axes, gridlines, bar rectangles, colours, legend.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "barChart"
          ],
          "signatures": [
            "pub fn bar_chart(spec: &BarChartSpec) -> BarChart"
          ],
          "reference": "fune!(charts.bar-chart@^1);  // then call bar_chart(…)",
          "url": "http://www.functionalweave.com/functions/charts.bar-chart"
        }
      ],
      "gaps": []
    },
    {
      "key": "amounts",
      "title": "Amounts",
      "purpose": "Hold money as currency-tagged integer minor units and do plain arithmetic on it safely.",
      "from": "money-basics",
      "members": [
        {
          "id": "money.amount",
          "tier": "core",
          "why": "The Money type ({ minor, currency }) every other money capability takes and returns.",
          "section": "amounts",
          "from": "money-basics",
          "version": "1.0.2",
          "summary": "A currency-tagged monetary amount held in integer minor units.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "money"
          ],
          "signatures": [
            "pub fn money(minor: i64, currency: &str) -> Money"
          ],
          "reference": "fune!(money.amount@^1);  // then call money(…)",
          "url": "http://www.functionalweave.com/functions/money.amount"
        },
        {
          "id": "money.add",
          "tier": "core",
          "why": "Adds and subtracts amounts, refusing to mix currencies by accident.",
          "section": "amounts",
          "from": "money-basics",
          "version": "1.0.1",
          "summary": "Add, subtract and negate monetary amounts, refusing to mix currencies.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "addMoney"
          ],
          "signatures": [
            "pub fn add_money(a: &Money, b: &Money) -> Money"
          ],
          "reference": "fune!(money.add@^1);  // then call add_money(…)",
          "url": "http://www.functionalweave.com/functions/money.add"
        },
        {
          "id": "money.sum",
          "tier": "core",
          "why": "Totals a list of amounts, with an explicit currency for an empty list.",
          "section": "amounts",
          "from": "money-basics",
          "version": "1.0.1",
          "summary": "Total a list of monetary amounts, with an explicit currency for the empty case.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "sumMoney"
          ],
          "signatures": [
            "pub fn sum_money(amounts: &[Money], currency: &str) -> Money"
          ],
          "reference": "fune!(money.sum@^1);  // then call sum_money(…)",
          "url": "http://www.functionalweave.com/functions/money.sum"
        },
        {
          "id": "money.compare",
          "tier": "core",
          "why": "Compares, mins, maxes and clamps amounts of one currency without unwrapping them.",
          "section": "amounts",
          "from": "money-basics",
          "version": "2.0.0",
          "summary": "Compare two amounts of one currency, refusing to compare across currencies; min, max and clamp too.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "compareMoney",
            "minMoney",
            "maxMoney",
            "clampMoney"
          ],
          "signatures": [
            "pub fn compare_money(a: &Money, b: &Money) -> i64",
            "pub fn min_money(a: &Money, b: &Money) -> Money",
            "pub fn max_money(a: &Money, b: &Money) -> Money",
            "pub fn clamp_money(amount: &Money, min: &Money, max: &Money) -> Money"
          ],
          "reference": "fune!(money.compare@^2);  // then call compare_money, min_money, max_money, clamp_money",
          "url": "http://www.functionalweave.com/functions/money.compare"
        }
      ],
      "gaps": []
    },
    {
      "key": "rates",
      "title": "Rates and rounding",
      "purpose": "Apply percentages and divide amounts with the rounding stated, never a float.",
      "from": "money-basics",
      "members": [
        {
          "id": "money.apply-rate",
          "tier": "core",
          "why": "Applies a basis-point rate (VAT, discount, commission) to an amount with explicit rounding.",
          "section": "rates",
          "from": "money-basics",
          "version": "1.0.1",
          "summary": "Apply a rate expressed in basis points to a monetary amount, with an explicit rounding mode.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "applyRate"
          ],
          "signatures": [
            "pub fn apply_rate(amount: &Money, basis_points: i64, mode: &RoundingMode) -> Money"
          ],
          "reference": "fune!(money.apply-rate@^1);  // then call apply_rate(…)",
          "url": "http://www.functionalweave.com/functions/money.apply-rate"
        },
        {
          "id": "math.round-div",
          "tier": "core",
          "why": "Integer division with a named rounding mode, for any money maths the others do not cover.",
          "section": "rates",
          "from": "money-basics",
          "version": "1.0.0",
          "summary": "Integer division with an explicit rounding mode, for money arithmetic that must not drift.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "roundDiv"
          ],
          "signatures": [
            "pub fn round_div(numerator: i64, denominator: i64, mode: &str) -> i64"
          ],
          "reference": "fune!(math.round-div@^1);  // then call round_div(…)",
          "url": "http://www.functionalweave.com/functions/math.round-div"
        },
        {
          "id": "math.basis-points",
          "tier": "optional",
          "why": "Converts \"12.5%\" to and from basis points exactly, for rates typed in by users.",
          "section": "rates",
          "from": "money-basics",
          "version": "2.0.0",
          "summary": "Convert a rate between percent, basis points and a plain ratio exactly, as decimal text or integer basis points.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "convertRate",
            "toBasisPoints",
            "fromBasisPoints"
          ],
          "signatures": [
            "pub fn convert_rate(value: &str, from_unit: &str, to_unit: &str) -> String",
            "pub fn to_basis_points(value: &str, unit: &str) -> i64",
            "pub fn from_basis_points(basis_points: i64, unit: &str) -> String"
          ],
          "reference": "fune!(math.basis-points@^2);  // then call convert_rate, to_basis_points, from_basis_points",
          "url": "http://www.functionalweave.com/functions/math.basis-points"
        },
        {
          "id": "math.rational",
          "tier": "optional",
          "why": "Exact fractions for rates that must not drift across many steps.",
          "section": "rates",
          "from": "money-basics",
          "version": "2.0.0",
          "summary": "Exact fraction arithmetic, always reduced, for rates and ratios that must not drift.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": true,
          "functions": [
            "calculateRational",
            "rational",
            "addRational",
            "subtractRational",
            "multiplyRational",
            "divideRational",
            "compareRational",
            "rationalToInteger"
          ],
          "signatures": [
            "pub fn calculate_rational(a: &Rational, op: &str, b: &Rational) -> Rational",
            "pub fn rational(numerator: i64, denominator: i64) -> Rational",
            "pub fn add_rational(a: &Rational, b: &Rational) -> Rational",
            "pub fn subtract_rational(a: &Rational, b: &Rational) -> Rational",
            "pub fn multiply_rational(a: &Rational, b: &Rational) -> Rational",
            "pub fn divide_rational(a: &Rational, b: &Rational) -> Rational",
            "pub fn compare_rational(a: &Rational, b: &Rational) -> i64",
            "pub fn rational_to_integer(r: &Rational, mode: &RoundingMode) -> i64"
          ],
          "reference": "fune!(math.rational@^2);  // then call calculate_rational, rational, add_rational, subtract_rational, multiply_rational, divide_rational, compare_rational, rational_to_integer",
          "url": "http://www.functionalweave.com/functions/math.rational"
        },
        {
          "id": "math.round-div-big",
          "tier": "optional",
          "why": "Rounded division on numbers past 2^53, for very large totals.",
          "section": "rates",
          "from": "money-basics",
          "version": "1.0.0",
          "summary": "Divide two integers of any size with an explicit rounding mode, on decimal strings, for exact money sums past 2^53.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "roundDivBig"
          ],
          "signatures": [
            "pub fn round_div_big(numerator: &str, denominator: &str, mode: &RoundingMode) -> String"
          ],
          "reference": "fune!(math.round-div-big@^1);  // then call round_div_big(…)",
          "url": "http://www.functionalweave.com/functions/math.round-div-big"
        }
      ],
      "gaps": []
    },
    {
      "key": "splitting",
      "title": "Splitting",
      "purpose": "Divide an amount between people, periods or lines so the parts add back to the whole.",
      "from": "money-basics",
      "members": [
        {
          "id": "money.allocate",
          "tier": "core",
          "why": "Splits an amount by ratios (60/40, by quantity) without losing or inventing a penny.",
          "section": "splitting",
          "from": "money-basics",
          "version": "1.0.1",
          "summary": "Split an amount across ratios without losing or inventing a single minor unit.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "allocate"
          ],
          "signatures": [
            "pub fn allocate(amount: &Money, ratios: &[i64]) -> Vec<Money>"
          ],
          "reference": "fune!(money.allocate@^1);  // then call allocate(…)",
          "url": "http://www.functionalweave.com/functions/money.allocate"
        },
        {
          "id": "money.split-even",
          "tier": "optional",
          "why": "Splits an amount into n near-equal parts that sum exactly, for instalments or shares.",
          "section": "splitting",
          "from": "money-basics",
          "version": "1.0.0",
          "summary": "Split an amount into n near-equal parts that add back up to exactly the whole.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "splitEven"
          ],
          "signatures": [
            "pub fn split_even(amount: &Money, parts: i64) -> Vec<Money>"
          ],
          "reference": "fune!(money.split-even@^1);  // then call split_even(…)",
          "url": "http://www.functionalweave.com/functions/money.split-even"
        }
      ],
      "gaps": []
    },
    {
      "key": "display",
      "title": "Display and input",
      "purpose": "Show amounts to people and read what they type.",
      "from": "money-basics",
      "members": [
        {
          "id": "money.format",
          "tier": "core",
          "why": "Renders an amount as text with the currency's real number of decimal places.",
          "section": "display",
          "from": "money-basics",
          "version": "1.0.0",
          "summary": "Render a monetary amount as text, using the currency's real minor-unit precision.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "formatMoney"
          ],
          "signatures": [
            "pub fn format_money(amount: &Money) -> String"
          ],
          "reference": "fune!(money.format@^1);  // then call format_money(…)",
          "url": "http://www.functionalweave.com/functions/money.format"
        },
        {
          "id": "money.parse",
          "tier": "optional",
          "why": "Reads \"£1,234.50\" or \"1.234,50 €\" typed by a user into minor units, strictly.",
          "section": "display",
          "from": "money-basics",
          "version": "1.0.0",
          "summary": "Parse \"£1,234.50\" or \"1.234,50 €\" into integer minor units, strictly: no guessing, no rounding.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseMoney"
          ],
          "signatures": [
            "pub fn parse_money(text: &str, currency: &str, style: &str) -> Money"
          ],
          "reference": "fune!(money.parse@^1);  // then call parse_money(…)",
          "url": "http://www.functionalweave.com/functions/money.parse"
        },
        {
          "id": "money.currency-digits",
          "tier": "optional",
          "why": "How many decimal places each ISO 4217 currency has, for input boxes and validation.",
          "section": "display",
          "from": "money-basics",
          "version": "1.0.0",
          "summary": "How many decimal places a currency's minor unit has, from the ISO 4217 list.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "currencyDigits"
          ],
          "signatures": [
            "pub fn currency_digits(currency: &str) -> i64"
          ],
          "reference": "fune!(money.currency-digits@^1);  // then call currency_digits(…)",
          "url": "http://www.functionalweave.com/functions/money.currency-digits"
        }
      ],
      "gaps": []
    },
    {
      "key": "currencies",
      "title": "Other currencies",
      "purpose": "Convert between currencies at a rate you hold.",
      "from": "money-basics",
      "members": [
        {
          "id": "money.convert",
          "tier": "optional",
          "why": "Converts an amount at a supplied exact rate with explicit rounding.",
          "section": "currencies",
          "from": "money-basics",
          "version": "1.0.0",
          "summary": "Convert an amount to another currency at a supplied exact rate, with an explicit rounding mode.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "convertMoney"
          ],
          "signatures": [
            "pub fn convert_money(amount: &Money, rate: &ExchangeRate, mode: &RoundingMode) -> Money"
          ],
          "reference": "fune!(money.convert@^1);  // then call convert_money(…)",
          "url": "http://www.functionalweave.com/functions/money.convert"
        }
      ],
      "gaps": []
    },
    {
      "key": "vat",
      "title": "VAT",
      "purpose": "Work out VAT from the dated rates HMRC publishes.",
      "from": "uk-tax",
      "members": [
        {
          "id": "finance.tax.add-vat",
          "tier": "core",
          "why": "Net to gross: VAT on a net amount at the rate on the date of supply.",
          "section": "vat",
          "from": "uk-tax",
          "version": "1.0.1",
          "summary": "Add VAT to a net amount using the rate in force on the date of supply.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "addVat"
          ],
          "signatures": [
            "pub fn add_vat(net: &Money, jurisdiction: &str, category: &str, on_date: &str) -> VatBreakdown"
          ],
          "reference": "fune!(finance.tax.add-vat@^1);  // then call add_vat(…)",
          "url": "http://www.functionalweave.com/functions/finance.tax.add-vat"
        },
        {
          "id": "finance.tax.remove-vat",
          "tier": "core",
          "why": "Gross to net: the VAT already inside a VAT-inclusive price.",
          "section": "vat",
          "from": "uk-tax",
          "version": "1.0.2",
          "summary": "Extract the VAT already included in a gross amount, using the rate in force on the date of supply.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "removeVat"
          ],
          "signatures": [
            "pub fn remove_vat(gross: &Money, jurisdiction: &str, category: &str, on_date: &str) -> VatBreakdown"
          ],
          "reference": "fune!(finance.tax.remove-vat@^1);  // then call remove_vat(…)",
          "url": "http://www.functionalweave.com/functions/finance.tax.remove-vat"
        },
        {
          "id": "finance.tax.vat-return",
          "tier": "optional",
          "why": "The nine-box VAT return totals for a period from categorised sales and purchases.",
          "section": "vat",
          "from": "uk-tax",
          "version": "1.0.1",
          "summary": "UK nine-box VAT return totals for a period from categorised sales and purchases (VAT Notice 700/12).",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "vatReturn"
          ],
          "signatures": [
            "pub fn vat_return(transactions: &[VatTransaction], period_start: &str, period_end: &str) -> VatReturn"
          ],
          "reference": "fune!(finance.tax.vat-return@^1);  // then call vat_return(…)",
          "url": "http://www.functionalweave.com/functions/finance.tax.vat-return"
        },
        {
          "id": "finance.tax.vat-flat-rate",
          "tier": "optional",
          "why": "VAT due under the Flat Rate Scheme by business sector, for small businesses on it.",
          "section": "vat",
          "from": "uk-tax",
          "version": "1.0.1",
          "summary": "UK VAT Flat Rate Scheme: VAT due on gross turnover at the sector's dated rate, limited cost rate and first-year 1% off.",
          "status": "unreviewed",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "flatRateVat"
          ],
          "signatures": [
            "pub fn flat_rate_vat(turnover: &Money, sector: &str, limited_cost_business: bool, registration_date: Option<&str>, supply_date: &str) -> FlatRateVat"
          ],
          "reference": "fune!(finance.tax.vat-flat-rate@^1);  // then call flat_rate_vat(…)",
          "url": "http://www.functionalweave.com/functions/finance.tax.vat-flat-rate"
        }
      ],
      "gaps": []
    },
    {
      "key": "tax-year",
      "title": "Tax periods",
      "purpose": "Put dates into the UK periods tax is worked out over.",
      "from": "uk-tax",
      "members": [
        {
          "id": "dates.uk-tax-year",
          "tier": "core",
          "why": "Which UK tax year (6 April to 5 April) a date falls in.",
          "section": "tax-year",
          "from": "uk-tax",
          "version": "1.0.0",
          "summary": "The UK personal tax year containing a date: 6 April to 5 April, with HMRC's label.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "ukTaxYear"
          ],
          "signatures": [
            "pub fn uk_tax_year(iso: &str) -> UkTaxYear"
          ],
          "reference": "fune!(dates.uk-tax-year@^1);  // then call uk_tax_year(…)",
          "url": "http://www.functionalweave.com/functions/dates.uk-tax-year"
        }
      ],
      "gaps": []
    },
    {
      "key": "identifiers",
      "title": "Tax identifiers",
      "purpose": "Check the numbers HMRC and Companies House issue before storing them.",
      "from": "uk-tax",
      "members": [
        {
          "id": "validation.uk-vat-number",
          "tier": "core",
          "why": "Checks a UK VAT registration number's checksum.",
          "section": "identifiers",
          "from": "uk-tax",
          "version": "1.0.0",
          "summary": "Check a UK VAT registration number against HMRC's mod-97 checksum, including the post-2010 9755 variant.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isUkVatNumber"
          ],
          "signatures": [
            "pub fn is_uk_vat_number(value: &str) -> bool"
          ],
          "reference": "fune!(validation.uk-vat-number@^1);  // then call is_uk_vat_number(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-vat-number"
        },
        {
          "id": "validation.uk-utr",
          "tier": "optional",
          "why": "Checks a Unique Taxpayer Reference's check digit.",
          "section": "identifiers",
          "from": "uk-tax",
          "version": "2.0.1",
          "summary": "Check an HMRC Unique Taxpayer Reference's modulus 11 check digit, accepting the K and spacing people type.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkUtr"
          ],
          "signatures": [
            "pub fn validate_uk_utr(value: &str) -> UkUtr"
          ],
          "reference": "fune!(validation.uk-utr@^2);  // then call validate_uk_utr(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-utr"
        },
        {
          "id": "validation.uk-company-number",
          "tier": "optional",
          "why": "Checks and pads a Companies House company number.",
          "section": "identifiers",
          "from": "uk-tax",
          "version": "2.0.1",
          "summary": "Check a Companies House company number's format and prefix (SC, NI, OC, LP...) and zero-pad it to eight characters.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkCompanyNumber"
          ],
          "signatures": [
            "pub fn validate_uk_company_number(value: &str) -> UkCompanyNumber"
          ],
          "reference": "fune!(validation.uk-company-number@^2);  // then call validate_uk_company_number(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-company-number"
        }
      ],
      "gaps": []
    },
    {
      "key": "contact",
      "title": "Contact details",
      "purpose": "Check and normalise a person's contact details before storing them.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.email",
          "tier": "core",
          "why": "Is an email address plausible, by a documented subset of RFC 5322.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Is this a plausible email address? A documented, pragmatic subset of RFC 5322, not the full grammar.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isEmail"
          ],
          "signatures": [
            "pub fn is_email(value: &str) -> bool"
          ],
          "reference": "fune!(validation.email@^1);  // then call is_email(…)",
          "url": "http://www.functionalweave.com/functions/validation.email"
        },
        {
          "id": "validation.phone-e164",
          "tier": "core",
          "why": "Normalises a phone number to +44… E.164 for a default country.",
          "section": "contact",
          "from": "validation-basics",
          "version": "2.0.0",
          "summary": "Normalise a phone number to E.164 (+447700900123) using a default country's trunk and dialling prefixes.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validatePhoneE164"
          ],
          "signatures": [
            "pub fn validate_phone_e164(value: &str, default_country: &str) -> PhoneE164"
          ],
          "reference": "fune!(validation.phone-e164@^2);  // then call validate_phone_e164(…)",
          "url": "http://www.functionalweave.com/functions/validation.phone-e164"
        },
        {
          "id": "text.normalise-name",
          "tier": "optional",
          "why": "Tidies a typed name: spacing and title case with Mc, O' and hyphen rules.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Tidy a personal name: trim, collapse whitespace and title-case, with Mc, Mac, O', hyphen and van/de rules.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "normaliseName"
          ],
          "signatures": [
            "pub fn normalise_name(value: &str) -> String"
          ],
          "reference": "fune!(text.normalise-name@^1);  // then call normalise_name(…)",
          "url": "http://www.functionalweave.com/functions/text.normalise-name"
        },
        {
          "id": "auth.normalise-email",
          "tier": "optional",
          "why": "Lower-cases an email's domain for storage and lookup.",
          "section": "contact",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Trim an email address and lower-case its domain for storage and lookup, or null if it is not a plausible address.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "normaliseEmail"
          ],
          "signatures": [
            "pub fn normalise_email(value: &str) -> Option<String>"
          ],
          "reference": "fune!(auth.normalise-email@^1);  // then call normalise_email(…)",
          "url": "http://www.functionalweave.com/functions/auth.normalise-email"
        }
      ],
      "gaps": []
    },
    {
      "key": "bank",
      "title": "Bank details",
      "purpose": "Check payee and customer bank details before money moves.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.iban",
          "tier": "core",
          "why": "Checks an IBAN's mod-97 checksum and its country's length.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check an IBAN against the ISO 13616 mod-97-10 checksum and the registered length for its country.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isIban"
          ],
          "signatures": [
            "pub fn is_iban(value: &str) -> bool"
          ],
          "reference": "fune!(validation.iban@^1);  // then call is_iban(…)",
          "url": "http://www.functionalweave.com/functions/validation.iban"
        },
        {
          "id": "validation.bic",
          "tier": "optional",
          "why": "Checks a SWIFT/BIC code's format and splits it.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check the format of a SWIFT/BIC code (8 or 11 characters) and split it into its parts.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateBic"
          ],
          "signatures": [
            "pub fn validate_bic(value: &str) -> BicCheck"
          ],
          "reference": "fune!(validation.bic@^1);  // then call validate_bic(…)",
          "url": "http://www.functionalweave.com/functions/validation.bic"
        },
        {
          "id": "validation.uk-sort-code-account",
          "tier": "optional",
          "why": "Checks a UK sort code and account number by the Vocalink modulus rules, against a table you supply.",
          "section": "bank",
          "from": "validation-basics",
          "version": "3.0.0",
          "summary": "Check a UK sort code and account number with the Vocalink/Pay.UK modulus rules, against a table you supply.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateUkSortCodeAccount"
          ],
          "signatures": [
            "pub fn validate_uk_sort_code_account(sort_code: &str, account_number: &str, table: &UkModulusTable) -> UkSortCodeAccount"
          ],
          "reference": "fune!(validation.uk-sort-code-account@^3);  // then call validate_uk_sort_code_account(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-sort-code-account"
        },
        {
          "id": "validation.uk-modulus-table",
          "tier": "optional",
          "why": "Parses Vocalink's VALACDOS and SCSUBTAB files into that table.",
          "section": "bank",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Parse Vocalink's VALACDOS.txt and SCSUBTAB.txt into the table UK sort code modulus checking needs.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "parseUkModulusTable"
          ],
          "signatures": [
            "pub fn parse_uk_modulus_table(valacdos: &str, scsubtab: &str) -> UkModulusTable"
          ],
          "reference": "fune!(validation.uk-modulus-table@^1);  // then call parse_uk_modulus_table(…)",
          "url": "http://www.functionalweave.com/functions/validation.uk-modulus-table"
        }
      ],
      "gaps": []
    },
    {
      "key": "check-digits",
      "title": "Identifiers and check digits",
      "purpose": "Catch mistyped reference numbers by their check digits.",
      "from": "validation-basics",
      "members": [
        {
          "id": "validation.luhn",
          "tier": "optional",
          "why": "Luhn mod-10 check, for card numbers and many reference numbers.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check a digit string against the Luhn mod-10 checksum used by payment cards and many identifiers.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "isLuhn"
          ],
          "signatures": [
            "pub fn is_luhn(value: &str) -> bool"
          ],
          "reference": "fune!(validation.luhn@^1);  // then call is_luhn(…)",
          "url": "http://www.functionalweave.com/functions/validation.luhn"
        },
        {
          "id": "validation.lei",
          "tier": "optional",
          "why": "Checks a Legal Entity Identifier's check digits.",
          "section": "check-digits",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Check a 20-character Legal Entity Identifier against its ISO 17442 / ISO 7064 MOD 97-10 check digits.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "validateLei"
          ],
          "signatures": [
            "pub fn validate_lei(value: &str) -> LeiCheck"
          ],
          "reference": "fune!(validation.lei@^1);  // then call validate_lei(…)",
          "url": "http://www.functionalweave.com/functions/validation.lei"
        }
      ],
      "gaps": []
    },
    {
      "key": "masking",
      "title": "Displaying sensitive values",
      "purpose": "Show stored identifiers without exposing them.",
      "from": "validation-basics",
      "members": [
        {
          "id": "text.mask",
          "tier": "optional",
          "why": "Masks all but the last digits of a card, account or phone number.",
          "section": "masking",
          "from": "validation-basics",
          "version": "1.0.0",
          "summary": "Mask all but the last n characters of a card, account or phone number, optionally keeping separators.",
          "status": "stable",
          "license": "NOASSERTION",
          "languages": [
            "typescript",
            "python",
            "rust"
          ],
          "available": true,
          "group": false,
          "functions": [
            "mask"
          ],
          "signatures": [
            "pub fn mask(value: &str, visible: i64, mask_char: &str, keep_separators: bool) -> String"
          ],
          "reference": "fune!(text.mask@^1);  // then call mask(…)",
          "url": "http://www.functionalweave.com/functions/text.mask"
        }
      ],
      "gaps": []
    }
  ],
  "gaps": [
    {
      "key": "product-search",
      "text": "Faceted product search, filters and variants (size, colour) are your catalogue's data model.",
      "section": "catalogue",
      "from": "ecommerce"
    },
    {
      "key": "product-images",
      "text": "Image upload, resizing and a CDN for product photos.",
      "section": "catalogue",
      "from": "ecommerce"
    },
    {
      "key": "payment-processing",
      "text": "Taking card, wallet or BNPL payments through a provider (Stripe, Adyen, PayPal), 3-D Secure and webhooks.",
      "section": "basket",
      "from": "ecommerce"
    },
    {
      "key": "basket-storage",
      "text": "Persisting baskets across sessions and reserving stock during checkout.",
      "section": "basket",
      "from": "ecommerce"
    },
    {
      "key": "us-local-tax-rates",
      "text": "US county and city sales tax rates by address: Functional Weave takes the local rate as an input.",
      "section": "tax",
      "from": "ecommerce"
    },
    {
      "key": "carrier-labels",
      "text": "Booking collections and printing labels through a carrier's API (Royal Mail, DPD, Evri).",
      "section": "delivery",
      "from": "ecommerce"
    },
    {
      "key": "stock-levels",
      "text": "Tracking stock on hand per location as orders and deliveries happen.",
      "section": "stock",
      "from": "ecommerce"
    },
    {
      "key": "order-emails",
      "text": "Order confirmation, dispatch and refund emails.",
      "section": null,
      "from": "ecommerce"
    },
    {
      "key": "customer-accounts",
      "text": "Customer sign-up and login: see the auth suite.",
      "section": null,
      "from": "ecommerce"
    },
    {
      "key": "fx-rate-source",
      "text": "Where exchange rates come from: money.convert takes a rate you supply; fetching and storing daily rates (ECB, a provider's API) is yours.",
      "section": null,
      "from": "money-basics"
    },
    {
      "key": "money-storage",
      "text": "Storing amounts: the database columns (integer minor units plus a currency code) and their migrations are yours.",
      "section": null,
      "from": "money-basics"
    },
    {
      "key": "mtd-submission",
      "text": "Making Tax Digital: submitting the return to HMRC's VAT API (OAuth, fraud-prevention headers, obligations) is yours; caps gives the nine boxes.",
      "section": null,
      "from": "uk-tax"
    },
    {
      "key": "corporation-tax",
      "text": "Corporation tax and self-assessment income tax computations are not in caps.",
      "section": null,
      "from": "uk-tax"
    },
    {
      "key": "vat-number-lookup",
      "text": "Checking that a VAT number is registered (HMRC's lookup API) is network work; caps checks the checksum only.",
      "section": null,
      "from": "uk-tax"
    },
    {
      "key": "address-lookup",
      "text": "Turning a postcode into a list of addresses (Royal Mail PAF or a lookup API) is network work and not in caps.",
      "section": null,
      "from": "validation-basics"
    },
    {
      "key": "email-deliverability",
      "text": "Whether a mailbox exists (MX lookups, a confirmation email): validation.email checks the syntax only.",
      "section": null,
      "from": "validation-basics"
    },
    {
      "key": "vocalink-data",
      "text": "The Vocalink modulus tables themselves: download them from Pay.UK under its terms; caps parses them but does not ship them.",
      "section": null,
      "from": "validation-basics"
    }
  ],
  "counts": {
    "members": 57,
    "core": 24,
    "optional": 33,
    "unreviewed": 6,
    "deprecated": [],
    "unavailable": [],
    "missing": [],
    "gaps": 17
  },
  "warnings": [
    "3 core capabilities are unreviewed - they pass their tests but await an expert's sign-off; tell the user before relying on them, and never use one silently for tax, payroll, legal, clinical or other regulated rules: finance.tax.vat-rate, finance.tax.add-vat, finance.tax.remove-vat",
    "3 optional capabilities are unreviewed too: retail.sales-tax-us, finance.tax.vat-return, finance.tax.vat-flat-rate",
    "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."
  ],
  "url": "http://www.functionalweave.com/suites/ecommerce",
  "owner": null
}
