fix(year-end): typed preflight blocker codes so remediation links render (#1420)

* fix(year-end): typed preflight blocker codes so remediation links render

validateYearEndReadiness emits Swedish blocker strings but the wizard's
BlockerRow matched English phrases, so no remediation link ever rendered,
and the voucher-gap branch pointed at /bookkeeping/voucher-gaps which only
exists as an API route. Blockers now carry stable machine codes end to end
(YearEndBlockerCode on YearEndValidation.blockers, mirrored additively as
blockerItems on BokslutReadinessReport); errors stays the plain string
mirror so the v1 compliance check and MCP tool keep their exact shapes.
BlockerRow matches on code and links only to pages that exist; the
voucher-gap and dead-link branches are removed.

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

* fix(year-end): code the unbooked-transaction blockers #1414 added

#1414 landed two new blockers in validateYearEndReadiness using the old
errors.push style, which this branch had already renamed to a typed
blockers array. Merging main left them referencing a variable that no
longer exists.

Converted both to the typed scheme: UNBOOKED_TRANSACTIONS (the safety
guard that stops executeYearEndClosing from aborting at the step 7 lock
AFTER the closing entry posted at step 4) and UNBOOKED_CHECK_FAILED (the
fail-closed variant). Neither behaviour changes; both keep their Swedish
wording verbatim.

The MCP year_end_readiness classifier now routes on the stable
YearEndBlockerCode instead of regexing the Swedish message, with the
wording heuristic kept as a fallback for an unmapped or legacy English
message. The public `kind` values are unchanged, so MCP consumers see the
same output; both new codes map to 'unbooked_transactions' as before,
since an agent reacts to "we could not tell" the same way it reacts to a
real count.

UNBOOKED_TRANSACTIONS gets a /transactions remediation link in the
preflight step: that page is where a transaction is booked or marked
private, the two remedies the message names. UNBOOKED_CHECK_FAILED gets
none: the remedy is to re-run the check.

---------

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-06 10:04:42 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 31fba5e8f9
commit 78a37f6396
8 changed files with 315 additions and 78 deletions
+30
View File
@@ -3399,8 +3399,38 @@ export interface SequenceMismatch {
// Year-End Closing Types (Årsbokslut)
// ============================================================
/**
* Stable machine codes for year-end readiness blockers. One code per
* blockers.push site in validateYearEndReadiness: the wizard matches on
* these to attach remediation links, so codes must never be renamed once
* shipped. The Swedish message stays the display text.
*/
export type YearEndBlockerCode =
| 'PERIOD_NOT_FOUND'
| 'PERIOD_NOT_ENDED'
| 'PERIOD_ALREADY_CLOSED'
| 'CLOSING_ENTRY_EXISTS'
| 'DRAFT_ENTRIES'
| 'UNEXPLAINED_VOUCHER_GAP'
| 'SEQUENCE_COUNTER_BEHIND'
| 'TRIAL_BALANCE_UNBALANCED'
| 'CONTINUITY_MISMATCH'
| 'NEXT_PERIOD_HAS_IB'
| 'UNBOOKED_TRANSACTIONS'
| 'UNBOOKED_CHECK_FAILED'
export interface YearEndBlocker {
code: YearEndBlockerCode
/** Swedish, user-facing: bokslut is a stays-Swedish surface. */
message: string
}
export interface YearEndValidation {
ready: boolean
/** Blocking errors with stable machine codes. */
blockers: YearEndBlocker[]
/** Blocker messages only; mirrors `blockers`. Kept so existing consumers
* of the string list (v1 compliance check, MCP tool) stay unchanged. */
errors: string[]
warnings: string[]
draftCount: number