aboutsummaryrefslogtreecommitdiff
path: root/docs/DECISIONS.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/DECISIONS.md')
-rw-r--r--docs/DECISIONS.md268
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.