docs(arap): B-41, the bookspec examples, and the four registers (P4b S7)

rules.md B-41 states the write-off: the direct method the owner chose
(A-5), Dr 6990 / Cr 1200 with the receivable tagged, at most what is
owed and at most the least owed from that day on (a partial write-off as
§1.166-3(a) allows), refused on cash-basis books (§1.166-1(e)), its
evidence filed in the same call (§1.166-2(a)); bills are never written
off (ASC 405-20-40-1); and a recovery is not a new movement (D-49): the
same year, reverse the write-off and record the payment; a later year, a
plain entry to a bad-debt-recoveries revenue account, the invoice staying
written off (IRC §111).

bookspec states B-41 for the first time and gives B-38 its example: two
new examples, bad-debt-direct-write-off (two partial write-offs of one
invoice, the balances and the trial balance worked by hand) and
arap-hand-control-posting-refused (a hand line an earlier version wrote
replays, the same line written today is refused). The example schema
gains the dated write_off event, lets a refused entry say so (an entry
posts whole or not at all; older and refused never combine), and its
refusal vocabulary gains untagged_control_line, B-38's control guard.

The registers: docs/architecture.md names the new tool group;
docs/receipt-store.md lists invoice.written_off and what replay does with
it; docs/parity.md records the slice (no row changes status, the floor
stands); docs/testing.md records every test and what P12 must mutate;
docs/verification.md the two Lean theorems and what they cover.

No figure moves: no golden file changed.
This commit is contained in:
swaits committed 2026-10-27 12:44:45 -06:00
1 parent b731d7ed04
commit 0322778683
13 files changed
+547 -25

No files matched your search

+3
View File
@@ -160,6 +160,9 @@ groups are:
- `parties` — parties, vendors, customers, roles, W-9s, the TIN vault,
1099s, entry parties;
- `attachments` — receipts, filed documents, and their links;
- `write_offs` — `books.write_off_invoice`, the bad debt (P4b S7; rules.md
B-41), kept apart from `documents` so S4.4's document tools and this one
land in different files;
- `settings` — the handbook, `books.settings` and the recorded FX rates
(the module is `settings_tools`, since `crate::settings` is the live
settings);
+11
View File
@@ -104,3 +104,14 @@ door on loopback for Caddy or a tunnel. `crates/ledger-cli/tests/two_user.rs`
proves the account boundary and the move's head in CI
(`.github/workflows/two-user.yml`); the recipe is `deploy/README.md`.
The cutover rule in `docs/capability-parity.md` is unchanged.
## P4b S7: bad debts (write-offs), 2026-10-27
A/R gained the direct write-off the owner chose (A-5; plan 8.2 S7): an
invoice that will not be collected comes off 1200 with Dr 6990 Bad Debt
Expense, its receivable line tagged `WriteOff` so the receipt's figures
stay derived, and its evidence of worthlessness filed in the same call
(rules.md B-41; IRC §166, §111; Treas. Reg. §1.166-1(e), (f), -2(a),
-3(a)). A same-year recovery is the reversal D-49 records, not a new
movement; bills are never written off. No row above changed status (the
A/R and A/P rows already say `done`), so the floor stands (10 done).
+8 -2
View File
@@ -57,7 +57,7 @@ apply is safe — on startup the applier replays every journal line with
- `seq`: 1-based, gapless, assigned by the writer.
- `ts`: UTC, RFC 3339. Recorded, never trusted for ordering (`seq` orders).
- `type`: `account.added`, `entry.posted`, `entry.reversed`, `invoice.created`,
`bill.created`, `receipt.attached`, `receipt.linked`, `receipt.unlinked`,
`invoice.written_off`, `bill.created`, `receipt.attached`, `receipt.linked`, `receipt.unlinked`,
`document.filed`, `attachment.withdrawn` (plus the A/R, A/P, asset,
period, payroll, and class events listed on `JournalLine`).
- `payload`: the full domain facts (account fields, entry with lines, blob
@@ -74,7 +74,13 @@ apply is safe — on startup the applier replays every journal line with
entry exists): it is a `receipt.attached` on the invoice's own entry
(`INV-0007.pdf`, `application/pdf`, "Issued invoice"), its SHA-256 the
`blob_sha`, linked to the invoice with `attachment.linked` and found
through that link (plan 8.3.11).
through that link (plan 8.3.11). An `invoice.written_off` (plan 8.3.6,
P4b S7) carries `{invoice_id, entry_id, date, amount, reason}`; replay
tags the entry's receivables line `WriteOff`, and the projection keeps
no written-off figure either: it is the tagged lines' sum (rules.md
B-38, B-41). Its evidence of worthlessness rides beside it, as a
`receipt.attached` on the write-off's entry and an `attachment.linked`
to the invoice, filed in the same call.
- `prev`: hex SHA-256 of the previous line's canonical JSON (keys sorted, no
whitespace). The genesis line uses `"prev": "000...0"` (64 zeros).
+102
View File
@@ -3114,3 +3114,105 @@ starts from empty books (legacy lines are covered by the identity
coding of `DocRef`), and an account becoming a control account by its
first document, holding lines from before, is outside it (those lines are
listed as unattached).
## B-41 A debt that will not be collected is written off, and a recovery is income
**bookspec:** `BS-arap-5`
**Status:** CHOICE (the owner, 2026-10-23, plan 8.3.1 A-5: bad debts by
the **direct write-off** method, and 8.3.2: for a small company's
receivables a CPA accepts the direct method and notes it, where GAAP
prefers an allowance under ASC 326-20). AUTHORITY for what the rule
rests on: IRC §166(a) (a debt that becomes worthless is deductible in
that year), Treas. Reg. §1.166-1(e) (a debt never counted as income has
no basis to deduct), §1.166-2(a) (worthlessness on all the pertinent
evidence), §1.166-3(a) (a partial write-off, to the extent charged off),
and IRC §111 (a recovery of an amount deducted is income in the year
received). ASC 405-20-40-1 for a bill's release.
1. **The write-off.** Writing off an invoice posts one balanced entry:
Dr the bad-debt expense role (`bad_debts`, 6990) and Cr receivables
for the amount, the receivable line carrying the invoice with the
movement `WriteOff`, so what the invoice owes falls by the amount from
the write-off's day and what was written off rises by it. The amount
is above zero and at most what the invoice owes, and at most the least
it owes on any day from the write-off's on (so no later day, a payment
after it included, takes the document below nothing: B-38 INVARIANT 2).
A partial write-off is allowed (§1.166-3(a)); the rest stays owed and
aged.
2. **The day.** The write-off's day is in the calendar, in an open period
(B-12), and not before the invoice's. A document's entry is reversed,
never edited (B-8, B-9): a write-off is not a void and a void is not a
write-off.
3. **Cash basis refused.** Books on the cash basis never counted the
invoice as income, so the debt has no basis to deduct (§1.166-1(e)):
the command refuses, changing nothing.
4. **Bills are never written off** (ASC 405-20-40-1): a liability leaves
the books only when paid or when the creditor releases it, which is a
vendor credit (not scheduled). The command takes an invoice.
5. **A recovery is not a new movement** (plan D-49). In the same tax year,
reverse the write-off: the invoice reopens and owes what the write-off
took off, and the payment is recorded as any payment. In a later year,
record a plain entry Dr the deposit account / Cr a "Bad debt
recoveries" revenue account (tax category other income), its memo
naming the invoice: the recovery is income (§111) and the invoice stays
written off. There is no `Movement::Recovery` and no bad-debt-recoveries
role.
6. **Evidence.** The write-off's evidence of worthlessness — collection
correspondence, a bankruptcy or liquidation notice (§1.166-2(a)) — is
filed in the same call: a `receipt.attached` on the write-off's entry,
linked to the invoice. A write-off with no evidence is refused. The tax
judgments (whether the debt is worthless, and in which year) stay with
the CPA: the books flag each write-off made this year for the CPA's
note.
7. **Journal.** `invoice.written_off {invoice_id, entry_id, date, amount,
reason}` is a new event type and needs journal feature
`documents_v2` (B-27, D-52): an older version refuses it rather than
misreading a document as still owed. Replay tags the receivable line
`WriteOff`; the projection keeps no document figure (B-38).
```text
GOAL: write off what will not be collected, as the tax rules require.
SOURCE: IRC §166(a), §111; Treas. Reg. §1.166-1(e), (f), -2(a), -3(a);
ASC 326-20 and ASC 405-20-40-1 noted; CHOICE: the direct
write-off method (plan 8.3.1 A-5).
INVARIANTS:
1. amount <= outstanding on the write-off's day and on every day from
it; Dr bad_debts / Cr receivables [d, WriteOff]; the evidence of
worthlessness filed in the same call.
2. Its reversal re-instates the debt; bills are never written off.
3. Refused on cash-basis books.
4. A refusal leaves the books and the journal unchanged; NEVER panics.
SCENARIOS:
1. Half paid, the rest written off: Written off, with 50 paid / 50
written off.
2. A write-off of 0, a negative amount, or more than is owed: SHOULD
refuse, naming what is owed.
3. Cash-basis books: SHOULD refuse (no basis to deduct).
4. The write-off reversed in the same year and the payment recorded:
Paid; receivables back to zero.
5. A later year's recovery: a plain entry to a bad-debt-recoveries
revenue account; the invoice stays written off.
```
**Enforced:** `ledger_core::Ledger::write_off` (and its replay
`replay_invoice_write_off`), `LedgerError::{WriteOffOnCashBasis,
InvalidWriteOff}`, the `bad_debts` control role (`BAD_DEBTS`, 6990);
`ledger_store::Store::write_off_invoice` (feature `documents_v2`; the
evidence filed and linked in the same call), `WriteOffEvidence`,
`StoreError::WriteOffNeedsEvidence`; `books.write_off_invoice`
(`crates/ledger-mcp/src/tools/write_offs.rs`); the viewer's timeline
(`Movement::WriteOff`); Needs attention's write-off item.
**Checked:** `ledger-core` `tests/spec_documents.rs`
(`half_paid_rest_written_off_is_written_off`,
`write_off_refused_on_cash_basis`, `write_off_above_outstanding_refused`,
`write_off_then_recovery_same_year`); `ledger-store`
`tests/arap_standing.rs` (`write_off_waits_for_documents_v2`,
`write_off_replays_and_projects`, `write_off_without_evidence_is_refused`),
`tests/arap_props.rs` (`status_is_a_function_of_journal_and_date` with
write-offs); over the wire `crates/ledger-cli/tests/documents.rs`
(`write_off_then_recovery`, `write_off_needs_evidence`); bookspec
`bad-debt-direct-write-off`; Lean `Documents.Step.writeOff_unattached`,
`Documents.writeOff_within_bounds`; the viewer
`a_write_off_shows_on_the_timeline`; the item
`a_write_off_is_flagged_for_the_cpa`.
+77
View File
@@ -3050,3 +3050,80 @@ and `BooksServer::new`'s copy (a dropped field must fail
`isolation_label_is_enforced_under_the_unit`); the host's `server_flags`
extension and `parse_args`' passthrough of `--isolation` and its optional
`--http` value.
## P4b S7: bad debts (write-offs), 2026-10-27
The write-off (plan 8.2 S7; rules.md B-41): an invoice that will not be
collected leaves the receivables as a deduction, with its evidence filed
in the same call.
- **ledger-core, `tests/spec_documents.rs`:**
`half_paid_rest_written_off_is_written_off` (B-38 SCENARIO 7: 50 paid,
50 written off, one balanced entry Dr 6990 / Cr 1200, the receivable
line tagged `WriteOff`, receivables down by exactly the write-off);
`write_off_refused_on_cash_basis` (B-41 INVARIANT 3: refused, the
snapshot unchanged, no draft left); `write_off_above_outstanding_refused`
(the bound at the day, a zero and a negative amount, then the exact
amount owed taken); `write_off_then_recovery_same_year` (D-49: the
reversal re-instates the debt, the payment settles it, the invoice reads
Paid and the write-off entry no longer stands). The first test's book
is also the identity's: `control_check` agrees after the write-off.
- **ledger-store, `tests/arap_standing.rs`:**
`write_off_waits_for_documents_v2` (R-A: refused with
`FormatUpgradePending`, journal and head unchanged, in the old-books
fixture; taken after the upgrade; the evidence filed on the entry and
linked to the invoice; replay equals live);
`write_off_replays_and_projects` (the journal holds one
`invoice.written_off` with its five facts, `receipt.attached` then
`attachment.linked`; 6990 carries the expense, 1200 is zero, the
control check agrees, `verify` passes and a reopened store has the same
books and the same figures); `write_off_without_evidence_is_refused`
(no evidence, and an attachment that isn't there, journal nothing; one
already filed is linked to the entry and the invoice).
`tests/arap_props.rs` gains `Op::WriteOff`: random write-offs (partial
and over-large, refused ones included) run through the store, which is
then reopened and read by a `Snapshot`; `status_is_a_function_of_journal_and_date`
still holds at every date.
- **Over the wire, `crates/ledger-cli/tests/documents.rs`:**
`write_off_then_recovery` (an upload as the evidence, `confirmed`, the
invoice reading written_off with its figures and its timeline, the
receipt on the entry, the reversal and the payment, the control check
agreeing) and `write_off_needs_evidence` (neither or both evidence
arguments refused; an already-filed attachment linked to the entry and
the invoice).
- **The viewer, `crates/ledger-gui/tests/invoices.rs`:**
`a_write_off_shows_on_the_timeline` (the shipped screenshot
`invoice-written-off`: status Written off, still owed $0.00, the
timeline's "Written off" row opening its entry).
`crates/ledger-gui/tests/view_model.rs`: `a_write_off_is_flagged_for_the_cpa`
(one item, "Write-off awaiting the CPA's note", the same through the
server's read of the store and the viewer's snapshot).
- **Lean, `Wanderbooks/Documents.lean`:** `Step.writeOff` (a tagged
control line and an untagged expense line: a `Step.document` in shape),
`Step.writeOff_unattached` (it moves nothing unattached, so
`control_agrees` and `Reachable.control_is_documents` still hold),
`through_append`, `docSum_writeOffLines`, and `writeOff_within_bounds`
(a write-off of at most what is owed leaves nothing owed below zero over
the date-sum). Both theorems are in `Audit.lean`. No kernel function
was added, so no Kani harness.
- **bookspec:** `bad-debt-direct-write-off` (B-41, CHOICE: invoice 100,
two partial write-offs, the balances and the trial balance) and
`arap-hand-control-posting-refused` (B-38: an entry an earlier version
wrote on receivables replays, attempts the control guard now refuses,
and the books still tie out). The example schema gained the `write_off`
event and lets a refused `lines` event say so (an entry posts whole or
not at all); the refusal vocabulary gained `untagged_control_line`,
which belongs to B-38.
**For P12 (mutation targets, none run, D-33):** `Ledger::write_off_steps`'
cash-basis comparison (the `eq_ignore_ascii_case`, the early return), its
order of checks (amount before the day, the day before the room), the
`amount > room` bound and its `room < owed` message arm, the `WriteOff`
tag, `least_owed_from`'s `max` and its `min` over later days (the refactor
`payment_room` now shares), `replay_invoice_write_off`'s movement, and
`doc_state`'s `written_off` arm; `Store::write_off_invoice`'s evidence
arms (the `check_linkable` call, the already-linked-to-the-invoice check,
the blob-before-journal order, the `File`/`Existing` link steps) and the
feature gate; the MCP handler's amount default, the exactly-one evidence
check, and the preview's clone; attention's year comparison and its
`stands_on` filter; the bookspec loader's `may_refuse` arm.
+30 -1
View File
@@ -504,6 +504,35 @@ come from the kernel:
No Lean model: these are two arithmetic laws, not a new `Ledger`
command (S4.1 adds fields to the existing document commands).
### Bad debts (P4b S7)
A write-off (rules.md B-41) is `Ledger::write_off`: a new command, so a new
Lean step, and no new kernel function (its two lines are one debit and one
credit, and the bounds are the existing date-sum's).
34. **A write-off of at most what is owed leaves nothing owed below
zero.** The step posts the document's control line credited (tagged
with it, so it is the same sum B-38 fixed) and the amount of
bad-debt expense debited (untagged, off the control accounts).
`through_append` reads the write-off's own two lines through the day
separately; `docSum_writeOffLines` gives the control line's −amount;
`writeOff_within_bounds` then follows by arithmetic from the command's
own premise (the amount is at most the date-sum through the day).
`Step.writeOff_unattached` shows the step moves nothing unattached, so
`control_agrees` and `Reachable.control_is_documents` hold for it with
no new proof. A reversal re-instates: the same `reverse_twice` that
covers payments (`Step.reverse` mirrors every tagged line).
- Lean `Documents.Step.writeOff`, `Documents.Step.writeOff_unattached`,
`Documents.through_append`, `Documents.docSum_writeOffLines`,
`Documents.writeOff_within_bounds` (in `Audit.lean`) · tests
`half_paid_rest_written_off_is_written_off`,
`write_off_above_outstanding_refused`,
`write_off_then_recovery_same_year`, and the store's
`write_off_replays_and_projects` (live, replay and snapshot agree).
The model's limits are the documents model's (one `Nat` id space,
books built from empty); the cash-basis refusal is a policy on the
books' settings, not arithmetic, and is tested, not modelled.
## What the proofs found
The engine already had 190+ tests, property tests, mutation testing,
@@ -592,7 +621,7 @@ Be precise about the claims:
`tests/properties.rs` (trial balance, balance sheet, reversals,
immutability) and the golden files.
- **Out of scope:** currency (the ledger is single-currency, enforced
structurally), A/R and A/P documents, payroll, the calendar walk around
structurally), payroll, the calendar walk around
the proration kernel (`calendar::month_parts`, `assets::depreciation_schedule`,
`schedules::daily_plan`: property tests, `tests/properties.rs`, P3), and
persistence (the journal store's hash chain is tested in `ledger-store`).