fix(period): let klarmarkera close a migrated year whose native vouchers are balance-sheet only (#2393)

* fix(period): let klarmarkera close a migrated year whose native vouchers are balance-sheet only

markPeriodClosedExternally decided "migrated" by asking whether the period
had any non-imported verifikat at all. That was a proxy for the thing the
guard protects (a bokslutsverifikat transferring 3xxx-8xxx, BFL 5-6 kap),
and it shut out the migrated first year whose only native voucher re-keys
the opening balance (1930 D / 2081 K aktiekapital) after a failed SIE
import. With the next year's IB already imported, the normal year-end
refused too (NEXT_PERIOD_HAS_IB), so the year had no closing path at all.

The guard now asks the ledger the real question: are there lines on result
accounts (BAS class 3-8) in the period? Id-only entry fetch plus per-chunk
head counts with early exit, no line fetch, no journal_entries!inner embed.
The refusal message names what was found and why the normal year-end is
the remedy.

The loaded årsredovisning view also gets the FyPicker in its header. The
no-period branch auto-jumps to the remembered scope (or the newest year)
before its own picker is ever seen, so a user whose scope pointed at the
historical year had no way to reach the current year's årsredovisning
except by changing the scope on some other page and coming back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZjtjw5xnC2SVaEr5HqG8Q

* fix(period): PR #2393 round 1: require next-year IB for balance-sheet-only klarmarkera, bound result range to class 3-8

Triage of the swedish-compliance review:

- Balance-sheet-only native years bypassing the bokslut: partly real. The
  result transfer is not the only thing the normal year-end does; it also
  posts the next year's IB. A natively bookkept year with only balance-
  sheet vouchers (dormant AB, aktiekapital deposit only) could have been
  klarmarkerad and its balances would never have reached the next year.
  The leg now additionally requires the next period to carry IB already,
  which is exactly the shape where the normal year-end refuses
  (NEXT_PERIOD_HAS_IB) and klarmarkera is the only path. Without that IB
  the normal year-end works and stays the remedy, and the message says so.
  The suggested IB-correction tag on entries was not adopted: the ledger
  already answers the question, a tag would be one more concept to know.
- Unbounded account range: real, cosmetic. Bounded with lt '9' so class 9
  interna poster no longer count as result accounts. Storage format is the
  4-digit BAS string every classifier in the codebase already relies on
  (isBalanceSheetAccount reads charAt(0)); the text compare is as robust as
  those.

Tests: next year with IB passes, next year without IB refused, no next
year refused, lt('account_number', '9') asserted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZjtjw5xnC2SVaEr5HqG8Q

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-07 20:10:41 +02:00
committed by GitHub
co-authored by Claude Fable 5.1 Jakob Wennberg
parent 57a5af1310
commit 5d735097a1
4 changed files with 231 additions and 24 deletions
+2
View File
@@ -1649,3 +1649,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-07] WooCommerce wc-auth return_url is built on the initiating request's trusted origin (resolveRequestAppOrigin, brands-table validated, canonical on unknown host or lookup failure) while callback_url stays on the canonical app URL: sessions are per domain, so a white-label user returned to the canonical host hit the initiator check with no session and a foreign-branded login. No stored origin column and no provider_otc handoff (PR #2305 pattern) needed, because unlike a registered OAuth redirect URI the wc-auth return_url is free-form per handshake and can simply point at the brand host; the return route resolves its redirect base from the host it was reached on the same way.
[2026-09-07] PR #2373 review: the Skatteverket callback now peeks the flow row for its initiator and binds the session BEFORE consuming state or handoff (Superagent P2: a DELETE-before-auth let a signed-out or wrong-user arrival burn a live consent). A session-less arrival is sent to /login on the initiating origin and resumes into the same URL; a wrong user is refused with the row left claimable. This reverses the earlier 'no login resume' decision for this flow; the consume stays the atomic gate, the peek only decides who may attempt it. Handoff TTL raised from two to five minutes to fit a sign-in.
[2026-09-07] BankID confirmation mail and the Send Email hook resolve their link host through lib/domains/trusted-app-origin instead of the raw forwarded host / GoTrue's redirect_to: the BankID mail is the one auth link GoTrue's redirect allowlist never sees (built here, sent via Resend), and the hook's signature proves the sender, not the destination, while the GoTrue allowlist is a hand-configured glob. Unknown, lookalike, credential-bearing, non-default-port and malformed destinations collapse to the canonical /auth/callback with no next path; a registered brand host over http is upgraded to https. Brand sender identity is resolved from the RESOLVED host so mail branding and link destination always agree. A brands-table read failure refuses (BankID: step resolve_origin, signup rolls back; hook: 500 so Supabase retries) rather than mailing a canonical link to a white-label user. Dropped from the audit's plan 7 as already in place after #2376: signup route, HTTPS enforcement, credential/port checks, recovery/invite/email-change coverage.
[2026-09-07] Klarmarkera (markPeriodClosedExternally) now asks whether the period has any lines on result accounts (BAS class 3-8) instead of whether it has any non-imported verifikat at all. The old test was a proxy for the thing the guard protects (3xxx-8xxx transferred by a bokslutsverifikat, BFL 5-6 kap) and it shut out the migrated first year whose only native voucher re-keys the opening balance (1930/2081 aktiekapital) after a failed SIE import; with the next year's IB already imported the normal year-end refused too (NEXT_PERIOD_HAS_IB), so the year had no closing path at all (EHAL AB support case 2026-09-07). Existence check via id-only entry fetch plus per-chunk head counts with early exit, no line fetch, no journal_entries!inner embed. Same session: the loaded årsredovisning view got the FyPicker in its header, because the no-period branch auto-jumps to the remembered scope before its own picker is ever seen, which left users with no way to reach another year's årsredovisning.
[2026-09-07] Klarmarkera's new balance-sheet-only leg additionally requires the NEXT period to already carry IB (opening_balance_entry_id set). Without that, a natively bookkept year with only balance-sheet vouchers (dormant AB with just the aktiekapital deposit) could be klarmarkerad and its balances would never reach the next year, since klarmarkera skips the IB posting the normal year-end does. With the IB in place the normal year-end refuses (NEXT_PERIOD_HAS_IB) and klarmarkera is the only path; without it the normal year-end works and is the right path. Raised by the swedish-compliance bot on PR #2393 (its suggested alternative, a dedicated IB-correction tag on entries, was rejected: it adds a concept for the user to know about where the ledger already answers the question). Same round: the result-account range is bounded to class 3-8 (lt '9'), class 9 interna poster are not transferred by a bokslut.