summaryrefslogtreecommitdiff
path: root/docs/DECISIONS.md
blob: a108f484268d840e00251e79ed7087f435e483d9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# 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.
24. **Payroll (2026-09-21)**: `docs/PAYROLL.md` is the chapter. Schema v10
    (v11 adds `employees.email`): `employees` with the personnummer encrypted
    at rest (`secret_encrypt`, masked in every response; only `payroll.agi`
    returns it in clear), `payroll_runs`/`payroll_run_lines`, and national
    `tax_tables`/`tax_table_meta` without `org_id` (the one exception to the
    tenant rule). Monthly runs post D salary (7210 default; 7010 for
    kollektivanställda) / D 7510 / K 2710 / K 1930 / K 2731 with
    `source:"payroll"`; `payroll.pay_tax` does D 2710 + D 2731 / K 1630.
    Tax tables are fetched from Skatteverket's public fixed-width
    `allmanna-tabeller-manad.txt` (one button, stale-year warning) and
    over-80k % records are stored but refused until the rule is settled.
    Lönebesked PDF + mail (wave 2). **User decisions**: only the owner is
    employed now but everything is multi-employee; no semester accrual;
    tax tables from day one; manual step-buttons (Bokför, Lönebesked,
    AGI-underlag, Betala skatt & avgifter); bank phase 3 (`bank_rule.*`) is
    dropped.

## 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.