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

# Worked cases: the Bookkeeper

> Eleven cases, 55 graded assertions, against a real migrated Postgres. The proposal is right, the ledger is empty, and the gate holds - graded as three separate facts.

# Worked cases: the Bookkeeper

The Bookkeeper turns an approved document into a proposed journal entry and then **stops**.

Nothing here is doubled.
The compiled graph, the real posting rules and the real ledger writer run against a **real `alembic upgrade head` Postgres**, so the advisory lock, the 18 GL invariants, the hash chain and the GL idempotency key are all live in every case.

The line that makes this page worth reading is on the first case, and it is a number: **`expect_entry_count: 0`**.

***

## `bk-propose-material-vat-invoice` · right answer, empty ledger

**Given** an approved intake row: doc type `supplier_vat_invoice`, bill kind `material-VAT`.

**The agent produced** a proposal:

|    | Account                 |      Debit |     Credit |
| -- | ----------------------- | ---------: | ---------: |
| Dr | `1200` Inventory        | 57,240,000 |            |
| Cr | `2000` Accounts payable |            | 57,240,000 |

**Graded on**

| Assertion                 | Expected   |
| ------------------------- | ---------- |
| `expect_status`           | `proposed` |
| `expect_debit_accounts`   | `1200`     |
| `expect_credit_accounts`  | `2000`     |
| `expect_balanced`         | true       |
| `expect_gate_before_post` | true       |
| **`expect_entry_count`**  | **0**      |

<Note>
  **The proposal is correct and the ledger is still empty.**

  Three separate facts are graded here, and they are graded separately on purpose: the accounts are right, the proposal balances, and **nothing was written**.

  An agent that proposed the right entry and posted it would pass the first two and fail the third.
  That is the whole design of this lane, expressed as an assertion.
</Note>

***

## `bk-propose-input-vat-split` · why balance is not enough

**Given** a supplier VAT invoice with the amounts stated separately: net 1,000,000, VAT 100,000, gross 1,100,000.

**The agent produced** a three-leg proposal:

|    | Account                                   |     Debit |    Credit |
| -- | ----------------------------------------- | --------: | --------: |
| Dr | `1200` Inventory (VAS 152, raw materials) | 1,000,000 |           |
| Dr | `1300` Input VAT recoverable (VAS 133)    |   100,000 |           |
| Cr | `2000` Accounts payable (VAS 331)         |           | 1,100,000 |

**Graded on** debit accounts `1200` **and** `1300`, credit `2000`, amount 1,100,000, balanced, gate held, ledger empty.

**Why it matters, and it is the sharpest illustration on this page.**

The old behaviour lumped the recoverable input VAT into the inventory leg: `Dr 1200 1,100,000 / Cr 2000 1,100,000`.

That **balances**.
It passes every GL invariant.
It hashes into the chain identically well.
It is also wrong, in a way that costs real money: the recoverable VAT is an asset the business can reclaim, and buried inside inventory it is neither reclaimed nor visible.

<Warning>
  **Balance cannot tell those two entries apart.** Only the accounts can.

  Which is exactly why the judgment layer needs its own board: the deterministic engine is doing its job perfectly on a wrong answer, and no invariant in the system will ever object.
</Warning>

***

## `bk-propose-canteen-by-doc-type` · fallback, never suspense

**Given** an intake row with **no** bill kind, doc type `canteen_invoice`, amount 350,438,400.

**Graded on** debit `6400` (office and admin expense) resolved by the doc-type fallback, credit `2000`, balanced, gate held.

**Why it matters.**
There is a wrong answer available that looks tidy: book it to a suspense account and let someone sort it later.
The registry resolves the account instead.
A suspense fallback is a decision deferred into a place nobody revisits, which is the next case.

***

## `bk-unmapped-account-hard-error` · the refusal must actually raise

**Given** an unmappable document: doc type `unknown_doc`, bill kind `mystery-kind`.

**Graded on**

| Assertion              | Expected                                  |
| ---------------------- | ----------------------------------------- |
| `expect_raises`        | `UnmappedAccountError`                    |
| `expect_entry_count`   | 0                                         |
| `expect_intake_status` | `active` - left for a human, not consumed |

<Note>
  **The grading here is deliberately stronger than "nothing posted".**

  A silent no-op also posts nothing.
  So does a crash, a swallowed exception and a graph that quietly ends.

  This case grades that the error is **actually raised**, because the legacy poster this replaced booked an unmappable bill to suspense `6900` and reported success.
  An unmapped account is a question for a human, and the only honest way to ask it is to stop.
</Note>

***

## `bk-post-one-balanced-entry` · the full loop, exactly once

**Given** an approved intake, bill kind `service`, amount 5,000,000, with a distinct third person approving the posting.

**The agent produced** exactly one entry: `Dr 6410 Services 5,000,000 / Cr 2000 Accounts payable 5,000,000`.

**Graded on** status `posted`, the two accounts, balanced, `expect_entry_count: 1`, the posted entry balances **in base currency in the ledger**, the intake row flipped to `posted`, and the row stamped with its resulting entry id.

**Why it matters.**
This is the one case where something does reach the book of record, and it is graded end to end rather than at the graph's own report: the entry count and the balance are read out of Postgres, and the link back to the source document is checked in the row.

<Warning>
  **This case was permanently red on `main` and nobody noticed.**

  The eval fixture set an approver and a checker but never a **submitter**, and strict separation of duties needs three distinct named people - so every posting scenario died at the gate before account mapping was even reached.

  The Bookkeeper's two most important cases, "it posts a balanced entry" and "it does so exactly once", had never passed.

  A permanently red case hides a regression exactly as well as a permanently skipped one.
  That lesson produced [the expected-baseline marker](/greatbook/proof/harness), so a known failure now has to declare itself by name.
</Warning>

***

## `bk-idempotent-relaunch` · a double-approve is a no-op

**Given** the same intake, approved and posted, then relaunched on a fresh thread.

**Graded on** the relaunch short-circuits at ingest, proposes **nothing**, reports `posted`, and the ledger entry count is **still 1**.

**Why it matters.**
A double-click on Approve, a retried request, a resumed run after a restart: all three are the same event arriving twice.
The [GL idempotency key](/greatbook/capabilities/idempotency-and-durability) makes the second one a no-op at the writer, and this case checks the agent does not paper over it by proposing a second, different entry.

***

## Separation of duties, graded twice

| Case                                 | Given                                                             | Graded on                                                       |
| ------------------------------------ | ----------------------------------------------------------------- | --------------------------------------------------------------- |
| `bk-maker-cannot-be-checker`         | the person who approved the document tries to approve its posting | raises `MakerIsCheckerError`, entry count 0, row still `active` |
| `bk-rejected-posting-writes-nothing` | the posting is rejected at the gate                               | nothing written, row left `active` for re-work                  |

The refusal happens **before the transaction opens**, so nothing reaches the ledger and nothing needs rolling back.
See [two gates, three people](/greatbook/capabilities/maker-checker) for the rule, and [the posting mandate](/greatbook/capabilities/posting-mandate) for what makes it hold when someone attacks it directly.

***

## `bk-non-postable-intake-refused` · eligibility

**Given** a row still `pending` human review.

**Graded on** it never reaches `propose`.

The gate is not the only thing standing between a document and the ledger.
A document that no human has approved is not a candidate for posting at all, and the graph declines it a step earlier.

***

## What the whole page adds up to

Seven of the eleven cases assert **`expect_entry_count: 0`**.

That is not padding.
The most valuable property of this agent is what it does **not** do, and a property nobody asserts is a property nobody will notice losing.

## Related

* [The Bookkeeper](/greatbook/agents/bookkeeper) - the graph and its nodes
* [The general ledger](/greatbook/capabilities/general-ledger) - the single writer these cases post through
* [Two gates, three people](/greatbook/capabilities/maker-checker) - the rule two of these cases exercise
* [The scoreboard](/greatbook/proof/scoreboard) - the Bookkeeper's 16 score keys
