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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user