{
  "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": "python",
  "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": [
            "def generate_sku(prefix: str, attributes: Sequence[str], segment_length: int) -> str"
          ],
          "reference": "from fune.retail.sku_generate import generate_sku  # retail.sku-generate@^1",
          "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": [
            "def validate_gtin(value: str) -> GtinCheck"
          ],
          "reference": "from fune.validation.gtin import validate_gtin  # validation.gtin@^1",
          "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": [
            "def round_price(price: Money, step: int, ending: int, direction: PriceDirection) -> Money"
          ],
          "reference": "from fune.retail.price_rounding import round_price  # retail.price-rounding@^1",
          "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": [
            "def unit_price(price: Money, quantity: str, unit: str, per: UnitPriceBasis) -> Money"
          ],
          "reference": "from fune.retail.price_per_unit import unit_price  # retail.price-per-unit@^1",
          "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": [
            "def markup_price(cost: Money, basis_points: int, basis: PricingBasis, mode: RoundingMode) -> Money"
          ],
          "reference": "from fune.finance.markup_price import markup_price  # finance.markup-price@^1",
          "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": [
            "def margin(cost: Money, price: Money) -> MarginBreakdown"
          ],
          "reference": "from fune.finance.margin import margin  # finance.margin@^1",
          "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": [
            "def basket_total(lines: Sequence[BasketLine], promotions: Sequence[Promotion], basket_discount: Money, delivery: Money, delivery_tax_category: str, prices_include_vat: bool, jurisdiction: str, on_date: str) -> Basket"
          ],
          "reference": "from fune.retail.basket_total import basket_total  # retail.basket-total@^1",
          "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": [
            "def apply_promotions(lines: Sequence[PromoLine], promotions: Sequence[Promotion]) -> PromotionResult"
          ],
          "reference": "from fune.retail.promotion_apply import apply_promotions  # retail.promotion-apply@^1",
          "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": [
            "def validate_coupon(coupon: Coupon, lines: Sequence[CouponLine], customer_uses: int, on_date: str) -> CouponCheck"
          ],
          "reference": "from fune.retail.coupon_validate import validate_coupon  # retail.coupon-validate@^1",
          "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": [
            "def redeem_gift_card(balance: Money, amount_due: Money) -> GiftCardRedemption"
          ],
          "reference": "from fune.retail.gift_card_balance import redeem_gift_card  # retail.gift-card-balance@^1",
          "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": [
            "def loyalty_points(scheme: LoyaltyScheme, spend: Money, qualifying_spend: Money, balance: int, redeem_points: int) -> LoyaltyOutcome"
          ],
          "reference": "from fune.retail.loyalty_points import loyalty_points  # retail.loyalty-points@^1",
          "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": [
            "def vat_rate(jurisdiction: str, category: str, on_date: str) -> VatRate"
          ],
          "reference": "from fune.finance.tax.vat_rate import vat_rate  # finance.tax.vat-rate@^1",
          "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": [
            "def 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": "from fune.retail.oss_vat import oss_vat  # retail.oss-vat@^1",
          "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": [
            "def sales_tax_us(amount: Money, state: str, local_rate_per_million: int, on_date: str) -> UsSalesTax"
          ],
          "reference": "from fune.retail.sales_tax_us import sales_tax_us  # retail.sales-tax-us@^1",
          "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": [
            "def shipping_rate(bands: Sequence[ShippingBand], service: str, zone: str, parcel: Parcel, basket_total: Money, on_date: str) -> ShippingQuote"
          ],
          "reference": "from fune.retail.shipping_rate import shipping_rate  # retail.shipping-rate@^1",
          "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": [
            "def chargeable_weight(length_mm: int, width_mm: int, height_mm: int, actual_grams: int, divisor: int, round_up_to_grams: int) -> ChargeableWeight"
          ],
          "reference": "from fune.retail.volumetric_weight import chargeable_weight  # retail.volumetric-weight@^1",
          "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": [
            "def delivery_date(order_date: str, order_time: str, cut_off: str, service_days: int, region: Optional[UkRegion], closures: Sequence[str]) -> DeliveryEstimate"
          ],
          "reference": "from fune.logistics.delivery_date import delivery_date  # logistics.delivery-date@^1",
          "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": [
            "def validate_uk_postcode(value: str) -> UkPostcode"
          ],
          "reference": "from fune.validation.uk_postcode import validate_uk_postcode  # validation.uk-postcode@^2",
          "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": [
            "def calculate_refund(lines: Sequence[SaleLine], basket_discount: Money, returns: Sequence[ReturnLine]) -> Refund"
          ],
          "reference": "from fune.retail.refund_calculate import calculate_refund  # retail.refund-calculate@^1",
          "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": [
            "def reorder_point(demand: int, demand_period_days: int, lead_time_days: int, safety_stock: int) -> int"
          ],
          "reference": "from fune.inventory.reorder_point import reorder_point  # inventory.reorder-point@^1",
          "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": [
            "def weighted_average_valuation(movements: Sequence[StockMovement], currency: str, mode: RoundingMode) -> AverageCostValuation"
          ],
          "reference": "from fune.inventory.valuation_weighted_average import weighted_average_valuation  # inventory.valuation-weighted-average@^1",
          "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": [
            "def percent_change(from_value: int, to_value: int, mode: RoundingMode) -> Optional[int]"
          ],
          "reference": "from fune.math.percent_change import percent_change  # math.percent-change@^1",
          "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": [
            "def bar_chart(spec: BarChartSpec) -> BarChart"
          ],
          "reference": "from fune.charts.bar_chart import bar_chart  # charts.bar-chart@^1",
          "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": [
            "def money(minor: int, currency: str) -> Money"
          ],
          "reference": "from fune.money.amount import money  # money.amount@^1",
          "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": [
            "def add_money(a: Money, b: Money) -> Money"
          ],
          "reference": "from fune.money.add import add_money  # money.add@^1",
          "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": [
            "def sum_money(amounts: Sequence[Money], currency: str) -> Money"
          ],
          "reference": "from fune.money.sum import sum_money  # money.sum@^1",
          "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": [
            "def compare_money(a: Money, b: Money) -> int",
            "def min_money(a: Money, b: Money) -> Money",
            "def max_money(a: Money, b: Money) -> Money",
            "def clamp_money(amount: Money, min: Money, max: Money) -> Money"
          ],
          "reference": "from fune.money.compare import compare_money, min_money, max_money, clamp_money  # money.compare@^2",
          "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": [
            "def apply_rate(amount: Money, basis_points: int, mode: RoundingMode) -> Money"
          ],
          "reference": "from fune.money.apply_rate import apply_rate  # money.apply-rate@^1",
          "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": [
            "def round_div(numerator: int, denominator: int, mode: RoundingMode) -> int"
          ],
          "reference": "from fune.math.round_div import round_div  # math.round-div@^1",
          "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": [
            "def convert_rate(value: str, from_unit: RateUnit, to_unit: RateUnit) -> str",
            "def to_basis_points(value: str, unit: RateUnit) -> int",
            "def from_basis_points(basis_points: int, unit: RateUnit) -> str"
          ],
          "reference": "from fune.math.basis_points import convert_rate, to_basis_points, from_basis_points  # math.basis-points@^2",
          "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": [
            "def calculate_rational(a: Rational, op: RationalOp, b: Rational) -> Rational",
            "def rational(numerator: int, denominator: int) -> Rational",
            "def add_rational(a: Rational, b: Rational) -> Rational",
            "def subtract_rational(a: Rational, b: Rational) -> Rational",
            "def multiply_rational(a: Rational, b: Rational) -> Rational",
            "def divide_rational(a: Rational, b: Rational) -> Rational",
            "def compare_rational(a: Rational, b: Rational) -> int",
            "def rational_to_integer(r: Rational, mode: RoundingMode) -> int"
          ],
          "reference": "from fune.math.rational import calculate_rational, rational, add_rational, subtract_rational, multiply_rational, divide_rational, compare_rational, rational_to_integer  # math.rational@^2",
          "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": [
            "def round_div_big(numerator: str, denominator: str, mode: RoundingMode) -> str"
          ],
          "reference": "from fune.math.round_div_big import round_div_big  # math.round-div-big@^1",
          "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": [
            "def allocate(amount: Money, ratios: Sequence[int]) -> List[Money]"
          ],
          "reference": "from fune.money.allocate import allocate  # money.allocate@^1",
          "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": [
            "def split_even(amount: Money, parts: int) -> List[Money]"
          ],
          "reference": "from fune.money.split_even import split_even  # money.split-even@^1",
          "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": [
            "def format_money(amount: Money) -> str"
          ],
          "reference": "from fune.money.format import format_money  # money.format@^1",
          "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": [
            "def parse_money(text: str, currency: str, style: NumberStyle) -> Money"
          ],
          "reference": "from fune.money.parse import parse_money  # money.parse@^1",
          "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": [
            "def currency_digits(currency: str) -> int"
          ],
          "reference": "from fune.money.currency_digits import currency_digits  # money.currency-digits@^1",
          "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": [
            "def convert_money(amount: Money, rate: ExchangeRate, mode: RoundingMode) -> Money"
          ],
          "reference": "from fune.money.convert import convert_money  # money.convert@^1",
          "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": [
            "def add_vat(net: Money, jurisdiction: str, category: str, on_date: str) -> VatBreakdown"
          ],
          "reference": "from fune.finance.tax.add_vat import add_vat  # finance.tax.add-vat@^1",
          "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": [
            "def remove_vat(gross: Money, jurisdiction: str, category: str, on_date: str) -> VatBreakdown"
          ],
          "reference": "from fune.finance.tax.remove_vat import remove_vat  # finance.tax.remove-vat@^1",
          "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": [
            "def vat_return(transactions: Sequence[VatTransaction], period_start: str, period_end: str) -> VatReturn"
          ],
          "reference": "from fune.finance.tax.vat_return import vat_return  # finance.tax.vat-return@^1",
          "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": [
            "def flat_rate_vat(turnover: Money, sector: str, limited_cost_business: bool, registration_date: Optional[str], supply_date: str) -> FlatRateVat"
          ],
          "reference": "from fune.finance.tax.vat_flat_rate import flat_rate_vat  # finance.tax.vat-flat-rate@^1",
          "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": [
            "def uk_tax_year(iso: str) -> UkTaxYear"
          ],
          "reference": "from fune.dates.uk_tax_year import uk_tax_year  # dates.uk-tax-year@^1",
          "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": [
            "def is_uk_vat_number(value: str) -> bool"
          ],
          "reference": "from fune.validation.uk_vat_number import is_uk_vat_number  # validation.uk-vat-number@^1",
          "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": [
            "def validate_uk_utr(value: str) -> UkUtr"
          ],
          "reference": "from fune.validation.uk_utr import validate_uk_utr  # validation.uk-utr@^2",
          "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": [
            "def validate_uk_company_number(value: str) -> UkCompanyNumber"
          ],
          "reference": "from fune.validation.uk_company_number import validate_uk_company_number  # validation.uk-company-number@^2",
          "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": [
            "def is_email(value: str) -> bool"
          ],
          "reference": "from fune.validation.email import is_email  # validation.email@^1",
          "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": [
            "def validate_phone_e164(value: str, default_country: str) -> PhoneE164"
          ],
          "reference": "from fune.validation.phone_e164 import validate_phone_e164  # validation.phone-e164@^2",
          "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": [
            "def normalise_name(value: str) -> str"
          ],
          "reference": "from fune.text.normalise_name import normalise_name  # text.normalise-name@^1",
          "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": [
            "def normalise_email(value: str) -> Optional[str]"
          ],
          "reference": "from fune.auth.normalise_email import normalise_email  # auth.normalise-email@^1",
          "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": [
            "def is_iban(value: str) -> bool"
          ],
          "reference": "from fune.validation.iban import is_iban  # validation.iban@^1",
          "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": [
            "def validate_bic(value: str) -> BicCheck"
          ],
          "reference": "from fune.validation.bic import validate_bic  # validation.bic@^1",
          "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": [
            "def validate_uk_sort_code_account(sort_code: str, account_number: str, table: UkModulusTable) -> UkSortCodeAccount"
          ],
          "reference": "from fune.validation.uk_sort_code_account import validate_uk_sort_code_account  # validation.uk-sort-code-account@^3",
          "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": [
            "def parse_uk_modulus_table(valacdos: str, scsubtab: str) -> UkModulusTable"
          ],
          "reference": "from fune.validation.uk_modulus_table import parse_uk_modulus_table  # validation.uk-modulus-table@^1",
          "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": [
            "def is_luhn(value: str) -> bool"
          ],
          "reference": "from fune.validation.luhn import is_luhn  # validation.luhn@^1",
          "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": [
            "def validate_lei(value: str) -> LeiCheck"
          ],
          "reference": "from fune.validation.lei import validate_lei  # validation.lei@^1",
          "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": [
            "def mask(value: str, visible: int, mask_char: str, keep_separators: bool) -> str"
          ],
          "reference": "from fune.text.mask import mask  # text.mask@^1",
          "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
}
