feat(onboarding): activation quick wins on the Hem checklist (#1461)

* feat(onboarding): activation quick wins on the Hem checklist

First implementation slice of the approved activation concept
(artifact de543d57, dev_docs/onboarding_activation_analysis.md §9):

- New "Kvitton och underlag" checklist step, gated on the invoice-inbox
  extension like the Skatteverket step; done once the company has ever
  received an inbox item (email/WhatsApp/upload). Non-AI companies route
  to billing, matching the assistant step.
- Personalized VAT line in the Skatteverket step: the company's real next
  momsdeklaration due date from the deadlines table, with an explicit
  "välj momsperiod" prompt when vat_registered is set but moms_period is
  null (that state silently generates zero VAT deadlines).
- Truthful Att göra empty state: while the setup checklist is open and no
  journal entry is posted, the all-clear reads "Bokföringen är tom än"
  instead of a false "Allt klart!".
- Activation funnel events (onboarding_setup_step_started / _completed /
  _dismissed) via posthog-js, mirroring the existing guarded capture
  pattern; sandbox never renders the block so no extra gate is needed.

Pure helpers live in lib/onboarding/checklist.ts with tests; step
numbering now adapts to both optional extensions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: record the receipts-signal and moms-period-guard decisions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(onboarding): review triage: error-safe emptyLedger, stale-state guard, copy

- A failed posted-entries count no longer reads as an empty ledger.
- Confirming a suggested match books an entry, so the empty-ledger copy
  retires for the rest of the session (postedSinceLoad).
- 'Bokföringen är tom än så länge' reads naturally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-08 15:14:34 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent ddbe9b1379
commit 44cff5e5e4
11 changed files with 356 additions and 34 deletions
+1
View File
@@ -831,6 +831,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-07-28] Transaction method (structured payment rail): the trailing channel phrase ("Överföring via internet", "Kortköp/uttag") is stripped from transactions.description at INGEST and by a one-shot BACKFILL, not merely hidden at render: description is the mutable working title, original_description keeps the full bank string, and every dedup surface (external_id: date+öre only; content bridge: prefix-containment over original_description ?? description, and a trailing strip leaves a prefix) is provably unaffected. transaction_method is text + CHECK (repo convention, no PG enums) beside verbatim bank_transaction_code / proprietary_bank_transaction_code evidence columns per data_quality_master Appendix B Layer-A; the dead `enrichment` jsonb was NOT reused (the Gokind lesson: opaque blobs with no readers die). mapping-engine now also matches original_description so user rules written against the full bank text keep firing on stripped rows.
[2026-07-29] Transaction-method backfill scope: classification and title-stripping are FEED-ROW concepts (import_source present, not manual/mcp), enforced identically at ingest and in the 20260808090100 backfill, plus an adjective guard so "Egen insättning"/"Eget uttag"/"Intern överföring" keep their full titles even on feed rows (the phrase IS the meaning after a possessive/scope adjective). Chosen over vocabulary tweaks because the failure mode for unknown bank phrasings must be "row unchanged", and user-authored titles must never be rewritten by a channel vocabulary. A read-only prod dry-run script exists for coverage measurement but prod reads were left to the founder (permission-gated).
[2026-08-08] Compliance-bot finding on the transaction_method backfill (booked rows' titles rewritten without a rattelse trail) triaged as satisfied-by-design, not a blocker: BFL 5 kap 5 attaches to bokforingsposter, and the backfill touches no journal table; the verifikat description is snapshotted into journal_entries at commit and SIE #VER export reads journal_entries only (both verified in code, no report reads transactions.description lazily); the bank original is preserved byte-identical in original_description by the same UPDATE (enforced since 80ef1ee0, and prod has 0/25,566 feed rows lacking it). The stricter TRANSACTION_TITLE_LOCKED gate on booked rows blocks arbitrary user free-text renames, a different mutation class from a deterministic trailing-vocabulary strip that skips user-edited titles and keeps the original adjacent. Period-lock triggers sit on the journal tables and fiscal periods, not on transactions; the pg-upgrade CI run applied the backfill against seeded booked rows with all enforcement triggers active.
[2026-08-08] Receipts checklist step (PR #1461) done-signal = any invoice_inbox_items row for the company, not a document_attachments count: document_attachments also archives outbound invoice PDFs, ROT/RUT payout files and Bolagsverket submissions, so its count would mark the step done when the user merely sends their first invoice; invoice_inbox_items is written by exactly the three intake channels (email/WhatsApp/upload) and carries a company_id index. Same PR: vat_registered with moms_period=null renders an explicit "välj momsperiod"-link in the checklist instead of silence, because that state makes the deadline engine generate zero VAT deadlines without any error (all three moms configs require a concrete period).
[2026-08-08] Per-voucher RC basis gaps (findRcBasisGaps) downgrade from filing-blocking ERROR to WARNING only under per-rate evidence: the 44xx/45xx basis accounts grouped by momssats must match ruta30/0.25, ruta31/0.12, ruta32/0.06 two-sided within 0.5 kr, all rate boxes non-negative, and no RC_OUTPUT_MISSING present. A first cross-rate-sum predicate was refuted by /skeptic (wrong-rate fiktiv moms reached parity and unblocked a 7 800 kr under-declaration; a negative rate box made the predicate vacuous), so the certificate is per-rate, which rutor alone cannot express (rutor 20-24 are partitioned by purchase type, not rate); evidence therefore flows from the account totals. Why downgrade at all: a moms-only rattelseverifikat carries fiktiv moms whose basbelopp lives in another (often reversed) verifikat, and no voucher arrangement satisfies both the per-voucher scan and the aggregate identity in that state, so the ERROR was an unfixable dead end (Orto Engineering 3DJake case 2026-08; support vouchers A169/A175/A177 joined the blocklist they were meant to clear). Data side repaired separately with voucher A177 restoring bank parity and the basis/moms identity exactly.
[2026-08-08] Fenced-JSON fix uses brace-slice, not fence-regex: also rescues preamble/postamble prose around the object, and degrades to the existing empty-result path when no braces exist.
[2026-08-08] extractJsonObject upgraded from brace-slice to depth-aware balanced scan after PR 1460 review: prose containing braces around the JSON no longer poisons the slice; first parseable candidate wins.