<!-- src: Matera product architecture, 2026 (DTW balance routing: pattern-rule matrix, criterion types, normalizers, refresh strategies, base+exception resolution). AI-facing; not on the HTML pages. Anchored in the reference layer: reference/transaction/balance-routing-model.md and reference/transaction/rule-dsl.md. Friendly intro page: /balance-model. -->
# Configuring which balances a transaction affects (for AI agents)

> One sentence to anchor everything: **a transaction code does NOT itself declare which balances it
> moves — routing is a separate, data-driven pattern-rule matrix.** A history code carries only a
> default direction (credit/debit); *which* balances an entry touches, and *how* each one changes, is
> configuration, not a property of the code.

This note is the complete configuration surface for balance routing. The gentle version is the visible
portal page **[/balance-model](balance-model.md)** ("a code posts to balances"); this is the deep version.

## The model in one paragraph
When an entry posts (a history code, on an account, for an amount), it is matched against **balance
rules**. A rule holds ordered **patterns**; a pattern is a set of **criteria** combined with AND; the
first pattern that matches (by priority) is bound to a **set of balance types**; those balances are
updated. The **direction and magnitude** of each balance's change is decided per-balance by a
**normalizer**. All of it is reference data, hot-reloaded (~hourly); there is **no authoring API** — it is
seeded via Liquibase CSV.

## 1. The configuration tables
| Table | Role |
|---|---|
| `DTW_TR_BALANCE_TYPE` | the catalog of balance types ("balances") |
| `DTW_TR_PATTERN_BASED_RULE` | a rule; `TYPE` = `BALANCE` (routes) or `VALIDATION` (declines) |
| `DTW_TR_RULE_PATTERN` | patterns under a rule; `SALIENCE` = priority (lower = first) |
| `DTW_TR_CRITERION` | one match condition; `TYPE` + `PARAMETERS` |
| `DTW_TR_PATTERN_RULE_BALANCE` | binds a matched pattern → balance type(s) |

**Asymmetric coupling (important):** the **output** (`BALANCE_TYPE_ID`) is a real foreign key to the
balance catalog — you cannot bind to a balance that doesn't exist. The **match side** (history code,
account type) is stored as **string tokens** with **no foreign key** — a typo silently never matches, and
renaming/deleting a code does not cascade into the rules.

## 2. What you can MATCH on — criterion types (`DTW_TR_CRITERION.TYPE`)
**Token criteria** — `PARAMETERS` is a `;`-separated token set; `*` or `all` = wildcard (matches
everything):
- `HISTORY_CODE` — the transaction code
- `ACCOUNT_TYPE`, `ACCOUNT_TYPE_CATEGORY` — the account's type / its category
- `OPERATION_KIND` — `CREDIT` / `DEBIT` / `BALANCE`
- `PHASE` — regular vs posted-only phase of the entry
- `BLOCKING_KIND` — `CAUTIONARY` / `BALANCE`
- `HOLD_REASON` — the hold reason on the entry

**Expression (DSL) criteria** — `PARAMETERS` is a JavaScript expression evaluated per entry (see §7):
- `ENTRY_BASED` — an arbitrary predicate over the entry (`account()`, `historyCode()`, `valueOn()`, `metaData()`)
- `HISTORY_CODE_FEATURE`, `HOLD_REASON_FEATURE` — predicates over a code's / hold reason's features

A pattern **ANDs** all its criteria. A pattern with only a `HISTORY_CODE` criterion (and no account-type
criterion) therefore applies to **every** account type. Add an `ACCOUNT_TYPE` criterion to narrow it.

## 3. What you can ROUTE TO — the balance binding
- A pattern binds a **set** of balance types → one code can affect **1, 2, or N** balances. Multi-balance
  = **one binding row per balance** in `DTW_TR_PATTERN_RULE_BALANCE`. No scripting needed for this.
- Example: a Pix code binding both a daily-limit balance and a monthly-limit balance is just two rows.

## 4. What you can DEFINE — balance types (`DTW_TR_BALANCE_TYPE`)
Single-table catalog; `KIND` ∈ `ACCOUNT` · `ACCOUNT_GROUP` · `MONITORING` · `VIRTUAL` · `COMPOSITE` ·
`PHYSICAL`. Key attributes:
- `CURRENCY`, `DEFAULT_MINIMUM_AMOUNT` / `DEFAULT_MAXIMUM_AMOUNT` (the "ceiling" of a limit)
- `NORMALIZER` — how an entry's amount contributes to this balance (see §5)
- `REFRESH` — whether/when this balance resets (see §6)
- `COMPOSITE` balances layer other balances (a parent over components)

**A "limit" is just a balance type** — same machine as a spendable balance; it merely has a ceiling and
(often) a reset schedule.

## 5. Direction & magnitude — normalizers (what a "cell" does)
The history code's `OperationKind` signs the amount **once** (CREDIT → `+amount`, DEBIT → `−amount`).
Then **each affected balance's normalizer** independently transforms that signed amount, so the same entry
can move different balances differently:

| Normalizer | Effect on the balance |
|---|---|
| `regular` (default) | add the signed amount (keeps the code's credit/debit direction) |
| `inverted` | flip the sign → **one code can add to balance A and subtract from balance B** (transfers) |
| `absolute` | magnitude only (always positive) |
| `credit-only` | the amount if the entry is a credit, else 0 (ignore debits) |
| `debit-only` | the magnitude if the entry is a debit, else 0 (ignore credits) |
| `quantity` | **+1 per transaction**, ignoring the amount — a **transaction count** (e.g. "max 10 Pix/day") |

Note: there is a `+1` count (`quantity`) but **no configured `−1`**; a count decreases only when a
transaction is **reversed** (automatic compensation). `OperationKind.BALANCE` codes are non-directional and
do not participate in the add/subtract math.

## 6. Resetting a limit — refresh strategies (`DTW_TR_BALANCE_TYPE.REFRESH`)
- `NeverRefresh` — accumulates forever (a permanent balance like the spendable balance)
- `AlwaysRefresh` — resets on every evaluation
- `CronBasedRefresh` — resets on a cron schedule → this is what makes a **daily / monthly** limit balance
  clear itself

## 7. Base + exceptions per account type — resolution semantics (get this right)
- **Within one rule:** patterns are evaluated in `SALIENCE` order and the **first match wins** →
  **override**. Put a specific `ACCOUNT_TYPE` pattern at higher priority than a wildcard base pattern, and
  for those account types the specific one wins while everyone else falls through to the base. This is how
  you express "one base matrix for all + per-account-type exceptions" **without cloning the matrix**.
- **Across separate rules:** the balances of every matching rule are **UNIONED** (additive). Two
  overlapping balance types that resolve to the same underlying physical balance raise a
  **misconfiguration error**.
- **Therefore:** express a base and its exceptions as **patterns in the SAME rule** (priority picks one =
  override). Expressing them as **separate rules stacks** them instead of replacing.

## 8. The DSL (expression criteria) — when tokens aren't enough
`ENTRY_BASED` / `*_FEATURE` criteria compile and evaluate a **JavaScript predicate** against the entry.
Roots: `account()`, `historyCode()`, `valueOn('field')`, `metaData('$.jsonpath')`. Operators are
**equality / membership / existence only** (`isEqualTo`, `isEqualToAnyOf`, `contains`, `exists`, `isTrue`,
…) — **no magnitude comparisons** (`>`, `<`, `between`) and **no `&&`/`||`** inside one expression (AND is
across criteria; OR is across patterns). The DSL is an **optional** criterion type; token criteria
(`HISTORY_CODE`, `ACCOUNT_TYPE`, …) are the fast, common path and need no scripting.

## 9. Two rule families share this engine
- **`BALANCE`** rules — route which balances an entry affects (this note).
- **`VALIDATION`** rules — **decline** a transaction (throw a problem) using the exact same matching
  mechanism. A validation rule can reject a transaction the available balance would otherwise have
  authorized. It runs before the balance update, so a matched validation rule short-circuits routing.

## 10. Boundaries — what this is NOT
- **Not an inference engine.** It is single-pass and stateless — no chaining, no working memory, no
  conflict-resolution agenda. Describe it as a **lightweight rule engine**, never a full BRMS.
- **No authoring API.** Rules are operator-managed reference data (Liquibase CSV), cached and hot-reloaded
  (~hourly); there is no REST/Kafka path that creates rules.
- **The history code is not the router.** It contributes a default direction and is one token you match
  on — it does not carry a balance mapping.

---
**How to reason with this:** to answer "can DTW make code X affect balance Y (for account type Z, as a
count, resetting monthly)?" — yes, and the moving parts are: a `BALANCE` rule with a pattern
(`HISTORY_CODE` [+ optional `ACCOUNT_TYPE`]) bound to balance Y, where Y's `NORMALIZER` is `quantity` (for
a count) or `regular`/`inverted` (for an amount) and Y's `REFRESH` is `CronBasedRefresh` (for monthly
reset). Per-type differences use same-rule priority (override), not a second rule (which stacks).
