Skip to content

0015. Encode each country's rules as dated, sourced data behind one contract

  • Status: accepted
  • Date: 2026-10-26

Chapter 6 of the analysis designs the country packs: the core knows what every country shares, and a pack brings the rules of one country. That includes taxes, fiscal documents, guest reporting, identity, data retention and the presentation of prices. The research report (i18n-research.md, section D) confirms that design and adds precisions.

What shapes the code:

  • Rules change, and the date that decides is not always the same. Italy rounds cash totals to 5 cents only from 1 January 2018 (DL 50/2017, art. 13-quater). A tax may follow the night served, a document the day it is issued.
  • Jurisdictions overlap. A region, a province and a municipality each have their own codes and authorities.
  • Some answers are not yet known with certainty. For example, whether the Italian tourist tax belongs in the total shown to consumers, or the exact boundary of the 24-hour rule of Alloggiati Web.
  • Validation. Passing tests does not make a pack legally correct; a professional of the country has to validate it.

Cash rounding (payments.cash_rounding) and consumer prices (prices.consumer) are planned: not yet consumed by payment, folio or pricing workflows. Their dated data, pure calculations and startup validation are implemented; these contracts do not yet provide an operator workflow.

The contract lives in core/countries, pure like the rest of the core.

  • Kinds of rules. RuleKinds maps each capability to its kind of rule: the value of each version, and the date that decides which version applies.

    • payments.cash_rounding is a CashRoundingRule, decided by the day of the payment;
    • prices.consumer is a ConsumerPriceRule, decided by the day the price is offered.

    A capability joins the contract with its kind of rule. The function that applies the rule takes that date (roundCashTotal(policy, total, paidOn), consumerPriceRule(policy, offeredOn)), and the types refuse a value of another kind.

  • Dated rules. A DatedRule<T> has:

    • a first day, included, and optionally a last day, excluded;
    • its sources;
    • its value, or null when the sources say that no such rule applies in the period, as after a repeal.

    checkRules refuses overlaps, empty periods, unsorted versions and versions without a source. ruleOn returns the version in force on a date.

  • Known absence and gaps. Between versions there may be gaps, where nothing was checked. A use case tells them apart: the functions that apply rules answer none for a known absence and not_covered for a gap, and the preflight reports what is not covered. A gap is never read as “the law requires nothing”.

  • Dates come from the version history of the source. A version starts on the day its provision took effect, as the source’s version history shows: Normattiva lists every version of an article. Each source:

    • links to the version read, such as Normattiva’s !vig= links;
    • quotes the words relied on;
    • records the day it was read.

    Where the history cannot be verified, the version starts at the earliest version verified, and the earlier period stays a gap: a date is never guessed.

  • One date per rule. A transition that depends on two dates, such as stays from one day booked before another, is not a dated rule. When one arrives, the pack expresses it as a function, with its sources and tests.

  • Jurisdictions. Rules for a part of a country, such as a municipal tourist tax, will name it by a code under a named scheme, such as ISO-3166-2 IT-52 or ISTAT 048017. Jurisdictions overlap, so they are references, not one hierarchy. The type arrives with the first such rule.

  • Manifest. A CountryPackManifest gives:

    • the pack’s identifier and the countries it covers;
    • the contract version, and the ruleset version (YYYY.MM.N), separate from the code’s;
    • the status: experimental, or validated with who validated it and when;
    • its maintainers.
  • Policy and capabilities. A CountryPolicy is a manifest and the versions of each capability’s rule. Its capabilities are the ones it has versions for (capabilitiesOf): a pack cannot declare what it does not provide.

    • Cash rounding and consumer prices are the capabilities now.
    • Taxes, fiscal documents, guest reporting, identity and data policy join as their layers arrive, in the shape of chapter 6. Those implemented by adapters will join the same way.
  • Registry. The composition root builds it once (countryPackRegistry) from the packs it includes. The registry:

    • takes at most one pack per country;
    • refuses a pack whose versions do not pass checkRules, or whose values the core could not apply (checkRuleValues: a positive whole increment, a known rounding mode, no part listed twice);
    • loads nothing dynamically.

    A country without a pack supports no capability, and the preflight says so.

The packs live in @guestcraft/countries (AGPL), one subpath per country.

  • Rules and adapters. The rules are pure data and functions, under the core’s gates: no clock, randomness, locale or I/O, which Biome and dependency-cruiser check. Adapters (protocols, portals, devices, credentials per legal entity) come later, in adapters/ subpaths outside those gates.
  • it. The Italian pack is the reference. It is experimental until an Italian professional validates it, and holds only rules checked against primary sources:
    • cash rounding to 5 cents for totals paid wholly in cash, from 1 January 2018 (DL 50/2017, art. 13-quater, commi 1, 2 and 6);
    • consumer prices with taxes included, from 21 September 2007, when the decreto legislativo 146/2007 rewrote articles 18 to 27 of the Codice del consumo (the Italian Consumer Code). The words of art. 22, comma 4, lettera c are the same in every version since.
  • fictitious. A country that does not exist, for tests. It has consumer prices without taxes, a rule that changes over time, a year when the rule was repealed, and nothing checked before 2020. It covers no country, so no installation can choose it.
  • Conformance kit. CI runs checkPack on every pack, and the composition root runs it at startup with the date of its clock, refusing to start on a problem. It checks:
    • a well-formed manifest: the identifier, a ruleset version of a past or current month, who validated a validated pack and when, maintainers with names;
    • at least one capability, and no empty list of versions;
    • valid dated rules, with values that the core’s functions can apply, as the registry checks too;
    • sources with a title, an https link with a host, the words relied on if given, and a reading no later than today.

Rules for the code.

  • Use cases never test a country code: they ask the property’s pack.
  • A language or a locale never chooses a pack: the property’s country does.
  • A pack never formats text, numbers or dates (ADR 0012).
  • Adding a country means adding a subpath with a manifest, sourced rules and their tests, and registering it in the composition root. The kit says when the pack is incomplete.
  • Adding a capability means a member of RuleKinds, its value checks in the kit, and the function that applies it. The compiler finds every place to change.
  • The history of a rule stays in the code, with its sources and their words. A document can always be explained by the version in force at its date.
  • Open questions stay visible as missing rules rather than hidden in code, until a professional answers them:
    • whether the tourist tax and other mandatory charges belong in Italian consumer totals. Art. 49, comma 1, lettera e of the Codice del consumo (“ogni altro costo”, any other cost) is the candidate source for distance contracts, such as a booking engine makes;
    • whether a cash refund is rounded like a payment;
    • whether the 6-hour deadline of Alloggiati Web applies to stays of up to 24 hours or of less than 24 hours.
  • Updating a rule is a pull request with its source, reviewed like code. Installations choose when to update.
  • Rules in a database edited at runtime. Rejected: rules are code that must be reviewed, tested and versioned with the release, and an installation should not invent its own tax law.
  • A universal rule engine with expressions. Rejected in chapter 6: each country’s algorithm is clearer and safer as code, with data as its parameters.
  • One package per country. Possible later. One package with subpaths keeps the boundaries simple while there are few packs.
  • Guessing a rule’s history back to its origin. Rejected: an unverified date is worse than a gap, which the use case can handle explicitly.
  • Starting every rule at the version that was read. Rejected after the first review: it denies a rule that the version history shows in force. The Italian consumer price rule would have started in 2026 instead of 2007.
  • Declaring the capabilities in the manifest. Rejected: the list repeated the rules provided, and only a runtime check kept the two in step.