# The vocabulary that crosses module boundaries.
#
# A module can be as opinionated as it likes INSIDE its own prefix. This file is the part it
# does not get to decide alone, because a second module has to agree: the keys that join two
# modules, the tables everyone shares, and how money is carried.
#
# It exists because the four-module suite run on 2026-09-18 found the same bug twice in a
# different costume — one name, two meanings, discovered at runtime:
#
#   * `bp_id` is a BIGINT in the party master and a VARCHAR in invoicing, so the join the
#     party master exists to provide fails outright rather than returning wrong rows;
#   * `discount_pct` is a FRACTION in the CRM and a PERCENTAGE in invoicing, so a 10% discount
#     handed across the seam bills as 0.10% and nothing errors.
#
# Neither is exotic. Both are what happens when two modules are built a month apart and each
# one is right on its own. `npm run check:packages` reads this file and says so before a
# customer does.

# ── keys that appear in more than one module ───────────────────────────────────────────────
# One type, everywhere. Changing one of these after a module has stored it is a data
# migration, not a rename — which is why they are declared before the second module lands.
keys:
  bp_id:
    type: BIGINT
    owner: business-partner
    not_to_be_confused_with: >-
      bp_key — the customer's OWN identifier for a party (a domain, a legacy customer number,
      a tax id), which is a VARCHAR and is not this. Invoicing named a bp_key column "bp_id"
      and the two meanings ended up in one column, told apart only by whether they parse as a
      number. A module that accepts an arbitrary customer key is storing a bp_key; say so.
    why: >-
      the party id is walked as a graph node, and graph_khop() takes an integer start node, so
      the integer is the constraint and every other module carries it as one.
  account_id:
    type: BIGINT
    owner: gl
    why: the chart of accounts is walked for rollups, same reason as bp_id
  journal_id:
    type: BIGINT
    owner: gl
    why: derived by the caller from the source document, so a retry names the same journal
  tax_code:
    type: VARCHAR
    owner: gl
    why: >-
      a jurisdiction's code (S20, RC, Z) is a string the customer recognises; invoicing owns
      the rates only until a ledger is installed, and then the ledger owns them.
  invoice_id:
    type: VARCHAR
    owner: ar
    why: derived from the source document, and a customer's own numbering is not always numeric

# ── units, where the name does not carry them ──────────────────────────────────────────────
# A column called `discount_pct` reads as a percentage to everyone except the module that
# wrote it as a fraction. Say which, once.
units:
  discount_pct: percent          # 18 means 18% — a module storing 0.18 must convert at its seam
  tax_rate: percent

# ── money ──────────────────────────────────────────────────────────────────────────────────
money:
  sql: NUMERIC(18, 2)
  json: string                   # exact on the wire; a float re-introduces the drift we avoid
  columns_matching: "amount|total|price|balance|debit|credit|subtotal|cost"
  # a name can contain a money word and hold nothing of the kind: cost_centre is a label,
  # price_book_id is an identifier. Suffixes are how they are told apart.
  not_money_suffixes: ["_id", "_no", "_code", "_key", "_book", "_pct", "_centre", "_center", "_mode", "_ref", "_by"]
  also_not_money: ["normal_balance"]

# ── table ownership ────────────────────────────────────────────────────────────────────────
# The naming IS the contract: a prefix says "this module owns it", no prefix says "this is
# shared master data and exactly one module writes it". A module-owned table sitting
# unprefixed is a collision waiting for the module that has not been written yet.
prefixes:
  business-partner: ""           # owns the shared party master; see shared_tables
  crm: crm_
  ar: ar_
  gl: gl_
  purchasing: pur_               # the sixth module, prefixed from its first table
  hire: hire_                    # declared before the module is built — that is the point

shared_tables:
  - business_partner             # written only by business-partner
  - bp_role
  - bp_edges
  - tax_code                     # invoicing seeds it; the ledger owns it once installed
  - platform_meta                # what this bucket IS — written once by migrate.py --stamp,
                                 # read by db.py before the first query. Platform layer, not
                                 # a module's: every app in the bucket reads the same row.
