* feat(bookkeeping): Ny verifikat modal, ledger-style list, SIE no-underlag exemptions Verifikat UX - "Ny verifikat" opens in a modal (NewJournalEntryDialog) instead of an inline tab; the review step renders inline in the dialog rather than stacking a second dialog. - JournalEntryForm: konteringsrader are the focus, with a compact pre-filled metadata bar (datum/serie/text/valuta/period) on top; verifikationstext auto-fills from the first row's account. - JournalEntryList: belopp shown on collapsed rows; expanded view is an aligned Konto/Benämning/Debet/Kredit table. SIE imports no longer flood "Att hantera: saknade underlag" - Import gains an opt-in (off by default) toggle to mark imported verifikat as "Inget underlag krävs"; a "Rekommenderas vid migrering" badge nudges it for historical years. - Multi-select batch-mark in the list for selective cleanup. - Filter-scoped bulk mark (POST /api/bookkeeping/no-doc-required/bulk-missing): marks every missing-doc verifikat matching the active filters across all pages, with a dry_run count to confirm scope — the scalable remedy for a post-import flood. - Shared helper markEntriesNoDocRequired + per-entry batch route. Tests: no-doc helper, batch route, bulk-missing route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): address PR #698 review findings - JournalEntryForm: restore the explicit "no underlag" acknowledgement in the modal's inline review. When no document is attached, the confirm button reads "Bokför utan underlag" (BFL 5 kap 6-7 §§), equivalent to the blocking dialog the non-bare flow shows — the bare path no longer posts behind only a passive banner. - batch no-doc route: guard the ownership query with source_type IN NEEDS_DOC_SOURCE_TYPES so a crafted request can't exempt non-document-requiring entries (defense in depth on top of company + posted scoping). - bulk-missing route: resolve doc/exemption status by querying only the candidate ids (chunked) instead of loading the company's full document_attachments and journal_entry_no_doc_required tables into memory — data minimisation + bounded memory for large migrations (the most-repeated reviewer finding). Triaged as non-issues (left as-is): partial-import exemption (gated on result.success == zero errors), reason write-back (sidecar row is FK-linked and carries the reason), and "bulk-exempting manual entries" (consistent with the existing per-entry NoDocRequiredToggle). No DB migration — reuses the existing journal_entry_no_doc_required table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): centralize bulk-missing date/series validation in Zod Move the ISO-date and verifikationsserie format checks into the Zod schema so malformed input is rejected with a clean 400 instead of being silently nulled (or, for a shaped-but-invalid date, throwing a 500 via fetchAllRows). The date refinement rejects values like 9999-99-99 / 2026-02-30 that a bare /^\d{4}-\d{2}-\d{2}$/ regex lets through. Addresses the PR #698 reviewer nit on split schema-vs-runtime validation. +2 route tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { z } from 'zod'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { validateBody } from '@/lib/api/validate'
|
|
import { markEntriesNoDocRequired } from '@/lib/bookkeeping/no-doc-required'
|
|
import { NEEDS_DOC_SOURCE_TYPES } from '@/lib/worklist/categories'
|
|
|
|
const BatchNoDocSchema = z.object({
|
|
journal_entry_ids: z.array(z.string().uuid()).min(1).max(500),
|
|
reason: z.string().trim().max(200).nullable().optional(),
|
|
})
|
|
|
|
/**
|
|
* Batch-mark posted verifikationer as "Inget underlag krävs". Lets the user
|
|
* clear many entries (e.g. historical SIE imports) out of "Att hantera: saknade
|
|
* underlag" in one action instead of toggling each one.
|
|
*
|
|
* The exemption is shared bookkeeping metadata (company-scoped, like mapping
|
|
* rules) — the audit_log trigger records the actor.
|
|
*/
|
|
export const POST = withRouteContext(
|
|
'journal_entry.batch_no_document_required',
|
|
async (request, { supabase, companyId, user }) => {
|
|
const validation = await validateBody(request, BatchNoDocSchema)
|
|
if (!validation.success) return validation.response
|
|
|
|
const { journal_entry_ids, reason } = validation.data
|
|
|
|
// Defense in depth: only exempt posted entries that belong to this company.
|
|
// Validate ownership in chunks so the PostgREST `in()` URL stays bounded.
|
|
const ownedIds: string[] = []
|
|
for (let i = 0; i < journal_entry_ids.length; i += 200) {
|
|
const chunk = journal_entry_ids.slice(i, i + 200)
|
|
const { data, error } = await supabase
|
|
.from('journal_entries')
|
|
.select('id')
|
|
.eq('company_id', companyId)
|
|
.eq('status', 'posted')
|
|
.in('source_type', [...NEEDS_DOC_SOURCE_TYPES])
|
|
.in('id', chunk)
|
|
|
|
if (error) {
|
|
return NextResponse.json({ error: error.message }, { status: 400 })
|
|
}
|
|
ownedIds.push(...(data ?? []).map((r) => r.id))
|
|
}
|
|
|
|
if (ownedIds.length === 0) {
|
|
return NextResponse.json({ data: { exempted: 0 } })
|
|
}
|
|
|
|
const exempted = await markEntriesNoDocRequired(
|
|
supabase,
|
|
companyId,
|
|
user.id,
|
|
ownedIds,
|
|
reason ?? null,
|
|
)
|
|
|
|
return NextResponse.json({ data: { exempted } })
|
|
},
|
|
{ requireWrite: true },
|
|
)
|