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
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
|
|
import { getErrorEntry } from '@/lib/errors/structured-errors'
|
|
import { setTransactionIgnored } from '@/lib/transactions/ignore'
|
|
|
|
/**
|
|
* POST /api/transactions/[id]/ignore
|
|
*
|
|
* Mark a bank transaction as ignored so it stops surfacing in the bank
|
|
* reconciliation view (and other "to book" funnels) without creating a
|
|
* verifikation. Use case: tiny ränteintäkter, rounding noise, opening-balance
|
|
* artefacts, PSD2 ghost rows: anything the user wants off the unmatched list
|
|
* but doesn't want to fabricate a journal entry for. No verifikat is written,
|
|
* so a locked or closed period does not block it (issue #1661).
|
|
*
|
|
* Refuses when the transaction is already booked (directly or through a
|
|
* payment allocation / voucher link: lib/transactions/is-booked.ts); once a
|
|
* verifikation exists, the proper way to revisit it is /uncategorize (storno).
|
|
*/
|
|
export const POST = withRouteContext<{ params: Promise<{ id: string }> }>(
|
|
'transaction.ignore',
|
|
async (_request, { supabase, companyId }, { params }) => {
|
|
const { id } = await params
|
|
|
|
try {
|
|
const outcome = await setTransactionIgnored(supabase, companyId, id, true)
|
|
if (!outcome.ok) {
|
|
if (outcome.status === 404) {
|
|
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
|
}
|
|
return NextResponse.json(
|
|
{ error: getErrorEntry(outcome.code)?.message_sv ?? 'Transaktionen är redan bokförd.' },
|
|
{ status: outcome.status },
|
|
)
|
|
}
|
|
if (!outcome.changed) {
|
|
return NextResponse.json({ success: true, already_ignored: true })
|
|
}
|
|
return NextResponse.json({ success: true })
|
|
} catch (err) {
|
|
return NextResponse.json({ error: getUserErrorMessage(err) }, { status: 500 })
|
|
}
|
|
},
|
|
{ requireWrite: true },
|
|
)
|
|
|
|
/**
|
|
* DELETE /api/transactions/[id]/ignore
|
|
*
|
|
* Reverse a previous ignore. The row comes back into the unmatched list with
|
|
* no further side effects: we never created a verifikation, so there's
|
|
* nothing to storno.
|
|
*/
|
|
export const DELETE = withRouteContext<{ params: Promise<{ id: string }> }>(
|
|
'transaction.unignore',
|
|
async (_request, { supabase, companyId }, { params }) => {
|
|
const { id } = await params
|
|
|
|
try {
|
|
const outcome = await setTransactionIgnored(supabase, companyId, id, false)
|
|
if (!outcome.ok) {
|
|
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
|
}
|
|
return NextResponse.json({ success: true })
|
|
} catch (err) {
|
|
return NextResponse.json({ error: getUserErrorMessage(err) }, { status: 500 })
|
|
}
|
|
},
|
|
{ requireWrite: true },
|
|
)
|