{
  "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": "typescript",
  "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": [
            "export function generateSku(prefix: string, attributes: readonly string[], segmentLength: number): string"
          ],
          "reference": "import { generateSku } from \"#fune/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": [
            "export function validateGtin(value: string): GtinCheck"
          ],
          "reference": "import { validateGtin } from \"#fune/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": [
            "export function roundPrice(price: Money, step: number, ending: number, direction: PriceDirection): Money"
          ],
          "reference": "import { roundPrice } from \"#fune/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": [
            "export function unitPrice(price: Money, quantity: string, unit: string, per: UnitPriceBasis): Money"
          ],
          "reference": "import { unitPrice } from \"#fune/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": [
            "export function markupPrice(cost: Money, basisPoints: number, basis: PricingBasis, mode: RoundingMode): Money"
          ],
          "reference": "import { markupPrice } from \"#fune/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": [
            "export function margin(cost: Money, price: Money): MarginBreakdown"
          ],
          "reference": "import { margin } from \"#fune/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": [
            "export function basketTotal(lines: readonly BasketLine[], promotions: readonly Promotion[], basketDiscount: Money, delivery: Money, deliveryTaxCategory: string, pricesIncludeVat: boolean, jurisdiction: string, onDate: string): Basket"
          ],
          "reference": "import { basketTotal } from \"#fune/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": [
            "export function applyPromotions(lines: readonly PromoLine[], promotions: readonly Promotion[]): PromotionResult"
          ],
          "reference": "import { applyPromotions } from \"#fune/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": [
            "export function validateCoupon(coupon: Coupon, lines: readonly CouponLine[], customerUses: number, onDate: string): CouponCheck"
          ],
          "reference": "import { validateCoupon } from \"#fune/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": [
            "export function redeemGiftCard(balance: Money, amountDue: Money): GiftCardRedemption"
          ],
          "reference": "import { redeemGiftCard } from \"#fune/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": [
            "export function loyaltyPoints(scheme: LoyaltyScheme, spend: Money, qualifyingSpend: Money, balance: number, redeemPoints: number): LoyaltyOutcome"
          ],
          "reference": "import { loyaltyPoints } from \"#fune/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": [
            "export function vatRate(jurisdiction: string, category: string, onDate: string): VatRate"
          ],
          "reference": "import { vatRate } from \"#fune/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": [
            "export function ossVat(net: Money, sellerCountry: string, customerCountry: string, euSalesPreviousYear: Money, euSalesThisYear: Money, optedIn: boolean, onDate: string): OssVat"
          ],
          "reference": "import { ossVat } from \"#fune/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": [
            "export function salesTaxUs(amount: Money, state: string, localRatePerMillion: number, onDate: string): UsSalesTax"
          ],
          "reference": "import { salesTaxUs } from \"#fune/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": [
            "export function shippingRate(bands: readonly ShippingBand[], service: string, zone: string, parcel: Parcel, basketTotal: Money, onDate: string): ShippingQuote"
          ],
          "reference": "import { shippingRate } from \"#fune/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": [
            "export function chargeableWeight(lengthMm: number, widthMm: number, heightMm: number, actualGrams: number, divisor: number, roundUpToGrams: number): ChargeableWeight"
          ],
          "reference": "import { chargeableWeight } from \"#fune/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": [
            "export function deliveryDate(orderDate: string, orderTime: string, cutOff: string, serviceDays: number, region: UkRegion | null, closures: readonly string[]): DeliveryEstimate"
          ],
          "reference": "import { deliveryDate } from \"#fune/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": [
            "export function validateUkPostcode(value: string): UkPostcode"
          ],
          "reference": "import { validateUkPostcode } from \"#fune/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": [
            "export function calculateRefund(lines: readonly SaleLine[], basketDiscount: Money, returns: readonly ReturnLine[]): Refund"
          ],
          "reference": "import { calculateRefund } from \"#fune/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": [
            "export function reorderPoint(demand: number, demandPeriodDays: number, leadTimeDays: number, safetyStock: number): number"
          ],
          "reference": "import { reorderPoint } from \"#fune/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": [
            "export function weightedAverageValuation(movements: readonly StockMovement[], currency: string, mode: RoundingMode): AverageCostValuation"
          ],
          "reference": "import { weightedAverageValuation } from \"#fune/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": [
            "export function percentChange(fromValue: number, toValue: number, mode: RoundingMode): number | null"
          ],
          "reference": "import { percentChange } from \"#fune/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": [
            "export function barChart(spec: BarChartSpec): BarChart"
          ],
          "reference": "import { barChart } from \"#fune/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": [
            "export function money(minor: number, currency: string): Money"
          ],
          "reference": "import { money } from \"#fune/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": [
            "export function addMoney(a: Money, b: Money): Money"
          ],
          "reference": "import { addMoney } from \"#fune/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": [
            "export function sumMoney(amounts: readonly Money[], currency: string): Money"
          ],
          "reference": "import { sumMoney } from \"#fune/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": [
            "export function compareMoney(a: Money, b: Money): number",
            "export function minMoney(a: Money, b: Money): Money",
            "export function maxMoney(a: Money, b: Money): Money",
            "export function clampMoney(amount: Money, min: Money, max: Money): Money"
          ],
          "reference": "import { compareMoney, minMoney, maxMoney, clampMoney } from \"#fune/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": [
            "export function applyRate(amount: Money, basisPoints: number, mode: RoundingMode): Money"
          ],
          "reference": "import { applyRate } from \"#fune/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": [
            "export function roundDiv(numerator: number, denominator: number, mode: RoundingMode): number"
          ],
          "reference": "import { roundDiv } from \"#fune/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": [
            "export function convertRate(value: string, fromUnit: RateUnit, toUnit: RateUnit): string",
            "export function toBasisPoints(value: string, unit: RateUnit): number",
            "export function fromBasisPoints(basisPoints: number, unit: RateUnit): string"
          ],
          "reference": "import { convertRate, toBasisPoints, fromBasisPoints } from \"#fune/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": [
            "export function calculateRational(a: Rational, op: RationalOp, b: Rational): Rational",
            "export function rational(numerator: number, denominator: number): Rational",
            "export function addRational(a: Rational, b: Rational): Rational",
            "export function subtractRational(a: Rational, b: Rational): Rational",
            "export function multiplyRational(a: Rational, b: Rational): Rational",
            "export function divideRational(a: Rational, b: Rational): Rational",
            "export function compareRational(a: Rational, b: Rational): number",
            "export function rationalToInteger(r: Rational, mode: RoundingMode): number"
          ],
          "reference": "import { calculateRational, rational, addRational, subtractRational, multiplyRational, divideRational, compareRational, rationalToInteger } from \"#fune/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": [
            "export function roundDivBig(numerator: string, denominator: string, mode: RoundingMode): string"
          ],
          "reference": "import { roundDivBig } from \"#fune/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": [
            "export function allocate(amount: Money, ratios: readonly number[]): readonly Money[]"
          ],
          "reference": "import { allocate } from \"#fune/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": [
            "export function splitEven(amount: Money, parts: number): readonly Money[]"
          ],
          "reference": "import { splitEven } from \"#fune/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": [
            "export function formatMoney(amount: Money): string"
          ],
          "reference": "import { formatMoney } from \"#fune/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": [
            "export function parseMoney(text: string, currency: string, style: NumberStyle): Money"
          ],
          "reference": "import { parseMoney } from \"#fune/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": [
            "export function currencyDigits(currency: string): number"
          ],
          "reference": "import { currencyDigits } from \"#fune/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": [
            "export function convertMoney(amount: Money, rate: ExchangeRate, mode: RoundingMode): Money"
          ],
          "reference": "import { convertMoney } from \"#fune/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": [
            "export function addVat(net: Money, jurisdiction: string, category: string, onDate: string): VatBreakdown"
          ],
          "reference": "import { addVat } from \"#fune/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": [
            "export function removeVat(gross: Money, jurisdiction: string, category: string, onDate: string): VatBreakdown"
          ],
          "reference": "import { removeVat } from \"#fune/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": [
            "export function vatReturn(transactions: readonly VatTransaction[], periodStart: string, periodEnd: string): VatReturn"
          ],
          "reference": "import { vatReturn } from \"#fune/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": [
            "export function flatRateVat(turnover: Money, sector: string, limitedCostBusiness: boolean, registrationDate: string | null, supplyDate: string): FlatRateVat"
          ],
          "reference": "import { flatRateVat } from \"#fune/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": [
            "export function ukTaxYear(iso: string): UkTaxYear"
          ],
          "reference": "import { ukTaxYear } from \"#fune/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": [
            "export function isUkVatNumber(value: string): boolean"
          ],
          "reference": "import { isUkVatNumber } from \"#fune/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": [
            "export function validateUkUtr(value: string): UkUtr"
          ],
          "reference": "import { validateUkUtr } from \"#fune/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": [
            "export function validateUkCompanyNumber(value: string): UkCompanyNumber"
          ],
          "reference": "import { validateUkCompanyNumber } from \"#fune/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": [
            "export function isEmail(value: string): boolean"
          ],
          "reference": "import { isEmail } from \"#fune/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": [
            "export function validatePhoneE164(value: string, defaultCountry: string): PhoneE164"
          ],
          "reference": "import { validatePhoneE164 } from \"#fune/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": [
            "export function normaliseName(value: string): string"
          ],
          "reference": "import { normaliseName } from \"#fune/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": [
            "export function normaliseEmail(value: string): string | null"
          ],
          "reference": "import { normaliseEmail } from \"#fune/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": [
            "export function isIban(value: string): boolean"
          ],
          "reference": "import { isIban } from \"#fune/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": [
            "export function validateBic(value: string): BicCheck"
          ],
          "reference": "import { validateBic } from \"#fune/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": [
            "export function validateUkSortCodeAccount(sortCode: string, accountNumber: string, table: UkModulusTable): UkSortCodeAccount"
          ],
          "reference": "import { validateUkSortCodeAccount } from \"#fune/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": [
            "export function parseUkModulusTable(valacdos: string, scsubtab: string): UkModulusTable"
          ],
          "reference": "import { parseUkModulusTable } from \"#fune/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": [
            "export function isLuhn(value: string): boolean"
          ],
          "reference": "import { isLuhn } from \"#fune/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": [
            "export function validateLei(value: string): LeiCheck"
          ],
          "reference": "import { validateLei } from \"#fune/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": [
            "export function mask(value: string, visible: number, maskChar: string, keepSeparators: boolean): string"
          ],
          "reference": "import { mask } from \"#fune/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
}
