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

# Settlement, rails and the AP/AR spine

> Obligations, settlements, allocations and advances, the nine payment rails, and the single obligation writer that keeps AP control explainable.

# Settlement, rails and the AP/AR spine

Sub-ledger 1, migration `0003`.
It is the largest of the seven and the one every other family eventually settles through.

## What it records

| Row type                | What it is                                                                                  |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| **obligation**          | something owed or owing: a supplier bill, a customer invoice                                |
| **settlement**          | a movement of money on a rail: a payment out, a receipt in                                  |
| **allocation**          | which settlement paid which obligation, and how much of it                                  |
| **advance application** | a supplier advance or customer deposit applied against an obligation, with no cash movement |
| **deposit**             | collateral placed with a counterparty, or held from one                                     |
| **float movement**      | an employee cash float: advanced, spent, settled                                            |

Every one of these posts its journal entry through the single writer in the same transaction.

## The account legs

Straight from the module docstring, which is the authoritative statement of them:

| Operation                 | Entry                                                             |
| ------------------------- | ----------------------------------------------------------------- |
| create bill (AP)          | `Dr <expense/asset>` / `Cr 2000` (VAS 331 AP)                     |
| pay a bill                | `Dr 2000` / `Cr <rail GL account>`                                |
| create invoice (AR)       | `Dr 1100` (VAS 131 AR) / `Cr <revenue>` \[+ `Cr 2100` output VAT] |
| receive on invoice        | `Dr <rail GL>` / `Cr 1100`                                        |
| supplier advance out      | `Dr 1150` (advance to supplier) / `Cr <rail GL>`                  |
| apply AP advance          | `Dr 2000` / `Cr 1150` - non-cash                                  |
| customer deposit in       | `Dr <rail GL>` / `Cr 2150` (customer advance)                     |
| apply AR advance          | `Dr 2150` / `Cr 1100` - non-cash                                  |
| place deposit (VAS 244)   | `Dr 1520` / `Cr <rail GL>`                                        |
| receive deposit (VAS 344) | `Dr <rail GL>` / `Cr 2350`                                        |
| employee float advance    | `Dr 1280` (VAS 141) / `Cr <rail GL>`                              |
| employee float spend      | `Dr <expense>` / `Cr 1280`                                        |
| employee float settle     | `Dr/Cr <rail GL or expense>` / `Cr/Dr 1280`                       |

## The rails

A **rail** is a channel money actually moves through, and it reconciles against **its own** source rather than against one global cash figure.

| Rail                | Type             | GL account | Currency | Reconciles against |
| ------------------- | ---------------- | ---------- | -------- | ------------------ |
| `bank-vn`           | company bank     | `1010`     | VND      | bank statement     |
| `bank-hk-airwallex` | company bank     | `1010`     | USD, CNY | bank statement     |
| `alipay-hk`         | company bank     | `1010`     | CNY      | bank statement     |
| `cash-fund`         | cash on hand     | `1000`     | VND      | cash box count     |
| `employee-float`    | employee float   | `1280`     | VND      | float ledger       |
| `company-card`      | company bank     | `1010`     | VND, USD | card statement     |
| `fac-factoring-vcb` | finance facility | `2400`     | USD      | facility statement |
| `fac-revolving-vcb` | finance facility | `2400`     | VND      | facility statement |
| `loan-owner`        | finance facility | `2500`     | VND      | facility statement |

The discipline is **per-rail, per-currency, never one global cash check**.
A single "cash reconciles" test passes while a Vietnamese bank shortfall is offset by a Hong Kong surplus, which is two errors reported as no errors.
Nine rails times their currencies gives the Close list a reconciliation per channel, each against evidence that exists independently of the book.

## The single obligation writer

This is the most important rule on the page, and it was learned from a live failure.

> **A control-account leg and its sub-ledger row are one fact. Never post one without the other.**

`settlement.record_obligation` is the **single writer** of the `obligations` table.
Every path that credits AP control `2000` or debits AR control `1100` goes through it:

| Path                             | When                                                                                                           |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `create_bill` / `create_invoice` | a sub-ledger-raised bill or invoice                                                                            |
| `costing.clear_grni_with_bill`   | the material bill that clears a goods-received-not-invoiced accrual                                            |
| `ledger_bridge.post_and_mark`    | a payable that arrived as a **document** through the intake pipeline, in the **same transaction** as the entry |

The third row is the fix.
Before it, the Bookkeeper credited AP `2000` through the ledger writer, and that path never called `settlement.py`.
So every supplier bill that entered as a document was invisible to the AP sub-ledger, and `ap_control_tie` could never tie while intake postings existed.

It was found by the **first live Close run**, which is the correct way for that class of defect to be found: a reconciliation that reports rather than refuses, run against a real book, naming the exact accounts that do not explain themselves.

<Note>
  The obligation the agent path opens is derived in `posting_rules.build_proposal` **from the same amount that built the control leg**, so the sub-ledger row and the GL credit cannot disagree about what the document said.
  It reaches the writer through `graph.trusted_record`, never through checkpoint state, so it inherits [the posting mandate's](/greatbook/capabilities/posting-mandate) trust exactly as the lines do.
</Note>

### What it refuses, and what it does not

* **A payable naming no supplier raises `MissingCounterpartyError` and posts nothing.** `obligations.party_code` is `NOT NULL`, because an unattributable payable cannot be aged or chased.
* **A missing due date is not a refusal.** The column is nullable, `derive.obligation_due` never reads it, and no `creates_ap` document schema carries payment terms. Recording an honest NULL beats inventing a date.
* **`party_code`** is the resolved party-master id when the counterparty resolves, and the extracted supplier **name** when it does not. So the rule *narrows* the one-supplier-two-keys split rather than closing it. The outcome is recorded as `party_resolution` in the posting audit detail, and like the obligation id it is never read back as an input.

### Replay-safe source identity

Obligations now carry the durable journal source tuple introduced by migration `0048`: organization, source type, source id and entry set, linked to the opening journal entry.
That identity follows the same idempotency authority as the control-account write rather than guessing from a human document number.

An exact replay returns the existing obligation.
A replay under the same source identity with different accounting facts conflicts instead of inserting a second row or silently adopting the new values.
Two genuine documents that quote the same supplier bill number remain distinct when their source identities differ.

### AP aging reads obligations, not journal descriptions

The AP aging surface is obligations-authoritative.
Each row begins with the durable obligation identity, counterparty-resolution state, document and nullable due date, native/base opening amounts and opening journal link.
Allocations, advance applications and adjustment notes derive settlement components and the open balance; journal rows contribute exact event/control evidence rather than acting as the payable inventory.

All money remains fixed-point strings, and an amount that cannot be established is `null`, never zero.
If two obligations share a human document number and later cash or adjustment activity cannot be assigned to one exact obligation, GreatBook keeps both rows but reports their affected balances **inconclusive** instead of applying the activity twice.
The book-wide reconciliation compares obligations-derived AP with control account `2000` in base currency and retains complete, partial and inconclusive coverage.

**Pre-fix live rows.**
Payables the Bookkeeper posted before it opened obligations carry no `obligations` row and still fail the AP control tie until a backfill or a book reset.
That is a data decision, not a code one.

## Capability and MCP status

The 116-record `1.14.1` release-source registry describes obligations-authoritative AP aging as the current outcome `greatbook.ap.aging.read@1`.
Registry `1.14.1` releases the dedicated read-only `greatbook_ap_aging_read` adapter as well as the shared Books tools for Settlement registers and tie-outs.
The two reads remain different contracts, and both are part of the live public MCP inventory at registry `1.14.1`.
Neither read creates or changes an obligation, allocation, settlement, or payment.

## Where it is enforced

| Concern                                                                        | Code                                           |
| ------------------------------------------------------------------------------ | ---------------------------------------------- |
| the operations and their account legs                                          | `backend/app/logics/finance/settlement.py`     |
| the rail catalog and the GL account each maps to                               | `backend/app/logics/finance/rails.py`          |
| the schema                                                                     | migration `0003_settlement_ap_ar_spine`        |
| durable obligation source identity                                             | migration `0048_obligation_source_identity`    |
| the derived balances: `due`, `unapplied`, control totals, pools, rail balances | `backend/app/logics/finance/derive.py`         |
| obligations-authoritative AP aging                                             | `langgraph_chat/devcenter/ap_aging_service.py` |
| the reconciliation surface                                                     | `close_checks.run_settlement_checks`           |

## The test that would fail if it broke

`backend/tests/test_settlement.py` covers the operations and their legs.
`backend/tests/test_obligation_idempotency.py` covers exact replay, conflicts and distinct same-number obligations.
`langgraph_chat/devcenter/tests/test_ap_aging.py` covers obligations-derived balances, exact evidence, ambiguity, pagination and base-currency reconciliation.

The regression for the obligation fix is `langgraph_chat/agents/bookkeeper/tests/test_ap_control_tie.py`, and it has an unusual shape worth naming: `test_the_pre_fix_shape_leaves_the_control_account_untied` posts the **old** shape and asserts the tie **fails**.
That means the file cannot pass against a check that has quietly stopped checking, which is the failure mode a passing assertion alone cannot catch.

## What goes wrong without it

An untied control account is a book that says you owe 2.4 billion dong and cannot tell you to whom.
Every aging report, every supplier statement reconciliation, every cash-flow forecast is built on a total that nothing itemises.

And because the GL entry balanced, nothing refused it.
This is exactly the class of defect that Close checksums exist for and GL invariants cannot catch: the entry was internally valid and externally unexplainable.

## Related

* [The seven sub-ledgers](/greatbook/capabilities/subledgers/index) - how the families interlock
* [The Close checksums](/greatbook/capabilities/close-list) - `AP-CONTROL`, the per-rail reconciliations, the cross-foot
* [Nothing stored that can be derived](/greatbook/capabilities/derived-balances) - why there is no `due` column
* [The Bookkeeper](/greatbook/agents/bookkeeper) - the document path that opens obligations
