feat(transactions): steer private marking in locked periods to ignore, with v1 and MCP ignore verbs (#1661) (#2031)

Decision (option a): a private marking stays a real booking (eget uttag/insattning), so it remains blocked in a locked or closed period; the legal escape for rows that are not affarshandelser is ignore. Private + locked now returns TX_CATEGORIZE_PRIVATE_PERIOD_LOCKED with remediation naming the ignore paths instead of a bare PERIOD_LOCKED, on all four categorize surfaces and the bulk driver. New v1 POST/DELETE /transactions/{id}/ignore (isTransactionBooked-based 409, idempotent) and a staged MCP gnubok_ignore_transaction (+ accounted_ alias, search visibility to respect the tools/list payload ceiling) with operation_type ignore_transaction; the CHECK pair 20260831070000/070001 rebuilds the constraint from main's newest list plus the new value. Dashboard toast gains an Ignorera i stallet action. Closes #1661
This commit is contained in:
Jakob Wennberg
2026-08-31 08:39:04 +01:00
committed by GitHub
parent 79edee659f
commit dc07ca8872
44 changed files with 2062 additions and 73 deletions
+33 -1
View File
@@ -327,7 +327,7 @@ const BOOKKEEPING: Record<string, StructuredErrorEntry> = {
message_en: 'Period is locked or closed; entries cannot be added.',
remediation: {
description:
'Either unlock the period via gnubok_unlock_period (if status is "locked", not "closed") or change the entry date to fall inside an open period.',
'Either unlock the period via gnubok_unlock_period (if status is "locked", not "closed") or change the entry date to fall inside an open period. A bank transaction that is not a business event (duplicate, never executed) needs no verifikat: ignore it instead (POST /transactions/{id}/ignore, gnubok_ignore_transaction).',
tool: 'gnubok_unlock_period',
},
},
@@ -497,6 +497,38 @@ const TRANSACTIONS: Record<string, StructuredErrorEntry> = {
description: 'Reload and retry categorization. Report the conflict if it persists.',
},
},
// Issue #1661: a private (is_business=false) marking is a real booking
// (eget uttag/insättning on 2013/2018, or 2893 for an AB), so a locked or
// closed period blocks it exactly like any other verifikat. The row the
// caller usually wants to clear (a PSD2 ghost row, a duplicate, a never
// executed transfer) is not an affärshändelse at all: ignoring it writes no
// verifikat and is therefore allowed in a locked period. Returned instead
// of PERIOD_LOCKED so the remediation names that path. The wording must not
// contain "Bokföringen är låst" (inferCode maps that phrase to PERIOD_LOCKED).
TX_CATEGORIZE_PRIVATE_PERIOD_LOCKED: {
httpStatus: 400,
message_sv:
'Perioden är låst. En privat markering bokförs som eget uttag eller insättning i perioden. Är raden ingen affärshändelse (dubblett, aldrig genomförd)? Ignorera den i stället. Annars: lås upp perioden.',
message_en:
'The period is locked. A private marking is booked as an owner withdrawal or deposit inside the period. If the row is not a business event (a duplicate, never executed), ignore it instead; otherwise unlock the period.',
remediation: {
description:
'If the row is not a business event, ignore it: POST /api/v1/companies/{companyId}/transactions/{id}/ignore, the Ignorera action on the Transaktioner page, or gnubok_ignore_transaction. Ignoring writes no verifikat, so it is allowed in a locked or closed period. Otherwise unlock the period via gnubok_unlock_period (status "locked", not "closed") and categorize again.',
tool: 'gnubok_ignore_transaction',
},
},
TX_IGNORE_ALREADY_BOOKED: {
httpStatus: 409,
message_sv:
'Transaktionen är redan bokförd: använd Avmatcha eller backa verifikationen för att ändra status.',
message_en:
'The transaction is already booked (directly, via a payment allocation, or via a voucher link). Unlink it or reverse the verifikat (storno) before ignoring it.',
remediation: {
description:
'A booked bank row cannot be ignored: the booking IS its status. Reverse it with gnubok_uncategorize_transaction (storno) or unlink the payment/voucher first, then ignore.',
tool: 'gnubok_uncategorize_transaction',
},
},
TX_CATEGORIZE_SUGGEST_SI_MATCH: {
httpStatus: 409,
message_sv: