Skip to content

Reconcile invoices against the ledger - #83

Merged
JetSquirrel merged 1 commit into
feat/billing-snapshotsfrom
feat/invoice-reconciliation
Nov 3, 2026
Merged

JetSquirrel merged 1 commit into
feat/billing-snapshotsfrom
feat/invoice-reconciliation

Conversation

@JetSquirrel

@JetSquirrel JetSquirrel commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Closes #78

Stacked on #82: review and merge that first. After #82 merges, retarget this PR to main before merging it. #75 and #76 were merged into their stacked bases and never reached main, which is what #81 had to fix.

Adds a Reconciliation page that answers "does the provider's final bill match what was imported?". It holds no charges and changes none.

What it does

  • Evidence: enter a total, or import a CSV on desktop. Invoices live in their own invoice_evidence table (schema v11), apart from fct_charge. A CSV with one bad line is refused whole, and so is one naming an unknown account. Re-entering the same reference for the same month and account replaces the earlier entry.
  • The figure: only authoritative billed cost in the invoice's own currency. Effective cost, derived or estimated amounts, usage with no amount and FX conversions are never in it.
  • Basis: a row opens onto a comparison with the current ledger or any frozen revision from Freeze a billing period as an immutable month-end snapshot #82.
  • Table: problems first, with a text filter, status pills and sortable columns. A row opens in place with the breakdown by charge category, the coverage, the invoice's source and what to check next.

How each status is decided

Status When
Matched Figures agree within one minor unit, and nothing blocks the comparison
Difference Comparable figures disagree; shows the exact delta and any likely cause
Cannot verify No rows, wrong currency, month not ended, estimated or derived rows, several billing accounts with no scope, or a partial export that disagrees
Missing evidence An ended month has billed charges and no invoice

Worth a look in review

  • I added a new sidebar page, where the issue suggests "a small account-level utility". The issue's table across provider and account needs somewhere to live, and Account detail is per account. It has a command palette entry but no keyboard shortcut, because all ten digits are taken.
  • Filters are one text box plus status pills, not separate provider, account and month controls. The box matches all of those.
  • A partial export that agrees exactly still reads as Matched. The figures are equal, so coverage only turns a disagreement into Cannot verify. "Partial" means rows stop more than two days short of the month's edges, and that slack is a judgment call.
  • Several invoices of one scope and currency are compared as a sum, since one provider month can have more than one.
  • Missing evidence covers each account's three most recent ended months, so old gaps don't bury current ones.
  • Other currencies don't block a match. A USD invoice can match while EUR rows exist in the month; the EUR rows are named in the detail and get their own Missing evidence row.
  • Not in this PR: column visibility controls, and a CSV import on the web demo (Enter total works there, in memory).

Testing

Run locally on Windows against this branch:

  • cargo test: 617 passed, 2 ignored, on the run before the last commit's import-only change, after which the reconcil tests were re-run. New tests cover consolidated payer and linked accounts, revised bills against a snapshot, partial exports, credit and tax scenarios, CSV validation and the seeded demo states.
  • cargo clippy --all-targets -- -D warnings and cargo fmt --check: clean.
  • cargo +nightly-2026-07-28 check --lib --target wasm32-unknown-unknown: passes. This is a type-check, not the full build-web.sh.

CI has not run on this PR. The workflow only runs for PRs into main, so it starts once this is retargeted. Wait for it before merging.

Not tested in a real window. Nothing here has been clicked. Please check on desktop, using demo data:

  • The page lists a Difference, a Cannot verify, Missing evidence rows and a Matched row, in that order.
  • A row opens, and switching Compare with to a frozen revision changes the figures.
  • Enter total: a bad amount shows an error in the dialog, and Save closes it and adds the row.
  • Import a CSV with one bad line: nothing is imported and the line is named.
  • Keyboard: Tab reaches a row, and Enter or Space opens it.
  • The table fits at minimum window width and in 简体中文. It has fixed-width columns, so this is the most likely thing to need adjusting.

🤖 Generated with Claude Code

Closes #78

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant