feat(reports): log behandlingsregler changes and program versions (BFNAR 2013:2 p. 9.16) (#2097)

* feat(reports): log behandlingsregler changes and program versions (BFNAR 2013:2 p. 9.16)

Part 3 of the behandlingshistorik series (#1787 report, #1790 PDF). BFNAR
2013:2 punkt 9.16 second paragraph requires the behandlingshistorik to record
"forandringar i bokforingssystemet som paverkar bokforingsposternas behandling
samt nar dessa forandringar infordes", and BFN's commentary names
behandlingsregler (automatkonteringar, fasta procentsatser) and new program
versions as the examples. Until now both changed without a trace.

Audit triggers on the behandlingsregler tables and the import logs:
mapping_rules, booking_template_library, categorization_templates,
salary_payroll_config, sie_imports, bank_file_imports. categorization_templates
learns on every booking (occurrence_count, confidence, last_seen_date), so
those telemetry-only updates are excluded by a WHEN clause the same way the
api_keys request counters are (20260721115701): only real rule changes are
logged. Measured against prod that is roughly 3 800 new audit rows a month
against an audit_log already taking 371 688, so about +1 %.

app_releases is an append-only log of program versions seen in production,
written by the runtime the first time a build answers a request. Vercel exposes
no build hook we can trust to write the row, so /api/version records it inside
after(): the handler returns synchronously and a floating promise could be
frozen before the insert lands, which is how a version log ends up silently
empty. The service client is constructed lazily so the constantly polled public
probe pays nothing once the module guard is set.

Program versions are rolled up per Swedish calendar day in the report. main
takes ~570 merges a month, so one event per version would be on the order of
7 000 a fiscal year: enough to trip the PDF's own 4 000-event guard and bury the
~400 events a real company's year contains. The statutory unit is the date, and
the same sentence qualifies the requirement to changes that affect processing,
which a deploy list cannot distinguish anyway. app_releases keeps the
per-version truth for anyone who needs to go deeper.

AuditLogEntry.user_id becomes string | null. The column is nullable and
write_audit_log() falls back to auth.uid(), which is NULL for a service-role or
global write; the company-less salary_payroll_config rows are the first that
routinely hit it, and the read model already coded for it.

Also restores the point citations the 2026-07-27 pass removed while the chapter
was unverified: it is kapitel 9, not kapitel 8 (which is arkivering), verified
against BFN's consolidated text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3P2hr19PhQuCoTSGoegcY

* test(pg): fix two fixture bugs in the behandlingshistorik trigger tests

pg-real caught both, and neither is in the migration: the inserts fail
before the trigger is reached.

mapping_rules.rule_type is constrained to mcc_code / merchant_name /
description_pattern / amount_threshold / combined; the test used
'merchant'.

booking_template_library's btl_insert policy requires
current_user_can_write() and company_id = current_active_company_id(),
so the authenticated insert needs a company_members row and a
user_preferences.active_company_id, the same setup
booking-template-hidden.pg.test.ts uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3P2hr19PhQuCoTSGoegcY

* test(pg): assert the booking-template audit row inside the user transaction

withUserContext always rolls back, so the audit row the trigger writes
is gone before an outside connection can see it. The trigger fires in
the same transaction as the write, so the assertion belongs there too.
The other cases in this file write on the pool (autocommit) and are
unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3P2hr19PhQuCoTSGoegcY

* fix(reports): name every build id in the per-day program-version entry

Raised by the compliance review on #2097: the roll-up listed five ids
and a count, which leaves an auditor unable to reconstruct which
versions ran that day. app_releases keeps the full record, but the
report is the surface anyone actually reads. A day is bounded by the
deploy rate (~19), so the full list stays one readable cell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3P2hr19PhQuCoTSGoegcY

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-01 20:31:10 +02:00
committed by GitHub
co-authored by Claude Opus 5 Jakob Wennberg
parent 169e7eaf4e
commit a08bf51ced
24 changed files with 18637 additions and 40 deletions
+2
View File
@@ -1423,6 +1423,8 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-01] multi_user seat gate enforced app-side via a NEW gated RPC (resolve_active_company_gated) instead of editing resolve_active_company/current_active_company_id: the zero-arg RPC and the RLS twin also run on self-hosted DBs where the paywall must never bite, and the app picks the gated overload only when isMultiUserEnforced(). RLS convergence rides the existing used_fallback write-back. company_capability_config deliberately does not apply to multi_user (no expiry to hang the 20-day grace on).
[2026-09-01] EU reverse-charge packs book directly on 4515/4535 instead of adding a 45xx D / 4598 K basbelopp pair (Anders' literal suggestion): same ruta 20/21 outcome, standard BAS practice for a template that owns the cost account anyway, and a 3-business-line pack would return null from convertLibraryToBookingTemplate and silently vanish from the transaction picker. The 4598 motkonto pattern remains the right tool only where the user's own cost account must be preserved (engine-generated bookings, supplier invoices).
[2026-09-01] Floating supplier-invoice underlag gets a standing daily reanchor cron (/api/documents/reanchor/cron) instead of another one-off repair migration: prod case 2026-08-28 (kontantmetod payment verifikat, doc eligible on every static condition, inline anchor silently did nothing, no log line recorded why) is the second time a hand-written sweep (20260727180000, 20260824150000) was needed; the inline anchor is best-effort by design, so the retry belongs in infrastructure. anchorSupplierInvoiceDocument also stops claiming success on a zero-row guarded update and logs its silent bail branches.
[2026-08-21] BFNAR 2013:2 chapter for systemdokumentation/behandlingshistorik verified against BFN's consolidated PDF (bfnar13-2-kons2) and the vägledning: it is kapitel 9 (punkt 9.1-9.17; behandlingshistorik = 9.16, where/how = 9.15), kapitel 8 is arkivering. Code comments, the compliance skill and the public systemdokumentation template now cite kap 9 / the points; this reverses the 2026-07-27 removal of the point citations, which was right to do while unverified.
[2026-09-01] app_releases is written by the runtime from /api/version inside after(), not by a deploy hook or a floating promise: Vercel exposes no build hook we can trust to write a row, the handler returns synchronously so a bare void could be frozen before the insert lands (a silently empty version log is worse than none), and the service client is now constructed lazily so the constantly-polled public probe pays nothing once the module guard is set.
[2026-09-01] MCP worked examples ship in the inputSchema for five tools only, not the top twenty (#2066): the tools/list ceiling is the binding constraint, not the writing. After #2089 reclaimed 3 763 tokens the policy in payload-size.bench.test.ts required ratcheting the ceiling down with it, so real headroom was 317 tokens, not the 4 300 the #2089 PR body implied. Ten examples cost 199, leaving ~118. Tools were picked by 30 days of mcp.tool_called crossed with the combinations the descriptions already warn about (account_override without vat_treatment, representation without deltagare, confirmed on a high-risk approval, and get_kpi_report where a caller sent `metric` 604 times to a tool whose only parameter is period_id). Examples are also surfaced in the unknown-parameter error, which costs zero catalog budget and reaches the caller that already failed; a test validates every example against its own schema with the same guard the server runs, because an example our boundary would reject teaches exactly the mistake it then punishes.
[2026-09-01] SKV connector instance wiring (PR6b-2): system (CCG/ombud) auth is deliberately NOT brokered; it stays on the direct path and fails SYSTEM_AUTH_FAILED on a credential-less self-host, because the org certificate and ombud grants are hosted-only. getSkatteverketEnvironment() hard-reports 'prod' in connector mode: the upstream env is resolved from HOSTED's config, and the instance's unset base URLs would otherwise show a Testmiljo badge on real filings (reporting hosted's actual env via /api/connector/status is a #2090 follow-up). buildAuthorizeUrl stays direct-only rather than going async: the connector authorize needs to return the broker's redirect_uri + connector_state for persistence, so index.ts branches to startConnectorAuthorization instead of overloading one function. Connector-layer 4xx bodies (code CONNECTOR_*) are classified BEFORE the SKV-shaped 401/403 sniffing so a broker refusal never tells a self-host operator to check SKATTEVERKET_APIGW_CLIENT_ID; broker refresh 404 CONNECTOR_NOT_OWNED maps to SESSION_EXPIRED (terminal, reconnect fixes) while broker 502 stays a raw error (transient SKV outage must not re-arm the reconnect banner, #1155).
[2026-09-01] multi_user skeptic fixes: Stripe cancel EXPIRES the multi_user stripe grant instead of deleting it (grace anchor; other grants still deleted per freeze-and-retain); app-side state checks go RPC-first via SECURITY DEFINER company_multi_user_state (capability_grants RLS hides team rows from non-team users, byrå clients would misread as frozen); byra-kind teams get a standing team-scoped multi_user grant via backfill + teams trigger (WL-10 assumption made real; partner billing is out-of-band); PGRST202 on resolution fails OPEN (pre-migration DB has no multi_user rows: gated fallback would freeze all non-owners); /api/v1 got the same dormancy gate as MCP. RLS-level enforcement and the mid-session API fallback write-back window stay v2 follow-ups (documented, same class as pre-existing stale-preference fallback).