feat(bookkeeping): dashboard deep link filters verifikat utan underlag server-side (#1840)

* feat(bookkeeping): dashboard deep link filters verifikat utan underlag server-side

The dashboard "Verifikat utan underlag" card (and the push-notification
link) pointed at /bookkeeping?missingUnderlag=true, but nothing read the
param: the user landed on the plain unfiltered ledger. The existing
"Visa saknade underlag" toggle also only filtered the already-fetched
page, so it could not represent the badge count across pages.

- lib/bookkeeping/missing-underlag.ts: shared resolver of "posted
  verifikat lacking underlag" (document-requiring source types, no
  current-version document, no anchored supplier-invoice reference per
  BFL 5 kap 7 §, no exemption), extracted from the bulk "Inget underlag
  krävs" route so list, bulk remedy and dashboard badge share one
  predicate.
- GET /api/bookkeeping/journal-entries?missing_underlag=true: resolves
  the full missing set server-side, applies the active sort stack, pages
  it, and returns the full-set count, fetching page rows in id chunks so
  the "Alla" page size cannot blow the PostgREST URL limit.
- JournalEntryList: the toggle is now server-backed (refetch on change,
  honest count in the dialog badge); client-side re-filtering against
  late-arriving attachment counts removed. Deep-link arrival turns the
  filter on and scopes the visit to all fiscal years in memory only,
  matching the all-years badge count without touching the saved
  preference.

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

* fix(bookkeeping): harden the saknade-underlag filter after skeptic review

Three skeptic subagents refuted the first cut; this fixes every confirmed
finding in one pass:

- FyPicker: new suppressAutoRestore prop. The deep-link visit opens as
  "Alla räkenskapsår" in memory, and FyPicker's on-load restore of the
  persisted year (value === null) snapped the scope back right after
  load, desyncing the list from the all-years badge that launched it.
  Manual picks still persist as usual.
- Voucher-label search: the resolver now carries the same parseVoucher
  OR-branch as the direct list path, so searching "A209" with the filter
  on finds verifikat A209 instead of silently returning 0 rows.
- Staleness while the filter is on: batch exempt, the single-row "Inget
  underlag krävs" toggle, and a row gaining its first underlag now
  refetch in place so fixed rows leave the filtered list and the count
  stays honest (the pre-server-filter behavior). The attachment-driven
  refetch is guarded per entry id against predicate-disagreement loops.
- Drafts view: the filter switch is disabled there; the predicate is
  posted-only and the badge would mislabel the draft count.
- Perf: the bulk-exempt route resolves ids only, skipping the per-row
  total_amount computed column on its full post-import candidate scan.

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

* fix(bookkeeping): keep the underlag resolver statically checkable

The skeptic-fix commit tripped the phantom-column scanner ceiling
(tests/schema/no-phantom-columns.test.ts, 382 > 380): a computed
select() string and a runtime-built .or() are expressions the scanner
cannot resolve against the schema. Restructured instead of raising the
ceiling: the idOnly/full column choice is two literal select() calls
behind a lazy branch, and a voucher-label search fans out to two
statically-checkable candidate queries (description ilike, series+number
eq) unioned by id, same semantics as before.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-24 15:06:30 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 2e669a64e3
commit 0676f5a564
8 changed files with 673 additions and 146 deletions
+1
View File
@@ -1176,6 +1176,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-24] Assistant empty answer is a typed failure (EmptyModelAnswerError, 502 'Assistenten gav inget svar. Försök igen.'), one manual retry, NO auto-retry: the silent 200 with an empty answer was the 'Tänker then nothing' bug after the RIP-3 cutover (1500-token cap on /api/agent/ask), and auto-retry would double model spend while hiding the regression. Related deliberate request-shape change: anthropic-family's step-exhausted fallback now keeps tools with tool_choice none, because replaying a transcript containing tool_use/tool_result without declaring tools is rejected by the Messages API (the old shape 400ed every step-exhausted answer). general.help stays on the single-call runtime (founder decision); the fix raises headroom to 5400 tokens and surfaces the failure instead.
[2026-08-24] Issue #1820 self-billed credit fix: creditConfirmNumber()/originalRef fall back invoice_number -> external_invoice_number (typed 400 INVOICE_CREDIT_NO_NUMBER if both null) instead of relaxing the DB numbering constraint or dropping the type-the-number confirm step; the confirm step stays (dropping it is a founder call). The invoice-date Forval chip surfaces in ALL editor modes, not only self-billed: the silent today-default exists in every mode and the chip line already carries the due date. In self-billed mode fakturadatum + mottagningsdatum render uncollapsed next to the external number (transcription fields, not defaults); the panel rows are hidden there because registering the same RHF field twice desyncs the inputs. The v1 credit route's existing id-slice fallback was left unchanged (public API behavior).
[2026-08-24] No-IBAN reconnect pairing (issue #1709) uses only per-currency exactly-one-each-side elimination, deliberately WITHOUT name equality: ASPSPs reformat product names between consents, so requiring it would silently disable the fix for the banks that need it, while the one-per-currency guard already bounds a mis-pair to skipping rows whose account+date+amount+occurrence all collide. upsertFromPsd2 needed no change: its explicit reuse_cash_account_id promote path already covers a same-connection holder, so the fix only names the paired row from the callback.
[2026-08-24] Dashboard "Verifikat utan underlag" deep link filters server-side via a TS resolver (lib/bookkeeping/missing-underlag.ts, extracted from the bulk-missing route) with a two-step ids-then-rows fetch, NOT by extending list_fiscal_period_entries_with_related or the verifikat_without_documents RPC: no migration, and the predicate stays byte-identical to the badge count's bulk-remedy path. Three deliberate sub-choices: collapse_corrections is ignored while the filter is on (the badge has no collapse notion, hiding corrected originals would desync list from count); the deep link scopes the visit to all fiscal years in memory only (badge counts all years; the saved per-company year preference is not overwritten); client-side re-filtering against attachmentCounts was removed when the filter is on (late-arriving counts could hide server-included rows and desync rows from count).
[2026-08-23] Reconciliation engine (PR 1): the skattekonto status engine lives in core lib/reconciliation (reads the core table + the extension snapshot row in extension_data directly) instead of in the skatteverket extension: core must never import @/extensions/*, and the reconciliation facade must work with zero extensions; the matcher stays in the extension and writes its proposals to the row at sync time. Proposals are propose-only (suggested_journal_entry_id is never a link); the same per-entry one-to-one assignment replaces per-row "exactly one candidate". Ledger balances everywhere in reconciliation use the trial-balance predicate status IN (posted, reversed): the drift check summed posted only, which misstated 1630 for every company with a storno on the account.
[2026-08-23] Reconciliation doors (PR 2): dashboard routes, the v1 API and the MCP tools all call lib/reconciliation/{service,items,actions}.ts; no door re-implements a link. Policy lives in the door: page + REST apply directly, MCP stages (reconciliation_match / reconciliation_unmatch pending operations, executors in commit.ts). The MCP write tools are catalogVisibility search (and gnubok_link_transaction_to_journal_entry moved to search) because the tools/list payload ceiling (59 900 tokens) left no room for them in the default catalog; the reads (status with account_key, items) stay default and the items description points at the write. The skattekonto link now has its canonical implementation in core lib/skatteverket/skattekonto-link.ts (needed by core doors; core must not import the extension); the extension route still uses its own matchSkattekontoToEntry until its queued-mock tests are ported, then it delegates. New scopes reconciliation:read/write; gnubok_get_reconciliation_status keeps reports:read and the legacy bank routes keep transactions:* so no existing key is cut off.
[2026-08-23] Avstämning page (PR 3) ships without the period picker, the manual two-pane match mode and the sign-off button: the page renders the approved 'Vald riktning' layout (rail + tiles + bridge + actions + banded table) over the PR 2 dashboard routes only, so that it is verifiable on its own; period + sign-off arrive together in PR 4 (both are period-bound), manual N:M matching with residual booking in PR 5. Bank accounts get the same generic body plus links to the existing bank view for the matcher run rather than embedding the 1900-line BankReconciliationView: one body for every account kind is the point of the page, and embedding would have doubled the header.