Skip to content

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:

  1. Stores the raw bytes in object storage (Cloudflare R2) under a content-addressed key.
  2. OCRs the document via LlamaParse and writes the structured output (vendor_name, invoice_date, gross_amount, vat_amount, etc.) into the document_fields table.
  3. Tags the kind — invoice / expense-receipt / bank-statement / company-doc / tax-rule / other. The matcher only considers invoice and expense-receipt as match candidates.
  4. 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 record
  • client_name, journal_date, term_days, liability_accounts_id
  • An items array (cl_purchase_articles_id, purchase_accounts_dimensions_id, cl_fringe_benefits_id, amounts)
  • notes field: 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.