Receipts, reconciliations, e-Fin entries
The agent's job is to take documents (the things you forward), match them to reconciliations (the bank-side facts), and write e-Fin entries (the actual book entries). Everything else is mechanics around that triangle.
Documents
A document is anything the operator drops into the Discord channel — typically a PDF receipt or invoice, occasionally an image. The ingest pipeline:
- Stores the raw bytes in object storage (Cloudflare R2) under a content-addressed key.
- OCRs the document via LlamaParse and writes the structured
output (
vendor_name,invoice_date,gross_amount,vat_amount, etc.) into thedocument_fieldstable. - Tags the kind —
invoice/expense-receipt/bank-statement/company-doc/tax-rule/other. The matcher only considersinvoiceandexpense-receiptas match candidates. - Chunks + embeds the markdown for retrieval (the chat agent
uses these for
/ask).
Schema lives in agent/src/accounting_agent/store/schema.py (the
documents + document_fields + chunks tables).
Reconciliations
A reconciliation is the bank-side fact: "on this date, this amount moved in (or out) of this account, referencing this description." One row per bank transaction. The Enable Banking poller writes them; nothing else does.
State machine:
unmatched → proposed → booked → (e-Fin entry stamped)
│
└→ rejected (terminal)
- unmatched: no candidate document found yet.
- proposed: the matcher found a candidate above HITL_THRESHOLD; waiting for operator approval.
- booked: operator approved; booker daemon will write to e-Fin.
- booked + efin_entry_id set: write succeeded; terminal.
- rejected: operator declined the match (or the client-create proposal); terminal.
The (source_kind, source_reference) pair is UNIQUE — re-running
the poller produces the same end state.
e-Fin entries
When the booker daemon picks up a booked reconciliation, it
writes a purchase_invoice (or sale_invoice for the credit-side
flow) to e-Financials via POST /v1/purchase_invoices. The entry
carries:
clients_id— the e-Fin supplier recordclient_name,journal_date,term_days,liability_accounts_id- An items array (
cl_purchase_articles_id,purchase_accounts_dimensions_id,cl_fringe_benefits_id, amounts) notesfield: the idempotency marker"{source_kind}:{source_reference}". This is how the booker recovers if a previous run created the draft but crashed before registering.
After create: update_totals (PATCH), attach_document (PUT the
PDF as base64), then register (PATCH the /register subpath
to transition PROJECT → REGISTERED).
The e-Fin entry id gets stamped back onto the reconciliation row
as efin_entry_id. That row is now terminal.
How the three relate
Documents (R2 + Turso) Reconciliations (Turso)
│ │
└──────── matcher ─────────────┤
│
▼
e-Fin entries (RIK servers)
notes: source_kind:source_reference
Documents are mutable until indexed. Reconciliations evolve through the state machine above. e-Fin entries are immutable once registered — they are the books.