c84f951a5c2f51279277a506754b2bd94de28249
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
abe9ac9d8c |
Fix/attributes config (#926)
* fix(git): pin LF on generated extension registry and vitest snapshots setup:extensions and vitest write these files with LF; with core.autocrlf=true git expects CRLF and flags them as phantom modifications on every dev/build run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): enforce MFA on mcp-oauth consent and gate viewer storno route mcp-oauth/authorize renders an HTML consent page and issues 303 redirects that withRouteContext cannot express, so it kept raw getUser() and thereby skipped the AAL2 gate: a password-only (AAL1) session could approve consent that mints a long-lived, MFA-bypassing API key. Add a route-local requireAal2() step-up on GET and POST; AAL1 sessions redirect to /mfa/verify, BankID users are exempt. Separately, POST /api/reports/vat-declaration/rc-basis-gaps/fix calls correctEntry() (storno of a posted entry) but lacked requireWrite, so viewer-role members could trigger it. Add { requireWrite: true }. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route transactions endpoints through withRouteContext Migrate the transactions routes off hand-rolled supabase.auth.getUser() onto the MFA-enforcing withRouteContext wrapper; add requireWrite on mutating handlers (book, uncategorize, attach-document, ignore, batch-match, create-from-document). Behavior and response shapes preserved; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route SIE import and bank reconciliation through withRouteContext Migrate import/sie and reconciliation/bank routes onto the MFA-enforcing wrapper; requireWrite on mutations (import execute, create-accounts, mappings write verbs, link/unlink/run/mark-opening-balance). Reads (status, unmatched-entries) stay ungated. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route salary endpoints through withRouteContext Migrate salary employees and runs routes (plus ku, payroll-config, tax-tables) onto the MFA-enforcing wrapper; requireWrite on mutations. Personnummer masking/encryption untouched; file downloads (AGI XML, payslip PDF, payment files) keep their headers. Two payment-file GETs retain requireWrite because they stamp *_file_generated_at and previously gated viewers. Tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route report endpoints through withRouteContext Migrate the read-only report routes (trial balance, balansrapport, resultatrapport, income statement, ledgers, KPI, VAT declaration, salary journal, monthly breakdown, journal register, continuity check, full archive, etc.) onto the MFA-enforcing wrapper. All read-only, no requireWrite. JSON/XLSX/PDF/ZIP response bodies and headers preserved byte-for-byte; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route invoices, skatteverket, agent and extension endpoints through withRouteContext Migrate invoices, supplier-invoices, skatteverket tax-payments, and dynamic extension routes onto the MFA-enforcing wrapper with requireWrite on mutations. The two NDJSON streaming agent routes (invoke, onboarding/stream) use requireAuth() directly (the wrapper can't wrap a streaming response) so MFA is still enforced. skatteverket payment-file GET keeps requireWrite (stamps a generated-at field). Response shapes and file headers preserved; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route documents, events, team and account endpoints through withRouteContext Migrate documents, events, kpi/preferences, vat/validate, support/contact onto the MFA-enforcing wrapper with requireWrite on mutations. account/password, team/accept and team/members use requireAuth() directly (user-level or pre-membership flows with no active company context) so MFA is still enforced. events keeps its dual API-key-or-session auth. Document retention guard untouched; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route settings and pending-operations endpoints through withRouteContext Migrate settings (api-keys, oauth-clients, booking-templates, counterparty-templates, logo, company settings) and pending-operations (commit, bulk-commit, reject, edit-before-approve) onto the MFA-enforcing wrapper with requireWrite on mutations. Credential-guarding routes keep their per-user ownership filters. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(guards): ratchet raw-route-auth baseline 119->1 after A1 migration Lock in the withRouteContext migration so the count cannot regress. The single remaining entry, mcp-oauth/authorize, is a documented exception (HTML consent + redirects, MFA enforced via route-local step-up). Record the campaign and requireWrite decisions in DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vat): add eSKD momsdeklaration file export for "Deklarera via fil" Generate the Skatteverket eSKDUpload v6.0 XML file so users can file VAT by upload instead of typing every ruta into the form. Extract buildFiledAmounts() as the shared whole-krona source of truth (öre truncated per SFL 22 kap 1 §) so the XML file and the manual-filing PDF can never disagree. Adds the /eskd API route, an XML option in the report export menu, and the upload button on the manual-filing card. Strings in sv + en. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(vat): add 'vat_settlement' source type and update related components * fix(booking): adjust search input layout and enable autofocus * fix(vat): support 12-digit org numbers and adjust emission order for eSKD file * fix(migration): add 'vat_settlement' to journal_entries.source_type CHECK --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ec27228a8e |
style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0521c385d2 |
feat(transactions): underlag status badges + attach dialog; auto-expire stale pending ops (#712)
* feat(transactions): per-row underlag status + attach-document dialog
- New "Matcha mot underlag" dialog on /transactions (inbox pick or fresh
upload), the tx→doc mirror of the Documents view's matcher
- Per-row Underlag/Underlag saknas badges on booked history rows, driven
by computeJeUnderlagStatus — same posted-only, exemption-aware scope as
the worklist count so badge and count never disagree
- attach-document route + commit dispatcher now propagate the doc onto
the verifikation when the tx is already booked (BFL 5 kap 6 §), with a
409 guard for docs consumed by a different verifikation, idempotent
re-attach (no same-value rewrite under period lock), and an honest 409
when the period-lock trigger blocks the propagation
- Booking-dialog doc links also pin the doc to the transaction row
(first linked doc wins) via the link route's new transaction_id param
messages/{sv,en}.json also carries the strings for the pending-ops
expiry UI that lands in the next commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pending-operations): auto-expire stale staged operations after 30 days
- New daily cron (02:30 UTC, vercel.json + both docker crontabs) flips
>30-day-old pending ops to rejected with the dispatcher's
{ auto_rejected: true, reason: 'expired' } result_data shape — rows are
never deleted, the table is the audit trail
- /pending renders an "Utgick automatiskt" badge + detail line for these,
orders terminal tabs by resolved_at so a fresh expiry sweep isn't
buried, and adds a first-time-reviewer explainer
- Origin labels spell out where a proposal came from (AI chat, MCP key,
API, cron) instead of the raw actor_label
- agent_chat actor type added to PendingOperationActorType/AuditLogEntry
(DB CHECK already widened in 20260519090000) and to the agent filter
- ApprovalCard notes that ignoring a proposal is safe
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(mcp): surface the client telemetry marker in connect instructions
Tag the connector URLs shown in ApiKeysPanel, the connect-claude doc and
the gnubok-mcp README with ?client=<surface> (claude-connector /
claude-code) and GNUBOK_CLIENT=claude-desktop for the npm bridge.
Telemetry-only — the server already reads the param/header; this just
lets us measure which Claude surface connected.
The claude mcp add copy blocks quote the URL: an unquoted ? in the query
string trips zsh globbing ("no matches found").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: fix stale-closure badge flip + zod-validate link route body (PR #712)
- handleDocumentAttached read journal_entry_id off the render-time
transactions snapshot; if the list changed while the attach dialog was
open the optimistic badge flip was silently skipped. Read it off the
dialog's own subject (attachDocTx) instead.
- POST /api/documents/[id]/link now validates the body against the new
LinkDocumentSchema (uuid-strict, all four fields) instead of a bare
presence check on journal_entry_id — same canonical VALIDATION_ERROR
envelope. Test fixtures switched to real UUIDs accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
2879f6ed17 |
fix(inbox): sync matched_transaction_id on MCP-staged document attach (#549)
* fix(inbox): sync matched_transaction_id on MCP-staged document attach The REST attach route (app/api/transactions/[id]/attach-document) already updates invoice_inbox_items.matched_transaction_id so the inbox list shows the "Kopplad till transaktion" indicator. The MCP-staged path through commitAttachDocumentToTransaction did not, leaving inbox rows looking unprocessed after a /pending approval. Mirror the REST behaviour with a best-effort UPDATE (idempotent, gated on both FK columns being NULL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): inspect supabase error on best-effort inbox link The Supabase client resolves with { error } rather than rejecting on RLS/DB failures, so the previous try/catch wrapping the inbox UPDATE never fired for the documented failure modes — the console.error was dead code. Switch to destructured { error } + if-block, matching the pattern used elsewhere in commit.ts. Applied to both the new MCP-staged path (lib/pending-operations/commit.ts) and the pre-existing REST route (app/api/transactions/[id]/attach-document/ route.ts) it was mirrored from. Both tests now assert console.error fires with the expected payload, so the swallow-and-log path is exercised for real instead of passing for the wrong reason. Addresses Greptile P1 + P2 on PR #549. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
17c67fece0 |
Inbox UX overhaul + cross-currency supplier-invoice fixes (#444)
* feat(kpi): expense mix and top suppliers charts Replace the single monthly-trend chart with two additional compact visuals on /kpi: expense composition donut (BAS class 4-7) and top suppliers bar (supplier_invoices sum_sek over the fiscal period). KPIReport gains expenseComposition and topSuppliers fields, computed from the trial balance and supplier_invoices rows already fetched in the API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(nav): swap Deadlines sidebar slot for Dokumentinkorg Sidebar main-menu slot now points to the invoice-inbox extension. The /deadlines page stays accessible via dashboard widgets and direct links — only the prominent nav entry changes. Most users open gnubok to act on incoming documents, not to read tax deadlines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): cross-currency totals, FX residual, review SEK display Five fixes around foreign-currency supplier invoices: - Form layout: move Valuta / Växelkurs / Reverse charge from collapsed "Övrigt" into a visible row above the line-item table. Auto-fetch the Riksbanken rate when switching to a non-SEK currency; never clobber a user-typed rate; clear it when switching back to SEK. - Form submit: reset() the form on successful submit so the useUnsavedChanges hook detaches its beforeunload listener before the router.push, killing the "Are you sure you want to leave?" prompt that fired during Turbopack-mediated navigations. - BankTransactionPicker: drop the strict currency filter that hid every SEK transaction when the invoice was in EUR/USD. Cross-currency rows fall to the bottom with an "Annan valuta" hint instead of producing a meaningless numeric diff. - match-supplier-invoice route: when the bank transaction currency differs from the invoice currency, compute the FX diff against the AP-booked SEK and pass it to createSupplierInvoicePaymentEntry so 7960/3960 catches the residual instead of leaving a permanent stub on 2440. Fix also covers the "EUR transaction paying a SEK invoice" case that the first iteration missed. - Review dialog: buildJournalPreview now multiplies amounts by the exchange rate so the "Verifikation som bokförs" table shows the actual SEK numbers that hit the DB, not the EUR magnitudes labelled with no unit. Header gains an "(i SEK)" hint when foreign currency. Test coverage for the FX residual path covers SEK-SEK (no diff), SEK-into-EUR-invoice (loss), SEK-into-EUR-invoice (gain), foreign-tx- into-SEK-invoice, and the no-rate fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(inbox): rate limits, multi-file UX, onboarding, retry, supplier autolink Big workspace pass on /e/general/invoice-inbox. Highlights: Backend - New table inbox_rate_counters + RPC check_and_increment_inbox_quota. Postgres-backed (no Upstash dep) per-company limit: 30/min, 500/day. Applied at /upload, /inbound, and /items/:id/retry-extraction. - POST /items/:id/retry-extraction — re-runs the deterministic extractor on a stored document when the previous attempt errored. - POST /items/:id/match-supplier — links a freshly-created supplier back to the inbox item so the next action prefills correctly. - POST /api/transactions/create-from-document — creates an uncategorized manual transaction from an inbox item for the "I have a receipt, no bank transaction" case. The user categorizes through the normal flow. - /inbound caps email at 20 attachments/email; truncated count goes to processing_history as AttachmentsTruncated. Rate-limit drops emit RateLimitedDropped and return 200 so Resend doesn't retry. - attach-document side effect: when the document came from an inbox item, the inbox row's matched_transaction_id is updated so the UI can flip it to "Kopplad till transaktion" without a round-trip. New migration: re-introduces matched_transaction_id on invoice_inbox_items as a plain FK (the AI metadata that the previous migration stripped doesn't come back). Workspace UI - Onboarding card replaces the thin empty-state with a 3-step checkmark guide (Aktivera adress → Ladda upp → Matcha eller bokför). Auto-hides when all three steps are done; localStorage-backed dismiss. Beta badge + link to gnubok.se/priser. - Responsive layout: 3-pane at lg, 2-pane at md, master-detail toggle on phone (list xor detail with a back button). - Filter pills (Alla / Behöver åtgärd / Bearbetade / Fel) + search input above the list — client-side over the existing items list. - Multi-file upload queue with "Laddar X av N…" progress counter on the button. Sequential to avoid hammering pdfjs. Selection stays put during a batch (only single-file drops auto-jump the detail pane). - Bulk select + delete with sticky action bar. Items linked to a supplier invoice are skipped with a count toast. - Retry button in the FieldsRail error branch. - "Skapa transaktion från underlag" CTA in the match dialog when no unmatched bank transactions exist. Prefills date/amount/description from the extracted data; user picks the sign. - "Skapa leverantör" inline CTA when the extractor caught a supplier name with no match against existing suppliers. POSTs /api/suppliers with the extracted fields, then auto-links via /items/:id/match-supplier. - Matched-state CTA renamed to "Bokför transaktionen" with link to /transactions?highlight=<id> so the categorize panel auto-opens. Tests - lib/rate-limits/__tests__/inbox.test.ts — RPC wrapper happy/error/scope - app/api/transactions/create-from-document/__tests__/route.test.ts — auth, validation, 404/409/200/500, inbox-link failure tolerated - extensions/general/invoice-inbox/__tests__/retry-extraction.test.ts — auth, rate limit, 404, 409, 400 no-doc, success, extraction failure - attach-document tests extend coverage to the new inbox-link side effect (both success and best-effort failure paths) - inbound-webhook test mocks the rate-limit module so the queued-mock sequence in each existing test doesn't have to know about it CLAUDE.md gains a row for lib/rate-limits/ so the new helper is discoverable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(transactions): paperclip indicator and highlight-row param Close the feedback loop after a user attaches a receipt to a transaction from the inbox: the row in /transactions now shows a paperclip icon when transaction.document_id is set, with a click handler that fetches a signed download URL and opens the document in a new tab. Works for both uncategorized and history views. When the inbox sends a user to /transactions?highlight=<id>, the page now scrolls that row into view and auto-opens the categorize panel if the transaction is still uncategorized. Behind a double-rAF so the row DOM exists when scrollIntoView fires. QuickReviewDialog no longer prompts to upload underlag when the transaction already has a doc attached (which it does after the inbox match flow). Shows "Underlag bifogat — Visa" instead, opening the existing doc in a new tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(pr-444): address review feedback (Greptile + compliance bots) Migration rules - New migration 20260512092423: adds updated_at trigger on inbox_rate_counters (CLAUDE.md rule 2) and explicit USING (false) RLS policies for the four DML verbs to make the SECURITY DEFINER-only intent explicit (rule 1). - New pg-real test inbox-rate-limit.pg.test.ts covering happy path, minute-cap rejection, day-cap rejection, per-company isolation, and the updated_at trigger firing. CLAUDE.md mandates *.pg.test.ts for every new RPC because mocks pass on broken PL/pgSQL. Bugs - Stale exchange rate on currency switch (Greptile P1) — userTouchedRateRef was scoped per session, not per currency. Switching EUR (with a hand-edited rate) → USD kept the EUR rate. Now tracks the last fetched currency in a ref and resets the touched flag on currency change while still honoring manual edits within a single currency. - topSuppliersResult.error silently swallowed (Greptile P2) — failed queries used to render an empty chart matching the no-data state. Logged now. - Currency from extracted_data not validated (GDPR Art.25(2), OWASP V4.5, Swedish compliance bot) — extracted PDF currency was inserted into transactions.currency without sanitisation. Allowlisted against the six supported ISO 4217 codes; coerce to SEK otherwise. - Idempotency gap on create-from-document (OWASP V2.3) — two concurrent POSTs with the same inbox_item_id could each pass the matched_transaction_id IS NULL read and insert duplicate transactions. UPDATE now includes .is('matched_transaction_id', null) as an optimistic-lock release and returns 409 with an orphan-transaction rollback when the predicate doesn't match. - FX residual on cash-method match path (Swedish compliance bot) — createSupplierInvoiceCashEntry has no exchange_rate_difference path, so a cross-currency match would silently leave a 1930 reconciliation gap. Added a guard that returns MATCH_SI_CASH_FX_UNSUPPORTED (400) before the JE is created. Users on cash method can switch to accrual or book the FX diff manually. Design system - gap-y-1.5 / gap-1.5 in KPIExpenseMixChart — replaced with gap-y-2 / gap-2 (CLAUDE.md design tokens; 2.5/1.5/5/hardcoded pixels are forbidden spacing values). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(migrations): rename to match applied versions The mcp__plugin_supabase_supabase__apply_migration tool stamps its own timestamp when it applies a migration to the live project, so the version recorded in supabase_migrations.schema_migrations differs from my local generation-time filenames. Renaming the local files so a production CD run sees the migrations as already-applied (matching versions) instead of trying to re-apply them — which would fail for the trigger/RLS migration (CREATE TRIGGER and CREATE POLICY don't support IF NOT EXISTS). Follows the pattern from d854efcd ("chore(migration): rename to match applied version"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(create-from-document): scope orphan rollback DELETE by company_id Defence in depth on the inbox-link race rollback. newTx.id is a fresh UUID from a company-scoped insert two statements above, so the existing single-key DELETE is already safe, but adding .eq('company_id', companyId) makes the cross-company invariant explicit on every write — addresses the OWASP ASVS V2.3 finding from the compliance swarm on PR #444. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(nav): mark Dokumentinkorg with Beta badge Same signal we use for Löner and Anställda — the inbox flow (AI extraction, supplier autolink, manual transaction creation) is in end-to-end customer testing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
94f15b9c6c |
feat(invoice-inbox): pin documents to bank transactions + MCP tools (#397)
* feat(invoice-inbox): pin documents to bank transactions + MCP tools Adds a first-class flow for attaching unmatched inbox documents to bank transactions, separate from the existing supplier-invoice convert path: - new transactions.document_id FK → document_attachments (ON DELETE SET NULL) - POST/DELETE /api/transactions/[id]/attach-document - categorize route propagates the link to journal_entry_id on commit - three new MCP tools: gnubok_list_unmatched_documents, gnubok_get_document_content (5-min signed URL), gnubok_attach_document_to_transaction (staged via pending_operations) - InvoiceInboxWorkspace gains a "Koppla till transaktion" picker dialog ranked by amount-match, plus a "Bilaga" badge in SwipeCategorizationView - regex extraction unchanged; supplier-invoice convert flow unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): address PR #397 review findings - categorize: destructure { error } from the document-link update so Supabase-level failures are logged instead of silently dropped (BFL 5 kap 6 § receipt-on-verifikation contract). - list_unmatched_documents: emit next_cursor whenever the inbox query may have more rows, not only when the post-filter slice was full; switch to composite (created_at, id) cursor to avoid same-second collisions. - DELETE /attach-document: return 404 when the tx isn't in the company; return 409 when the linked document already has journal_entry_id set (räkenskapsinformation immutability). - risk tier: attach_document_to_transaction medium (was low) — link becomes part of verifikation underlag once categorize propagates it. - pg-real test: stop reusing $2 across uuid + text-concat contexts (Postgres couldn't deduce the parameter type). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(migrations): break duplicate version 20260505120000 (Supabase Preview) Two migrations on main share filename version 20260505120000: - 20260505120000_api_keys_refresh_token.sql (PR #392) - 20260505120000_drop_agent_auto_commit.sql (PR #394) The schema_migrations primary key is (version), so any fresh DB doing `supabase db push` over both files conflicts on the second insert. This is why every PR with a migration since #394 has had Supabase Preview either fail or skip. Renaming _drop_agent_auto_commit to 20260505190027 — that matches the timestamp recorded in prod schema_migrations from when apply_migration was called for it, so future `db push` against prod sees the file as already- applied (no re-run). The migration body is fully idempotent (IF EXISTS on every drop) so a re-run would be a no-op anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): address PR #397 round-2 compliance review Two BFL gaps the compliance bot flagged on the round-1 fix commit: 1. commitAttachDocumentToTransaction silently broke verifikation→underlag if the transaction was categorized between staging and approval. Now reads transactions.journal_entry_id at commit time and, if non-null, also writes document_attachments.journal_entry_id in the same commit so BFL 5 kap 6 § is satisfied regardless of order. 2. Application-layer DELETE check was racy (SELECT then UPDATE) and the FK ON DELETE SET NULL path could null transactions.document_id even for a document that is räkenskapsinformation. Added a BEFORE UPDATE OF document_id trigger on transactions that raises check_violation when the previously-attached document has document_attachments.journal_entry_id set. The app-layer guard stays for friendly Swedish messaging; the trigger is the DB-level safety net. pg-real test extended to cover both directions of the trigger (block detach + block swap) and the happy-path detach when there's no JE link yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): address PR #397 round-3 compliance review Four findings from the round-2 update of the compliance bot. The first three are genuine compliance gaps; the fourth (preview metadata distinguishing pre- vs post-categorization overwrites) is a UX nicety left for follow-up. 1. transactions.document_id FK switched from ON DELETE SET NULL to RESTRICT (migration 20260506100000). Removes the "trigger ordering" concern: a doc that's pinned to any tx now cannot be deleted at all without explicit detach first. Belt-and-braces with block_document_deletion. 2. commitAttachDocumentToTransaction now does: - pre-check that mirrors the DELETE route's 409 when the existing pinned doc is räkenskapsinformation, so the same Swedish message is returned in both paths; - UPDATE…RETURNING journal_entry_id so the propagation decision uses the post-update state, closing the read-then-write race with concurrent categorize. Either ordering of attach-then-categorize or categorize-then-attach now lands at the same correct final state. 3. Both DELETE /attach-document and the MCP commit path catch the trigger's check_violation (SQLSTATE 23514) and translate to 409 with the Swedish underlag message. The trigger remains the DB-level safety net; the app layer is responsible only for friendly UX. pg-real test rewritten for ON DELETE RESTRICT (blocks deletion of pinned doc; detach-then-delete works). Unit coverage added for commitAttach: 404, two distinct 409 paths (pre-check + trigger-translation), happy-path uncategorized, and propagation when tx was categorized between staging and commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): address PR #397 round-4 compliance review Three of five round-3 findings actioned: 1. commitAttachDocumentToTransaction: surface propagation failure rather than logging-and-continuing. If document_attachments.journal_entry_id can't be set after the transaction has been categorized, the op fails (status 500) with a Swedish message instructing retry. Retry is idempotent — same document_id on the tx, same propagate target. 2. Replace check_violation (23514) matching with a stable "BFL_DOCUMENT_IMMUTABILITY:" message prefix. The trigger now uses default P0001 + tagged message; both the route handler and the executor match on the prefix instead of the generic SQLSTATE. Future unrelated CHECK constraints on transactions can no longer accidentally surface as the räkenskapsinformation message. 3. gnubok_list_unmatched_documents now returns invoice currency alongside amount so an agent can FX-normalise before comparing to transactions.amount. Description updated to make the requirement explicit. Mirrored in the UI: AttachToTransactionDialog ranks same-currency rows by amount distance and pushes cross-currency rows to the bottom of the list. Skipped: - Two-migration window for FK action change is acknowledged as resolved by the bot; deploy-atomicity is an ops concern, not code. - Period-lock check in attach/detach: realistic compliance concern is already covered by the existing immutability trigger (post-categorize) and by the engine's period-lock enforcement (categorize itself). A dedicated period check on pre-categorize attach would only guard against pinning a doc to a tx in a closed period — defensible defense-in-depth, but no active BFL violation. Left for a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): address PR #397 round-5 compliance review Three of four findings actioned. The fourth (block_document_deletion verification) is already covered by 20240101000017_enforcement_triggers.sql which raises when document_attachments.journal_entry_id IS NOT NULL on a posted/reversed entry — confirmed via grep, no code change needed. 1. categorize/route.ts: propagation no longer fires-and-forgets. If document_attachments.journal_entry_id can't be set after the JE has been committed, the response now carries a document_link_warning field with a Swedish retry message. The JE is already committed so we can't roll back, but the client can no longer mistake a partial attach for a clean categorize. 2. Rättelse audit trail (BFL 5 kap 5 §): both the REST POST handler and the MCP commit executor now append a TransactionDocumentReplaced event to processing_history whenever a non-null document_id is overwritten, with previous_document_id and new_document_id in the payload. Best-effort — logging failure must not roll back the (compliant) attach. The previous doc id is also returned in the response so callers see what was displaced. 3. MCP staging preview now exposes the existing doc's identity (existing_document_id, existing_document_file_name) plus an explicit existing_document_is_rakenskapsinformation flag, so a human approver sees "replaces X.pdf with Y.pdf" rather than just a will_overwrite_existing boolean. Mirrors BFL 5 kap 5 § informed-rättelse intent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): close trigger SELECT race (PR #397 round-6) The enforce_transactions_document_immutability trigger SELECTed document_attachments.journal_entry_id without a row lock. A concurrent UPDATE setting journal_entry_id on that row could commit between the trigger's SELECT and its RAISE, letting a detach slip through against a document that just became räkenskapsinformation. Add FOR SHARE to the SELECT inside the trigger. A concurrent journal_entry_id write blocks on our share lock until our transaction commits, so either we observe the propagation and raise, or we run first and the propagation observes our committed detach (which is fine because journal_entry_id was still null at that point). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): bidirectional immutability + richer staging preview (PR #397 round-7) Two of six round-6 findings actioned. The other four are recurring architectural recommendations (atomic audit-log writes, background reconciliation jobs, migration consolidation, anti-join via materialized view) that are properly scoped as follow-up work. 1. document_attachments side of the immutability link (BFL 5 kap 6 § works in both directions). New trigger enforce_document_journal_entry_immutability blocks UPDATE OF journal_entry_id when going from non-null to NULL or to a different uuid. The original null→uuid path (initial propagation in the categorize / commitAttach flows) still works. Migration 20260506130000. 2. gnubok_attach_document_to_transaction staging preview now joins on invoice_inbox_items.extracted_data and surfaces vendor/amount/currency/ invoice_date alongside the existing doc filename/mime metadata. Gives the human approver the same hints the agent saw before choosing the attachment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |