15af0cd7eadcecafd6d17618d04bf67b4f736fd2
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2c59c3633f |
feat(invoices): cross-currency settlement + payment-status card (#615)
* feat(invoices): cross-currency settlement + payment-status card Two changes both surfaced by user feedback after PR #614: # 1. Invoice detail page: Betalningsstatus card The customer-invoice detail page now shows paid_amount + remaining_amount + the individual payment events whenever an invoice is partially_paid or paid (was previously only a single "Paid" line on fully-paid invoices, and nothing at all on partially_paid). Mirrors the supplier-invoice page's payment section. Each payment row links to its verifikat. # 2. Cross-currency match-invoice settlement Replaces the PR #614 round-9 block (MATCH_INVOICE_CURRENCY_MISMATCH) with proper FX-aware settlement. Flow: 1. Preview route detects tx.currency !== invoice.currency, fetches the Riksbanken spot rate for invoice.currency on tx.date (ML 8 kap 21–23§), and returns fx_conversion = { rate, rate_date, paid_in_invoice_currency }. When the lookup fails it returns fx_conversion.error = 'rate_unavailable'. 2. InvoiceMatchDialog renders a new Valutaomräkning card showing the rate + invoice-currency-equivalent + projected post-payment state + a one- line kursvinst/kursförlust note. When the lookup failed it swaps in a manual-rate input the user fills from their bank statement; the Confirm button blocks until a positive rate is supplied. 3. POST route does the same lookup (or accepts manual_exchange_rate from the request body), then: - paidInInvoiceCurrency = bankSek / rate (4dp precision) - invoice.paid_amount/remaining_amount accumulate in invoice currency - invoice_payments row records amount + currency = invoice.currency, exchange_rate = the rate actually used (not invoice.exchange_rate) - buildInvoicePaymentClearingLines gets paidInInvoiceCurrency so it credits 1510 by that × invoice.exchange_rate (booking rate) and posts the FX-diff line on 3960 (gain) or 7960 (loss) 4. buildInvoicePaymentClearingLines gains an optional fourth param. When supplied: proportional FX-aware AR-leg + balanced FX-diff. When omitted: pre-existing fallback (full-clear gets FX, partials defer). The change fixes the invoice.paid_amount accumulator bug that PR #614 round-9 worked around by blocking the case entirely. Now SEK→USD settlements actually work, with the verifikat balanced to the öre and the GL+sub-ledger in sync per BFL 5 kap 4–5§. Tests: - 3 new helper tests (paidInInvoiceCurrency happy path + edge cases) - 3 new route tests (Riksbanken happy path, lookup failure, manual rate) - All 4321 tests pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoices): align cross-currency match preview with commit + review cleanups Addresses PR #615 review feedback. Preview/commit divergence (Greptile P1): preview/route.ts computed paidAmount / isFullyPaid / useCashEntry from the raw SEK transaction.amount before the FX conversion ran. A 1 000 SEK payment against a 140 USD invoice made max(0, 140 − 1000) = 0 → is_fully_paid=true, so a cash-method unbooked invoice previewed a cash entry (Dr 1930 / Cr 30xx) while the POST handler — which converts first — commits the clearing entry (Dr 1930 / Cr 1510). The user approved one verifikat and a different one was booked. Move the FX lookup above the paid/remaining math so paidAmount derives from the invoice-currency conversion, mirroring the POST handler. Rate-unavailable stays non-fully-paid so the cash shape is never previewed on a guess. Add a preview-route regression test (cross-currency → clearing + not fully paid; same-currency cash path still previews the cash entry). Cleanups: - Bound manual_exchange_rate with .max(100000) as a sanity ceiling against pasted/garbage input corrupting the FX-diff posting (swarm V2.3). - Remove the invisible disabled placeholder retry button and its unused fx_manual_rate_retry i18n keys (Greptile P2). - Remove the now-unreachable MATCH_INVOICE_CURRENCY_MISMATCH error code (Greptile P2 dead code; confirmed zero references). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): record FX rate provenance + cover kursförlust path Follow-up to the PR #615 review (compliance swarm V16 / SOC 2 CC6.1 / GDPR Art.5(1)(f); Swedish accounting review). A manually-supplied cross-currency rate is a user-controlled money-path override of the ML 8 kap 21–23§ obligation and was indistinguishable from an automatic Riksbanken lookup in the audit trail. Tag the resolved rate with source: 'manual' | 'riksbanken' and: - write a "Manuell valutakurs <rate> <ccy>/SEK (betalningsdatum …)" note onto the existing invoice_payments.notes column when manual (BFL 5 kap 6–7§ — the verifikation must reflect the actual affärshändelse); - record rate_source + exchange_rate in payment_match_log.new_state. No schema change — both are existing columns/JSON. Tests: - cover the kursförlust (7960 Dr) branch of the cross-currency paidInInvoiceCurrency path — previously only the 3960 gain was asserted; - assert rate_source provenance ('manual' and 'riksbanken') reaches the match-log new_state on both FX paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
13be0c569a |
feat(mcp): expose multi-tx RPCs (match_batch_allocate + bulk_book_transactions) (#614)
* feat(bulk-book): manual booking mode + document inheritance Two pieces of user feedback from PR #606: 1. "How come it is only mallar? Is it not possible to have manuell bokfoering?" - BulkBookDialog was template-only. Added a Tabs primitive with Mall / Manuell tabs. Manual tab pre-fills lines from the selected txs (one line per tx on 1930 + counterparty placeholder on 3001/5800 by direction), then the user edits Konto / Debet / Kredit / Beskrivning. Live balance + bank-leg checks drive the confirm button - same invariants the RPC enforces server-side. 2. "Documents attached does not follow into the bookkeeping. And if there are two different documents attached, none of them follow." The bulk_book_transactions RPC now propagates each tx's document onto the target verifikat (new in Branch B, existing in Branch A) as verifikationsunderlag. Per BFL 5 kap 6§ + BFNAR 2013:2 kap 4 a verifikat may have multiple underlag; every receipt that justified a tx is now retention-protected on the combined entry. The dialog shows a small count chip ("N bilagor foeljer med") so the user sees what will inherit. Also dropped p_user_id from the RPC signature (round-3 hardening pattern applied consistently across all multi-tx RPCs after PR #607). Caller resolves from auth.uid() inside the function. Schema: BulkBookSchema is now a 3-way XOR (existing_journal_entry_id | template_id+mode | manual_lines), with manual_lines validated as accountNumber + nonNegativeAmount per line. pg-real tests: - doc inheritance into a new combined verifikat (mixed: 2 of 3 txs have docs - docs_linked should be 2, not 3) - doc inheritance into an existing posted verifikat (link branch) - manual lines path (no template expansion artifacts in the resulting JE - just the 2 user lines) - unbalanced manual lines still rejected by BULK_BOOK_UNBALANCED Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 review - pg-real signature, account allowlist, account-number validity Three review findings on PR #610: 1. pg-real failure: 2 link-existing tests still used 5-arg SELECT bulk_book_transactions($1::uuid[], $2, $3, $4, $5) after the userId removal. My earlier replace_all caught only the patterns that had ::jsonb on $3; the link-existing tests pass null for new_entry and used a bare $3 so they slipped through. (Greptile P1) 2. Manual lines bypassed chart_of_accounts validation. A typo or adversarial caller could post to a BAS account that doesn't exist in this company's chart, corrupting the hauptbok and breaking SIE export. Both compliance-swarm (OWASP V2.3) and swedish-compliance flagged this. Added a single-roundtrip allowlist check in the route: query chart_of_accounts for distinct account_numbers in manual_lines and reject with BULK_BOOK_INVALID_ACCOUNT if any are missing or inactive. 3. UI canConfirm guard missed invalid account numbers. Account input allows 1-3 digits and JS string comparison '193' >= '1900' is false, so a 3-digit entry escapes bankLineNet, the bank match could pass via other lines, and the server returned 400 only after submit. Added previewLines.every(l => /^\d{4}$/.test(l.account_number)) to canConfirm so the Confirm button stays disabled inline. (Greptile P2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 round 2 - RPC chart-of-accounts, doc tenant isolation, GRANTs Seven compliance findings from the round-1 bot reviews: Migration (20260602121000_bulk_book_round2_fixes.sql): - RPC chart-of-accounts allowlist (defense-in-depth): every line in p_new_entry.lines is now verified to be an active BAS account for p_company_id. Closes the gap where the template branch and direct DB callers (psql, future MCP) bypassed the route's manual-branch check. Returns BULK_BOOK_INVALID_ACCOUNT with the offending list. (OWASP V8.2.1 + SOC 2 CC6.3) - Document inheritance CTE: added "AND d.company_id = p_company_id" to the UPDATE join so the tenant isolation is enforced on both sides (tx + doc), not just the tx side. Four bots converged on this finding (V1.2.5, A.8.2, CC6.6, swedish-compliance). - Bank-leg range check: "length(account_number) = 4 AND account_number BETWEEN '1900' AND '1999'" replaces the bare lexicographic comparison. Lexicographic-on-4-digit is safe today; the length guard is defense-in-depth against schema drift. (swedish-compliance) - Explicit role grants: REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO authenticated on both bulk_book_transactions and match_batch_allocate. (SOC 2 CC6.1) UI (BulkBookDialog): - Manual-mode prefill no longer suggests a hardcoded 3001/5800 counterpart. Reason (swedish-compliance): a user accepting the prefill could submit a verifikat with no VAT line (26xx), under-reporting utgaaende moms. The bank side stays pre-filled (unambiguous); the counterpart row scaffolds blank for the user to choose. Schema (BulkBookSchema): - manual_lines.debit_amount + credit_amount bounded at 99,999,999 SEK per line. Catches typos before the RPC. (compliance-swarm V4.5) i18n: - docs_inherit_hint terminology: "bilaga" -> "verifikationsunderlag" and an explicit "sparas i 7 ar enligt BFL 7 kap" reminder. swedish-compliance flagged that "bilaga" risks users treating the files as deletable attachments rather than retention-bound raekenskapsinformation. Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): seed chart_of_accounts in bulk-book pg-real seedTenant The round-2 RPC fix added a chart_of_accounts allowlist check inside bulk_book_transactions, but the test fixtures don't seed COA — so every existing test that submits lines (1930, 3001, 2611, etc.) now returns BULK_BOOK_INVALID_ACCOUNT instead of the expected error code. Seed the 8 accounts the suite actually uses directly in seedTenant (cheaper than calling seed_chart_of_accounts which inserts the full BAS 2026 chart). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mcp): expose match_batch_allocate + bulk_book_transactions as MCP tools Surfaces the multi-tx flows shipped in PRs #603/#606/#608/#610 so Claude Desktop/Code can drive them via chat. - migration 20260603120000: expand pending_operations.operation_type CHECK to include match_batch_allocate, bulk_book_transactions, plus undo_sie_import (which was missing from prior expansions despite being wired in risk-tiers.ts and the commit dispatcher). - types/index.ts: extend PendingOperationType. - lib/pending-operations/risk-tiers.ts: match_batch_allocate = medium (same tier as single-tx match), bulk_book_transactions = high (creates a verifikat with arbitrary lines, same surface as create_voucher). - lib/pending-operations/commit.ts: thin commit handlers that call the SQL RPCs and translate the structured error envelope. The RPCs themselves do all the locking, balance checks, JE creation, voucher number, payment/junction rows, and doc inheritance. - extensions/general/mcp-server/server.ts: two new tool definitions. Both stage via stagePendingOperation with period_status hint and pre-validate inputs (direction, sum-equals-tx-abs, same-date, not-already-booked) so the agent gets a clear error inline before the RPC runs. - payload-size.bench: bump from 30K to 31K tokens (with rationale). Two new tools earn the bump; descriptions already trimmed to fit the <=280-char description limit. Migration applied to remote and version aligned with local filename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 review - allocation guard, IDOR pre-check, currency + JE-date Round-1 review fixes on PR #614: - Greptile P1: per-allocation invoice_id / supplier_invoice_id guard. The inputSchema marks both as optional (they're mutually exclusive by kind), so JSON Schema can't express "X required iff Y=A". Added explicit check in the execute handler: customer_invoice rows must carry invoice_id; supplier_invoice rows must carry supplier_invoice_id. - OWASP V8.2.1: IDOR pre-check on match_batch_allocate. Verify every invoice / supplier_invoice referenced in the allocations belongs to this company BEFORE staging. The RPC re-checks (BATCH_INVOICE_NOT_FOUND), but failing fast at the MCP layer gives the agent a clear error. - OWASP V8.2.1: same pre-check on bulk_book_transactions for existing_journal_entry_id. Fetches the JE at stage time, verifies status=posted and company_id, throws if not found. - swedish-compliance: currency homogeneity check on bulk_book. Mixed SEK + EUR in one samlingsverifikat violates BFL 5 kap 6§ st 3 motpart clarity. Cross-currency batches go through match_batch_allocate instead (which handles FX diff on 7960/3960). - swedish-compliance: period-lock check on the link-existing branch now uses MAX(tx_date, JE.entry_date), not just tx_date. Otherwise a tx in an open period could attach to a verifikat in a locked period and the guard would miss it. - A.8.11 + CC7.2: sanitised RPC error logging. log.error now emits only { code, message } instead of the full error object — error.details can echo invoice IDs, amounts, and counterparty identifiers. Not actioned (PR-comment, no code change): - V2.3 double-validation in commit handler — RPC enforces balance, accounts, bank-leg via the chart_of_accounts allowlist (PR #610 round 2). Commit handler is a thin pass-through by design. - A.8.2 step-up approval for high-tier ops — architectural change affecting all high-tier ops, not PR-scoped. - V2.4 rate limiting on bulk endpoints — platform-level concern. - 0.005 epsilon / account-class allowlist — pre-existing patterns. - undo_sie_import storno requirement — separate RPC, this PR only backfilled the missing CHECK constraint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 2 - trust-boundary comments + balance pre-check + audit log Round-2 review fixes (compliance-swarm went 14 -> 9 after round 1; remaining HIGHs are all "do the same tenant check at multiple layers"). The bot itself offers the alternative: "or document and reference the specific RPC line that enforces this." Following that. - commit.ts: trust-boundary comment blocks on both commitMatchBatchAllocate and commitBulkBookTransactions, citing the exact RPC + migration where tenant isolation + chart_of_accounts allowlist are enforced authoritatively. The commit handler stays a thin pass-through by design; re-querying would triple the same check without adding security. (V8.2.1, A.8.2) - commit.ts: structured success-path log.info() on both handlers with companyId, operationType, journal_entry_id, and tx count. No raw amounts or IDs that could echo PII. (V16) - server.ts: balance pre-check on bulk_book create-new path. RPC enforces BULK_BOOK_UNBALANCED authoritatively, but failing fast at staging gives the agent a clear error before pending_operations is even touched. (V2.3 / swedish-compliance) Not actioned this round: - V2.2 oneOf/if-then-else in JSON Schema for mutual exclusivity — JSON Schema vocabulary support is shaky across MCP clients; runtime check in execute() is the canonical pattern across the existing toolset. - CC6.1 generic error string to caller — RPC error codes are user-actionable (BULK_BOOK_UNBALANCED, BATCH_INVOICE_NOT_FOUND); a generic string would degrade UX. - CC7.2 audit RPC RAISE messages for PII — separate audit; not PR-scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 3 — last 5 LOWs + salary_run/agi constraint backfill Compliance-swarm went 14 → 9 → 5 (all LOW). Cleaning the last 5 + the swedish-compliance findings. - migration 20260603121000: backfill create_salary_run + generate_agi into pending_operations.operation_type CHECK. Both have risk-tier entries and commit executors but were never added (same bug class as undo_sie_import). Production has no rows of either type today. (swedish-compliance) - server.ts: Number.isFinite guard in bulk_book balance pre-check. Number(x) || 0 silently treats NaN as 0 — a malformed amount could pass the balance check by accident. (compliance-swarm A.8.28) - server.ts: count-equality + missing-set assertion in match_batch_allocate tenant pre-check. Belt-and-suspenders so a null/undefined row in the Supabase JSON response can't pass silently. Same pattern on both invoice and supplier_invoice branches. (CC6.1) - server.ts: fix BFL paragraph citation in currency-homogeneity comment. Was "BFL 5 kap 6§ st 3", should be "BFL 5 kap 2§" (SEK denomination) read with 5 kap 6§ (valutakurs). (swedish-compliance) - server.ts: clarify 0.005 tolerance comment — it's for floating-point equalisation only, not a rounding allowance. RPC enforces exact balance to the öre. (swedish-compliance) - commit.ts: expand audit-log txId comment — included intentionally for trail-to-source join, scoped to companyId already logged. (compliance-swarm A.8.15/CC7.2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 4 — Swedish plural typo + balance comment parity + agent-routing hint Round-3 review caught: - swedish-compliance: \`kundfakturaor\` typo (real räkenskapsinformation defect under BFL 5 kap 7§). Swedish plural for \`kundfaktura\` is \`kundfakturor\` (drop the final \`a\`, add \`or\`), same for \`leverantörsfaktura\` → \`leverantörsfakturor\`. Fixed via slice(-1) + 'or'. - swarm A.8.28: match_batch_allocate balance tolerance check was missing the equivalent "RPC enforces exact balance" comment that bulk_book has. Added. - swedish-compliance: currency-mismatch error message now routes the agent to gnubok_match_batch_allocate for cross-currency allocations instead of letting it retry with hand-built FX lines. Not actioned (out of pattern / out of scope): - Integer arithmetic for balance checks (codebase pattern is float + epsilon; would diverge from match_batch_allocate, supplier-payment, invoice-payment, etc.) - DSD docs / runbook for txId-in-log and stripped-error.details trade-offs (out of PR scope; tracked separately) - Link-existing target verifikat description match (architectural; every link-existing op would need this) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mcp): expose link_transaction_to_journal_entry as MCP tool The REST endpoint /api/transactions/[id]/link-journal-entry already lets the duplicate-payment UI attach a bank tx to an already-posted verifikat without creating new bookkeeping. Agents had no equivalent — closing that parity gap so users on Claude can match bank txs against vouchers they booked manually. The core link logic moves to lib/transactions/link-journal-entry.ts so both the REST route and the new commit handler share one implementation (preserves all structured-error codes, optimistic-lock invoice update, and compensating rollback). New 'link_transaction_journal_entry' op type wired through the risk tiers (medium), TOOL_SCOPE_MAP (transactions:write), and dispatcher. Bumps the tools/list payload-size ceiling 31K → 31.5K — same family bump PRs #603/#606 made when adding match_batch_allocate / bulk_book_transactions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 5 — bot findings on link_transaction_journal_entry Addresses the swedish-compliance + compliance-swarm findings on commit 5b884c3a: 1. **CHECK constraint backfill** — new migration adding 'link_transaction_journal_entry' to pending_operations.operation_type. Same bug class as the salary_run/agi backfill in 20260603121000; without it, every staged op would be rejected silently in production (BFL 5 kap 6–7§ audit-trail gap). 2. **Payment-date exchange rate** — invoice_payments.exchange_rate now uses transaction.exchange_rate (rate on payment date) instead of invoice.exchange_rate (rate on invoice date), per BFL 5 kap 2§ + ML 8 kap 21–23§. The full 3960/7960 posting still belongs to createInvoicePaymentJournalEntry by contract — this path only links to an EXISTING verifikat. 3. **voucherLabel format centralized** — exported formatVoucherLabel helper returns the canonical `A-12` format (with hyphen, matches gnubok_link_invoice_to_voucher and SIE #VER cross-references). Both the MCP staging preview and the committed service result import it, so the user can't approve one label and have a different one land in the audit trail. 4. **Rollback warn log restored** — txLog.warn-equivalent (IDs only, no PII) when the compensating rollback itself fails, surfacing partial-state gaps for reconciliation per GDPR Art.5(1)(f) / SOC 2 CC7.2. Lost in the refactor that extracted the shared service; now present in both rollback call sites. 5. **Commit-layer log.info** — structured success log mirroring commitMatchBatchAllocate / commitBulkBookTransactions (companyId, tx/JE IDs, settledInvoice boolean). No raw amounts or counterparty names. 6. **Data minimization on invoice fetch** — explicit column list replaces select('*, customer:customers(name)') in the shared service; the MCP staging pre-check now fetches only invoice_number + remaining_amount (drops total + paid_amount). voucher_description omitted from preview_data per Art.25. Test impact: existing route + dispatcher tests updated to expect `A-12` instead of `A12`. All 4308 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoices): correct FX bookkeeping + UI for match-invoice flow User report: matching a 230 SEK bank tx against a 140 USD invoice produced 1930 Dr 2 142,50 / 1510 Cr 2 142,50 — fictitious numbers that didn't match either the bank receipt or the booked AR. Root cause: the preview route called resolveSekAmount(tx.amount, null, INV.currency, INV.rate), treating the SEK tx number as if it were in the invoice's currency and multiplying by the invoice's stored rate. Both the preview and the commit then used the bogus number on both legs and silently dropped the FX gain/loss. A second issue surfaced in the same dialog: for a 1 250 SEK invoice with a prior 230 SEK partial, the comparison row showed "Differens: 250 kr" (off the original total) instead of "20 kr" (off the actual 1 020 kr remaining). This patch: 1. **New shared helper** lib/bookkeeping/invoice-payment-lines.ts - buildInvoicePaymentClearingLines(tx, invoice, description) → bank-leg, AR-leg, fx-diff, and a balanced line array. Bank-leg is always the actual SEK that hit the bank (resolveSekAmount with the TX's currency context, honouring tx.amount_sek when set). AR-leg is the SEK value of the customer-debt reduction at the invoice's stored rate. Diff posts to 3960 (gain) or 7960 (loss) so the verifikat balances per BFL 5 kap 4–5§. Mirrors the match_batch_allocate RPC's contract: when the tx is cross-currency, the single match fully clears the invoice's remaining amount. 2. **Preview route** uses the helper for the clearing branch — replaces the buggy resolveSekAmount call. Now byte-identical to what commit builds. 3. **Match-invoice POST** uses the helper + createJournalEntry directly for the clearing path, bypassing createInvoicePaymentJournalEntry on this single flow. mark-paid and other callers of that function still work as before (full payment + caller-supplied exchangeRateDifference). 4. **InvoiceMatchDialog** compares the bank tx against invoice.remaining_amount (not invoice.total) for both customer and supplier branches; cross-currency dialogs now show the different- currencies warning instead of a meaningless numeric diff. The dialog's invoice card also displays remaining_amount. 8 new unit tests cover same-currency full/partial, cross-currency gain/loss, exact match (no FX line), sub-öre tolerance, and USD-on-USD with pre- populated amount_sek. All 4316 tests pass. Scope note: this expands PR #614 beyond the original "expose multi-tx RPCs as MCP tools" since the same FX bug class affected the new MCP tool too (round 5 already addressed the invoice_payments.exchange_rate side). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 7 — CI build + 4 HIGH bot findings Core Build was failing on e29a0ba2/5e9d4c3d due to a TypeScript type-cast error in linkTransactionToJournalEntry. Plus the swedish-compliance review flagged four substantive bugs in my recent commits. 1. **TS build error** — `invoice = invoiceRow as typeof invoice` inferred `never` because the LHS type included `null`. Switched to a named `FetchedInvoice` alias and `as unknown as FetchedInvoice`. 2. **TOOL_SCOPE_MAP missing two write-capable tools** (🟠 HIGH OWASP V8.2.1). `gnubok_match_batch_allocate` and `gnubok_bulk_book_transactions` (added in PRs #603/#606) were never registered, meaning any API key could invoke them regardless of scope. Backfilled both with `transactions:write`. 3. **`paymentExchangeRate` fallback wrong-date rate** (swedish-compliance). `transaction.exchange_rate ?? invoice.exchange_rate ?? null` falls back to the INVOICE date's rate when the tx rate is null. Per ML 8 kap 21–23§ the payment row must record the PAYMENT-date rate. Removed the fallback — `null` is correct when the tx is SEK; downstream lookups can populate it lazily from Riksbanken if needed. 4. **Currency-mismatch corrupts paid_amount** (swedish-compliance). The link path was accumulating `tx.amount` into `invoice.paid_amount` without checking that the currencies matched. A 230 SEK tx applied to a USD invoice would record "230 USD paid" silently. Added explicit LINK_TX_INVOICE_CURRENCY_MISMATCH guard (400) — cross-currency settlement must go through the match-invoice flow which routes through buildInvoicePaymentClearingLines. 5. **Cross-currency PARTIAL overstates FX gain/loss** (swedish-compliance, BFL 5 kap 4–5§). `buildInvoicePaymentClearingLines` was crediting the FULL invoice remaining to 1510 on every cross-currency match — zeroing the GL balance while the invoice row stayed at status=partially_paid, and booking a fake huge FX diff to 3960/7960. Fix: only book FX-diff when `bankSek >= arSekFullRemaining`. Partials default to 1930 = 1510 = bankSek, deferring the FX adjustment to the final settlement (or to a manual mark-paid with explicit exchange_rate_difference). Documented the helper as customer-invoice- only (supplier-side has different DR/CR polarity and goes through match_batch_allocate RPC). Test impact: 1 helper test updated to match the defer-on-ambiguous-loss behavior, 1 new test covers the partial-defers-FX path explicitly. All 4317 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(mcp): PR #614 round 8 — close out remaining bot findings CI green on round 7 (4 of 4 checks), HIGH count 2 → 1. Round-8 closes the remaining HIGH and the smaller doc/guard items. 1. **PI1.3 risk acknowledgment restored** (SOC 2 HIGH). The shared rollbackTxLink helper already had warn-level logging on rollback failure, but the explicit PI1.3 reference comment from the original route was lost in the refactor. Added inline so the reconciliation- gap risk is visible to future maintainers. 2. **MCP currency-mismatch pre-stage check.** gnubok_link_transaction_to_ journal_entry now fetches invoice.currency and rejects cross-currency matches before staging, saving the user an approval round-trip when the commit handler's LINK_TX_INVOICE_CURRENCY_MISMATCH guard would fire anyway. 3. **fxDiffSek JSDoc clarified.** The sign convention (positive = loss, negative = gain) is correct for verifikat balancing but counter- intuitive at a P&L glance. Documented explicitly + pointed callers needing a "gain" number at `bankSek - arSek`. 4. **Reject both invoice_id + supplier_invoice_id** on the same match_batch_allocate row (V4.5). Extra IDs previously leaked into preview_data silently. 5. **Reject zero-amount tx** in bulk_book_transactions direction guard (A.8.28). A txs[0].amount === 0 would have mis-classified the batch as 'expense'. Mirrors the existing guard in match_batch_allocate. 6. **Reject debit=0 && credit=0 lines** in bulk_book new_entry (BFL 5 kap 6§ — every verifikat line must represent a real bokföringspost with a non-zero amount). 7. **Data-minimization comments** added on the match-invoice preview route (amount_sek + exchange_rate fetch is for the FX-fix bank-leg math) and on the bulk_book_transactions preview_data block (aggregate counts only — no per-tx PII). Mirrors the pattern already documented on gnubok_link_transaction_to_journal_entry. Skipped: - 1510 vs 1515 (osäkra kundfordringar) — future improvement, needs reading the original invoice JE's account, not a single-tool fix. - transaction_description PII masking in preview_data — needs product call on the truncation strategy and would degrade approval-UX. - "invoice.match_confirmed event removed" finding — false positive; the event is emitted at lib/transactions/link-journal-entry.ts:270-280. All 4317 tests pass; payload-size guard still under ceiling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoices): PR #614 round 9 — block cross-currency in single match-invoice path Closes the swedish-compliance finding from round-8 review: a SEK bank tx matched against a USD invoice through /api/transactions/[id]/match-invoice would silently corrupt invoice.paid_amount (accumulator treats SEK as USD) and flip a 140 USD invoice to status='paid' after a tiny partial. The round-6/7 FX fix corrected the JOURNAL ENTRY lines but the invoice STATE update still ran the same broken accumulator. Proper cross-currency settlement on this path requires converting tx.amount to invoice.currency at the bank-date rate AND storing invoice_payments rows with the right (amount, currency) pair. That's a larger design call that belongs in its own PR. This change blocks cross-currency on the single-allocation path: - New MATCH_INVOICE_CURRENCY_MISMATCH structured error (400, bilingual) - Same-currency check inserted right after MATCH_INVOICE_NOT_OPEN - Mirrors the LINK_TX_INVOICE_CURRENCY_MISMATCH guard added to the link path in round-7 - Routes the user to the multi-allocation flow (gnubok_match_batch_allocate) which DOES handle 3960/7960 FX-diff postings end-to-end Same-currency (SEK→SEK or USD→USD) remains fully supported including partials; the buildInvoicePaymentClearingLines helper handles those correctly. For SEK tx → USD invoice the user now gets a clean 400 error pointing at the right flow, instead of silently corrupted ledger state. 1 new route test covers the guard. All 4318 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ea1bf01f1e |
Fix/m sprint fixes (#613)
* fix(dashboard): exclude ignored and already-triaged transactions from stale count The "Gamla transaktioner" widget counted transactions that had been ignored or already marked as is_business=true but not yet booked, so users saw a nag for a row they had already dealt with — and the /transactions inbox correctly hid it. Align the count with the inbox criterion (is_business IS NULL, is_ignored = false) so the widget clears when the row leaves the inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): read entity_type from settings response wrapper The transactions page read entityRes.entity_type directly, but /api/settings returns { data: { entity_type, ... } }. The expression was always undefined, so setEntityType never fired and entityType stayed at its initial 'enskild_firma'. The template picker's entity_type filter then dropped every aktiebolag-tagged user template for AB customers — only entity_type='all' templates made it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale templates bank sync journal entry from transaction * fixed pr comments * fixed pr comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7bcd46d503 |
feat(transactions): match overshoot guards + supplier voucher linking (#602)
* feat(transactions): match overshoot guards + supplier voucher linking
Three changes that together close the "I can't link a bank transaction
to an already-booked verifikat on the supplier side" gap and fix a
latent data-corruption bug on the per-tx match endpoints.
1. fix: clamp paid_amount on match endpoints when tx > remaining
/api/transactions/[id]/match-{invoice,supplier-invoice} previously
used transaction.amount wholesale as the paid amount, pushing
invoice.paid_amount past invoice.total whenever the bank tx was
larger than what was owed. Both endpoints now reject with
MATCH_AMOUNT_EXCEEDS_REMAINING / MATCH_SI_AMOUNT_EXCEEDS_REMAINING
and a structured { transaction_amount, remaining_amount, excess }
payload that points the user at the future split-payment flow.
FX branch already clamps to invoice.remaining_amount and is
unchanged.
2. feat: supplier-side "link existing verifikat" (mirror of #591)
lib/invoices/supplier-voucher-matching.ts mirrors the customer
voucher-matching module: finds posted JEs that debit 2440
(Leverantörsskulder), validates currency + remaining-amount, and
atomically links them as supplier_invoice_payments rows. New
/api/supplier-invoices/[id]/{voucher-candidates,link-to-voucher}
routes wrap it. LinkVoucherPicker gains a mode='supplier_invoice'
prop so the same component renders both flows. The supplier-invoice
mark-paid dialog now uses Tabs ("Ny betalning" / "Befintlig
verifikation") to match the customer-side UX.
3. infra: transaction_voucher_links junction + denorm guard
Foundation migration for upcoming multi-tx ↔ multi-voucher flows.
Adds the junction table (with RLS, updated_at, indexes), a
block_contradictory_invoice_denorm trigger on transactions that
refuses to set invoice_id/supplier_invoice_id to a value that
contradicts an existing payment row, and is_transaction_booked(uuid)
as a single source of truth for "is this tx anchored?" once
multi-allocation leaves denorm columns NULL. No application code
uses these yet — they unlock the batch allocation and bulk-book
flows in follow-up PRs.
Tests: 98 unit tests pass across the touched paths (match-invoice,
match-supplier-invoice, supplier-voucher-matching, link-to-voucher).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(supplier-invoices): PR review — atomic link RPC, computeRemaining edge case, pg-real tests
Addresses the three real issues raised by Greptile on PR #602.
1. (P1) Atomic supplier voucher linking — new
link_supplier_invoice_to_voucher PL/pgSQL RPC. The TS-side
linkSupplierInvoiceToVoucher() previously did UPDATE-then-INSERT with
a manual unconditional rollback. Under concurrent linking against the
same invoice, request A's rollback could overwrite a sibling B's
successful write while leaving B's payment row in place. Moving both
writes into a single PG transaction (one RPC call) lets PG's own
rollback handle the failure path correctly. TS wrapper now just
translates the structured RPC return into the lib's Result type.
2. (P1) pg-real tests — tests/pg/transaction_voucher_links.pg.test.ts.
CLAUDE.md mandates *.pg.test.ts for any PR adding a trigger, RPC, or
RLS. The Phase 1A foundation migration added all three but had no
pg-real coverage. Tests now cover:
- trg_block_contradictory_invoice_denorm refusing contradictory
UPDATEs on invoice_id and supplier_invoice_id
- the same trigger PERMITTING a matching UPDATE (no false positives)
- is_transaction_booked() returning true via journal_entry_id, via
invoice_payments, and via transaction_voucher_links rows.
3. (P2) computeRemaining edge case — trust remaining_amount whenever
the column is non-null (including the legitimate 0 for fully-paid
invoices). The old "> 0" guard fell through to total - paid_amount,
which under rounding drift could compute a tiny positive residue and
slip a fully-paid invoice past LINK_SI_VOUCHER_INVOICE_FULLY_PAID.
The fourth Greptile comment (overdue invoices silently get no
candidates) was a misread: 'overdue' IS in the open-state list at
route.ts:35. No code change needed there.
Tests: 100 unit tests pass (16 in the directly-touched paths).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(supplier-invoices): PR review round 2 — broaden AP range, log event failures
Addresses the actionable findings from the compliance-swarm and
Swedish-accounting-compliance bot reviews on PR #602.
1. (swedish-accounting-compliance, high) AP account hardcoded to 2440
rejected legitimate samlingsverifikationer that debit 2441
(Leverantörsskulder i utländsk valuta), 2443 (Skuldfakturor), etc.
BAS 2026 reserves the full 2440–2449 range for Leverantörsskulder.
The TS-side AP_ACCOUNT constant becomes AP_ACCOUNT_PREFIX ('244')
used with .like() and .startsWith(). The PL/pgSQL RPC's
account_number filter becomes LIKE '244%'. The
LINK_SI_VOUCHER_NO_AP_DEBIT error message updates to reference the
244x range with examples.
2. (ISO 27001:2022 A.8.15 / OWASP V16) Empty catch on the
supplier_invoice.paid event emission now logs with log.warn so a
failure in the downstream reminder/audit subscriber leaves an
auditable trail without blocking the response.
3. (GDPR Art.5(1)(c)) Documented design rationale for retaining
select('*') on the post-link invoice re-fetch: the
supplier_invoice.paid event payload is typed as
`supplierInvoice: SupplierInvoice` in lib/events/types.ts, narrowing
would break the subscriber contract. The event stays in-process
and consumers legitimately need the full context.
Skipped findings:
- V8.2.1 ownership concerns: route + RPC already filter by
company_id from withRouteContext; the RPC's WHERE clause covers it.
- DELETE policy scoping: matches the gnubok pattern across all
company-scoped tables — any member with write access manages records.
- transaction_id = NULL on the voucher-link path: by design — the
flow has no bank tx (the voucher's 1930 line represents it).
- Reverse-charge VAT (2614/2647) validation on linked vouchers:
real concern but invasive change; tracked for follow-up.
- Storno-chain integrity (linking the original of a storno pair):
edge case; tracked for follow-up.
Tests: 26 unit tests pass in the directly-touched paths. RPC patch
applied to remote via Supabase MCP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d27c3dd3dc |
Bug/customer cron job (#516)
* fix(reminder-processor): filter out credited invoices in overdue reminders * feat: implement linking of transactions to journal entries - Added POST endpoint for linking a bank transaction to an existing journal entry without creating new bookkeeping. - Implemented validation for required fields and error handling for various scenarios (e.g., missing journal_entry_id, transaction already linked, journal entry not found). - Created tests for the new endpoint to cover various cases including successful linking, error responses, and invoice handling. - Introduced duplicate payment detection logic to prevent double-booking of bank receipts. - Added a new component for correction affordance in the UI to facilitate user corrections on journal entries. * feat(invoice-matching): enhance force matching with expected journal entry validation |
||
|
|
97db09a3ff |
feat(invoices): allocate-on-save, makulera flow, manual invoice picker (#405)
* feat(invoices): allocate-on-save, makulera flow, manual invoice picker
Three coordinated invoice changes:
1. Allocate F-series number when the draft is created (Fortnox-style),
not at send time. Users can download a numbered draft and send it
manually. If number allocation fails, the invoice + items are rolled
back so no orphaned rows remain. Adds INVOICE_CREATE_NUMBER_ASSIGN_FAILED.
2. DELETE /api/invoices/[id] now soft-cancels (status='cancelled') instead
of hard-deleting. The F-series number is retained, keeping the sequence
gap-free per ML 17 kap 24§ and BFNAR 2013:2 — no voucher_gap_explanations
needed. Sent/paid invoices stay immutable (credit note required). Adds
"Makulerade" tab to the invoice list; cancelled invoices are hidden from
"Alla" by default. PDF draft banner stays visible on numbered drafts and
only clears when the invoice is marked sent.
3. New InvoicePicker component lets users manually match an income
transaction to an open invoice from the booking dialog ("Matcha med
faktura..."), complementing the existing auto-match flow.
Also: new-invoice review dialog reads accounting_method from settings and
shows a cash-vs-accrual warning so users know when the verification posts.
seed-demo-account adds year-end closing + opening balance helpers so
multi-year demo data is balanced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(invoices): address review feedback on PR #405
Greptile P1 + Swedish compliance reviewer findings:
- app/api/invoices/route.ts — replace hard-delete rollback on number-
allocation failure with a soft-cancel (status='cancelled'). If
generate_invoice_number bumped the sequence before failing to write
the number back, hard-deleting would leave a permanent gap in the
F-series in violation of ML 17 kap 24§. Re-fetch invoice_number
first so any partially-written value is logged for operator follow-up.
Log loudly if the cancel itself fails so an orphan row doesn't go
unnoticed.
- app/api/invoices/[id]/route.ts — close TOCTOU race on the cancel
update. The .eq('status','draft') guard prevented data corruption
but Supabase returned error: null with 0 affected rows on a
concurrent flip, and the handler reported success. Add .select('id')
and return new INVOICE_CANCEL_RACE (409) when no row updated.
- components/transactions/InvoicePicker.tsx — memoize createClient()
so the supabase reference is stable across renders. Without this,
including supabase in the useEffect dep array fires the open-invoices
fetch on every render.
- app/(dashboard)/transactions/page.tsx + match-invoice/route.ts —
read category from the match-invoice response instead of hardcoding
'income_services' client-side. Server now echoes the category it
actually booked; client falls back to 'income_services' if absent.
- lib/invoices/pdf-template.tsx — add MAKULERAD banner for cancelled
invoices (red, distinct from the yellow draft banner). A cancelled
invoice PDF previously rendered with no warning if it had a number,
or with the draft banner if it didn't — both could be mistaken for a
valid faktura. Cancelled takes precedence over draft so the legacy
un-numbered-cancelled case is also covered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(invoices): guard cancelled status on send + rollback symmetry
Two follow-up fixes from the second-round Swedish compliance review on
PR #405:
- app/api/invoices/[id]/send/route.ts — reject sending a cancelled
invoice. The existing flow had no status guard before
.update({ status: 'sent' }), so a cancelled invoice could be silently
re-activated to sent and a "MAKULERAD"-watermarked PDF could be
delivered to the customer as if it were a live faktura. New
INVOICE_SEND_CANCELLED (400) returned at the top of the handler.
- app/api/invoices/route.ts — add .eq('status', 'draft') to the
rollback-cancel update so the rollback is symmetric with the DELETE
handler's only-drafts-may-be-cancelled rule. At the create flow's
current shape the row can't realistically be anything other than
draft, but the symmetry prevents a future caller adding a status flip
between insert and number-allocation from accidentally cancelling a
posted invoice.
mark-sent (rejects non-draft), mark-paid (only sent/overdue), and
convert (explicitly rejects cancelled proformas) already guard
correctly — no changes needed there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(invoices): InvoicePicker filters settled invoices; drop dead error code
Two cleanups from the third-round Swedish compliance review on PR #405:
- components/transactions/InvoicePicker.tsx — add .gt('remaining_amount', 0)
defensively. The picker filtered by status IN (sent, overdue,
partially_paid), but a stale 'sent' or 'overdue' row with
remaining_amount=0 (data inconsistency) would otherwise be selectable
here and could be matched a second time, double-booking the income —
a direct BFL 5 kap accuracy violation.
- lib/errors/structured-errors.ts — remove INVOICE_DELETE_NUMBERED.
The numbered-draft refusal was replaced by the soft-cancel path
earlier in this PR; the entry has no remaining callers.
Verified-safe and not changed:
- Cancel-without-storno concern: createInvoiceJournalEntry only fires
inside mark-sent (after the draft→sent guard) or send (after the
cancelled-status reject). Drafts never have posted verifications, so
cancelling a draft cannot leave an orphaned bokföringspost.
- Hardcoded category: 'income_services' in match-invoice is a
pre-existing classification concern that warrants a larger refactor
(derive from invoice's revenue accounts) rather than a one-line patch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(invoices): InvoicePicker excludes proforma invoices
Add .eq('document_type', 'invoice') to the open-invoice query. A
proforma is not a faktura per ML 17 kap 24§ — no VAT obligation, no
binding commercial document — and must never be matched against a
bank receipt. Without this guard a sent proforma could be selected
in the picker, triggering a payment booking and VAT-rate journal
entry that violates BFL 5 kap accuracy rules.
Other findings from the third-round Swedish compliance review were
verified-safe and not changed:
- Cancelled-invoice PDF download path: the MAKULERAD watermark added
earlier in this PR is the safeguard. Blocking the download endpoint
outright would prevent legitimate audit access; the visible banner
prevents the doc being mistaken for a valid faktura.
- Cancel-without-storno: createInvoiceJournalEntry only fires inside
mark-sent / send / pending-operations, all behind status guards.
Drafts never carry a posted verifikation, so cancel can't orphan one.
- Allocate-on-save for proforma uses F-series: not true. The
generate_invoice_number RPC (migration 20260427150100) routes
document_type='proforma' to a separate 'PF-' prefix sequence; the
F-series is untouched.
- closeYearForSeed 2099 → 2091 transfer: real demo-data correctness
issue but a seed-script polish item — separate PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(match-invoice): server-side document_type='invoice' guard
The InvoicePicker filter excluding proformas (added in the previous
commit) is client-only. A direct API call to /api/transactions/[id]/
match-invoice with a proforma id would otherwise still book a payment
journal entry against a document that has no VAT obligation per
ML 17 kap 24§. Add a defense-in-depth check after the invoice fetch.
New error code MATCH_INVOICE_NOT_INVOICE_TYPE (400). Test added.
Other findings from the latest compliance review were verified-safe and
not changed:
- Cancelled-invoice PDF download path: /api/invoices/[id]/pdf always
re-renders through InvoicePDF, so the MAKULERAD banner is always
present. The bot's "cached pre-cancellation PDF" scenario does not
apply to this codebase.
- Proforma F-series allocation: the generate_invoice_number RPC routes
document_type='proforma' to a separate 'PF-' prefix; the F-series is
not polluted.
- Soft-cancel rollback gap when number not written: the RPC is a
single-transaction PL/pgSQL function — sequence bump (UPDATE
company_settings) and row write (UPDATE invoices) commit or roll
back together. The "sequence advanced but row null" scenario the
bot describes is impossible by construction; a thrown exception in
the row-write step rolls back the bump.
- closeYearForSeed obeskattade reserver: seed-script demo accuracy,
separate PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
5725c25bf1 |
Logs/improved logging (#398)
* feat(mcp): add create_transactions tool with /pending approval gate New MCP tool gnubok_create_transactions stages 1–10 transactions per call as pending_operations of type create_transaction (risk: medium). Each item becomes its own card on /pending; on confirm, the executor inserts the row into transactions with import_source='mcp' so MCP-staged ingestion is distinguishable from PSD2 sync. Designed for skill workflows that pull external data (e.g., Airtable) and want the user to gate the writes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bas): strip concatenated group headers from corrupted account names A chart-data import bug had glued the next group's header onto the last account in each preceding group across all eight bas-data class files (e.g. account 2670 read "Utgående moms på försäljning inom EU, OSS 27 PERSONALENS SKATTER, AVGIFTER OCH LÖNEAVDRAG"). The corrupted names surface in transaction dropdowns, ledgers, SIE exports and årsredovisning, and risk VAT miscategorization on the OSS (2670) and blandad-verksamhet (6999) accounts specifically. - Cleans 69 account_name and 64 description fields across class-1..8 files - Adds a regression test asserting no name contains a concatenated header - Ships an idempotent safety-net migration that updates already-seeded chart_of_accounts rows, gated on the corrupted string so user customizations are preserved Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(errors): add structured error codes and handling for various operations - Introduced a new structured error registry in `structured-errors.ts` to standardize error handling across the application. - Added Swedish and English messages for various error scenarios, including validation, authorization, and bookkeeping errors. - Implemented a client-side error toast in `use-error-toast.ts` to display user-friendly error messages with remediation hints. - Created a wrapper for recording operation outcomes in `record-operation.ts`, enhancing audit capabilities for operations. - Developed a provider call wrapper in `with-provider-call.ts` to handle external HTTP calls with structured logging and error mapping. - Added a new SQL migration to extend the processing history with new event types and aggregate types for better operational telemetry. * Refactor supplier API routes to use context-based logging and error handling - Replaced direct Supabase client usage in GET and POST routes with context-based approach using `withRouteContext`. - Enhanced error handling to provide structured error responses for supplier creation and listing. - Updated logging to include request IDs for better traceability. - Introduced new error codes for supplier-related operations. - Refactored tax deadlines cron job to utilize context and improved error handling. - Updated ESLint configuration to enforce logging practices across API and lib directories. - Enhanced arcim migration extension with structured error handling and logging. - Added classification for provider errors to improve user-facing error messages. - Introduced request ID in extension context for better log correlation. * fix(route-context): update DynamicParams type for improved type safety in route handlers * feat(transactions): add 'create_transaction' operation to PendingOperationType * fix(route): ensure companyId is non-nullable in loadAndDeriveAbsence function * fix(route-context): ensure companyId is always non-null by short-circuiting with COMPANY_CONTEXT_MISSING --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a1a816b4a5 |
Delete features (#218)
* Implement company and account deletion features - Add event types for company and account deletion to CoreEvent. - Enhance Supabase middleware to handle company context resolution and cookie management for archived companies. - Create API routes for deleting accounts and companies, including necessary validations and event emissions. - Implement tests for account and company deletion endpoints to ensure proper functionality and error handling. - Add retention notice component to inform users about bookkeeping data retention during destructive actions. - Create database migrations to support soft deletion of companies and anonymization of user accounts, ensuring compliance with retention laws. * feat: enhance account deletion process and update user notifications * Add service client for onboarding completion check and update escape hatch visibility * Enhance invite flow and email handling for company members * Refactor company context and RLS policies for active company isolation - Update `switchCompany` to remove unnecessary revalidation as client handles navigation. - Revise `getActiveCompanyId` to prioritize `user_preferences` and validate against non-archived memberships. - Modify `setActiveCompany` to ensure `user_preferences` is the authoritative source while maintaining cookie compatibility. - Enhance middleware to resolve active company using `user_preferences` and fallback to first non-archived membership. - Introduce new API route `/api/company/current` to fetch the active company ID for cross-tab synchronization. - Implement `CompanyTabSync` component for real-time active company enforcement across tabs. - Create migration for RLS policies to enforce single-active-company isolation using `current_active_company_id()`. * feat: implement viewer role enforcement for write permissions - Added `useCanWrite` hook to determine if the current user has write permissions based on their role in the active company. - Updated various components (JournalEntryForm, CustomerForm, DeadlineForm, etc.) to disable write actions and show a lock icon with a tooltip for users without write permissions. - Introduced `requireWritePermission` function to enforce write permissions at the API level, returning a 403 response for viewers. - Created tests to verify the behavior of the viewer role and write permissions. - Added database migration to enforce read-only access for viewers at the database level. |
||
|
|
0dd1f5ebc1 |
feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19) Introduce companies table, company_members, and user_preferences to support multiple companies per user. All data scoping changes from user_id to company_id across the entire codebase. Key changes: - Database migration: new tables, company_id on 40+ tables, backfill, RLS rewrite from user_id to company-member-based, updated RPCs - Types: Company, CompanyMember, CompanyRole, UserPreferences types; company_id added to all entity interfaces; companyId on all events - Engine: all 7 core functions take companyId; storno, period, year-end services updated; 16 report generators updated - Middleware: company context resolution (cookie → prefs → first company) - API routes: ~120 routes updated with requireCompanyId() - Frontend: CompanyProvider context, layout/dashboard/onboarding updated - Extensions: context factory, 9 extensions, all lib files updated - Tests: 1880 tests passing, all helpers updated with company_id defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add database migrations for multi-tenant company and team system (GNU-19) Adds company_invitations, company creation RPC, team_members, account deletion RPC, and teams table refactor migrations. Updates base multi-tenant migration with cascading FKs and onboarding_step column. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team types and update core infrastructure for multi-tenancy (GNU-19) Adds TeamRole, MemberSource, and Team types. Refactors Supabase service client to be stateless, updates middleware for team-aware routing, extends CompanyContext with team/role fields, and updates extension service types to accept companyId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through business logic functions (GNU-19) Replaces user_id scoping with company_id across all lib modules: bookkeeping, documents, transactions, invoices, reconciliation, tax, deadlines, and import. Updates corresponding tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through API routes and extensions (GNU-19) Updates all existing API routes to extract and pass companyId. Updates enable-banking and arcim-migration extensions for company-scoped transaction ingestion and sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add company and team management API routes (GNU-19) Adds CRUD endpoints for company members, company invitations, team members, and team invitations. Includes invite token utilities, email templates, and company switch server action. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team/company UI components, pages, and dashboard updates (GNU-19) Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company members and team management panels. Updates dashboard layout for team-aware routing, onboarding for multi-step role choice, and auth callback for team invite acceptance. Ignores supabase/.branches/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in import page (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move appUrl declaration to outer scope in invite route (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for second company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in extension components (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update tests to use companyId instead of userId and improve type handling --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
7c3518a080 |
feat: payment matching audit trail and partial payments (#89)
* feat: payment matching audit trail, partial payments, and match improvements - Add payment_match_log table (append-only audit trail per BFL 7:1) with immutability triggers and proper RLS - Add invoice_payments table for partial payment tracking, mirroring supplier_invoice_payments pattern - Add remaining_amount column + partially_paid status to invoices - Add match-log.ts service for recording match/unmatch state transitions - Improve match-invoice route: support partial payments, record audit log, emit payment.matched events, handle storno conflicts - Improve match-supplier-invoice route: audit logging, partial payment support - Update invoice-entries.ts for partial payment journal entries - Update transaction ingest to detect and auto-suggest invoice matches - Update bank-reconciliation to handle partial payment state - Add invoice_payments, payment_match_log types and helpers - Extend tests for match-invoice route and transaction ingest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback — payment matching correctness bugs - P1: Return 500 on non-23505 payment insert failures instead of silently continuing with corrupted invoice state (both routes) - P1: Fix foreign-currency partial payment matching — use proportional remaining SEK amount instead of full total_sek - P2: Check error when clearing journal_entry_id after storno - P2: Add updated_at column + trigger to invoice_payments table - P2: Make supplier_invoice_payments.user_id NOT NULL after backfill Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
86348c80c7 |
feat: BFL-compliant journal descriptions and cancelled entry status (#55)
* fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2) Reversed entries (storno) must appear alongside their original posted entries in reports for a complete audit trail. Previously, filtering by status='posted' excluded them, causing discrepancies when corrections had been made. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: semi-manual invoice payment booking with editable journal lines When marking an invoice as paid, users now see a dialog where they can: - Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.) - Review and edit the proposed journal entry lines before committing - The happy path remains fast — lines are pre-filled correctly Implementation: - Pure proposePaymentLines() function for line computation (accrual + cash) - PaymentBookingDialog with AccountCombobox, balance validation, date picker - API accepts optional custom lines, falls back to auto-generation without them - 18 tests (8 unit + 10 API) all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — validation fallback, balance check, error handling - P1: Return 400 on invalid body instead of silently falling back to auto-generated lines (split JSON parse from schema validation) - P1: Add server-side balance check for custom lines before committing (debit must equal credit, totalDebit > 0) - P2: Wrap PaymentBookingDialog init() in try/catch with toast on failure and auto-close instead of silent empty state - Add 2 new tests: unbalanced lines → 400, invalid schema → 400 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: OAuth callback redirect for local dev and timeout resilience - Pass redirectUri dynamically from NEXT_PUBLIC_APP_URL so OAuth callbacks work on localhost (not just production) - Encode consentId/provider in OAuth state (base64url JSON) so the callback doesn't depend on session storage - Add skipAuth flag to extension API routes for OAuth callbacks (external provider redirects have no user session cookie) - Wrap AbortError in descriptive timeout messages in arcim-client - Make preview endpoint resilient to partial failures (company info and SIE fetch are individually non-blocking) - Simplify login page (remove unused magic link auth mode) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: create journal entry before marking invoice as paid Move journal entry creation before the invoice status update so that if accounting fails, the invoice is not permanently marked paid without a corresponding entry. Previously the error was silently swallowed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update mark-paid tests for journal-first ordering Reorder mock queue to match new flow (settings before update), update failure test to expect 500 instead of silent success, add try-catch with proper error response in route handler. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add reverse charge VAT (ruta 20-32) and improve mobile UX across dashboard Add full reverse charge (omvänd skattskyldighet) support to the VAT declaration: - Map accounts 2614/2624/2634 to ruta 30/31/32 for self-assessed output VAT - Calculate purchase bases (ruta 20-24) from supplier invoices by supplier type - Include ruta 30-32 in ruta 49 formula and totalOutputVat summary - Display reverse charge section in reports UI and composition chart - Add comprehensive test coverage for all reverse charge scenarios Improve mobile UX across the app: - Convert nav drawer to bottom sheet with drag handle and safe area padding - Add mobile card layout for PaymentBookingDialog journal lines - Replace settings tab pills with dropdown selector on mobile - Make wizard step indicators responsive (collapsed on mobile) - Ensure all dialog footers stack buttons full-width on mobile - Add 44px minimum touch targets throughout - Make onboarding buttons full-width on mobile Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — indentation, query efficiency, tab dedup - Fix misleading try-block indentation in mark-paid route - Filter reversed entries at DB level (.eq('status', 'posted')) instead of fetching then discarding in memory - Extract shared settingsTabs array so mobile Select and desktop TabsList stay in sync automatically Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add resilience fallbacks, Arcim retry logic, and client tests Add FallbackPrompt component and integrate it across banking and migration error states so users always have a manual import escape hatch. Add retry with exponential backoff to Arcim API client for transient failures (429, 502, 503, 504) and timeouts. Expand import page deep-linking with ?mode= parameter. Add persistent error banner on settings page for bank connection failures. Include 18 new tests for the Arcim client covering retry, backoff, pagination, timeout, env validation, and singleton resource unwrapping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — setActiveTab, test cleanup, redundant clearTimeout - Add missing setActiveTab('banking') when handling bank_error query param so the error banner is actually visible (P1) - Guard env-var cleanup with try/finally in arcim-client tests to prevent state leakage on assertion failure (P2) - Only mock retry-range setTimeout delays in backoff test, letting AbortController timers pass through real setTimeout (P2) - Remove redundant clearTimeout in catch block — finally handles it (P2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add BFL-compliant counterparty names to journal descriptions and cancelled entry status Journal descriptions now include customer/supplier names for traceability (e.g. "Kundfaktura 1001, Foretag AB"). Failed draft entries are marked as 'cancelled' instead of deleted, respecting immutability constraints. Includes DB migration for the new journal_entries status value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — Swedish typos, missing source type, trigger and reversal cleanup - Fix Swedish spelling: leverantor → leverantör in all supplier description prefixes - Add supplier_credit_note to supplierSourceTypes in VAT declaration so credit notes correctly reduce reverse-charge bases (ruta 20–24) - Mark orphaned concurrent reversals as cancelled instead of attempting deletion that the immutability trigger blocks - Allow posted → cancelled transition in trigger for orphaned reversal cleanup - Restrict cancelled entry line trigger to DELETE-only (block INSERT/UPDATE) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use main's Step3TaxRegistration (onboarding restructured in PR #54) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: retrigger Greptile review Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
03b569d708 |
refactor: consolidate extension system to general-only with manifest-driven architecture
- Remove all sector-specific extensions (construction, ecommerce, export, hotel, restaurant, tech) — only general-purpose extensions remain - Move NE-bilaga and SRU export from extensions to core reports (lib/reports/) - Move moms-box-mapping from extensions/export/shared to lib/vat/ - Replace per-extension API routes with catch-all dispatcher (app/api/extensions/ext/[...path]/route.ts) - Add manifest.json for each extension with metadata, env vars, and deps - Add api-routes.ts pattern for extension-defined API endpoints - Add code generation scripts (generate-extension-registry, create-extension) - Add extensions.config.json for opt-in extension loading - Add extensions.schema.json for config validation - Add email service interface with noop default (lib/email/service.ts) - Add CI workflow (core-build.yml) to verify core builds with zero extensions - Add migration 045: expand account_type CHECK for untaxed_reserves - Update CLAUDE.md with comprehensive extension system documentation - Update all report engines and bookkeeping services for new imports - Clean up extensions.schema.json to only list existing extensions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
acb85edf4a |
feat: wire Zod validation into API routes, improve types and components
- Add 8 new Zod schemas (UpdateCustomer, UpdateSupplier, UpdateSupplierInvoice, UpdateAccount, BankUnlink, RunReconciliation, CorrectJournalEntry, EvaluateMappingRules) and wire validateBody() into 24 JSON-body API routes - Remove redundant manual validation checks replaced by Zod - Add comprehensive schema tests (222 tests) - Improve type definitions in types/index.ts with expanded interfaces - Refactor extension types (push-notifications, receipt-ocr) for cleaner imports - Update transaction components (BatchCategorySelector, SwipeCategorizationView, QuickReviewDialog, VatTreatmentSelect) and invoice inbox workspace - Add invoice-inbox utilities and type decoupling tests - Fix NE-bilaga, SRU export, and invoice PDF template type usage - Update CLAUDE.md with expanded architecture documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
fa5e14de83 | Fixing bug | ||
|
|
2f83f232c6 |
test: add API route tests for 10 critical endpoints
Add test coverage for invoices, transactions, bookkeeping, and supplier invoice API routes. Includes test helpers (createMockRequest, parseJsonResponse, createMockRouteParams, createQueuedMockSupabase) and fixture factories (makeInvoice, makeCustomer, makeSupplier, makeSupplierInvoice, makeCompanySettings). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |