> ## 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.

# Posting rules: the map

> The three places posting rules live, the refuse-never-guess law they share, and the index of every document and transaction type's journal entry.

# Posting rules: the map

A **posting rule** answers one question: *given this business document, which accounts move, in which direction, by how much?*

GreatBook answers it in three places, deliberately kept apart, and every one of them obeys the same law.

## The three rule surfaces

| Surface                          | Code                                                                                                         | What it maps                                                                                             | Who triggers it                                                                        |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| **the document path**            | `langgraph_chat/agents/bookkeeper/posting_rules.py`                                                          | an approved intake record - a supplier bill, a VAT invoice - to a balanced entry plus its sub-ledger row | the Bookkeeper, after a human approves the document, before a human approves the entry |
| **the sub-ledger operations**    | `backend/app/logics/finance/`                                                                                | a named business operation - pay a bill, receive goods, accrue payroll - to its entry                    | a service call or a connector, each posting through the single writer                  |
| **the accountant's adjustments** | `langgraph_chat/agents/accountant/adjustments.py`, gate-checked by `posting_rules.validate_adjustment_lines` | one of the Accountant's typed adjusting entries to its lines                                             | the Accountant at close, staged at the Bookkeeper's gate                               |

They are separate because they are triggered by different things and reviewed by different people, not because they disagree about accounting.
Where they touch the same fact they share code: the Bookkeeper's post-back opens its AP obligation through the *backend's own* `settlement.record_obligation` and writes its VAT register line through the backend's own `tax.record_vat_entry`, rather than inserting rows of its own.

<Note>
  A parallel insert would drift the moment either side gained a column.
  Reusing the single writer per row type is what lets a bill that arrived as a scanned document and a bill raised through `create_bill` be read the same way by `derive.obligation_due`, by the AP-by-party index, and by `close_checks.ap_control_tie`.
</Note>

## The law: recognise, or refuse - never guess

Every rule surface refuses rather than defaults.
This is the single most load-bearing convention in the posting layer, and each refusal below exists because the alternative *balances* and therefore passes every downstream check.

| Refusal                     | Raised when                                   | What the default would have done                                                   |
| --------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------- |
| `UnmappedAccountError`      | no rule maps the record to a real account     | booked it to suspense `6900`, silently, which is what the legacy poster did        |
| `MissingCounterpartyError`  | a payable names no supplier                   | credited AP with nobody owed, leaving a payable nobody can chase                   |
| `InconsistentVatError`      | net + VAT do not equal the gross              | picked one of three extracted figures and been wrong invisibly                     |
| `UnrecognisedCurrencyError` | the currency field is not a currency          | read it as the base currency, valuing a foreign document at par                    |
| `MissingBaseCurrencyError`  | the org's own base currency could not be read | defaulted to `VND` - a value every posted line *asserts* is not a value to default |
| `BaseAmountRequiredError`   | a foreign obligation states no base amount    | written a native number into the column the control tie reads as base              |
| `PartyRequiredError`        | an obligation has no party                    | left the control account unattributable                                            |
| `RateUnavailableError`      | no FX rate exists on or before the entry date | posted at par                                                                      |

<Warning>
  **The suspense accounts are reserved for humans.**
  `4900` (uncategorized income) and `6900` (uncategorized expense) exist, and a person may deliberately code something to them.
  No agent path may *default* to them - `resolve_expense_account` raises instead, and `validate_adjustment_lines` refuses any proposed line that names one, so the rule holds even for lines the Bookkeeper did not build itself.
</Warning>

## Three things every posted line states

These are properties of the line, not of the document, and they hold on all three rule surfaces.

**A real account code.**
Resolved from the [dual chart](/greatbook/capabilities/chart-of-accounts) - the internal code, with its Circular 200 twin carried as a mapping.

**Its currency.**
Stated on *every* line, including a base-currency one, because `_prepare_line` reads `line.currency or base_currency`: an absent currency is not "unknown", it is an assertion that the amount is already in the book's own currency.
Omitting it is what once posted a 1,000 USD bill as 1,000 dong.

**Its Tier-1 dimensions.**
`activity` and `flow` are required on every line; the writer raises `DimensionRequiredError` without them.
The document path defaults `activity` to `operating` and derives `flow` as `domestic` or `cross_border` from whether the line's currency equals the base currency, and carries the resolved party as source lineage when the match step found one.

<Info>
  No rule surface proposes an `exchange_rate`.
  The rate is the engine's to resolve inside the posting transaction, as of the entry date.
  A rate read at propose time would be a second opinion about the same number, taken at a different moment, with nothing forcing the two to agree.
</Info>

## What the document path covers today, stated plainly

`posting_rules.build_proposal` implements **`creates_ap` only**.

A supplier payable posts; any other `accounting_effect` raises `UnmappedAccountError` rather than guessing.
That is a deliberate floor, not an oversight: the effects a document can carry are declared across 146 registry types, and each one reaches the ledger only once its rule is written and tested.

Everything else in the rule catalogue - receipts, payments, the costing chain, payroll, adjustment notes, FX revaluation - is reachable through the **sub-ledger operations**, which are complete and tested, and through the Accountant's adjustment path.

## The catalogue

| Family                                                                                        | Page                                                                        |
| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| supplier bills, VAT invoices, GRNI clearing, landed cost, FCT                                 | [Payables and input VAT](/greatbook/gl/rules-payables)                      |
| AR invoices, receipts, customer deposits, factoring                                           | [Receivables and revenue](/greatbook/gl/rules-receivables)                  |
| payments, rails, advances, deposits, employee float, loans                                    | [Payments, rails and treasury](/greatbook/gl/rules-money-movement)          |
| the costing chain, the 627 pool, payroll, depreciation                                        | [Inventory, costing and payroll](/greatbook/gl/rules-inventory-and-payroll) |
| credit and debit notes, Accountant adjustments, intercompany, equity, typed business reversal | [Adjustments, FX and consolidation](/greatbook/gl/rules-adjustments-and-fx) |

## Rules are code, not configuration - on purpose

There is no data-driven posting-rules layer today.
Which accounts a document type moves is a rule with **ledger consequences**, so it lives in deployed, tested code, and changing it is a deploy.

What *is* editable as knowledge is the agent's judgment: how the Clerk classifies a document among the 146 registry types, what counts as a supplier bill.
That lives in the SOP store.
The dividing line is drawn in full on [SOP versus code](/greatbook/agents/sop-vs-code).

<Warning>
  The Bookkeeper's SOP rulebook is **generated from `posting_rules`**, not retyped from it.
  A hand-maintained rulebook drifts, and a drifted rulebook is worse than none: the agent then cites, with a citation, a procedure the system no longer follows.
  A deploy that changes a posting rule must reseed the SOP datasets, or the agents keep describing the old entry.
</Warning>

## Where it is enforced

| Concern                                                                          | Code                                                   |
| -------------------------------------------------------------------------------- | ------------------------------------------------------ |
| the document path's rules, the account map, the VAT split, the currency decision | `langgraph_chat/agents/bookkeeper/posting_rules.py`    |
| the refusal taxonomy for the document path                                       | `langgraph_chat/agents/bookkeeper/errors.py`           |
| the sub-ledger operations, one module per family                                 | `backend/app/logics/finance/`                          |
| the accountant's gate-check for proposed adjustment lines                        | `posting_rules.validate_adjustment_lines`              |
| the single obligation writer and the single VAT register writer                  | `settlement.record_obligation`, `tax.record_vat_entry` |

## The test that would fail if it broke

`langgraph_chat/agents/bookkeeper/tests/` carries one file per rule, each named after the defect it pins: `test_input_vat_split.py`, `test_ap_control_tie.py`, `test_fx_par_posting.py`, `test_trust_boundary.py`, `test_duplicate_double_post.py`.

Three of those files post the **pre-fix shape** and assert the old behaviour is still detectable - `test_the_pre_fix_shape_leaves_the_control_account_untied`, `test_the_pre_fix_shape_leaves_input_vat_off_the_books`, `test_the_pre_fix_shape_posts_a_thousand_usd_as_a_thousand_dong`.
A test suite that can no longer see the bug cannot prove the fix.

## Related

* [How a journal entry is formed](/greatbook/gl/index) - the engine these rules feed
* [What guards the engine](/greatbook/gl/guards) - what refuses a bad entry, and what reconciles a bad book
* [The Bookkeeper](/greatbook/agents/bookkeeper) - the agent that applies the document path
* [SOP versus code](/greatbook/agents/sop-vs-code) - why these rules are not editable knowledge
