diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-21 08:33:36 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-21 08:33:36 +0200 |
| commit | fcbfd1c23ed91082fcbb77a7337567a1b9cedd91 (patch) | |
| tree | 8f0dbda537024c17aca1a6acaa7ebaaf0978551c /docs/DECISIONS.md | |
| parent | f290736c33a6df85cd83eaed18e80530681955b9 (diff) | |
| download | bokf-fcbfd1c23ed91082fcbb77a7337567a1b9cedd91.tar.gz bokf-fcbfd1c23ed91082fcbb77a7337567a1b9cedd91.zip | |
docs: prune STATE into a decision archive; record the branch rule
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/DECISIONS.md')
| -rw-r--r-- | docs/DECISIONS.md | 268 |
1 files changed, 268 insertions, 0 deletions
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 0000000..b84e7fa --- /dev/null +++ b/docs/DECISIONS.md @@ -0,0 +1,268 @@ +# bokf — settled decisions + +Settled decisions, newest last; the log exists for archaeology, not for +daily reading. The live snapshot is `docs/STATE.md`; the rules are in +`AGENTS.md`; UI conventions are in `docs/TUI-GUIDELINES.md`. Entries are +kept verbatim from the STATE.md they were pruned from (2026-09-21). + +## Locked decisions + +0. **TUI widget layer**: `clients/tui.[ch]` is the only module that touches + ncurses; screens compose its widgets and never keep local input/drawing + patterns. Every widget has a spec in `TUI-GUIDELINES.md` and unit tests + in `tests/test_tui.c` (run by `make test`; pure logic separated from + drawing). Extend the layer, not the screen. + +1. **Name/license**: `bokf`, daemon `bokfd`, clients `bokfctl` (scriptable) + and `bokftui` (ncurses); GPL-3.0-or-later; repo `~/work/bokf`. +2. **Stack**: C11, Makefile, vendored SQLite 3.53.4 / yyjson 0.13.0 / Argon2 + 20190702 / SHA-256 (public domain). Only system dep: libncursesw. +3. **Storage**: SQLite WAL, `synchronous=FULL`, STRICT tables, composite-key + tenant isolation, append-only triggers, `VACUUM INTO` snapshots; forward + migrations snapshot to `pre-migration-v<old>-<stamp>.db` first and refuse + to migrate without it. Postgres + deliberately rejected for now; keep DB access behind one layer for a later + port. +4. **Protocol**: NDJSON over Unix socket (+ optional plain TCP and a native + TLS listener), protocol v1. + `dry_run` on every mutation, `client_ref` idempotency, stable error codes, + `describe` + `agent.instructions`, money in integer öre. +5. **Auth**: multi-org; memberships owner/bookkeeper/viewer; API tokens bound + to user+org with scopes, shown once, revocable; sessions in memory; + Argon2id. Server messages English, UI Swedish. +6. **Compliance design**: SHA-256 audit chain, SHA-256 voucher chain + (canonical encoding in `SCHEMA.md` §7.1/§9.1), period locks, fiscal year + close, corrections only as ändringsverifikat, SIE 4 (CP437) in/out. +7. **Verifikat ids**: series is free text (`A`, `V-`, `A ` …); unbroken + numbering per fiscal year+series; id displayed as `series+number` + (`V-8`). Org setting `default_series` (Inställningar) for new vouchers and + new templates. +8. **Templates** (`konteringsmallar`): server-side; formula language over `x` + with `+ - * /` and parentheses, positive=debit, negative=credit, zero rows + dropped; rounding remainder assigned to the largest row; `{x}` in the + description; `template.*` commands + `voucher.post {template,x}`; archive + instead of delete. +9. **Ingående balans**: one series `IB` voucher per fiscal year (dated at + year start). Reports treat series IB as IB, not period movement; SIE + export/import round-trips without double counting; TUI editor shows the + year's *effective* opening balances (carry-forward plus IB vouchers) and + posts deltas, so nothing is ever edited and a target balance can be + entered directly. +10. **Attachments**: content stored in the DB (BLOB), immutable, linked via + append-only `voucher_attachments`. Default limit 10 MiB + (`max_attachment_bytes`); the socket line limit is derived from it + (base64). TUI `Ctrl+F` attaches via file browser; the client checks the + size from `meta` first. Setting `attachment_dir` (tilde expanded). +11. **UI keys**: Ctrl+N = add, F5 = refresh, Esc/q = back (never exits), + Ctrl+C = quit, 1–9/g = jump, F7 = clear row in editors, F9 = save. + Shared line editor and date field; see TUI-GUIDELINES.md. The dashboard + is sectioned (Bokföring / Rapporter & bokslut / Register / Räkenskapsår + / Övrigt) with non-selectable headers; theme/styles and pager markup + live in the widget layer (`tui_style`, `tui_markup`). Year-end + **Bokslut** and the former "Information om året" are one screen + (`yearinfo` is an alias). +12. **Login**: two steps — credentials, then org picker ("Välj organisation + att representera"). No org switch in the dashboard; fiscal year switch is + on the dashboard. `--org ID` bypasses the picker. +13. **Settings**: `settings.get`/`settings.set`; keys `default_series`, + `attachment_dir`. +14. **Transport**: Unix socket for host clients; plain TCP loopback-only; + native TLS listener (`BOKFD_TLS`, `BOKFD_TLS_CERT/KEY`, OpenSSL, TLS 1.2+, + cert reload on file change) with client targets `tls:host:port` and + system-trust verification (`BOKFD_TLS_CA` for private CAs). Certificates + come from a lego sidecar using INWX DNS-01 (`compose.yaml`). Externals + get accounts/roles/tokens, never VPN access. The runtime image is + **Alpine + backend only** (`bokfd`, `bokfctl`; the ncurses TUI is a + frontend built on the client machine). `scripts/deploy.sh` builds locally + and ships over SSH, or — when the architectures differ — + cross-compiles the backend on the dev machine as **static aarch64 + (glibc + OpenSSL archives, runs directly on Alpine, DNS verified) and + assembles the image in the host's Docker** (`deploy/Dockerfile.cross`, + ~20 s; the image carries no `libssl3`). `scripts/deploy.sh --dev` + hot-reloads the binaries in the running container (SIGHUP re-exec via + `docker cp`), cross-compiling first when the architecture differs. +15. **Reports in the TUI**: rendered as fixed-width Swedish tables that mirror + the Kapitas PDF exports (Saldobalans, Resultatrapport with previous-year + column and 89xx bokfört/ej bokfört, Balansrapport with Ing balans/Ing + saldo/Period/Utg balans and Beräknat resultat, Momsrapport ruta för ruta). + The TUI never shows report JSON. Amounts are Swedish formatted + (`1 234,56`); moms rutas are whole kronor truncated like Kapitas. + Report tables mark their column-header row with `\x04` (`TUI_MARK_STICKY`) + so the pager pins it above the scrolling body; the verifikat detail shows + column headers and separates the underlag section with a rule. PgUp/PgDn + page without wrapping and stop at the first/last row. +16. **Moms rules (schema v3)**: default seed covers 05 over 3000-3019, + 3100-3199, 3300-3399, reverse-charge sales (32xx) in 41, EU purchases in + 20/21, reverse-charge output VAT 2614/2624/2634 in 30/31/32, and box 48 + signed negative. Rules may share a box and are summed; box 49 is the sum + of the moms boxes only. v3 migrates existing databases. +17. **Year-end hub and menu IA (2026-09-19)**: the forms widget layer gained + an action list (`tui_form_run_actions`; fields and actions in one focus + ring, `TUI_FORM_ACTION + i` for a chosen action, dimmed-but-selectable + rows with a `disabled_reason`, `-1` heading/status rows). The **Bokslut** + screen is the year-end hub: the eight year fields, the derived status + row `Bokslut bokfört: ja/nej` (`voucher.list` with + `text:"Skatt på årets resultat"`; a closed year counts as posted) and + the actions Årsredovisning (K2), Inkomstdeklaration (INK2/SRU), + Bokslutsplan (torrkörning) and Bokför bokslut (dimmed with a reason on a + closed or already posted year). Årsredovisning and INK2/SRU left the + Rapporter menu, and Ingående balans moved from Bokföring to the + dashboard's Räkenskapsår section next to Räkenskapsår. + The form widgets remember focus across runs (`int *focus` as the last + argument to `tui_form_run*`: clamped to a selectable row on entry and + written back on every return; screens own a `static int sel`), so a + sub-screen round-trip or a field save returns the highlight to the + same row. +18. **Momsregler (2026-09-20)**: the per-org vat `report_rules` are edited + with `report.rule_list/create/update/delete` (mutations owner-only, + audited, `dry_run`; rules are config, not ledger) and in the TUI + **Momsregler** screen under Register. `report.vat` honors `match_type` + `account`, `range` and `type`; several rules may share a box and are + summed. Only the `vat` report is consumed today, so the commands accept + only that report. +19. **Bank reconciliation (2026-09-20, schema v8)**: phase 1 is mechanical + only — `bank.import` (SEB CSV, idempotent via a per-row source hash), + `bank.list` with exact-amount suggestions within ±5 days, + `bank.match`/`bank.unmatch`. Imported rows are statement evidence, + matches are mutable, and **nothing is booked automatically**. Setting + `bank_account` (default `1930`). Phase 2 (decided shape): from an + unmatched row, open Nytt verifikat with date/description/amount + prefilled (F4 template still there); after posting, auto-match the new + voucher and stay in the list. Later: `bank_rule.*` pattern suggestions + and "skapa verifikat från transaktion". TUI screen **Bankavstämning** + under Bokföring. **Phase 2 implemented 2026-09-20**: Ctrl+N (or + `Skapa nytt verifikat…` in the match list) prefills the form from the + transaction and auto-matches the posted voucher, returning to the list + with the cursor kept. +20. **Menu IA (2026-09-20)**: one top-level section per workflow domain + (Bokföring, Fakturering, Lön, …), Register owns master data, Rapporter + owns read-only output, Räkenskapsår its own, and a feature adds at most + one section. Sections stay stable; screens come and go inside them. +21. **Invoicing (2026-09-20, design)**: `docs/INVOICING.md` is the chapter. + bokf owns invoices end to end: customer register, a global + always-increasing number series in `invoice_sequence` (start 17761), + `OCR = number + MOD10`, a one-page PDF reproducing the Google Sheets + original (Helvetica + Comfortaa outlines, no new deps), issue = number + + PDF attachment + voucher + link in one transaction, F5 preview of the + real PDF without consuming the number, and SMTP sending with the + password encrypted in the DB (AES-256-GCM, `BOKFD_SECRET_KEY`). + Implementation waves: schema v9 + PDF, SMTP, TUI (Fakturering). + **Wave 1 done 2026-09-20**: schema v9 (`customers`, `invoice_sequence`, + `invoices`, `invoice_rows`, widened `vouchers.source` with a table + rebuild), the PDF renderer (`src/invoice.c`, 2.2 % raw pixel diff vs the + Google Sheets original — all structure exact; only Arial-vs-Helvetica + glyphs differ), and `customer.*`, `invoice.sequence_get/set`, + `invoice.preview/issue/list/get/pdf`. Issue is atomic: number + PDF + attachment + voucher (D 1510/K 3xxx+26xx, `source:"invoice"`) + links. + Settings `invoice_receivable_account`, `invoice_revenue_account`, + `invoice_bankgiro`. **Swish QR: decided 2026-09-20 — not supported** + (invoice 1's QR is dropped; the generator has no image support). + **Wave 2 done 2026-09-20**: settings secrets are AES-256-GCM encrypted + with `BOKFD_SECRET_KEY` (`smtp_password`; `settings.get` never returns + it), SMTP over TLS/STARTTLS/plain (`src/smtp.c`) and `invoice.send` + (subject `Faktura <nr>`, PDF attached, `last_sent_*`, audited, + `SMTP_NOT_CONFIGURED`/`SMTP_FAILED`). **Wave 3 done 2026-09-20**: TUI + **Fakturering** with `Fakturor` (list, detail `p` = PDF via `xdg-open`, + `s` = send/resend), the invoice form (customer picker, auto due date, + row table, F5 = preview of the real PDF, Ctrl+Enter = issue + send + confirm), **Kunder** in Register, and the SMTP/invoice fields in + Inställningar. pty golden scenarios `invoices`, `customers`, + `invoice-form`. + +22. **Command table split (2026-09-20)**: `src/commands.c` keeps only + discovery (`describe`, `agent.instructions`), `command_find` and the + registry; handlers, argument schemas and `g_cmd_<domain>[]` tables live in + `src/cmd_<domain>.c`, with shared helpers in `src/cmd_util.[ch]`. + `g_command_tables[]` registers the files in the original catalogue order + so `describe` output is byte-identical, and + `scripts/check-consistency.sh` / `make check` scans `src/commands.c` and + every `src/cmd_*.c`. +23. **TUI screen split (2026-09-20)**: the same idea for the TUI: + `clients/bokftui.c` keeps `main` (arg parsing, login, startup), + `config_path`/`config_mkdirs`, the Ctrl+R reload and the scene registry + (`struct scene SCENES[]` + `open_scene`); shared helpers and `struct app` + live in `clients/ui.[ch]`; each view group lives in + `clients/screens_<group>.c` (dashboard, vouchers, accounts, reports, + rules, attachments, bank, audit, templates, ib, bokslut, settings, + invoices). Pure move: no key, string or scene-name changes; the registry + keeps every name including the `yearinfo` alias. + +## Completed work formerly listed under "Pending decisions" + +- Attachments are complete: download (voucher detail `f`, Underlag `Enter`, + SHA-256 verified, text inline), attach to an existing voucher (`^F`), + remove a link (`d` in the `f` picker) and link an inbox item to a voucher + (`k`). +- K2/SRU is complete: `sru.export` with the INK2 view, `bokslut.post` with + the Bokslut screen, and a K2 årsredovisning text draft with a save action + in Rapporter. The draft follows the filed reports (whole kronor, no + account numbers, säte, förvaltningsberättelse with the org's static + verksamhetsbeskrivning, flerårsöversikt and changes-in-equity from the + books, resultatdisposition, styrelsens yttrande, fastställelseintyg, + board signatures) and needs no prompting: everything comes from the + books, the org record (Företagsuppgifter) and the per-year year booklet + now edited on the **Bokslut** screen (`fiscal_year.update`, schema v6: + events, AGM and payment dates, proposed dividend, employees, notes — + inherited from the previous year when a year is opened; fields 0–5 save + per field, the periodiseringsfond/tax rate fields are local). Board + members are a list per + org (`board.*`, edited under Företagsuppgifter) and the stämma decision + is posted with a `Utdelning` template (D 2099/K 2898). Imported history + years are read with their "Stäng" closings skipped, so comparisons and + the flerårsöversikt show real figures. The moms `report_rules` are now + owner-editable in the **Momsregler** screen (decision 18). + +## Completed backlog items (original entries) + +1. ~~eSKD file generation for momsdeklaration.~~ `report.vat_eskd` (eSKDUpload + 6.0, ISO-8859-1, whole kronor) with a save action in the TUI momsrapport. +2. ~~Bokslut automation~~ done: `bokslut.post` (entries/avskrivningar, + periodiseringsfond, skatt at a given rate, resultatdisposition, dry-run + plan, audited), the TUI Bokslut screen (fond/rate, F5 plan, ^Enter posts + after confirmation) and the K2 resultatrapport order. +3. ~~K2 årsredovisning document~~ done as a text draft built in the TUI from + `report.income_statement`/`report.balance_sheet` (Förvaltningsberättelse, + K2 RR/BR with previous-year column and the result inside equity, noter, + underskrifter); `s` saves `Årsredovisning <år>.txt`. Placeholders mark + qualitative facts, and incomplete jämförelsetal for imported history + years are flagged in the document. ~~SRU files (INK2/INK2R/INK2S)~~ done + as `sru.export` (official 2025P4 field tables, BAS mapping, TUI save in + Rapporter -> Inkomstdeklaration). +4. ~~`audit.verify` must also verify the **voucher** hash chain (today only the + audit chain is verified).~~ Done: recomputes every org's voucher chain in + posting order (SCHEMA.md §7.1), flags unbalanced vouchers as a backstop, + and `full:true` re-hashes attachment content; result carries + `vouchers_checked`, `unbalanced_vouchers`, `attachments_checked` and the + first bad voucher/audit/attachment id. TUI Revision shows both counts and + the bad ids. Fixed in schema v7: `attachments` now has + `no_update`/`no_delete` triggers; `audit.verify full:true` still detects + on-disk tampering. +5. ~~`report.general_ledger` and `report.voucher_list`~~ implemented + (Huvudbok, Verifikationslista) with Kapitas-style TUI tables; the ledger + API supports `accounts`/`from`/`to`, the list an optional `series`. +6. ~~`describe` argument schemas (currently name/summary/permission only).~~ + Done: every command carries a `CMD_ARGS` type/required/default schema, + the dispatcher validates before the handler and `describe` emits `args[]`. +7. ~~Pre-migration `VACUUM INTO` snapshot (promised in SCHEMA.md, not built).~~ + Done: forward migrations snapshot to + `backup/pre-migration-v<old>-<stamp>.db` first and abort if that fails. +8. ~~Docker image + compose (multi-arch amd64/arm64, GHCR) and systemd unit.~~ + Done as a Dockerfile + `compose.yaml` (amd64/arm64 build stage) and + `scripts/deploy.sh` over SSH; no registry and no systemd unit (the + container is the unit). +10. ~~Bank import/reconciliation (CSV first).~~ Phase 1 done: SEB CSV import + + matching against vouchers (schema v8, decision 19). Next: phase 2 + prefill-from-transaction and `bank_rule.*`, then PSD2; + invoicing/reskontra and AGI/payroll if employees. +11a. Imported history years whose SIE contains the source's P&L closings + ("Stäng intäktskonton/kostnadskonton") net to zero in the income + statement (Kapitas 2022-2026). **Decided 2026-09-19: no importer change.** + Locked years stay locked and the source's closings stay in the books; the + årsredovisning export flags incomplete jämförelsetal for those years and + points to the previous year's annual report. +11. ~~SIE import only into an empty fiscal year; consider broader import.~~ + Chronological multi-year import works (CRLF, `#RAR 0`, zero rows, `#IB` + rule handled); each year must still target an empty fiscal year. Note: + years whose source system kept corrected `#IB`/`#UB` that the vouchers + do not reproduce (like the Kapitas 2022-2026 books) diverge from the + source when imported as history; the latest year imports exactly. |
