> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vinmake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dimensions, not sub-accounts

> Per-party, per-order and per-activity detail lives on the journal line. Two of the six dimensions are hard-required by the writer.

# Dimensions, not sub-accounts

## What it guarantees

Analytical detail lives on the **journal line**, never as multiplied account codes.
The chart stays small; the analysis stays arbitrary.

And two dimensions - `activity` and `flow` - are **hard-required on every line**.
A line that cannot say what activity it belongs to and whether it crossed a border does not post.

## The six dimension columns

Every row in `journal_entry_lines` carries these:

| Dimension        | Tier            | What it answers                                                             |
| ---------------- | --------------- | --------------------------------------------------------------------------- |
| `activity`       | **1, required** | which activity this belongs to: `operating`, `production`, `financing`, ... |
| `flow`           | **1, required** | `domestic` or `cross_border`                                                |
| `party_id`       | 2               | the resolved counterparty                                                   |
| `party_type`     | 2               | supplier, customer, employee, lender                                        |
| `cost_object_id` | 2               | the production order, sales order or project this attaches to               |
| `cost_nature`    | 2               | the nature of the cost, for management reporting                            |

Tier 1 is enforced by [GL invariant 8](/greatbook/capabilities/gl-invariants).
Missing either raises `DimensionRequiredError` and nothing is written.

Tier 2 is optional at the writer.
Individual sub-ledgers impose their own requirements on top: `settlement.record_obligation` refuses a payable naming no supplier with `MissingCounterpartyError`, because an unattributable payable cannot be aged or chased.

## Vocabulary and read coverage

The dimension catalog and its seeded definitions are built, so `cost_object_id`, `flow` and `cost_nature` are backed by a controlled vocabulary rather than bare journal-line columns.
Analytical read coverage remains bounded: Accounting Analysis preserves visible rows and marks a missing journal API column as blocked instead of presenting an empty selector or silently widening an unattributed total.

## Why Tier 1 is required rather than encouraged

`activity` and `flow` are the two axes every downstream statement needs and no downstream process can reconstruct.

* **Activity** is what separates operating from financing in a cash-flow statement. Guessing it from the account code works until the first entry that hits an account used by both.
* **Flow** is what separates domestic from cross-border for VAT and foreign-contractor tax. Guessing it from the currency is wrong the moment a domestic supplier invoices in USD.

Requiring them at the writer means the book can always answer those two questions.
Making them optional means the book can answer them for the entries where somebody remembered, which is not an answer.

## The alternative, and why it does not scale

The sub-account approach encodes detail in the account code:

```
2000-SUPP-ACME       Accounts payable - Acme
2000-SUPP-BETA       Accounts payable - Beta
2000-SUPP-GAMMA      Accounts payable - Gamma
...
```

|                        | Sub-accounts                                                 | Dimensions on the line           |
| ---------------------- | ------------------------------------------------------------ | -------------------------------- |
| Chart size             | grows with every counterparty                                | around 60 accounts, stable       |
| "Payables by supplier" | `LIKE '2000-%'` and split the string                         | `group by party_id`              |
| "Payables by activity" | not answerable without re-coding the chart                   | `group by activity`              |
| Adding an axis         | a new code layout, and a migration of every existing account | a column that was already there  |
| Statutory mapping      | one map row per supplier                                     | one map row for `2000`           |
| A new supplier         | a chart change, which is a controlled artifact               | a dimension value, which is data |

The deciding argument is the fourth row.
Sub-accounts fix the analysis axes at the moment the chart is designed.
Dimensions let a question be asked that nobody thought of when the book was set up, which is most of the interesting questions.

## Where it is enforced

| Concern                                        | Code                                                                                    |
| ---------------------------------------------- | --------------------------------------------------------------------------------------- |
| the required set, and the refusal              | `backend/app/services/ledger_service.py` - `REQUIRED_TIER1_DIMENSIONS`, `_prepare_line` |
| the six columns the schema carries             | `ledger_service._DIM_COLUMNS`, migration `0001_gl_core`                                 |
| dimensions folded into the entry checksum      | `ledger_service._canonical_lines`                                                       |
| the dimension defaults the agent path supplies | `langgraph_chat/agents/bookkeeper/posting_rules.py`, `_entry_dimensions`                |
| the read API                                   | `langgraph_chat/devcenter/dim_api.py`                                                   |

<Note>
  Dimensions are part of the **hash chain**.
  `_canonical_lines` includes all six columns in the serialisation it hashes, so changing a line's cost object after the fact would break the chain exactly as changing its amount would.
  A dimension is a fact about the entry, not an annotation on it.
</Note>

## How the agent path fills them

The Bookkeeper's posting rules default `activity` to `operating` and derive `flow` from whether the line's currency matches the org's base currency (`domestic` when it does, `cross_border` when it does not).
The resolved party from the `match` node rides along as `party_id` and `party_type` when there is one.

Those defaults are deliberate and narrow.
The agent is not guessing at an analysis axis; it is filling the two required fields with the value that is correct for the overwhelming majority of intake documents, and a human at the posting gate sees the proposed line including its dimensions before it posts.

## The test that would fail if it broke

`backend/tests/test_ledger_engine.py` asserts that a line missing a Tier-1 dimension is refused.
The sub-ledger suites assert the dimension values each family posts, which is what makes per-activity and per-flow reporting reproducible rather than incidental.

## What goes wrong without it

Without required Tier 1, you get a book where 80% of lines carry an activity and the cash-flow statement is built from that 80% plus an assumption about the rest.

Without dimensions at all, you get the chart-of-accounts explosion, and then the thing that always follows it: a spreadsheet on someone's laptop that maps account-code fragments to the real analysis, maintained by hand, which is exactly the artifact GreatBook exists to replace.

## Related

* [The dual IFRS + TT200 chart](/greatbook/capabilities/chart-of-accounts) - the chart this keeps small
* [The 18 GL invariants](/greatbook/capabilities/gl-invariants) - invariant 8
* [Immutability and the hash chain](/greatbook/capabilities/immutability-and-hash-chain) - why dimensions are hashed
* [Settlement, rails and the AP/AR spine](/greatbook/capabilities/subledgers/settlement) - where `party_id` is not optional
