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
+50
View File
@@ -148,6 +148,8 @@ import { RetagLineDimensionsParamsSchema } from '@/lib/pending-operations/schema
import { CreateAccountParamsSchema, UpdateAccountParamsSchema } from '@/lib/pending-operations/schemas/account'
import { defaultRateForVatTreatment } from '@/lib/vat/account-vat-treatment'
import { SetVoucherNoteParamsSchema } from '@/lib/pending-operations/schemas/voucher-note'
import { IgnoreTransactionParamsSchema } from '@/lib/pending-operations/schemas/ignore-transaction'
import { setTransactionIgnored } from '@/lib/transactions/ignore'
import { UpdateCompanySettingsParamsSchema } from '@/lib/pending-operations/schemas/company-settings'
import { UpdateCustomerParamsSchema } from '@/lib/pending-operations/schemas/customer'
import {
@@ -1194,6 +1196,51 @@ async function commitSetVoucherNote(
}
}
async function commitIgnoreTransaction(
supabase: SupabaseClient,
companyId: string,
params: Record<string, unknown>
): Promise<ExecutorResult> {
let validated
try {
validated = IgnoreTransactionParamsSchema.parse(params)
} catch (err) {
if (err instanceof z.ZodError) {
const issue = err.issues[0]
return { error: `Invalid ${issue?.path?.join('.') ?? 'params'}: ${issue?.message ?? 'validation failed'}`, status: 400 }
}
throw err
}
// Issue #1661: the same core the dashboard and v1 routes use, so the three
// doors cannot drift. It refuses a booked row through all three anchors
// (journal_entry_id, payment allocations, voucher links), is idempotent,
// and writes no verifikat: a locked or closed period does not block it.
const outcome = await setTransactionIgnored(
supabase,
companyId,
validated.transaction_id,
validated.ignored,
)
if (!outcome.ok) {
const entry = getErrorEntry(outcome.code)
return {
error: entry?.message_sv ?? 'Transaktionen kunde inte ignoreras.',
errorCode: outcome.code,
status: outcome.status,
}
}
return {
data: {
transaction_id: outcome.transaction_id,
is_ignored: outcome.is_ignored,
// false when the row was already in the requested state.
changed: outcome.changed,
},
}
}
async function commitCreateSupplier(
supabase: SupabaseClient,
userId: string,
@@ -6647,6 +6694,9 @@ async function commitPendingOperationInner(
case 'set_voucher_note':
result = await commitSetVoucherNote(supabase, companyId, pendingOp.params)
break
case 'ignore_transaction':
result = await commitIgnoreTransaction(supabase, companyId, pendingOp.params)
break
case 'create_dimension_value':
result = await commitCreateDimensionValue(supabase, userId, companyId, pendingOp.params)
break