Audit fixes: party money, reconciliations, require policy, K-1 property
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
An independent recompute of the real books found no numeric disagreements; review found 15 defects, all fixed with spec-first regression tests: - k1: a prepaid credited straight to capital is property; a member paying a vendor through 2100 is cash. Docs and B-13 match. - parties: a reversal of a reversal counts on its root's side; liability-to-liability payments (member pays the card) count as paid. - viewer: party money uses the server's cash accounts and full calendar year; per-party "What for" scales multi-party entries. - reconciliations: refuse a withdrawn statement before journaling; new books.void_statement; reconciled lines can't be un-cleared; one shared reconcilable-accounts rule for digest and viewer. - CPA package includes receipts linked to entries; a file can't be both filed on and linked to one entry. - parties = "require" covers payroll, tax remittance (agency) and disposal (buyer); no-party marks follow reversal chains and clear when a vendor is tagged. - derived member shares are frozen to exact amounts when posted, so a settings change never re-splits history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
4c74169d18
commit
2275948a64
22 files changed
+1802
-343
No files matched your search
+73
-28
@@ -138,8 +138,9 @@ below.
|
||||
preview/confirm pattern as post.
|
||||
- `books.get_entry { entry_id }` → full entry with lines, receipts, and
|
||||
its `vendor`, `party_id`, and `party` (the party's label, when it names
|
||||
exactly one) tags, and `parties` / `parties_error` as in the
|
||||
`post_entry` preview.
|
||||
exactly one) tags, `parties` / `parties_error` as in the `post_entry`
|
||||
preview, and `no_party_reason` when it is marked as having no
|
||||
counterparty.
|
||||
- `books.search_entries { account?, date_from?, date_to?, text?, limit?, offset? }`
|
||||
→ entries with lines, newest first. `limit` caps at 100.
|
||||
- `books.trial_balance { as_of? }` → `as_of`, lines, totals, `balanced`
|
||||
@@ -165,10 +166,13 @@ below.
|
||||
ask the human, then update it upstream (pull request or issue at
|
||||
github.com/swaits/wanderbooks). `unreconciled_accounts` lists the cash
|
||||
and card accounts due for reconciliation (`{ code, name,
|
||||
reconciled_through?, due_through, next_step }`): every `[books]
|
||||
cash_accounts` account and every liability with a recorded statement
|
||||
that has posted activity through the end of last month but no
|
||||
reconciliation (not voided) through it.
|
||||
reconciled_through?, due_through, next_step }`): the cash accounts
|
||||
(`[books] cash_accounts`, else asset accounts named for cash: cash,
|
||||
bank, checking, savings, money market), the card accounts (liabilities
|
||||
named "card", before their first statement), and every other liability
|
||||
with a recorded statement, that have posted activity through the end of
|
||||
last month but no reconciliation (not voided) through it. The viewer's
|
||||
Needs attention uses the same rule (`ledger_store::reconcilable`).
|
||||
|
||||
## Tools — fixed assets
|
||||
|
||||
@@ -197,11 +201,16 @@ so the lifetime total is exactly (cost − salvage).
|
||||
→ per-asset `{ entry_id, amount, already_posted }`, plus `skipped` for
|
||||
assets outside their useful life. Idempotent per asset and period.
|
||||
- `books.dispose_asset { asset_id | tag, date, proceeds, proceeds_account,
|
||||
gain_loss_account, confirmed?, receipt_attachment_ids? }` → without
|
||||
gain_loss_account, buyer?, confirmed?, receipt_attachment_ids? }` →
|
||||
without
|
||||
confirmation: `{ status: "preview", ... }` and posts nothing; with
|
||||
`confirmed: true`: auto-posts missing final-period depreciation, then
|
||||
posts the balanced disposal entry. Subject to receipt policy (bill of
|
||||
sale); monthly depreciation is exempt as an allocation.
|
||||
sale); monthly depreciation is exempt as an allocation. `buyer` (a
|
||||
party) is the disposal entry's party; under `[books] parties =
|
||||
"require"` a sale with proceeds without one is refused before anything
|
||||
posts (`party_required`); a retirement with no proceeds and no buyer is
|
||||
marked as having no counterparty.
|
||||
- `books.export { report: "fixed_asset_register", format: json | csv,
|
||||
as_of? }`: every asset registered by the date (tag, serial, cost,
|
||||
depreciation by schedule, book value, disposal), totals, and the
|
||||
@@ -447,12 +456,19 @@ include vendor and customer when it has those records.
|
||||
payables and payroll withholding roles (`books.roles`). A liability
|
||||
counts as how the company paid or was paid: a card, a member paying for
|
||||
it, a loan.
|
||||
- `paid` / `received`: each entry's money lines, credits minus debits; a
|
||||
positive net was paid to the party, a negative one received from it; a
|
||||
reversal counts against its original's side. A bill isn't paid until its
|
||||
payment is; an entry only moving money between money accounts nets to
|
||||
zero. An entry naming several parties counts this party's resolved
|
||||
share only (an entry whose shares don't resolve counts for none).
|
||||
- `paid` / `received`: each entry's money lines, credits minus debits
|
||||
(`ledger_core::parties::entry_flow`), with a liability paid down
|
||||
counting once, as paid to its creditor: by cash (a reimbursement to the
|
||||
members, a card payment) or by another liability (a member paying the
|
||||
card from their own funds, a loan paying it off); cash raised by a
|
||||
liability is received from the lender. A positive net was paid to the
|
||||
party, a negative one received from it; a reversal counts against its
|
||||
root original's side (reversing the reversal counts on that side
|
||||
again). A bill isn't paid until its payment is; a transfer between the
|
||||
company's own cash accounts nets to zero. An entry naming several
|
||||
parties counts this party's resolved share only (an entry whose shares
|
||||
don't resolve counts for none). The year is the whole calendar year;
|
||||
the viewer's party page and directory show these same numbers.
|
||||
- `invoiced` / `billed`: its invoices' and bills' totals dated in the year.
|
||||
`owed` / `receivable`: its open bills' and invoices' balances now (every
|
||||
year).
|
||||
@@ -565,7 +581,8 @@ the receipt policy counts it.
|
||||
exists. `link_to` as in "Tools — attachments".
|
||||
- `books.link_receipt { receipt_id, entry_id }` → link a previously filed
|
||||
(unlinked, not withdrawn) receipt to its entry. A receipt linked elsewhere
|
||||
must be unlinked first.
|
||||
must be unlinked first; one already linked to that entry
|
||||
(`books.link_attachment`) is refused, as filing it there twice.
|
||||
- `books.unlink_receipt { attachment_id, reason }` → `{ ok, entry_id,
|
||||
remaining_receipts }`. Detaches a receipt from its entry; it stays filed,
|
||||
unlinked. Journaled as `receipt.unlinked` with the reason. Subject to the
|
||||
@@ -782,10 +799,17 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
|
||||
Records what a filed statement says (balances as printed, in the
|
||||
account's normal direction, cents at most) and links it to the account.
|
||||
Journaled as `statement.recorded` then `attachment.linked`. Refused for
|
||||
an unknown or withdrawn attachment, one already recording a statement,
|
||||
an account that isn't an asset or liability, or a period that runs
|
||||
backwards.
|
||||
- `books.statements { account? }` → `{ statements }`, by statement date.
|
||||
an unknown or withdrawn attachment, one already recording a statement
|
||||
(not voided), an account that isn't an asset or liability, or a period
|
||||
that runs backwards.
|
||||
- `books.statements { account? }` → `{ statements }`, by statement date:
|
||||
those not voided whose file isn't withdrawn (withdrawing a statement's
|
||||
file retires it).
|
||||
- `books.void_statement { attachment_id, reason }` → the statement as it
|
||||
was recorded. For a statement recorded wrong: journaled as
|
||||
`statement.voided`; it is no longer listed, backs no reconciliation, and
|
||||
the same attachment can record it again. Refused while a reconciliation
|
||||
not voided rests on it.
|
||||
- `books.reconcile { statement_attachment? | account, statement_date,
|
||||
statement_balance, confirmed? }` → `{ status: "preview" | "reconciled",
|
||||
account_code, statement_date, statement_attachment?, book_balance,
|
||||
@@ -796,8 +820,9 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
|
||||
account, date, and balance are the recorded statement's (any given must
|
||||
agree). Without `confirmed: true` it is a preview that records nothing;
|
||||
`problems` lists why a confirmation would be refused. Confirmed, it
|
||||
needs a recorded statement, a difference of exactly zero, and a date
|
||||
after the account's last reconciliation not voided; it then journals
|
||||
needs a recorded statement (its file not withdrawn: refused before
|
||||
anything is journaled), a difference of exactly zero, and a date after
|
||||
the account's last reconciliation not voided; it then journals
|
||||
`reconciliation.completed` (the cleared lines, the statement) and links
|
||||
the statement to it, returning `reconciliation`.
|
||||
- `books.reconciliations { account? }` → `{ reconciliations: [{
|
||||
@@ -807,6 +832,10 @@ R-FUTURE-3 and rides the engine roadmap, not the MCP layer.
|
||||
- `books.void_reconciliation { reconciliation_id, reason }` → the
|
||||
reconciliation, voided. Journaled as `reconciliation.voided`; the
|
||||
account can be reconciled through that date again.
|
||||
- `books.mark_cleared` refuses to un-clear a line a reconciliation not
|
||||
voided recorded as cleared: void the reconciliation first. Clearing a
|
||||
line dated on or before a reconciliation (an outstanding check that
|
||||
clears on a later statement) is allowed.
|
||||
|
||||
## Tools — A/R and A/P
|
||||
|
||||
@@ -851,9 +880,15 @@ entries.
|
||||
`employee` names the employee's party (id or name; it must have the
|
||||
employee role, else nothing is written); new runs should name it, and
|
||||
`employee_name` then defaults to the party's name. A line needs one or
|
||||
the other.
|
||||
- `books.remit_payroll_taxes { date, cash_account }` → `{ entry_id, total }`.
|
||||
Clears every payroll payable to cash in one entry.
|
||||
the other. When every line with net pay names its party, the entry
|
||||
names those parties, each with its net pay, so each employee's
|
||||
`books.party` counts it; under `[books] parties = "require"` a line
|
||||
with net pay and no party is refused before anything posts
|
||||
(`party_required`).
|
||||
- `books.remit_payroll_taxes { date, cash_account, agency? }` → `{
|
||||
entry_id, total }`. Clears every payroll payable to cash in one entry,
|
||||
naming `agency` (the tax agency's party) on it; required under
|
||||
`[books] parties = "require"` (`party_required`, nothing posted).
|
||||
- `books.list_payroll_runs` → `{ runs: [{ id, date, employee_count,
|
||||
total_gross, entry_id }] }`.
|
||||
|
||||
@@ -874,6 +909,12 @@ entries.
|
||||
- `books.export_cpa_package { as_of?, cash_accounts?, financing_accounts?,
|
||||
investing_accounts?, include_receipts?, include_tins?, save_to? }` →
|
||||
`{ file_name, content_base64?, saved_to?, tins_included, tin_gaps, note }`.
|
||||
**Receipts** (`include_receipts`, default true): `receipts/` holds every
|
||||
current file behind each posted entry of the fiscal year to date, filed
|
||||
on it or linked to it (`books.link_attachment`; a linked file counts as
|
||||
the entry's receipt), one copy per entry, each with a row in
|
||||
`receipts/index.csv`; `documents/<category>/` holds the files behind no
|
||||
entry, receipts excepted.
|
||||
**TINs** (`include_tins`, default true; DECISION, Stephen, 2026-10-15):
|
||||
the K-1 worksheet and the 1099 worksheet carry every member's and
|
||||
payee's full TIN from the vault, so the zip is written to `save_to` (a
|
||||
@@ -892,10 +933,14 @@ entries.
|
||||
contributions_cash, contributions_property, net_income, distributions,
|
||||
other, ending_capital` for the fiscal year to date: the statement of
|
||||
members' equity's figures (`ledger_core::k1`), contributions split into
|
||||
property (what a contribution debits to asset accounts other than cash,
|
||||
receivables, and prepaid accounts: a prepaid schedule's account, or one
|
||||
named "prepaid") and cash (the rest, including a service the members
|
||||
paid ahead). Property in an entry crediting several members is shared
|
||||
property (what an entry crediting a member's capital debits to asset
|
||||
accounts other than cash and receivables: equipment, software, or a
|
||||
prepaid service contract the members owned and handed over straight to
|
||||
capital, e.g. AppleCare+ contributed with a laptop) and cash (the rest:
|
||||
deposits, and what the members paid for the company, including a
|
||||
service they paid a vendor ahead for, booked through Due to
|
||||
Shareholders and then converted to capital; DECISION, Stephen,
|
||||
2026-10-15). Property in an entry crediting several members is shared
|
||||
in proportion to their credits. An entry and its reversal both in the
|
||||
year cancel: neither is a contribution or a distribution. A member's
|
||||
party is the party with the member's name (not a display name) and the
|
||||
|
||||
@@ -109,6 +109,16 @@ derived. Spec: `ledger_core::shares`.
|
||||
member gets what their own lines say (and those must account for all
|
||||
it moved); otherwise each gets their income share, rescaled to the
|
||||
members named, rounded as percents are.
|
||||
- **Posted is forever, the split included:** when an entry naming
|
||||
several parties with derived shares posts (or a posted entry is tagged
|
||||
so), the server records the exact amounts they resolve to
|
||||
(`entry.parties_set` with amounts), so a later change to
|
||||
`[[company.members]]` (a new income split, a member archived) never
|
||||
re-splits posted history. A draft's derived shares follow the settings
|
||||
until it posts. An archived member stays a member for their history
|
||||
(`Company::party_members` falls back to the archived party of that
|
||||
name). Entries posted with derived shares before this rule keep them
|
||||
until re-tagged with `books.set_entry_party`.
|
||||
- **Refused** (journaling nothing): a party named twice, an unknown
|
||||
party, mixed kinds of share, percents not summing to 100, an amount
|
||||
finer than a cent, and two parties with vendor records. A 1099 counts
|
||||
@@ -126,6 +136,14 @@ derived. Spec: `ledger_core::shares`.
|
||||
every share, so a reversal subtracts each party's part; re-tagging
|
||||
either one (parties, shares, or vendor) re-tags the other, so the pair
|
||||
always nets to zero.
|
||||
A reversal counts on its root original's side, so reversing the
|
||||
reversal reinstates the payment as paid (never a negative received).
|
||||
- **One number everywhere:** `books.party`, `books.parties`, and the
|
||||
viewer's party page and directory read cash as the server does
|
||||
(`[books] cash_accounts`, else every asset account but receivables)
|
||||
and count the whole calendar year, so a posted entry dated later this
|
||||
year counts in both. The viewer shows the year up to the later of today
|
||||
and its last posted entry.
|
||||
- **For the agent:** a joint member transaction (a reimbursement on Due
|
||||
to Shareholders, a contribution or distribution from joint funds)
|
||||
names both members with no shares and lets them derive; an uneven
|
||||
@@ -315,17 +333,26 @@ statement_balance, cleared: [(entry_id, line)], attachment_id}`. It
|
||||
refuses unless the difference is exactly zero and a statement for that
|
||||
account and date is attached (the receipt rule's twin). A reconciliation
|
||||
is never edited; a later one supersedes it; `reconciliation.voided
|
||||
{id, reason}` exists for mistakes. Viewer: per-account reconciliation
|
||||
{id, reason}` exists for mistakes. While it stands, the lines it
|
||||
recorded as cleared can't be un-cleared (void it first), and nothing is
|
||||
journaled when it is refused (a withdrawn statement file included). A
|
||||
statement recorded wrong is voided (`statement.voided {attachment_id,
|
||||
reason}`, refused while a reconciliation rests on it) and recorded again
|
||||
on the same file; withdrawing a statement's file retires its statement. Viewer: per-account reconciliation
|
||||
history with its statement; Needs-attention: an account with activity not
|
||||
reconciled through last month's end.
|
||||
|
||||
Tools (built; `docs/mcp-interface.md` "Tools — bank reconciliation"):
|
||||
`books.record_statement`, `books.statements`, `books.reconcile` (preview
|
||||
by default; `statement_attachment` + `confirmed: true` records it),
|
||||
`books.reconciliations`, `books.void_reconciliation`; `books.show
|
||||
reconciliation`; `books.digest` `unreconciled_accounts` (the `[books]
|
||||
cash_accounts` and liabilities with statements, not reconciled through
|
||||
last month's end). The statement of a reconciliation not voided can't be
|
||||
`books.reconciliations`, `books.void_reconciliation`,
|
||||
`books.void_statement`; `books.show
|
||||
reconciliation`; `books.digest` `unreconciled_accounts` and the
|
||||
viewer's Needs attention, one rule (`ledger_store::reconcilable`): the
|
||||
cash accounts (`[books] cash_accounts`, else assets named for cash), the
|
||||
card accounts (liabilities named "card", before their first statement),
|
||||
and other liabilities with statements, not reconciled through last
|
||||
month's end). The statement of a reconciliation not voided can't be
|
||||
unlinked from it.
|
||||
|
||||
### Import sources
|
||||
@@ -378,6 +405,18 @@ without forcing one style. `[books] parties = "off" | "suggest" |
|
||||
- `require`: posting is refused until the entry names a party or is
|
||||
marked as having none (transfers, depreciation, amortization, closes
|
||||
are exempt by kind).
|
||||
The engine's own posting paths name their counterparty or are refused
|
||||
first, so nothing posts around the rule: `books.run_payroll` names each
|
||||
line's employee party on the entry (each with their net pay) and, under
|
||||
`require`, refuses a line without one; `books.remit_payroll_taxes`
|
||||
takes `agency` (required under `require`); `books.dispose_asset` takes
|
||||
`buyer` (required under `require` for a sale with proceeds; a
|
||||
retirement with no proceeds and no buyer is marked as having none).
|
||||
- **Marks follow the chain:** a reversal carries its original's parties,
|
||||
shares, vendor tag, and no-party mark; marking or tagging any entry of a
|
||||
reversal chain marks or tags them all. Tagging a vendor (or any party)
|
||||
clears a no-party mark, so an entry never names a party and carries the
|
||||
mark at once.
|
||||
|
||||
Also: memorized matches (journaled rules like description contains
|
||||
"OPENROUTER" → OpenRouter, applied by bank-CSV import and new drafts,
|
||||
|
||||
+6
-2
@@ -238,8 +238,12 @@ closed period).
|
||||
both in a statement's span are neither contributions nor
|
||||
distributions (`reports::cancelled_in_span`; the K-1 worksheet reads
|
||||
the same). The K-1 worksheet's contributed property (Item L) is what
|
||||
the member owned and handed over, with its own basis (§723): never a
|
||||
prepaid service they paid for the company (cash), and shared among
|
||||
the member owned and handed over, with its own basis (§723): a prepaid
|
||||
service contract credited straight to capital is property (a contract
|
||||
handed over), while a prepaid service they paid a vendor for on the
|
||||
company's behalf, booked through Due to Shareholders (Dr prepaid / Cr
|
||||
2100, then Dr 2100 / Cr capital), is cash (DECISION, Stephen,
|
||||
2026-10-15); and it is shared among
|
||||
members credited in one entry in proportion to their credits
|
||||
(`k1` INVARIANTS 3, 4, 6).
|
||||
|
||||
|
||||
Reference in new issue
Block a user