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
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.
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.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.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 - 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.
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.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
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.Where it is enforced
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 - the engine these rules feed
- What guards the engine - what refuses a bad entry, and what reconciles a bad book
- The Bookkeeper - the agent that applies the document path
- SOP versus code - why these rules are not editable knowledge