4e2ca3f2a89e505d65d377af04eaec16b6f119e4
101 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
072aedeaf9 |
Fix/supp ag fb (#1023)
* fix: prevent credit notes from entering payment flow * fix: persist and display customer personal numbers * feat: configure automatic invoice reminder days * fix: issue credit notes through send flow * chore: add repository agent guidance * feat(mcp): route tools across user companies * fix(articles): delete unused register entries * feat(invoices): improve issued invoice actions * feat(supplier-invoices): retain uploaded source documents * docs: record implementation decisions * feat: enhance customer personal number handling and validation - Updated CustomerForm to allow personal numbers in the format of "********-1234" for individual customers. - Added validation to ensure personal numbers are only accepted for individual customers in CreateCustomerSchema. - Implemented masking and encryption for personal numbers to enhance data protection. - Introduced new utility functions for masking and encrypting personal numbers. - Added database migration to enforce unique constraints on credit note relationships and prevent duplicate entries. - Enhanced error handling and logging for credit note issuance and invoice processing. - Updated tests to cover new credit note creation guards and personal number handling. * test: enhance list companies test with supabase query mocks |
||
|
|
b6332e9ff4 |
Fix/skv connection flow (#1015)
* feat(salary): one-click AGI submission with filing state machine and success feedback The AGI panel required users to know that "Ladda ner AGI-fil" was the generate step, then click submit, signing link, and kvittens manually. A nollkorning filing stalled on "AGI-XML saknas" pointing at a UI path that does not exist. - New primary button "Lamna in till Skatteverket" chains the existing endpoints client-side: generate XML if missing, POST underlag, poll kontrollresultat, create signing link, open Mina Sidor in a tab opened synchronously at click (popup-blocker safe). Inline stepper shows each step; the four old buttons become collapsed advanced/recovery actions, auto-expanded in stale-draft and rejected states. XML download stays visible and free for manual filing. - deriveAgiFilingState() + useAgiSubmission() lift the per-period submission record to the run page: the progress rail and salary hero now render the real state machine (generated, underlag inskickat, vantar pa BankID-signatur, inlamnad med kvittensnummer) instead of telling users to "lamna in" an already-submitted declaration. - Success card with kvittensnummer and signature metadata once signed, plus a toast when a poll flips the state while the page is open. - AGI kvittens cron every 15 min instead of every 2 h so filings signed on another device get stamped and emailed promptly. - Advanced submit also auto-generates, and the stale "Lon -> AGI -> Generera" error text now points at the real buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(enable-banking): instant OAuth callback feedback and dead-attempt cleanup The bank redirect landed on a blank page for the several seconds the callback spent exchanging the PSD2 session and mirroring accounts, and every failed connect attempt left a status='error' row that rendered forever as an "Atgard kravs" card next to a successful retry, showing duplicate connections to the same bank. - Stream a branded "Slutfor bankanslutningen" progress page from the callback: the shell flushes before the session exchange starts and a script/meta redirect follows when the work completes, with a 30s slow-work escape hatch. Fast outcomes (denial, bad params, unknown state) keep their plain redirects. - Delete never-activated connection rows (no session_id, no accounts_data) on denial or exchange failure, and sweep leftovers for the same bank on the next connect. Established connections keep their "Atgard krävs" card via the accounts_data guard; FKs are ON DELETE SET NULL so deletion has no dependents. - Show "Banken ar ansluten: hamtar dina konton" while the settings panel loads after the callback instead of an anonymous spinner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): reject re-send of issued invoices and gate bookkeeping on the sent flip A direct POST to /api/invoices/[id]/send against an already-issued invoice re-emailed the customer and posted a second revenue verifikat (createInvoiceJournalEntry has no dedup), overwriting journal_entry_id and orphaning the first entry. Only the UI hid the button; the v1 route and the MCP commit executor already rejected non-drafts. - Non-draft invoices now return 409 INVOICE_ALREADY_SENT. - The draft to sent status flip is an optimistic lock (status guard plus row-count check); journal entry, accrual schedules, PDF archival and the invoice.sent event only run for the request that won the flip. - On a flip failure the journal entry is deferred: the row stays draft and a retry re-runs the pipeline, ending with exactly one verifikat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): payment links, failure visibility and sandbox guard for recurring auto-send - sendInvoiceFromSchedule now auto-creates an online payment link via applyPaymentLinkToInvoice before rendering and passes the payment link QR to the PDF: parity with the dashboard and v1 send routes, which recurring invoices silently lacked. - The recurring cron persists last_run_warning both when a claimed run throws (hourly retries stay visible on the schedule) and when a stale schedule is rolled forward, so a deterministic failure can no longer skip a month silently. - Auto-send is blocked for sandbox companies at the email chokepoint (freeze-and-retain: the invoice is still generated as a draft), covering both the cron and the run-now route with one guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): close the Fortnox payroll API gaps (phases 1-4) Payroll now runs end-to-end through the open API, including onboarding a client from another payroll system, with every write staged for approval. - v1: per-employee payslips (list/detail/PDF), payslip line writes, run roster attach/remove, absence ranges (per-day storage), jamkning fields, cutover opening balances (single + atomic bulk PUT), vacation balance + vacation-year-close. PUT added to the wrapper's idempotency/ test-key set (test keys could otherwise write through PUT). - MCP: 10 new tools (get_employee/get_payslip/list_absence/ get_vacation_balance reads + staged update_payslip_line, register_absence, create_employee, update_employee, set_employee_opening_balances, close_vacation_year), executors, risk tiers, op-type CHECK expansions. create_employee encrypts personnummer at staging: pending_operations never holds plaintext. - Scope-map audit retrofit: 11 formerly unmapped tools now scoped; BREAKING for keys that relied on the 4 default-allow writes. - Cutover: employee_opening_balances (derived lock trigger, self-unlocks on run correction), engine YTD/karens/liability integration, Ingaende saldon section in the employee editor. - Arbetsschema-lite: employees.hours_per_week/workdays_per_week drive the hourly/daily divisors; legacy 173/21 preserved exactly at defaults so existing pay math is byte-identical. - Vacation ledger + semesterberedning/arsavslut: recomputed per-year day balances (synced on book/correct, non-fatal), year-close with the min-20 floor, 5-year sparade-dagar expiry to forced payout, and a 2920/2940 drift adjustment via the bookkeeping engine; Semester dashboard card with preview-then-confirm dialog. - Fix: Zod 4 defaults leak through .partial(), which made every sparse employee PATCH fail validation and reset defaulted columns. Migrations 20260713100000/101000/110000/121000/122000 (applied to staging with version rows; prod via merge). vacation_ledger renamed from 20260713120000 to avoid colliding with vat_declaration_totals_rpc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: cut dashboard page-load latency (region, round trips, caching, VAT RPC) The dominant cost was infrastructure: Vercel functions ran in iad1 (Washington D.C.) while Supabase (DB + auth) lives in eu-north-1 (Stockholm), so every request paid 4-5 transatlantic round trips of auth + company resolution before doing any real work (measured 530-1900ms for single-query GETs in prod logs). Pin functions to arn1 and cut the redundant work on top: - vercel.json: functions to arn1, same city as the database - getActiveCompanyId: preference + first-membership queries run in parallel; the fallback result doubles as validation in the common single-company case (one round trip instead of two sequential) - withRouteContext: Server-Timing header and authMs/companyMs/handlerMs in the op-completed log, so latency is attributable per phase - dashboard layout: nav badge counts off the critical path; DashboardNav loads them client-side via the new use-worklist-badges SWR hook with debounced realtime revalidation - swr (new dependency, approved): global provider; useCompanySettings shares one cache entry across consumers and renders from cache on back-navigation instead of re-showing skeletons - /pending: realtime refetch debounced; bulk operations previously fired 4 requests per row-change event - VAT declaration: new get_vat_declaration_totals RPC returns per-account totals, settlement-shape detection (#984) and source_type counts in ONE round trip instead of paging every entry+line through PostgREST. Account lists stay TS-side parameters so ACCOUNT_RUTA remains the single source of truth. Shape-exclusion coverage moved to tests/pg/vat-declaration-totals-rpc.pg.test.ts; DDL already applied to staging. - bundle: CommandPalette lazy-mounts on first Ctrl/Cmd+K, AgentChat dynamic-imports the markdown parser, @vercel/speed-insights (new dependency, approved) added for real-user timings The /salary fetch-waterfall fix from the same effort already landed inside 2084a756. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): settle öre-rounded payments from the mark-paid flow An invoice with öresavrundning shows a rounded "Att betala" on the PDF; the customer pays that amount (up to 50 öre off the stored öre total) and the invoice-page mark-paid flow rejected it with MATCH_AMOUNT_EXCEEDS_REMAINING: a dead end, while the bank-transaction match flow already absorbed the residual to 3740. - PaymentBookingDialog now proposes the rounded bank leg plus the 3740 residual line (credit when rounded up, debit when rounded down), resolved via getDisplayTotal from the per-invoice override and company_settings.ore_rounding. - settleInvoicePayment and the v1 mark-paid route absorb the sub-krona residual, gated by planInvoicePaymentForLines: absorption applies ONLY when the caller lines carry the exact residual on 3740; otherwise the strict plan applies (sub-krona partials stay partial, no-3740 overshoots keep the 400), so the GL can never diverge from the AR sub-ledger. - planInvoicePayment absorb-band boundary tightened to >= 1 kr: an exactly-1-kr overshoot used to slip past both the guard and the absorb branch and silently over-record paid_amount (pre-existing on the bank-match path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): resolve all 7 PR compliance findings - ASVS V3.3: per-request CSP nonce on the enable-banking finalize page (mirrors the mcp-oauth consent page); inline scripts are nonce-bound - ASVS V16: decouple callback finalize work from the response stream (eager promise + next/server after()) so a client disconnect cannot drop session persistence or the consent_granted audit emit - ISO 27001 A.8.15: failed audit-event emits log through the structured logger with a stable message for log-based alerting - ASVS V2.3: recurring-invoice cron and run-now routes resolve isSandboxCompany themselves and pass an explicit suppressAutoSend flag (defence in depth around the email chokepoint, freeze-and-retain kept) - ISO 27001 A.8.11: stagePendingOperation rejects plaintext personnummer-bearing keys in params/preview_data (key-based guard; EF org numbers make value-matching unsafe) - ASVS V4.5: employee PATCH body is truly sparse; cleared number fields are omitted instead of resetting DB values to hardcoded fallbacks - ASVS V8.2.1: route-level tests pin the v1 cross-company deny (404 by convention, not 403) on the payslip PDF endpoint Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: implement vacation-year basis change validation and error handling - Added tests to block vacation-year basis changes when open balances exist. - Implemented error handling for open-balances guard query failures in the settings route. - Enhanced absence route to reject reversed date ranges with a validation error. - Updated absence handling to use atomic upserts instead of delete+insert for better performance and reliability. - Refactored salary calculation logic to correctly handle age-based avgifter rates according to Skatteverket's rules. - Improved error messaging for vacation year closure adjustments. - Adjusted employee opening balances handling to preserve audit information during upserts. * feat(settings): add validation to block vacation-year basis change with open balances feat(absence): reject reversed date ranges in absence queries fix(absence): update absence handling to use atomic upserts instead of delete+insert fix(employee): improve validation for jamkning dates in employee updates fix(opening-balances): ensure created_by field is preserved during upserts test(absence): enhance tests for absence range and date validations test(calculation): add tests for age-based avgifter rates and edge cases test(semesterberedning): validate vacation year closure adjustments and error handling test(employee-opening-balances): update tests to reflect changes in salary_run_employees schema * fix(migrations): implement NOT VALID constraints for pending_operations and add validation migration --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b51af3d80 |
feat(agent): 'Vad din agent vet' page rendering the ledger context (P2) (#935)
* feat(agent): 'Vad din agent vet' page rendering the ledger context (P2) The human-facing surface for the openwiki ledger-context: a read-only page that renders the exact payload the AI agent reads (Accounted://ledger/context + the briefing digest) as a legible profile of how this company books. - Route app/(dashboard)/agent-knowledge (server component) calls the shared buildLedgerContext(supabase, companyId) directly: one payload, two renderers, no new API or data path. - Sections mirror the payload 1:1: coverage/freshness strip, counterparty patterns (monochrome confidence bars + seen/agree evidence), supplier patterns, explicit rules shown as authoritative instructions distinct from observed patterns, account usage, VAT profile, conventions. - Nav entry in the Analys group (icon Brain), ungated so it doubles as an upsell; flip requiredCapability to paywall. - Design per .claude/rules/design.md (PageHeader, Card, Table, Badge, AccountNumber BAS tooltips); sv + en strings (agentKnowledge namespace). VAT/BAS labels stay Swedish in both locales per i18n rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): deep entity-resolved analysis + radial graph for the knowledge page Reworks the 'Vad din agent vet' page from tables into a radial-hub graph driven by a new full-history deep analysis, per founder feedback. - fix(rpc): median_booking_lag_days now measures real posting promptness via committed_at, not entry_date (which the bank flow sets to the transaction date, giving a ~0 tautology: 151/152 on prod). migration 20260708120000. - feat(rpc): get_ledger_deep_context (migration 20260708130000): full-history, deterministic, read-side. Merges counterparties by normalize_counterparty_key (e.g. Claude = 14 bookings across 12 name variants, weekly, 9 710 kr, always 5420), mines booked verifikat for SEK spend (coalesce amount_sek), detects recurrence cadence, dominant account + share, plus supplier entities. Storno excluded, corrections kept; 19xx/26xx excluded from the dominant contra. - LedgerGraph: radial SVG (company center, accounts inner ring, payees outer ring), hover/focus reveals variants + spend + cadence + account. Keyboard focusable nodes with per-node accessible names + a screen-reader data table. - Page fetches the deep context alongside the light context; coverage strip gains tracked-payee / recurring / tracked-spend stats. sv + en strings. - 14 pg tests (light + deep) green; both RPCs applied to prod + version-matched. Reviewed by an adversarial multi-lens pass (accounting/SQL, frontend/a11y, prod-fact verification); all four verified findings fixed (SEK currency, storno-lag guard, keyboard a11y, spacing tokens). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): gentle mount fade-in for the radial map (reduced-motion safe) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): render the page for a rules-only company (empty-state edge case) isEmpty ignored explicit_rules, so a company with configured mapping rules but no posted transactions hit the 'hasn't learned anything' empty state and lost its rules section. Rules are independent of bookings. (CodeRabbit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): show Kompetens (skills) + Fakta (memory) on the knowledge page The 'Vad din agent vet' page now shows the full picture of what the agent knows: alongside the booking map, a compact read-only view of its Kompetens (the Swedish accounting/tax knowledge atoms it ships with, grouped by tier as chips with active/dormant state) and the Fakta it remembers (top learned facts with kind + source), each linking to /settings/assistant for full management. Server-rendered via a new buildAgentCompetence() that mirrors GET /api/agent/skills + /api/agent/memory. Also renders in the no-bookings case so a new company still sees its agent's competence. sv + en strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(agent): restructure knowledge page - hero graph + tabbed detail Declutters the page per feedback: the booking map is the always-visible hero, and the supporting detail (Kompetens · Minne · Regler & profil) moves into tabs so only one view shows at a time instead of a long card stack. Split AgentCompetenceSections into standalone CompetenceCard + FactsCard for the tabs; removed the top stat row on request. sv + en. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): Reconciliation Aurora rewrite of the ledger knowledge graph Full rewrite of LedgerGraph: node area = sqrt(spend), colour = cadence, shape = supplier/counterparty, confidence = depth-of-field; on-mount descriptor-collapse animation with xN badge; cadence pulse veins; deterministic seeded layout; framer-motion only (no new deps); keyboard navigation, reduced-motion and sr-only support. Build-verified; 3-lens adversarial review findings fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): sample-size-honest confidence in the ledger knowledge graph dominant_account_share was raw cnt/total, so a counterparty with a single booking rendered as '100% säkerhet': fake certainty by construction (the data_quality_master Item-C / P3 finding). New migration replaces get_ledger_deep_context with a Laplace-smoothed share (cnt+1)/(total+2) (1/1 -> 0.67, 3/3 -> 0.80) and exposes the raw evidence as dominant_account_count / dominant_account_total. The detail card now shows 'Bokförd hit i k av n fall' under the confidence bar; the existing focus buckets, stroke widths and percent labels inherit the honest value unchanged. pg-real test updated to guard the n=1 case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
98d0c7f2d0 |
Add/stripe skv (#1004)
* fix(salary): align pain.001 salary file with the Swedish domestic bank dialect Verified against the Swedish Common Interpretation of ISO 20022 (Bankforeningen, Common Payment Types in Sweden, Appendix 1 Example 4: Salaries) and Nordea Corporate Access pain.001 examples v2.6 (2026-06-22), and XSD-validated against the official pain.001.001.03 schema: - drop SvcLvl SEPA (SEPA credit transfers are EUR-only; omitting SvcLvl gets the domestic NURG default) - drop RmtInf (not allowed for SALA salary payments; the beneficiary statement text comes from the Dataclearing LON code) - address employees domestically: clearing as CdtrAgt ClrSysMmbId SESBA, account WITHOUT clearing as CdtrAcct Othr with SchmeNm BBAN - share the clearing/account split (Swedbank 5-digit shift, Nordea personkonto prefix dedup) between the LB and pain.001 generators via splitDomesticBankAccount, fixing pain.001 duplicating the personkonto clearing - clamp MsgId/PmtInfId/InstrId/EndToEndId to Max35Text with the per-tx counter surviving truncation; carry the org number on Dbtr - return 400 from the pain001 route on an invalid clearing instead of emitting a broken file Also includes two unrelated decision-log lines from the parallel revisor-review session (DECISIONS.md is a shared append-only log). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(nav): surface the year-end chain in the sidebar Add Periodiseringar, Arsredovisning (aktiebolag only) and Inkomstdeklaration (INK2 for AB, NE-bilaga for EF) to the Skatt & bokslut group, in workflow order. Entity gating via a new entityOnly flag on NavItem; isActive carve-outs extended so exactly one row lights up for the new routes. Driven by an external revisor review that concluded these features did not exist because none of them were reachable from the nav. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(stripe): Stripe Connect integration behind config gate Connect OAuth per company (only the acct_ id is stored), automatic single-use Payment Links on invoice send, deterministic payment settlement against 1686 (BAS moved acquirer receivables 1580 -> 1686), payout booking with reverse-charge fees (6570 + 4535/4598 + 2645/2614), and a 15-minute sync cron. Non-deterministic events land as needs_review, never guessed at. Fully dark without STRIPE_CONNECT_CLIENT_ID: connect returns 503, the send hook and cron no-op, and the settings page shows 'Kommer snart' (hosted) until the Connect platform is verified. Self-hosted keeps the honest not-configured message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): add shared completeTaxDeadline and fix dead AGI deadline auto-complete generate-declaration.ts has updated non-existent columns (type/period/ status) since inception, so the arbetsgivardeklaration deadline was never auto-completed. Replace with a shared helper targeting the real schema (tax_deadline_type/tax_period/is_completed), also used by the kvittens crons and moms handlers in the follow-up commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rot-rut): import Skatteverket beslutsfil and record decisions on payout requests Parse the beslutsfil JSON from Skatteverkets rot/rut e-tjanst and record godkant belopp on the matching begaran: matched by stored skv_referensnummer first, then exact name among active undecided requests; arenden by fakturanummer then personnummer, exactly-one or the beslut errors (all-or-nothing). Never auto-settles: recording the beslut and booking the payout are separate acts. Exposed as an API route and the gnubok_import_rot_rut_beslut MCP tool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skatteverket): system auth for background reads, one-click VAT submit, kvittens notifications Hybrid auth program: system CCG (org certificate) for background reads while personal BankID stays for interactive submissions, since SKV per-flow refresh tokens live 65 min and crons structurally cannot run on them. All system-auth code sits behind SKATTEVERKET_SYSTEM_AUTH_MODE (default off) with a stub transport until the Expisoft cert and CCG avtal land; auth resolution is centralized in resolve-auth.ts. Also in this change: - One-click VAT submit chaining kontrollera -> utkast -> las server-side with a stage discriminator; step-by-step buttons demoted to the overflow menu. - Kvittens crons (AGI + new VAT schedule) with email-only notifications, deduped in notification_log under the new skv_kvittens type. - Ombud grant probe + verification UI in the connect panel, and a dashboard promo card for unconnected companies. - skatteverket_company_connections table with pg-real coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): auto-settle AGI tax payment from skattekonto and surface SKV reconnect on the tax card The "Skatt att betala" card only cleared via the manual mark-paid button on the run detail page; the promised automatic flip from the Skattekonto sync was never implemented, so paid periods stayed red. - settleAgiTaxPayments: during every skattekonto sync, a booked "Arbetsgivardeklaration YYYYMM" debit row settles the matching agi_declarations.tax_paid_at, but only when the amount equals the declared total to the ore and the account is not in deficit (deterministic; drift or deficit falls back to manual). - Salary overview card: reconnect hint when the SKV token needs re-consent (link to /settings/tax, silent when the extension is off), plus an inline "Markera som betald" button reusing the existing endpoint and salary_payments strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add cloud backup scheduling and alerting features - Implement unit tests for scheduling logic in `schedule.test.ts`, covering various scenarios for determining if a backup schedule is due. - Create a new module `backup-alert.ts` to handle failure alerts for cloud backup auto-sync, including email notifications for reauthentication and repeated failures. - Introduce `schedule.ts` to manage scheduling logic, including handling local time zones and converting between local and UTC hours. - Add CSV report generation functions in `archive-csv.ts` for trial balance, income statement, balance sheet, and general ledger, ensuring compatibility with Swedish Excel formats. - Create a README generator for the archive structure in `archive-readme.ts`, providing clear documentation for users accessing backup files. - Implement tests for CSV report generation in `archive-csv.test.ts`, ensuring correct formatting and content. - Establish a full-archive coverage contract test in `full-archive-coverage.pg.test.ts` to ensure all company-scoped tables are properly classified for backup. * fix(stripe): correct invoice clearing reference and improve type safety in sync logic * fix(invoices): narrow accountingMethod before resolveInvoicePaymentSourceType settleInvoicePayment takes accountingMethod as a raw settings string, but resolveInvoicePaymentSourceType requires the 'accrual' | 'cash' union. Normalize at the call site (anything but 'cash' books as accrual), matching the existing useCashEntry semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address CodeRabbit review findings and nitpicks on PR #1004 Review findings: - backup settings redirect: always force view=export over incoming params - AGI/VAT kvittens crons: isolate best-effort post-submit calls, check the signed-state persist error, guard recovery calls in catch blocks so one company cannot abort the rest; surface grant_revoked in the run summary - kvittens notifications: atomic claim-first dedup with a partial unique index; map non-uuid reference keys to deterministic uuids - grant probe: record the actual 2xx status; mTLS transport: handle response-stream errors - stripe: amount-aware idempotency keys for payment links; emit stripe.disconnected on upstream revocations - ROT/RUT beslut import: mutate in-memory request state after apply, move item + header writes into an atomic apply_rot_rut_beslut RPC, add rot_rut_payout to JournalEntrySourceTypeSchema - migrations: use NOT VALID + VALIDATE CONSTRAINT for CHECK constraints on journal_entries, notification_log and rot_rut_payout_requests - cloud backup: hour_utc-only schedule updates clear stale hour_local Nitpicks: - stripe sync: enforce the cron time budget inside per-connection event processing with idempotent cursor progress; maybeSingle for settings; honest partial-customer DTO shared with the settlement boundary - shared applyPaymentLinkToInvoice helper for both invoice send routes, v1 docblock documents step 6b and PAYMENT_LINK_FAILED - settings panel: drop redundant decodeURIComponent - cloud backup: document worst-case archive memory headroom Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
650c7be5e1 |
fix(bookkeeping): revive counterparty template learning (dead since the multi-tenant refactor) (#989)
* fix(bookkeeping): revive counterparty template learning, dead since the multi-tenant refactor (#865) The learning half of counterparty templates has written nothing since 2026-03-30 (prod: 750 SIE imports, zero new templates). Two stacked bugs: - The multi-tenant refactor re-scoped categorization_templates to company_id and the lib stopped writing user_id, but user_id kept its NOT NULL: every insert failed with a null violation that supabase-js returns rather than throws, so nothing was ever logged. Migration 20260711100000 drops the NOT NULL and the dead user_id indexes. - Four of six learning call sites (both categorize routes, categorize-core, the MCP server) passed the auth user id as companyId, so even with the column fixed the writes would fail FK/RLS and corrections could never find the template they were correcting. Hardening while in here: - insertOrUpdateTemplate now checks every write result, logs failures, and returns whether a row was written; populateTemplatesFromSieVouchers reports only templates actually persisted. - Sign-mismatched matches (an incoming refund matching an expense-learned template) previously booked backwards: debit expense / credit bank for money coming IN. They are now mirrored into the correct refund shape (VAT leg reversed for deductible input VAT), flagged requires_review, and excluded from template/rule learning so a refund can never flip a learned template. - Template amounts are computed from the SEK-resolved amount, so foreign-currency transactions no longer produce unbalanced multi-line entries (or VAT computed on foreign units). - SIE extraction no longer hardcodes 25% for 2641 (rate-agnostic in BAS): the rate is inferred from voucher amounts and snapped to 25/12/6%, and reverse-charge counterparties learn vat_treatment='reverse_charge' instead of losing the RC legs (which also no longer poison the ratio base). - New pg-real test locks the exact insert column set against the real schema, so a schema/code drift like this can't ship green again. Closes #865 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): mirror fiktiv-moms legs on RC credit notes, exclude import VAT accounts from ratio base Compliance-review follow-ups on #989: - REVERSE_CHARGE_VAT_ACCOUNTS gains the import output-VAT accounts (2615/2625/2635), which pair with 2645 in import vouchers exactly like the RC pairs and must not shrink the business ratio base. - A sign-mismatched match against a reverse_charge template (an RC supplier's credit note) now mirrors both fiktiv legs (credit 2645 / debit 2614) instead of booking gross, so Ruta 30/48 net back to zero. The income line-builder nets VAT credits against debit legs to keep the mirrored pair balance-neutral (identical result for all existing credit-only output-VAT paths). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(types): CategorizationTemplate.user_id is nullable since 20260711100000 (CodeRabbit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): use roundOre for the VAT netting, keep the ore-round ratchet at baseline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): review-gate stale 12% templates across the livsmedel transition, pattern-aware direction guard Compliance-review round 2 on #989: - Livsmedel VAT dropped 12% -> 6% on 2026-04-01 (Prop. 2025/26:55) while restaurang/hotell stay at 12%. A reduced_12 template whose last_seen_date predates the transition can no longer be trusted unreviewed: its match is flagged requires_review until a post-transition approval refreshes it (re-approval keeps 12%, a correction relearns 6%). Actively-confirmed 12% counterparties flow without friction. - The opposite-direction correction guard now falls back to the line pattern's business sides when the legacy fields are both settlement-ish and cannot classify a multi-line template. - Documented the accepted import-RC mirroring limitation (2614 vs 2615 ruta attribution) and the netted-vatCredit precondition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aec81cb7ad |
fix(db): lock down exchange_rates writes, drop duplicate JEL index, receipts anon read (#969)
Three Supabase-advisor findings from the 2026-07-09 production log triage: 1. exchange_rates (rls_policy_always_true): the exchange_rates_insert policy was WITH CHECK (true) for authenticated, letting any signed-in user poison the shared FX cache that feeds money math (amount_sek on ingested transactions, invoice SEK conversion). Migration 20260710100000 drops the policy and revokes INSERT from anon/authenticated; only the service role writes the cache now (the 05:00 enable-banking sync cron and the v1 API-key paths both use the service client). writeCachedRate() in lib/currency/riksbanken.ts was already fail-soft and never inspects the upsert result, so user-client paths (bank file import, refresh-exchange-rate) keep returning the fetched rate unchanged when the cache write is rejected; documented and covered by a new unit test. 2. journal_entry_lines (duplicate_index): idx_journal_entry_lines_entry and idx_journal_entry_lines_entry_id are byte-identical btree indexes on (journal_entry_id), verified via pg_indexes on prod. Migration 20260710101000 drops idx_journal_entry_lines_entry (created outside the migration history); the repo-defined _entry_id stays. 3. receipts bucket (public_bucket_allows_listing): receipts_public_read gave anon SELECT over every object in the bucket, enabling anonymous listing. The bucket is unused: no code references it, public.receipts has 0 rows in prod, 2 orphan objects from 2026-02-26. Migration 20260710102000 drops the anon policy; authenticated own-folder policies stay untouched. New tests/pg/db-advisor-lockdowns.pg.test.ts covers all three (authenticated INSERT rejected, SELECT still works, privilege revoked, duplicate index gone, anon cannot list receipts). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2e7931b36b |
feat(customers): let users set a customer number shown on the invoice (#957)
Implements #914 (kundnummer on customers, printed on the invoice PDF). - Migration: nullable text column customers.customer_number, no unique constraint in v1 so existing rows and imports keep working. - API: CreateCustomerSchema/UpdateCustomerSchema accept an optional customer_number (trimmed, max 32 chars, nullable-then-optional so the OpenAPI registry sees it as not required); create/update routes persist it and normalize empty string to null so it can be cleared. - v1 public API: customers create/detail/update round-trip the field (insert and update field lists, response projections, response schemas), and the invoices :send route fetches customer_number in its explicit customer join so the emailed PDF matches the downloaded one (the pdf route already selects customers(*)). - UI: optional Kundnummer field in CustomerForm (next-intl keys in both sv and en), wired into the edit dialog's initialData; read-only Kundnummer row on the customer detail page's business-details card. - Invoice PDF: renders "Kundnr:" / "Customer no.:" in the customer box when set; the PDF reads the live customers join, so no snapshot column is needed. - Tests: route tests cover 400 validation, trimming, clearing with null/empty, and omit-leaves-untouched on POST and PATCH; v1 tests cover the create/update round-trip (insert/update payload + response projection) and the :send customer-join projection. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bacc5914af |
Fix/dependabot cus feedback (#946)
* feat(bookkeeping): per-account default VAT, oresavrundning momsfri Add a per-account "Standard moms" setting to the chart of accounts and use it to auto-fill the moms on a leverantorsfaktura-rad when that konto is picked. Oresavrundning (3740) ships as "Ingen moms", so a rounding line no longer inherits the 25 % rad-default and skews the moms. - chart_of_accounts.default_vat_rate (0/0.06/0.12/0.25, CHECK-constrained) - BEFORE INSERT trigger ships 3740 momsfri on every insert path; backfills existing 3740 rows - kontoplan editor: dead free-text momskod replaced with a Standard moms select - supplier-invoice rad auto-fills the rate from the konto default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(supplier-invoices): configurable start number for the ankomstnummer series Add a company_settings.next_arrival_number start floor so a company can continue its leverantorsfaktura numbering from a previous system (e.g. Fortnox) instead of restarting the ankomstnummer at 1. get_next_arrival_number now floors the series via GREATEST(MAX(arrival_number)+1, next_arrival_number), so the floor can never move the series backwards or collide with the (company_id, arrival_number) unique index. The RPC is hardened while rewritten: SET search_path to empty, schema-qualified refs, and an auth.uid() membership check matching generate_invoice_number. Includes the settings UI field, sv/en strings, migration, and pg-real coverage. The CompanySettings type and Zod schema field for this feature landed earlier in 1bf3b641 (swept into the per-account VAT commit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dependabot): reduce open pull requests limit and group updates for better management --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8dde46ad96 |
fix(db): reconcile prod-orphaned migrations blocking Supabase branching (#942)
* fix(db): reconcile prod-orphaned migrations blocking Supabase branching Prod's schema_migrations carries three versions with no committed file on main, leaving the default Supabase branch in MIGRATIONS_FAILED and stopping preview branches from being created: 20260707113729 add_transactions_enrichment (adopted from #927) 20260708120000 ledger_stats_committed_at_lag (adopted from #935) 20260708130000 ledger_deep_context (adopted from #935) Adopt the byte-identical SQL under the exact apply-time versions, plus the matching pg-tests and fixtures for the two RPCs so pg-real stays green: 20260708120000 switches get_ledger_usage_stats' median_booking_lag_days to committed_at, so the existing test now asserts the new behavior. Idempotent (ADD COLUMN IF NOT EXISTS / CREATE OR REPLACE FUNCTION): no-op on prod, clean on fresh replays, no-op on #927/#935's next rebase. The knowledge-page UI/lib/i18n stay in #935. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deps): pin @anthropic-ai/bedrock-sdk to 0.29.1 0.32.0 (grouped dependabot bump #884) broke Bedrock streaming in prod: empty stream / "request ended without sending any chunks", taking down the in-app AI assistant and invoice OCR. Local dev ran the stale 0.29.1 in node_modules, so it only failed on deploys built fresh from the lockfile. Revert to the six-week-stable 0.29.1; creds/region were never the cause (proven AKIA key + eu-west-1). Guard against an accidental re-bump three ways: exact pin (no caret), a dependabot ignore, and a pinned-dep check in scripts/checks/no-new-antipatterns.mjs (check:guards). Unpin only once 0.32.x streaming is verified against Bedrock. See DECISIONS.md. |
||
|
|
fddc58f624 |
fix(mcp): exclude VAT contra accounts from ledger-context dominant pick (#932)
Found by the switch-on check (calling gnubok_get_agent_briefing on real prod data): counterparty patterns for reverse-charge foreign SaaS (Google/ngrok/Supabase) reported dominant_account 2614 (reverse-charge output VAT) instead of 5420 (software expense). The dominant_account CTE in get_ledger_usage_stats excluded only 19xx, so on a reverse-charge booking (expense + 2645 + 2614 + 1930) the three non-bank accounts tie at equal counts and the account_number ascending tiebreak picks the low VAT number. Migration 20260708110000 CREATE OR REPLACEs the function to also exclude 26xx (always moms in BAS, never characterizes a counterparty). Loan/tax counterparties booking to 23xx/24xx/25xx/27xx stay eligible. supplier_patterns is unaffected (it aggregates supplier_invoice_items.account_number, expense only). Regression pg test asserts 5420 over 2614 and was confirmed to fail on the old function. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a3c6566caf |
feat(mcp): ledger-context resource with per-company booking patterns (#928)
* feat(mcp): ledger-context resource with per-company booking patterns Adds Accounted://ledger/context: derived account usage, counterparty booking patterns with explicit confidence share (0.7 floor), explicit mapping rules kept separate as authoritative, observed VAT profile, and conventions. Backed by a SECURITY INVOKER get_ledger_usage_stats RPC so group-bys run SQL-side, and surfaced as a top-5 digest stanza on gnubok_get_agent_briefing so one call still bootstraps a session. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(mcp): fold source-quality prereqs into the ledger-context RPC Merchant-name normalization at the aggregation path (the splinter fix): new normalize_counterparty_key() SQL function mirroring normalizeCounterpartyName() so KORTKÖP/SWISH/date-suffixed labels merge into one counterparty key, which also makes the categorization_templates join exact. New supplier_patterns section (per-supplier dominant expense account + VAT treatment from supplier invoices; credit notes and reversed invoices excluded). account_usage excludes storno lines (they re-inflate the account a correction moved away from); the counterparty CTE keeps corrections because the transaction relink self-heals. Pattern confidence is now count-grounded evidence {seen_12m, agree, share, last_booked} instead of a bare ratio, and the digest frames it as historical frequency, never auto-book permission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent-context): use roundOre for the share ratio (antipattern ratchet) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent-context): defensive storno filter on counterparty CTE, fail-loud secondary reads Review follow-ups: the counterparty CTE now excludes source_type='storno' defensively (no live code path links a transaction to a storno, but legacy rows may predate reverseEntry's unlink; a linked storno would count the reversed category as precedent). Corrections stay included: they are the live booking after relink. Secondary reads (rules, templates, settings) now throw instead of silently reading as empty data: an agent must never be told 'no rules' when the truth is 'read failed'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8e7e7201d3 |
fix(db): drop delete_user_account RPC that bypassed BFL retention (#901)
* fix(db): drop delete_user_account RPC that bypassed BFL retention delete_user_account disabled the retention/immutability/audit triggers, deleted audit_log rows, and cascaded auth.users, destroying 7 years of legally retained rakenskapsinformation (BFL 7 kap 2 paragraf). It was SECURITY DEFINER with only a self-only guard and no REVOKE, so any authenticated user could call it via PostgREST. The product path already uses anonymize_user_account, which so far existed only on production (drift). This migration drops the dangerous RPC, commits the prod definition of anonymize_user_account verbatim, adds the profiles tombstone columns it writes (also drift), and locks grants down to authenticated only. Closes #342 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log profiles tombstone-column drift-capture decision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2c2743eb79 |
Check/salary bankid api (#892)
* fix(bankid): harden login/signup flow — polling, signup rollback, metadata merge, enrichment lookup - middleware: read BankID enrichment from the bankid_enrichment table (the extension_data path has been dead since the multi-tenant refactor), so company-less BankID users land on /select-company instead of the manual wizard - BankIdAuth: hard 6-min poll deadline; every failed poll counts toward the give-up limit; guard overlapping ticks so completion runs exactly once (a double /complete regenerated the magic link and invalidated the first, failing logins intermittently); retry clicks wait out the start cooldown instead of silently no-oping; Swedish messages for 429/unknown start errors - bankid/complete: all-or-nothing signup — delete the created user when the identity insert, app_metadata update, or magic-link generation fails, so a retry starts clean instead of hitting account_exists with an unusable account - bankid/unlink: read-merge-write app_metadata so has_password survives unlink (BankID-only users could otherwise strand themselves with no login method) - login: BankID "create account" CTA now links to /register instead of dismissing the notice; sv.json: fix missing å/ä/ö in settings_bankid strings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: move secondary guides into docs/, delete dead root files Move DOCKER.md, SELF-HOSTING.md, WHITELABEL.md and extensions.md (renamed EXTENSIONS.md) into a new docs/ folder and update all path references (README, setup.sh, .dockerignore image rules, docker-publish workflow comment, _example-branding, lib/branding/service.ts). Delete two dead root files: customer.json (stray API-test payload) and findings.md (point-in-time swarm audit export, criticals already filed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(api): security & correctness hardening + withRouteContext MFA migration across API routes Audit of ~100 app/api routes. Highlights: Security - agent/conversations: list leaked colleagues' titles + message previews (company-scoped RLS, no user filter) -> user-scoped - calendar/feed PUT: raw body into .update() allowed feed_token fixation on a public unauthenticated URL -> strict schema, content toggles only - bokslutsdispositioner: unbounded schablonintaktRate could inflate the IL 30 kap 25% periodiseringsfond cap base -> bounded - agent profile/composer/onboarding: viewers could rewrite the agent profile while sibling /verify blocked them -> role-gated Correctness - account-totals / listAssets: unbounded queries silently truncated at 1000 rows (under-counted money; skipped assets at year-end depreciation) -> fetchAllRows with stable order (+3 more pagination fixes) - voucher-gaps: swallowed detect_voucher_gaps RPC errors (BFNAR gap view could show "no gaps" when the check never ran) -> surfaced - 5 phantom-success writes (OK on zero matched rows) fixed - assets K3 component-sum validated against stale acquisition_cost -> fixed - invite silent email-send failure -> response carries email_sent; deadlines/calendar cast-then-check JSON crashes -> Zod Convention - ~44 legacy routes converted to withRouteContext (MFA); added Zod validation, corrected status codes, console.* -> lib/logger Response shapes preserved for existing callers. ~110 new tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): save a booking as a reusable template from Bokför direkt Add a "Spara som mall" action to the manual booking dialog so users can capture a kontering they just worked out as a booking template — right where they figured out how something should be booked. - derive amount-parameterised template lines from the concrete booking (settlement = the non-VAT leg nearest the total, 26xx = a VAT line with its rate snapped to the nearest standard rate, the rest = business ratios; line labels come from the loaded BAS chart) - extract the shared TemplateForm out of BookingTemplatesPanel so the booking dialog reuses the same editor, live preview and convertibility hints instead of duplicating them - save via the existing POST /api/settings/booking-templates endpoint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bokslut): render arsredovisning RR/BR at ÅRL post level — no kontonummer Bolagsverket rejected a user's filed årsredovisning with "Balansräkning och resultaträkning ska inte innehålla kontonummer": the PDF built every statement row as per-account "1930 Företagskonto" lines while the iXBRL filing path already aggregated to statutory posts, so the two artifacts diverged. The PDF statements now derive from the same K2 risbs mapping the iXBRL document uses (mapTrialBalancesToK2), via a new statement-rows.ts that emits post-level rows in uppställningsform order for both the K2 and K3 templates. Also fixed along the way: - Jämförelseår column (ÅRL 3:5 §) — previous-year trial balances now load and render; the old PDF had no comparatives at all. - mapping.warnings (unmapped accounts, RR ≠ 2099, obalans, reclass nudges) flow into ArsredovisningData.warnings so the wizard flags a non-fileable document before download. - Flerårsöversikt current/previous year overridden with the mapper's strict-3000–3799 Nettoomsattning, mirroring build-input's duplicate-fact rule, so the FB table ties to the RR. - FB eget kapital-table is post-level and drops obeskattade reserver (never eget kapital); K3 equity-changes statement uses real prior-year opening balances with derived utdelning/nyemission residuals that tie the roll-forward exactly to booked UB. - build-input dedupes warnings now that the PDF path runs the same mapping. Regression test asserts no RR/BR label ever contains a four-digit account number again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): diagnose untransferred prior-year results behind balance-sheet differens Prod incident (97 kr): a multi-year SIE migration lacked one year's omforing av arets resultat; the residual corrupted every later derived opening balance and Balansrakningen showed a bare "Differens: 97 kr" with no explanation. Continuity checking cannot catch this failure mode (prior-year UB and derived IB match per-account by construction) - the invariant that actually breaks is per-year P&L = 0 for all non-latest years. - lib/reports/imbalance-diagnosis.ts: shared detector (findUntransferredResults + buildImbalanceDiagnosis) - Balansrakning/Balansrapport attach imbalance_diagnosis when unbalanced, naming the exact culprit years; rendered in web views + PDF; MCP gnubok_get_balance_sheet inherits the field via spread - SIE import: parse-time warning when a completed year's vouchers leave a P&L residual, plus a post-import DB walk surfacing culprits as warnings and structured details.untransferredResults; the Arcim migration workspace previously dropped result.warnings entirely and now renders them - opening-balance/correct: pre-flight the company lock date and return 409 OB_COMPANY_LOCK_DATE (retryable: false, lock date interpolated in the client message) instead of the retryable 500 that invited blind retries; catch-path maps a raced trigger rejection to the same code Diagnosis runs only on unbalanced paths (zero cost when healthy) and never fails the report or the import. No migration, nothing persisted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: production error remediation — FX rates, deadlines, log levels, correction relink Batch of fixes for recurring Vercel runtime errors: - Riksbanken FX rates: persistent read-through cache (exchange_rates table), one retry honoring Retry-After on 429/5xx, bounded ingest concurrency, and an honest fallback — most recent cached observation or null, never a hardcoded rate silently booked into amount_sek. Unrated transactions stay repairable via refresh-exchange-rate. - Tax deadline regeneration inserts replacement rows before deleting the superseded set, so a failed insert no longer wipes a company's deadlines (the 23502 user_id regression did exactly that). Migration makes deadlines.user_id nullable for system-generated rows. - Route wrappers + errorResponse log 4xx outcomes at warn so only genuine 5xx reach Vercel's runtime-error clustering; client-supplied /api/log telemetry demoted to warn as well. - application/json documents (raw PSD2 responses archived per BFL) validate as parseable JSON with object/array root instead of always failing the magic-byte check. - correctEntry surfaces document-relink failures to callers, and the BFL document-immutability trigger now allows relinking underlag from a reversed entry to its correction (migration + pg test). - Middleware clears stale session cookies on /api requests too, using scope 'local' so cleanup doesn't re-trigger the failed token refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skatteverket): persist token health and stop retrying dead consents Terminal auth errors (SESSION_EXPIRED, REFRESH_EXHAUSTED, MISSING_SCOPE, TOKEN_CORRUPTED) mark the token row needs_reconsent with the error code and timestamp — SKV per-flow refresh tokens live 65 minutes, so once expired nothing recovers without a fresh BankID consent. The AGI kvittens and skattekonto sync crons skip flagged connections instead of failing every night, and the settings panel prompts for re-consent proactively. A successful reconnect resets the row to active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(banking): allocate distinct BAS ledger slots for PSD2 mirror accounts A bank returning N same-currency accounts used to map them all onto the currency default (1930/1932/1933/1934), tripping the UNIQUE (company_id, ledger_account) constraint per-account — swallowed errors left accounts silently unmirrored. allocatePsd2LedgerAccount now hands out the currency default first, then free 1931–1959 sub-account slots, skipping slots held by any existing row. - Callback persists allocations to accounts_data so the picker pre-fills reality; reconnect reuses previously mirrored ledgers instead of re-deriving (a user remap to 1935 survives). - Selection save resolves effective ledgers up front and rejects duplicates or cross-connection conflicts with a 400 instead of silently skipping the mirror. - Bank error codes + psu_type are forwarded to the settings page for every OAuth error, keying the Handelsbanken corporate fullmakt guidance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent): stage exact journal lines on categorization previews Categorization previews only carried debit/credit accounts, the GROSS amount, and separate VAT rows — read together that looks like an unbalanced 'gross on cost account + VAT debit' entry, and it misled both users and agents into rejecting correct proposals. The MCP preview and the pending-operation PATCH now materialize the exact lines the commit executor will post (net cost line, VAT line, gross bank line, SEK) via buildTransactionEntryLines, and PATCH re-derives them from the new mapping instead of spreading stale staged lines. ApprovalCard and /pending render the verifikat lines, falling back to the legacy summary only for operations staged before this fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): prune unused imported accounts from the chart SIE imports routinely bring in hundreds of accounts that were never used and clutter the kontoplan. New account_usage_counts RPC (one grouped query instead of a count per account) backs GET /api/bookkeeping/accounts/usage, and POST /api/bookkeeping/accounts/prune deletes zero-usage accounts — dry-run first, then an explicit account list capped at 2000. Accounts with journal lines are skipped, never deleted. The chart manager shows a usage column and a prune dialog grouping custom accounts vs unused BAS-seeded ones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): carry dimensions through v1 invoice and supplier-invoice surfaces Credit-note creation now copies default_dimensions and per-line dimensions from the original, so the reversing journal entry nets against the same dimension cells instead of dropping them. List/detail responses expose the dimension fields, and the OpenAPI spec snapshot follows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: batch serial Supabase round-trips on hot dashboard paths Every dashboard render pays the layout's query chain, so serialized awaits are direct wall-clock: the layout, chat conversation, invoice detail, supplier detail, select-company, and agent-onboarding pages now run their independent lookups in parallel batches, and getCompanyCapabilities folds its disabled-config read into the same round-trip. JournalEntryList hydrates the saved fiscal-year scope optimistically instead of serializing the first entries fetch behind the fiscal-periods request. The supplier detail page filters invoices server-side via a new supplier_id query param instead of fetching the whole company ledger, and the invoice editor (with its framer-motion dependency) lazy-loads so it stops shipping with the invoice list bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(salary): one-click runs, payslip delivery, payments settings, run cockpit Salary P1 batch, driving the 20-click flow toward 3 clicks: - One-click 'Starta lönekörning': POST /api/salary/runs accepts an empty body and resolves defaults server-side — period follows the latest non-corrected run, payment date from the new salary_pay_day setting, series from the per-source-type map. The separate /salary/runs/new page is gone. - Run detail page rebuilt as a step-railed cockpit (progress rail, KPI cards, employee ledger, journal preview) on a deliberately wider canvas; components extracted to components/salary/run/. - Payslip delivery: tokenized public payslip pages (/payslip/[token], backed by salary_payslip_links) plus per-employee email send with PDF — employees need no account, and the middleware exempts the route from auth redirects. - Payments settings: salary pay day, default bank, and pain.001 vs Bankgirot Lön format with per-bank upload instructions and an LB sunset warning (banks retire LB during 2026). - AGI panel: full submission status flows (stale drafts, signing links, kvittens polling, error reports); tax payment panel with skattekonto shortcut and mark-as-paid. - Salary calendar bulk editing, employee benefits/tax-card polish, municipality tax-table lookup improvements. messages/sv+en also carry the strings for the account-prune, skatteverket-reconsent, and banking surfaces committed just before this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: adopt Next 16 proxy.ts convention + repo housekeeping - Rename middleware.ts to proxy.ts with the proxy() export (Next 16 renamed the middleware convention; behavior unchanged). - Exclude dev_docs/ from tsconfig so stray snippets in planning docs don't break the build type-check. - Ratchet antipatterns-baseline down (raw-route-auth 165 → 119) to lock in the withRouteContext migration from 5cfd2b76. - template-library uses roundOre() instead of inline rounding. - database.md: drop account_balances from the key-tables list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): robust service-role detection in correction document relink relink_documents_to_correction() keyed its service-role branch on auth.role(), which reads the singular request.jwt.claim.role GUC that PostgREST v10+ and the pg-real harness no longer populate. Genuine service-role callers (pending-ops executor / MCP approve) landed in the auth gate and could not relink underlag. Read the role from the request.jwt.claims JSON directly, mirroring the canonical link_voucher_rpcs_tenant_guard convention. Validated on staging. Also: harden the salary run page's error paths (res.json().catch) against non-JSON error bodies, and roll back the pg-real service-role case in finally so an aborted transaction cannot poison a pooled connection for the next test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(documents): restore journal_entry_line_id link durability (BFL 7 kap) Migration 20260704103000 rewrote enforce_document_journal_entry_immutability to guard journal_entry_id but left journal_entry_line_id to the metadata trigger, which exempts draft-linked docs -- and the entry-level trigger only fired on UPDATE OF journal_entry_id, so a line-id-only UPDATE never invoked it at all. That let a set journal_entry_line_id be cleared to NULL, breaking the "link durable from first set" invariant (document-immutability.pg regression). Widen the trigger to fire on journal_entry_line_id too and guard it with the same uuid-durability rule as journal_entry_id (setting NULL -> uuid stays allowed; clearing/re-pointing a set value is blocked, status-independent). The correction-relink GUC path, which legitimately clears line_id when moving underlag to the posted correction, stays exempt. Validated on staging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> 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> |
||
|
|
764348e99c |
feat(dimensions): PR10 advanced — custom dimensions, hierarchy, account rules, commit enforcement (#886)
* feat(dimensions): PR10 advanced — custom dimensions, hierarchy, account rules, commit enforcement The final rung of the dimensions ladder (dev_docs/dimensions_implementation_plan.md §7 row 10): - custom dimensions: POST /api/dimensions creates registry dims (next free SIE number >= 20 when omitted; explicit numbers allowed — SIE import already mints reserved ones); register gets a 'Ny dimension' dialog with a quiet Avancerat disclosure for the #UNDERDIM parent; GET now carries parent_sie_dim_no (the column + SIE round-trip existed since PR1/PR5 — this exposes it) - account_dimension_rules (migration 20260703120000): one rule per (account, dimension) — required / default / fixed, per-rule is_active, company-scoped RLS, composite FK to the registry, value-presence CHECK - enforcement, opt-in BY CONSTRUCTION (zero rules = engine byte-identical; deliberately NO settings toggle — a rule that exists but is ignored is worse than either extreme): default/fixed apply onto line bags at draft creation (fixed overwrites, default fills); required asserts at commitEntry with a Swedish MANDATORY_DIMENSION_MISSING naming every account + dimension; the bulk-book route runs the same policy before its RPC; storno/correction paths never pass through commitEntry so history always reverses regardless of policy; rule fetches fail open incl. thrown exceptions - chart of accounts: per-account Dimensionsregler section in EditAccountDialog (Krävs/Förval/Låst, value picker, pause switch), gated on the existing dimensions toggle, quiet when empty - pickers: LineDimensionFields is registry-driven (one combobox per active dimension, cached fetch, hardcoded 1/6 fallback) — every existing mount lights up custom dims with zero changes - agent briefing: per-dimension required_on_accounts/default_on_accounts so agents self-correct instead of bouncing off the policy error - rules CRUD API with existence/active/company validation and qualified DTO ids; firm_id FK deferred until the firms table lands (per plan) 39 new tests (pure-fn rules, engine enforcement, both new API surfaces, pg-real RLS/CHECK/cascade suite); full suite 6,791 green; migration replayed on a fresh container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: renumber migration to 20260703200000 — version collision with prod The concurrent session shipped pending_operations_add_link_document_to_voucher as 20260703120000 today; the Supabase preview branch (cloned from prod) rejected the duplicate version key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: review round — auto-pick retry on collision, fail-open warnings, query schema - POST /api/dimensions retries once past a concurrent number claim when the number was auto-picked (explicit choices still 409) - every fail-open skip of the dimension-rules policy now logs a structured warning (engine draft/commit paths + bulk-book) — deliberate fail-open, but observable - GET /api/dimensions/rules validates its query through ListDimensionRulesQuerySchema instead of an inline regex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
237b77a366 |
feat: custom inbound mail domains, rot/rut payout file, invoice email texts, security hardening (#878)
* fix(security): guard MCP test keys, RLS role gate + voucher RPC guards, /api MFA gate, deps - MCP: force dry-run / block writes for test-mode API keys in tools/call (extensions/general/mcp-server) - DB: current_user_can_write role gate on write policies (40 tables) + tenant guards, SET search_path, REVOKE anon on commit_journal_entry / next_voucher_number / detect_voucher_gaps (migration 20260702093000) - Middleware: MFA (AAL2) gate on cookie-authenticated /api routes via apiPathSkipsMfaGate - Deps: npm audit fix clears mailparser/linkify-it/nodemailer/svix/uuid highs; xlsx -> SheetJS 0.20.3 Adds unit + pg-real tests. Does not touch in-progress ROT/RUT or invoice-email-texts work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): rot/rut begäran om utbetalning — HUS XML (V6), payout tracking + settlement, MCP tool Generates Skatteverkets begäran-om-utbetalning file (schema V6) from paid ROT/RUT invoices — no submission API exists, the file is uploaded manually at skatteverket.se. Headless by design for now: API routes + MCP tool (gnubok_generate_rot_rut_file), no UI surfaces. - lib/invoices/rot-rut-file.ts: pure XML generator with deterministic per-invoice blockers (hours, work type, personnummer, property info, mixed rot+rut, XSD limits) + 31 January deadline warnings - rot_rut_payout_requests(+items) tables: one active begäran per invoice (DB triggers incl. reactivation guard), RLS, audit, pg-real tests - Settlement: POST /settle books debit 1930 / credit 1513 via the engine (source_type rot_rut_payout); partial payouts → partially_paid - Work-type lists corrected against Begaran.xsd: IT-tjänster is rut-only, snöskottning/tillsyn/tvätt added (schablontjänster utfört-only) - Fix: invoice-level fastighetsbeteckning was validated but never persisted — now stamped onto rot lines in build-invoice-write; API accepts bostadsrätt pair (lägenhetsnr + BRF orgnr, editor UI deferred) - invoice_items.brf_org_number migration + MCP scope invoices:write Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): per-company editable invoice email texts Add an "E-posttexter" section under Settings -> Fakturering where the subject, greeting, body and sign-off of the standard invoice email can be customized per company in Swedish and English. Fields pre-fill with the standard texts and only diffs from the standard are stored (company_settings.invoice_email_texts JSONB), so future improvements to the stock wording still reach companies that have not customized. Each field has a reset-to-standard button; cleared fields snap back. Texts support a fixed placeholder set (invoice number, customer name, first name, company, due date, amount) substituted at send time in a single pass; unknown placeholders stay literal. Custom texts are HTML-escaped after substitution, newlines become <br> in the HTML variant, and subject lines are flattened to a single header line. Overrides apply to standard invoices only - credit notes, proforma and delivery notes keep the stock texts. All send paths (UI, v1 API, MCP approval, recurring) pick the texts up via the existing settings row. The Zod schema half of this change (InvoiceEmailTextsSchema in lib/api/schemas.ts) was inadvertently included in 8291f745. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(documents): accept PDFs with preamble before %PDF- header, surface content rejections as 400 detectFileMagic required the %PDF- signature at byte 0 (BOM aside), rejecting genuine PDFs that carry a leading newline or junk bytes — files every ISO 32000 reader opens fine. Now scan the first 1024 bytes for the signature, matching real-reader behavior. Image types stay strict at offset 0 to keep the anti-placeholder defense tight. Magic-byte rejections were also mislabeled as DOC_UPLOAD_STORAGE_FAILED (500 'Filen kunde inte sparas'), blaming storage for a client-side file problem. Both upload routes now map them to a new DOC_UPLOAD_INVALID_CONTENT (400) with an accurate message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): full keyboard flow for manual journal entry Enter now drives the whole verifikat flow: verifikationstext drops into the first row missing an account, konto commits advance to debet, Enter on an empty debet hops to kredit, and an entered amount jumps to the next row. Once the voucher balances, Enter opens the review (unchanged gate) and the auto-focused confirm posts it — including through the no-underlag warning dialog. Escape in the inline review goes back to the form. Also fixes an Enter footgun in AccountCombobox: a bare Enter on a freshly focused field no longer selects the first account in the list — selection now requires typing or arrow navigation; otherwise Enter re-commits the current value or bubbles to the form-level handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add custom inbound domains management for companies - Implemented functionality to allow companies to claim and manage their own inbound email domains via Resend's API. - Created a new table `company_inbound_domains` to store domain information, including status and DNS records. - Added necessary RLS policies to restrict access based on user roles (owner/admin). - Developed functions for domain normalization, validation, claiming, verification, and removal. - Implemented webhook handling for domain status updates from Resend. - Added comprehensive tests for RLS, constraints, and triggers related to the new domain management feature. * fix: address PR #878 review findings and CI failures - migrations: drop the ai_usage_tracking policy block from the role-gate migration — the table was removed by 20260504120000_remove_ai_subsystem and only lingers on staging as drift; a from-scratch chain (pg-real, Supabase preview) failed on it - invoice-inbox: never flip a custom domain to verified off a domain.updated webhook alone — confirm the receiving capability with Resend first (fail-closed); normalize both sides of the orphan-adoption domain match - rot/rut: block files where begärt belopp exceeds what the buyer paid (DEDUCTION_EXCEEDS_PAYMENT); tighten brf_org_number validation to real orgnr shapes; parameterize the settlement bank account (19xx, default 1930) - rot/rut routes: log acting user on financial mutations, stop swallowing item mirror errors, narrow response projections (no customer ids through the invoice join); document the deliberate inline-XML decision - documents: stop echoing raw storage-layer error messages to clients Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: round-2 CI + compliance findings on PR #878 - migrations: the role-gate migration targeted automation_webhooks, which 20260515170000_webhooks_v2 renamed to webhooks on the canonical chain (staging kept the old name — drift); gate public.webhooks instead, dropping legacy schema-sync policy names defensively. Restore the 20260623130000 owner fallback in next_voucher_number that the stale copied-verbatim body silently reverted (caught by engine.pg locally). Full migration chain verified from scratch against supabase/postgres:15. - mcp: bump the tools/list payload ceiling 44K -> 45K — main's #877 qualified-identifier schemas plus this branch's rot/rut tool crossed the ceiling only in combination; documented in the test's history log. - rot/rut: refuse partial settlement before Skatteverkets beslut is recorded (would bypass the PATCH lifecycle and strand the request); block zero-kronor ärenden (ZERO_DEDUCTION); require sekelsiffra 16 on 12-digit brf orgnr in both schema validation and normalizeBrfOrgNr Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: rename branch migrations off main's colliding versions After the merge with main, two versions were shared by two files each (20260702100000: rot_rut_payout_requests vs company_settings_dimensions_ enabled; 20260702130000: invoice_email_texts vs pending_operations_add_ create_dimension_value). psql-based CI applies by filename and doesn't care, but Supabase branching records migrations by version (PK) — the second file with the same version breaks the preview with a schema_migrations_pkey duplicate. Neither branch migration is version- recorded on staging or prod, so renaming to fresh 20260703 versions is safe; nothing between the old and new positions depends on these objects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): scope the /api MFA-gate bypass to real Bearer-auth surfaces Any Authorization header — attacker-controlled — used to skip the AAL2 gate for every /api route, so a stolen-password AAL1 cookie session could reach cookie-authenticated routes (which ignore the header) by attaching `Authorization: x`. The skip is now scoped to the surfaces whose auth contract IS the header (/api/v1 API keys, the MCP endpoint's OAuth tokens); pure Bearer callers elsewhere (cron secret, signed webhooks) carry no cookie session and were never touched by the gate, which only fires for cookie users. Superagent P2 on PR #878. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: normalize path separators in dimension statutory guard scan The route scan compared walked file paths against a POSIX-path allowlist, so the suite failed on Windows (backslash separators) while passing on Linux CI. Normalize the scanned paths to forward slashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b27f6cdb04 |
fix(db): NULL-safe tenant guards via caller_is_company_member + mechanical sweep (#881)
The house guard 'p_company_id NOT IN (SELECT public.user_company_ids())' skips the deny branch on UNKNOWN (NULL on either side). Not exploitable today (company_members.company_id is NOT NULL) but a NULL p_company_id passes the guard, and the shape fails silently under change. 9 live functions carried it (link_*_to_voucher, reserve/release_voucher_range, mark_entry_as_opening_balance, retag_line_dimensions, ensure_company_dimensions, company_has_capability, rotate_company_inbox). - caller_is_company_member(uuid): NULL-safe membership predicate (NULL -> false, always). - Mechanical rewrite: every public function carrying the raw pattern is re-created via pg_get_functiondef with the guard swapped — deliberate over hand-copying 9 bodies (the stale-copy hazard behind the 07-03 constraint clobber). Probe-validated locally: pattern swapped, NULL denied. - pg-real ratchet: after full replay no public function may contain the raw pattern (also blocks future reintroduction); detector self-test; helper semantics (member/foreigner/NULL). Existing tenant-guard suites re-assert deny semantics on the rewritten functions in CI. Part of dev_docs/mcp_optimization_plan.md (P2-3 follow-up, PR #872 review). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
21512db81a |
feat(mcp): unify missing-document surfaces on one predicate (#876)
* feat(mcp): unify missing-document surfaces on one predicate (P1-3) The two MCP surfaces told different truths: the transactions tool keyed 'has underlag' on transactions.document_id while the verifikat tool keyed on document_attachments — and neither respected the source-type semantics, version chains, or journal_entry_no_doc_required waivers that lib/worklist's canonical count applies. Measured on prod: 22,046 waived verifikat still listed to agents, 2,370 doc-exempt source types listed, ~87 docs attached to transactions but never propagated to the verifikat, 1,100 transactions flagged missing-receipt although their verifikat HAS the underlag. One predicate now lives in SQL — posted, needs-doc source type (mirrors NEEDS_DOC_SOURCE_TYPES), no current-version doc, no waiver: - verifikat_without_documents RPC v2 adopts the canonical predicate. - New transactions_without_documents RPC: the bank-driven subset of the same predicate, joined through transactions.journal_entry_id — a strict subset of the verifikat surface by construction. Rows expose qualified transaction_id (P1-2 forward-compat); bare id deprecated. - Both tools become thin RPC wrappers; descriptions state the actual set relationship. - lib/worklist countVerifikatMissingDocument delegates to the RPC (previously three full-table pulls set-differenced client-side) — badge count and agent surfaces can no longer drift. - Backfill: propagate transaction-attached docs to their verifikat where the attachment was never linked (open periods only; never steals a doc linked to another verifikat). pg-real: fixture matrix (no-doc/with-doc/waived/stale-version/ doc-exempt-source/import), strict-subset assertion, per-source-type pin of the SQL list against the TS constant, tenant guard. Part of dev_docs/mcp_optimization_plan.md (P1-3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(mcp): explicit grants restated + count-call comment (#876 review) - Restate REVOKE/GRANT on verifikat_without_documents so the migration is self-contained (CREATE OR REPLACE preserves the 20260703130000 grants — verified on prod: authenticated + service_role only). - Comment on the p_limit:1 count call: total_count is computed over the full filtered set, independent of page size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
45438d4d64 |
fix(mcp): SQL-side filtering and pagination for list_verifikat_without_documents (#872)
The tool applied min_amount in memory after the PostgREST .range() page: total_count ignored the filter, and next_offset advanced by the filtered row count while the DB page consumed 'limit' rows — consecutive pages overlapped and full backlog coverage could not be proven (reported via agent.feedback). gross_amount is an aggregate over journal_entry_lines that PostgREST cannot filter on, so filtering/counting/pagination move into a new verifikat_without_documents RPC (SECURITY DEFINER with the PR #625 tenant-guard pattern; id tiebreak for total ordering; filter-respecting total_count). Also indexes document_attachments.journal_entry_id, which the anti-join and the link tools both hit. pg-real invariants: disjoint + complete pages under min_amount, filter-respecting totals, since filter, doc/draft exclusion, tenant guard both ways. Part of dev_docs/mcp_optimization_plan.md (P0-2). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b5fe9ee57 |
fix(mcp): add link_document_to_voucher to pending_operations CHECK constraint (#871)
The gnubok_link_document_to_voucher tool shipped with its executor and risk tier but its operation type was never added to the pending_operations_operation_type_check constraint. Every real staging INSERT failed with check_violation while dry_run previews were clean (the INSERT is skipped). Reported 3 times by 2 companies via agent.feedback; blocked Bokio attachment migration. Adds a pg-real audit test that extracts every op type staged in server.ts plus all OPERATION_RISK_TIERS keys and asserts each is accepted by the constraint, so a staging tool can never again ship without its constraint expansion (and a stale expand-types migration can no longer silently drop a type). Part of dev_docs/mcp_optimization_plan.md (P0-1). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fb3f0a9cee |
feat(dimensions): PR9 cutover — cost_center/project become GENERATED columns, dual-write removed (#870)
The dual-write window ends (dev_docs/dimensions_implementation_plan.md PR9): journal_entry_lines.cost_center/project are now GENERATED ALWAYS AS (NULLIF(dimensions->>'1'/'6','')) STORED — divergence from the bag is impossible by construction instead of by convention. - migration 20260702230000: drift pre-flight (refuses cutover on inconsistent data; prod verified 0 drift across 593k rows), column swap (DROP metadata-only + one-rewrite ADD pair), and atomic redefinition of the two SQL writers — retag_line_dimensions (SET dimensions only) and bulk_book_transactions (INSERT names the bag only) - TS writers stripped of the mirror spread: engine buildLineInserts (covers create/update/reversal), storno-service (reversal + correction), SIE import bulk insert, sandbox seed - lineDimensionColumns() removed from dimension-resolver — nothing derives mirrors in TypeScript anymore; normalizeLineDimensions + the deprecated cost_center/project INPUT aliases stay (API contract, they normalize into the bag); JournalEntryLine ROW type keeps the fields (generated columns still SELECT) - immutability carve-out unchanged BY DESIGN: its whole-row diff already subtracts dimensions/cost_center/project on both sides, which is exactly what makes it correct with generated columns (BEFORE-trigger NEW carries not-yet-recomputed mirror values) - audited every reader (v1 journal-entries, MCP query_journal filters + group_by, rc-basis-gaps) — reads are untouched; no index, view, or constraint referenced the TEXT columns, so DROP COLUMN cascades nothing - new pg suite: generated derivation, explicit-mirror-write rejection, draft-update recompute; existing retag/substrate/bulk-book suites updated to bag-only writes (their mirror assertions now exercise the generation expression) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
755e0f7e47 |
feat(dimensions): PR7 producers — auto-tagged documents (invoices, supplier invoices, bulk-book, templates, MCP) (#868)
* feat(dimensions): PR7 producers — invoices/supplier invoices carry dims, generators propagate, BulkBook + templates + MCP bags
Source documents now carry dimension tags and every entry generator
propagates them onto journal lines (dev_docs/dimensions_implementation_plan.md PR7):
- invoices/supplier_invoices.default_dimensions + per-item dimensions
(migration 20260702200000; jsonb DEFAULT '{}' + object CHECK)
- invoice-entries: issuance/payment/cash/credit propagate — item bags merge
over the invoice default per revenue line (account+bag aggregation
identity), payment vouchers re-propagate the linked invoice's bag onto
every leg incl. FX result lines; ROT/RUT 1513 carries the item bag
- supplier-invoice-entries: registration/payment/cash/privately-paid/credit
propagate with the same merge rules (expense buckets keyed account+bag)
- bulk_book_transactions RPC persists per-line bags + derives
cost_center/project mirrors in SQL (migration 20260702201000; malformed
bags rejected with BULK_BOOK_INVALID_DIMENSIONS); route merges the header
default into template/manual lines
- counterparty templates: LinePatternEntry.dimensions learned from SIE
voucher history (kept only when every occurrence agrees), applied to
business lines on booking; QuickReviewDialog shows a dims badge
- categorize: staged dimensions bag tags business lines only (bank/VAT
untagged); credit/convert/inbox copy paths carry bags forward
- propose-payment/send-lines stamp the invoice default so the editable
payment grid books what the preview shows; mark-paid override lines
accept dimensions
- UI: InvoiceEditor + NewSupplierInvoiceForm header KS/Projekt pair with
per-row override; BulkBookDialog header default pair (both tabs)
- MCP: default_dimensions/items[].dimensions on create_invoice +
create_supplier_invoice_from_inbox, dimensions on categorize_transaction,
per-line bags on bulk_book_transactions — resolve-don't-select via the
shared registry helpers, resolutions echoed
32 new propagation unit tests + 4 pg-real tests for the RPC migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: use roundOre in new dims rounding assertions (ratchet)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: copy dimension bag per payment line, document dimensionsBagKey normalization contract (review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
816b1769c8 |
feat(dimensions): PR6 retro-tagging — audited retag carve-out, BulkTagWorkbench, staged MCP tool (#867)
* feat(dimensions): PR6 retro-tagging — audited retag carve-out, workbench, staged MCP tool
Tier-2 retro-tagging (founder decision №1, approved 2026-07-02): posted
entries in OPEN periods can have their dimension tags changed through ONE
audited path — everything about the verifikat itself stays immutable.
Carve-out (migration 20260702170000): the line-immutability trigger gains a
single narrow branch — while the transaction-local GUC set by the RPC is
active, an UPDATE of a posted line is admitted iff every non-dimension
column is unchanged, enforced by a whole-row to_jsonb diff (any future
column is protected by construction; mirrors cost_center/project are in the
changeable set because they are derived views of dimensions['1']/['6']).
Precedent: mark_entry_as_opening_balance (20260613120000).
retag_line_dimensions RPC: tenant guard (20260619130100 pattern), writer
gate (viewers rejected), posted-only, open period + company lock date
enforced, every code validated against the ACTIVE registry, immutable
dimension_retag_log row (before/after/actor/reason, INSERT-only via its own
trigger, no FKs so the trail survives hard-deletes) written BEFORE the
carve-out UPDATE. Idempotent no-op without a log row. Untag ({}) supported.
Legal position per the plan: dimensions are internredovisning metadata, not
BFL 5 kap 7§ verifikat content — this is strictly more conservative than
Fortnox/Visma (dimension-only diffs, open periods only, immutable log,
storno past locks — Tier 3 has no exceptions).
Mandatory pg suite (11 tests): GUC-less updates still blocked; amounts/
description can never change even under the GUC (transaction-local);
closed/locked/lock-date, role, registry, draft and cross-tenant rejections;
log immutability; gnubok.allow_delete bulk path unaffected.
UX (all writes through the ONE RPC): pencil on posted-voucher lines in
bookkeeping/[id] ("Påverkar endast internredovisningen, inte verifikatet")
+ retag-history card; BulkTagWorkbench at /dimensions/tagging (filters,
shift-select, merge vs "Ersätt tagg" replace mode, reversal-pair warning
with "Inkludera motverifikat" auto-selection, per-line failure display).
MCP: gnubok_tag_journal_lines (bookkeeping:write) — filter block resolved
via resolve-don't-select, ≤500 lines, staged via pending_operations (new
op type migration 20260702171000, medium risk tier, shared Zod validation
boundary between staging and commit; executor loops the RPC per line with
partial-success aggregation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): address #867 review — SQLSTATE classification, blocking storno confirm, documented divergence
- Retag route classifies RPC errors by SQLSTATE instead of message-regex:
P0001 (every rule violation in the RPC) → 409 verbatim, 42501 (tenant
guard) → 403, anything else → logged 500 with a generic message. No more
substring sniffing.
- The workbench's storno-pair warning escalates to a BLOCKING confirmation
naming the unselected counter-vouchers before apply (Srf U 14 gross
reporting — one-legged retags silently skew project P&L; the banner alone
was advisory).
- The empty-bag divergence is now documented on both schemas as intentional:
the direct dialog/workbench path allows {} (human untags phantom codes,
logged with reason), the MCP staged path rejects it (agents never
bulk-clear history).
Triage notes: the log's missing FKs are the point (behandlingshistorik must
survive undo_sie_import hard-deletes — a cascade would erase the trail);
SIE exports are generated fresh on demand, never cached, so post-retag
exports carry the new object lists automatically; date-scoped registry
values are deliberately not enforced at retag because entry creation does
not enforce them either — enforcing in one path only would be incoherent
(both belong to the PR10 rules engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8bb49c07a2 |
feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry (#858)
* feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry
Phase 2 of dev_docs/dimensions_implementation_plan.md. Companies with
dimensions_enabled=false (default) see zero change.
API:
- Dashboard CRUD: GET /api/dimensions (lazy-seeds system dims 1/6 via the
ensure_company_dimensions RPC), PATCH /api/dimensions/[id] (is_system
rename blocked), POST/PATCH/DELETE values (code immutable after creation;
strict Fortnox code format ^[A-Za-z0-9ÅÄÖåäö_+\-]{1,20}$ at the API layer;
retention-trigger deletes surface the Swedish "arkivera istället" message
as 409 DIMENSION_VALUE_REFERENCED).
- POST /api/dimensions/import-existing — scans journal_entry_lines.dimensions
for unregistered codes and mints inactive placeholder registry rows.
- v1 public API: GET dimensions + POST values (Idempotency-Key, dry-run),
registered in the OpenAPI spec (102→104 endpoints).
- dimensions_enabled boolean on company_settings (new migration,
UI-visibility only, never correctness-bearing) exposed through the
existing settings read/update path.
SIE export (lib/reports/sie-export.ts):
- Reads the new dimensions/dimension_values registry; legacy
cost_centers/projects tables now have zero readers (drop migration next).
- Fixes the latent Visma-rejection bug: #OBJEKT now declared for INACTIVE
values referenced by lines.
- Generic-N: #DIM/#UNDERDIM loop sorted by sie_dim_no; #TRANS object lists
serialize from the line JSONB map (sorted, '01'→'1' collapse); orphan
codes/dims synthesize declarations from the SIE reserved-number seed —
every referenced (dim, code) pair is guaranteed declared.
UI:
- /dimensions register (Register-recipe): tabs per dimension, search,
sortable table, value dialog (code immutable on edit, projekt dates on
dim 6), archive-not-delete affordances.
- DimensionCombobox shipped (mounts in the tagging PR).
- Settings toggle "Aktivera kostnadsställen & projekt" — toggle-on runs the
import-existing scan and links to the register.
- Nav row in redovisning, rendered only when dimensions_enabled (same
mechanism as pays_salaries).
- dimensions.* i18n namespace (51 keys, sv/en parity).
- Sandbox seed: demo dims + values, revenue line tagged {"1":"BUTIK","6":"P001"}.
Verified: 6328/6328 unit tests, guard + coverage gate green, tsc parity with
main (210=210), production build passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): PR2 review round — atomic archived-create, UNDERDIM ordering, import robustness, date semantics
- POST values accepts is_active so "create as archived" is atomic; the UI's
fragile create-then-PATCH fallback is deleted (PR Agent finding 1).
- DimensionCombobox blur revert reads the committed value/values through refs
so a selection landing inside the 150ms window always wins (finding 2).
- import-existing sanitizes candidate codes like the PR1 backfill and upserts
with ignoreDuplicates — one bad/duplicate code can no longer abort the
batch; created counted from returned rows (finding 3).
- SIE export emits all root #DIM before any #UNDERDIM so a parent always
precedes a lower-numbered child (SIE4 declaration order — Swedish review);
synthesized placeholder declarations now log one structured warning
(BFNAR 2013:2 behandlingshistorik) + defence-in-depth comment.
- Value dates rejected (400 DIMENSION_VALUE_DATES_NOT_ALLOWED) when the
parent dimension is flow-period (resets_annually=true); explicit null
still clears (Swedish review).
- Sandbox seed logs seeded dimension codes; GET /api/dimensions documents
the deliberate absence of dimensions_enabled gating (UI-visibility flag,
not a security boundary — compliance-swarm V8.2.1 rejected by design).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8cc2efb083 |
feat(dimensions): PR1 substrate — SIE-native registry + dimensions JSONB on journal lines (#857)
* feat(dimensions): substrate — SIE-native registry + dimensions JSONB on journal lines (PR1)
Implements phase 1 of dev_docs/dimensions_implementation_plan.md:
- New company-native registry tables: dimensions (= SIE #DIM/#UNDERDIM,
seeded is_system 1=Kostnadsställe / 6=Projekt via ensure_company_dimensions,
nullable bare firm_id) and dimension_values (= #OBJEKT), full RLS incl.
DELETE, audit + updated_at triggers, guard triggers (system dims undeletable,
sie_dim_no immutable, values referenced by posted lines archive-not-delete).
- journal_entry_lines.dimensions jsonb NOT NULL DEFAULT '{}' as the single
source of truth ({sie_dim_no: object_code}), CHECK object-typed, GIN
(jsonb_path_ops) + partial expression indexes on dims 1/6. Inherits posted-
line immutability from the existing trigger with zero new triggers.
- Backfill: representation copy of legacy cost_center/project text into the
JSONB map (trigger-disabled, schema_sync precedent); legacy cost_centers/
projects registry rows copied into dimension_values; inactive placeholder
values for orphaned free-text codes.
- Dual-write: engine buildLineInserts + storno/correction/date-move now derive
cost_center/project mirrors from the map via lib/bookkeeping/dimension-resolver.ts
(normalizeLineDimensions / lineDimensionColumns); reversal copies dims.
- CreateJournalEntryLineInput + shared Zod line schema gain a dimensions bag
(cost_center/project stay as deprecated aliases); pending-ops voucher lines
coerce it.
- CI ratchet: direct-jel-insert check in no-new-antipatterns.mjs — inserts into
journal_entry_lines outside sanctioned writers fail CI.
- pg-real suite: registry RLS/guards/retention, ensure_company_dimensions
tenant guard, dims frozen on posted lines, CHECK enforcement (13 tests).
Non-breaking: companies without dimensions see zero change; no UI yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): address review findings — canonical keys, boundary-validated staged bags, migration guidance
- normalizeLineDimensions canonicalizes numeric keys ('01' -> '1') so
leading-zero keys can't split values or miss the cost_center/project mirrors
(PR Agent finding).
- New coerceDimensionsBag() in dimension-resolver is the single boundary
validator for untyped staged payloads, enforcing the same constraints as the
Zod line schema (string-only values, 1-40 chars, no SIE-framing chars,
canonical keys). pending-operations normalizeVoucherLines now uses it —
staged payloads can no longer bypass API-layer validation via numeric
coercion (compliance-swarm V2.2/V1.2.5/PI1.1, Swedish review finding 4).
- Migration backfill comment now spells out the exact conditions under which
the trigger-disable pattern is defensible (BFL 5:5 / BFNAR 2013:2) and what
a future reviewer must verify before reusing it (Swedish review finding 2).
- 10 new resolver tests incl. reversal-parity (empty bag + aliases ==
alias-only) proving the reverseEntry and storno paths normalize identically
(PR Agent finding 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): round-2 review — shared Zod schema, transactional backfill, empty-string guard
- DimensionsBagSchema now lives in dimension-resolver as the single source of
truth; CreateJournalEntryLineSchema and coerceDimensionsBag both delegate to
it, so the API layer and the staged pending-operations path provably cannot
drift (compliance-swarm V2.2). coerceDimensionsBag switches to whole-bag
semantics: any invalid entry rejects the bag, exactly like the API schema.
- Migration backfill now runs DISABLE TRIGGER / UPDATE / ENABLE TRIGGER inside
one transaction — the ACCESS EXCLUSIVE lock from ALTER TABLE holds until
COMMIT, so no concurrent writer can slip an unguarded line write into the
window during a live apply (compliance-swarm V1.2, Swedish review finding 1).
- NULLIF guard: empty-string legacy mirrors can no longer mint {"n":""}
entries the resolver would interpret as "cleared" (PR Agent round-2 edge).
- COMMENT ON dimensions.resets_annually documenting the SIE4 #IB/#OIB
semantics the PR2+ export path must honour (Swedish review finding 2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f63d3e3100 |
Bug/open banking flow (#854)
* fix(enable-banking): pin Mobile BankID (decoupled) auth_method so Handelsbanken corporate connects We never sent auth_method to Enable Banking, so it fell back to the ASPSP's visible default — REDIRECT for Handelsbanken. For Handelsbanken *corporate* PSUs the redirect flow does not support Mobile BankID, so authorization failed right after the user approved in the BankID app. Mobile BankID at Handelsbanken is a DECOUPLED method flagged hidden_method=true, which Enable Banking only uses when requested explicitly. Resolve the bank's preferred auth method before /auth: query the ASPSP's auth_methods and pick the DECOUPLED (Mobile BankID) method when present, otherwise leave auth_method unset so banks that already work are untouched. The method name is read dynamically per psu_type, so it is robust across sandbox/production naming. - api-client: add approach/hidden_method to AuthMethod, fix ASPSP.auth_methods field name (was available_auth_methods, never populated), add getPreferredAuthMethod(), thread optional authMethod through startAuthorization - index: resolve authMethod in /connect and pass it on both fresh + reconnect - tests: cover method selection and request-body shaping Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(invoice-inbox): clean up bulk-selection toolbar UI Redesign the selection toolbar shown when inbox items are checked: one solid primary "Bokför valda" button with outlined secondary actions ("Fråga assistenten", "Ta bort") and a plain selection count. Removes the redundant "Avmarkera" button (users uncheck the still-visible box), fixes label clipping, and gives the toolbar more breathing room. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(entitlements): bypass paywall in local development Add isPaywallBypassed() so all gated capabilities are testable locally without a subscription. Fires only on NODE_ENV=development (npm run dev) or an explicit DISABLE_PAYWALL=true escape hatch — production builds run under NODE_ENV=production and the entitlement suite runs under 'test', so both keep exercising the real gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tic): resolve enskild firma bolagsuppgifter via 12-digit personnummer TIC's Lens search is fuzzy and only resolves an enskild firma from the 12-digit (century-prefixed) personnummer; a 10-digit form fuzzy-matched an unrelated entity. Expand personnummer to 12 digits before querying and reject hits whose registration number is unrelated to the request. Add a "Hämta" action to the settings Bolagsuppgifter panel to (re)fetch on demand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): implement categorize core for bank transaction categorization - Added `categorize-core.ts` to handle categorization of bank transactions, supporting single and bulk operations. - Introduced `categorizeMatchedTransaction` and `bulkBookMatchedInboxItems` functions for transaction processing. - Implemented fiscal period validation and duplicate booking detection. - Enhanced logging and error handling for transaction categorization. feat(scripts): add diagnostic script for Handelsbanken ASPSP metadata - Created `check-handelsbanken-aspsp.mjs` to fetch and display available authentication methods for Handelsbanken. - Outputs metadata for business and personal PSU types, including default authentication methods. fix(migrations): increase statement timeout for SIE bulk delete operations - Updated `20260629160000_sie_bulk_delete_statement_timeout.sql` to set a longer statement timeout for bulk delete RPCs to prevent cancellations during large imports. feat(migrations): add bulk book inbox items to pending operations - Expanded `pending_operations` table to include `bulk_book_inbox_items` operation type in `20260630120000_pending_operations_add_bulk_book_inbox_items.sql`. - Supports bulk booking of matched inbox items against bank transactions. test(pg): add tests for replace_period_opening_balance_link RPC - Implemented tests in `replace-period-opening-balance-link.pg.test.ts` to validate the functionality of the opening-balance correction flow. - Ensured immutability of opening balance links and proper handling of posted vs. non-posted entries. * fix(sie-export): update journal entries and lines handling in SIE export tests * fix(migrations): resolve version collision on 20260629160000 The SIE bulk-delete statement_timeout migration shared version 20260629160000 with journal_entries_list_series_filter (merged from main via #798/#823), causing a schema_migrations_pkey duplicate key error on apply. Rename the branch's migration to 20260629160100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compliance): resolve compliance-swarm + review findings - opening-balance/correct: compensating rollback for the non-atomic storno+rebook so a mid-sequence failure never leaves two posted OB entries (ASVS V2.3); durable audit event on every failure path (V16); reference the original verifikationsnummer in the corrected entry per BFL 5 kap 5§; document that requireWrite already enforces write-role + membership (V8.2.1 was a false positive) - reports sources routes: validate the cursor date component as ISO (/^\d{4}-\d{2}-\d{2}$/) before use, 400 on malformed (ASVS V1.2), applied to both the VAT-declaration and trial-balance routes - AgentSessionList: await the rename PATCH, revert the optimistic title and toast on failure (ASVS V4.5) - bank booking: exclude same-batch siblings from the booking-time duplicate guard so bulk-booking distinct same-(date,amount) transactions no longer false-positives; pre-existing duplicate detection is preserved - BulkBookInboxDialog: drop the unsafe currency-based reverse_charge default, add an omvänd skattskyldighet advisory, and type VAT options to the backend VatTreatment union - OpeningBalanceRowEditor: hold onChange in a ref (synced in effect, not during render) so an unstable callback can't cause a render loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
241959513b |
Fix/mcp and req (#753)
* feat(api): test-mode API keys force dry-run on the v1 REST API A key created with mode='test' (prefix gnubok_sk_test_) binds to the real company, but the v1 wrapper forces dry_run on every write so nothing is persisted or sent. Mutations on endpoints that can't be simulated (dryRunSupported=false or unregistered) are refused with 403 TEST_KEY_WRITE_BLOCKED — fail-closed. Reads pass through unchanged and every test-key response carries X-Gnubok-Mode: test. Live keys are unaffected (mode defaults to 'live'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): company default "Vår referens" + per-line sales-account override Add company_settings.default_our_reference (settings form, schema, type); the invoice editor pre-fills our_reference from it on new invoices only, never overwriting an edited draft. Separately, add an optional per-line försäljningskonto (class-3) override in the editor — left blank, the engine still derives the revenue account from the VAT rate, and reverse-charge/export lines ignore the override. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): render a Swish payment QR on invoice PDFs Build the Swish "Type C" QR payload offline (no Swish API call) and embed it as a PNG in the invoice PDF payment box when Swish display is enabled, the invoice is in SEK, and the amount is positive. Also surface the invoice number in the payment box. Wired through every PDF render path: send, mark-sent and pdf routes (both legacy and v1), the recurring-schedule sender, and the staged-send commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): draft exclusion + correction-chain collapse on verifikationslista Extend list_fiscal_period_entries_with_related with two opt-in params: p_exclude_draft (keep drafts off the committed list — they get their own surface) and p_collapse_corrections (render a correction group as the single live correction, hiding the mechanical storno and the reversed original). Both default false; nothing is deleted, every voucher keeps its number, and a "show all" toggle exposes the full chain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): link multi-year SIE periods so resultatrapport shows the prior year SIE import now sets fiscal_periods.previous_period_id in both directions when creating a period, so multi-year files chain correctly regardless of #RAR order. A backfill migration repairs periods imported before this (idempotent; only touches NULL links on first-of-month periods). generateResultatrapport falls back to the date-adjacent prior period when the chain is still null, so the comparison column works for legacy data too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(articles): hide the VAT field for non-momsregistrerade companies The article form reads company_settings.vat_registered and, when false, hides the moms field and forces vat_rate to 0 on submit — mirroring the invoice editor so a non-VAT-registered company never sets a rate it can't charge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(import): allow file-based imports in the sandbox Bank-file, CSV/Excel and SIE imports run entirely on uploaded data with no external service, so they're now reachable in the sandbox. Only the API-backed options that need live third-party credentials (PSD2 bank connection, provider migration) stay disabled. Updates the sandbox notice copy to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): add edit draft functionality for journal entries * feat(database): add default "Vår referens" column to company_settings for invoicing * fix(tests): set SHOW_SWISH_ON_INVOICE to false in PDF template mocks * @ fix(payments): use roundOre for Swish amount formatting Replace naive Math.round(x*100)/100 with roundOre from @/lib/money to satisfy the antipattern guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8322830f46 |
Add/issue in absurdum (#739)
* feat(assets): allow editing fixed asset fields before depreciation The fixed asset register only offered a "Dispose" action, so correcting a mis-entered acquisition date/cost/category meant running the disposal flow — which posts a real divestment voucher plus a Ch. 8a VAT adjustment. Disproportionate and wrong for a data-entry fix. Add an Edit action that allows correcting those fields directly, gated for correctness: - service: extend updateAsset() with category/acquisition_date/ acquisition_cost; block the change once the asset is disposed or has posted depreciation (AssetCorrectionBlockedError) where it would desync posted vouchers from the register; realign the BAS triple on category change. Name, useful life, and method stay editable. - api: extend the PATCH schema; annotate GET /api/assets with has_posted_depreciation so the UI can lock basis fields proactively. - ui: EditAssetDialog + pencil action; disables date/cost/category when depreciation has been booked, with an inline explanation. - errors: register ASSET_CORRECTION_BLOCKED (409). - tests: unit tests for the guard; pg test for pre-disposal editability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(assets): also block basis edits when depreciation was hand-posted The correction guard only consulted depreciation_schedules, so an avskrivning booked as a manual journal entry (no schedule row) slipped through and a basis correction was wrongly allowed. Add a ledger scan: any posted credit to the asset's ackumulerade- avskrivningar account (12x9) counts as depreciation. Entries that depreciation_schedules attributes to a *different* asset are excluded, so a sibling's engine avskrivning on a shared 12x9 account doesn't produce a false block. What remains is depreciation tied to this asset (engine or manual); a basis correction is blocked there and must go through storno. Adds two unit tests: blocks on a hand-posted credit, allows when the only 12x9 credit belongs to a sibling's engine entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): allow negative unit prices for discount lines The invoice creation form rejected negative unit prices via a frontend superRefine check, blocking valid discount lines (e.g. "Rabatt -100"). The unit_price error was never rendered inline, so submission failed silently. The backend schema already allows negative unit prices (see CreateInvoiceItemSchema test), so the form was simply out of sync. Remove the non-negative constraint; empty/NaN prices are still rejected by the base z.number() type. Drop the now-unused validation_price_positive translation key from both locale files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): allow editing draft invoices Drafts could be saved but not edited — the only way to change a draft's lines, customer, dates or amounts was to delete and recreate it. Add a "Redigera" action on draft invoices that opens the invoice editor pre-filled with the draft and saves changes in place. A verifikat is only created when an invoice is sent (or paid, under kontantmetoden), so every status=draft invoice is uncommitted and safe to edit; sent/paid invoices stay immutable and still require a credit note. - Extract buildInvoiceWriteData() with the shared validation + computation (VAT rules, ROT/RUT, accruals, totals, currency, item rows); POST now uses it too, behaviour unchanged. - Add UpdateInvoiceSchema and PATCH /api/invoices/[id], guarded to drafts (status=draft, no journal entry, not self-billed); number and status are preserved and no invoice.created is emitted. - Extract the invoice creator into a shared InvoiceEditor with create / edit modes; /invoices/new is now a thin wrapper and /invoices/[id]/edit is the new edit page. - Add a "Redigera" button on draft invoice detail pages + sv/en strings. - Tests for the builder, UpdateInvoiceSchema and the PATCH route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reports): make Huvudbok findable via account/saldo search terms Searching the command palette for natural phrases like 'saldo per konto', 'kontoutdrag', 'kontoanalys' or 'transaktioner per konto' returned nothing, so users couldn't find the general ledger. Enrich the Huvudbok entry's keywords with those synonyms, and let Saldobalans and Balansrapport match 'saldo per konto' too since they are genuinely per-account balance views. Companion change — the clearer Huvudbok report description ('Saldo och alla transaktioner per konto') — already landed in d5f474cb. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): let users edit their personal name Add an editable Namn field to /settings/account that updates profiles.full_name and best-effort syncs auth user_metadata. Previously the personal name was only ever set from BankID's legal name at signup with no way to correct it, so users whose tilltalsnamn isn't their first given name were greeted by the wrong name (and email/password users had no name at all). New POST /api/user/profile route (requireAuth, RLS-scoped update) mirrors /api/user/locale. sv/en strings added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(invoices): per-invoice öresavrundning override Add a display-only öresavrundning flag per invoice that wins over the company-wide setting. Resolution order in getDisplayTotal: per-invoice override -> company setting -> default-on. The stored total and the booked verifikat keep the exact öre; only the rendered total changes. Supplier invoices gain the same flag but resolve a null to off (they never had rounding historically), exposed via a toggle on the new-invoice form and a rounding row on the detail page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(transactions): warn on possible duplicate before booking Before committing a transaction (via book or categorize), detect an already-booked sibling with the same date and amount and return a 409 TRANSACTION_BOOK_POSSIBLE_DUPLICATE instead of silently double-booking. The user can override with force=true, which must be bound to the reviewed sibling via expected_duplicate_transaction_id; the candidate is re-detected server-side, so a stale or guessed id is rejected with TRANSACTION_BOOK_FORCE_CANDIDATE_MISMATCH. Detection is fail-open on the non-force path and fail-closed under force. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(transactions): shadow-mode scope-drift dedup counter in bank ingest Count rows that an enforcing same-feed scope-drift rule WOULD treat as re-imports (the IBAN-drift re-imports the external_id check misses) and surface it as IngestResult.shadow_scope_drift_candidates. Nothing is blocked yet -- the counter only measures how often the rule would fire so it can be validated against real data before enforcement. Also gitignore scripts/delete-duplicate-transactions.ts: a destructive, hand-run cleanup tool kept out of the repo so it can't run in CI/cron or be mistaken for a supported feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(bokslut): base bolagsskatt on post-disposition result Bokslutsdispositioner are booked as source_type='year_end', which the income statement excludes, so net_result alone overstates resultat före skatt and the booked tax ignored the periodiseringsfond avsättning (too-high tax, ÅR/INK2 mismatch). calculateBolagsskatt now accepts resultBeforeTaxOverride. The preview builder mirrors each proposal's P&L effect (+återföring, -avsättning, -SLP) onto the pre-disposition result; the commit path sums the already-posted dispositions via the new sumPostedYearEndDispositions (class 88 + 7533) since bolagsskatt is committed last. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(settings): fiscal years manager Add a FiscalYearsManager to the bookkeeping settings that lists fiscal periods with their status (closed > locked > open) and creates the next year via CreatePeriodDialog, seeded to chain forward from the latest period end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(api): return 400 when locking a period with unbooked transactions lockPeriod() refuses to lock a period that still has uncategorized business transactions. Detect that message in the lock route and surface it as a clear PERIOD_HAS_UNBOOKED_TRANSACTIONS (400) instead of a generic 500. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(invoices): implement isEditableInvoiceDraft utility and apply it across invoice edit routes feat(transactions): log duplicate dismissal events in behandlingshistorik test(invoices): add tests for isEditableInvoiceDraft function test(transactions): enhance tests to verify behandlingshistorik logging refactor(bokslut): update tax calculation test descriptions for clarity --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
db8983ba9e |
Add/bokslut (#718)
* feat(arcim-migration): Briox provider with SIE-over-API import - Briox auth via account ID + application token (no app-level credentials); both tokens rotate on refresh and are persisted - New sie-fetcher pulls the general ledger as SIE through the provider API for Fortnox, Briox and Bjorn Lunden - Wizard stops on a failed SIE import and surfaces the real errors instead of proceeding to the misleading migrate-guard message - PROVIDER_SIE_ONLY_FORTNOX renamed to PROVIDER_SIE_NOT_SUPPORTED; new PROVIDER_TOKEN_INVALID for rejected provider credentials Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): per-line accruals (periodisering) on invoices and supplier invoices Defer revenue/costs per invoice line to 29xx/17xx interim accounts with automatic monthly dissolution (nightly cron + catch-up at registration), schedule cancellation on credit, year-end auto-detect exclusion for already-scheduled invoices, invoice-inbox service-period extraction for prefill, and an MCP tool to list schedules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bokslut): iXBRL arsredovisning generation and Bolagsverket digital filing Generate the annual report as iXBRL from a generated taxonomy registry (K2 element lists, taxonomy:generate/check scripts + CI guard), expose it via the fiscal-period API, and add the bolagsverket extension for digital submission to eget utrymme with webhook-driven status tracking (submissions table + pg tests, lifecycle events, year-end wizard UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): raise origin-guard test timeout to 20s The dynamic import pulls in the full server module; the parse alone flirts with the 5s default under full-suite parallel load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add new scripts and documentation for K2 AB taxonomy generation and validation - Introduced `generate-taxonomy-registry.ts` to automate the generation of the iXBRL taxonomy concept registry from official element lists and tuple models. - Added `validate-ixbrl.mjs` for validating generated iXBRL reports against the official taxonomy package using Arelle. - Included new documentation files: - `k2-ab-arsredovisning-elementlista-2024-09-12_rev20250312_sv.xlsx` - `tuple-innehallsmodell-arsredovisning-k2-2024-09-12.xlsx` - `taxonomi-paket-2024-09-12_rev20250312.zip` * Add tests for bookkeeping accruals dissolution and supplier invoices - Implement tests for the POST /api/bookkeeping/accruals/[id]/dissolve route, covering success and error scenarios. - Add tests for the DELETE /api/supplier-invoices/[id] route, including authentication checks and validation of invoice deletion conditions. - Introduce tests for the Arcim migration provider client, ensuring token handling and error classification. - Create tests for the Bolagsverket extension, validating submission role enforcement and environment settings. - Add Zod schemas for Bolagsverket response payloads to ensure proper validation. - Implement tests for MCP server's list accrual schedules, confirming registration and scope mapping. - Add consistency tests for IXBRL document generation, ensuring duplicate facts and XML escaping are handled correctly. - Introduce typed domain errors for accrual schedules to improve error handling in the service. - Add tests for resolving consent with Briox token refresh concurrency, ensuring proper token management and error handling. * fix(tests): update payload size guard comments to reflect recent changes in tool descriptions and ceiling adjustments * fix(gitattributes): mark generated JSON files in bokslut taxonomy as linguist-generated * feat(migrations): add backfill for invoices.journal_entry_id and fallback for next_voucher_number user_id * feat(bokslut): enhance compliance and financial processing features with new submission details and security measures --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5078b4e02d |
fix(mcp): grace window + idempotent refresh-token replay for OAuth (#710) (#714)
* fix(mcp): grace window + idempotent refresh-token replay for OAuth (#710) OAuth refresh rotated BOTH the refresh token and the access key in one zero-grace CAS. Claude Code's MCP OAuth client fails to persist the rotated refresh token (or fires concurrent refreshes), re-presents the stale one, the CAS matches 0 rows, and the grant dies with invalid_grant — forcing a full re-authorization roughly every 60s in a loop. Regression from #392. Keep rotation (RFC 9700 §4.14.2 requires it for public clients) but add a bounded grace window with idempotent replay, atomic in one SECURITY DEFINER RPC: - Migration adds previous_key_hash / previous_refresh_token_hash (+ *_expires_at) shadow columns. validate_and_increment_api_key accepts the current OR an unexpired previous key_hash, with the rate-limit increment keyed off the resolved row id. - New rotate_mcp_refresh_token RPC: rotated | replayed | reuse_revoked | revoked | invalid. In-grace replay re-issues a fresh pair and slides the window so an actively-refreshing client that cannot persist the rotated token keeps working; reuse after the window revokes the grant family (RFC 9700 4.14.2 reuse detection preserved). - The refresh grant now calls the one RPC, closing the old SELECT-then-CAS TOCTOU gap. All previous_* columns default NULL, so existing keys are unaffected and the RPC return shape is unchanged (callers untouched). Tests: rewired the token-route unit tests to the RPC and replaced the test that codified the bug with a #710 regression (in-grace replay returns 200, not 400); added tests/pg/mcp-oauth-rotation-grace.pg.test.ts (grace accept/expire, revoke-never-graced, rotate->demote, idempotent replay, reuse-after-grace->revoke). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: retrigger checks for #714 No code change — re-running CI. The Supabase Preview check fails on a pre-existing main-branch migration-history drift ("Remote migration versions not found in local migrations directory"), not this PR; pg-real (full migration replay) passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7f60b23f5 |
fix(invoices): v1 mark-paid booking-state routing + journal_entry_id backfill (#713)
invoices.journal_entry_id means "the registration verifikat that booked this invoice at issuance" — payment flows route on it (set → clear 1510, NULL → kontantmetoden cash entry). Two bugs in v1 mark-paid broke that: - The pre-flight select omitted journal_entry_id, so invoiceAlreadyBooked always read false — a kontantmetoden company paying an already-registered invoice would re-recognise revenue + VAT (double-booking) and orphan the 1510 receivable. Fixed by fetching the column for routing only; the response contract and invoice.paid event payload are unchanged. - The update wrote the just-created PAYMENT/cash entry id into the column (wrong semantic) — once routing reads the column, a cash partial payment #1 would make payment #2 clear a 1510 that was never debited. Removed; the payment entry id still returns in the response body. New backfill migration links the earliest posted invoice_created entry to historical invoices (353 registered-but-unlinked rows in hosted prod), repairs any payment-type links, and links credit_note reversal entries to credit-note rows. Idempotent; rows with no registration entry stay NULL (correct for kontantmetoden/unsent invoices). Tests: 3 new unit tests lock the select projection, the already-booked→ clearing routing, and the no-write-back semantics (the supabase mock now records call args). New pg-real suite (11 tests) runs the actual migration SQL: earliest-wins, reversed/draft exclusion, no-overwrite, cash stays NULL, payment-link repair, credit notes, cross-company isolation, idempotency. insertDraftJournalEntry fixture gains optional sourceType/ sourceId/createdAt (defaults unchanged). Hosted prod requires manual migration apply after merge (Supabase MCP). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c0b006fcc1 |
feat(invoicing): artikelregister (product/article catalog) with per-article revenue account (#703)
* feat(invoicing): artikelregister (product/article catalog) with per-article revenue account Add a lean, non-inventory article catalog (artikelregister) so users can define reusable invoice-line presets (name, unit, price excl VAT, VAT rate) with an optional per-article BAS class-3 revenue-account override. - DB: articles table (RLS via user_company_ids(), audit + updated_at triggers, unique-per-company article_number), generate_article_number RPC (atomic + idempotent), company_settings counter, nullable invoice_items.revenue_account + article_id, pending_operations CHECK expansion. - Engine: generatePerRateLines groups revenue by (vat_rate, account) — byte-identical with no override, balance-safe when split (last account absorbs the rounding remainder), reverse_charge/export still force 3308/3305. - API: /api/articles CRUD (soft-deactivate); override validated against chart_of_accounts (active class-3) and frozen onto invoice lines at create. - Propagation: override carried through send/mark-sent/credit/convert/cash and the staged commit paths (recurring deferred — documented inline). - MCP: gnubok_list/create/update_article (staged, scoped, risk-tiered). - UI: articles register (list/detail/form) + nav + bilingual i18n + invoice-line article picker & "Spara som artikel" quick-create. - Tests: engine regression, route, and pg-real (RPC/RLS/triggers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): strip ILIKE _ wildcard from gnubok_list_articles search Underscore is a single-character ILIKE wildcard; stripping it (alongside the existing %,()\* set) keeps a stray char in the article search from matching every row. Read-only + RLS-scoped, so no security impact — addresses PR #703 reviewer + compliance-swarm CC6.3 notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
679b154ad2 |
feat(skatteverket): MCP wrappers for momsdeklaration + AGI filing (P0-5) (#692)
* feat(skatteverket): MCP wrappers for momsdeklaration + AGI filing (P0-5) Expose the complete Skatteverket extension as five MCP tools so VAT (momsdeklaration) and employer (AGI/arbetsgivardeklaration) filing can be driven from Claude. Commit = "send for BankID signing" (returns a signing link), never "file" — the user's signature in the browser is the irreversible act, kept outside the tooling. Tools (extensions/general/mcp-server/server.ts): - gnubok_vat_declaration_validate (compliance:read) — live POST /kontrollera - gnubok_vat_declaration_submit (skatteverket:write) — stages submit_vat_declaration - gnubok_vat_declaration_status (compliance:read) — GET /inlamnat + /beslutat - gnubok_agi_submit (skatteverket:write) — stages submit_agi - gnubok_agi_status (compliance:read) — local state + live kvittenser Architecture: - Core (lib/pending-operations/commit.ts) cannot import @/extensions (CI guard), so the two submit ops dispatch into the extension via the new Extension.services channel (first use): registry-resolved commitSubmitVatDeclaration / commitSubmitAgi run the SKV chain and return a shared SkvSubmitResult (lib/pending-operations/skatteverket-commit.ts). - Recoverable failures (extension disabled, no connection, rate-limited, still processing) release the op back to 'pending' via SkatteverketRecoverableError — same contract as AccountsNotInChartError — so the user reconnects and re-approves the SAME op. SKV business rejections reject the op. - No-drift: parseDeclarationRequest / loadAGIXml extracted to lib/declaration-prep.ts (buildMomsuppgift / buildAgiUnderlag / resolveRedovisare) so route, preview, and commit file identical figures. writeSkatteverketAudit hoisted to lib/audit.ts; read tools + executors write BFL audit rows too. - New scope skatteverket:write (opt-in, in STAGING_SCOPES so SoD ack fires), 4 structured error codes, sv/en strings, ApiKeysPanel row. - Migration 20260620120000 adds submit_vat_declaration / submit_agi to the pending_operations.operation_type CHECK (must apply to prod post-merge). Tests: 42 new across executors, MCP tools, declaration-prep, error-map, and the VAT commit chain. Full suite green (5287), build clean, lint-ratchet at baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: add PR-Agent AI review (SHA-pinned, dedicated Bedrock key) Greptile went silent after #682 (app/account-side, not repo config). Add the open-source PR-Agent GitHub Action as a replacement, hardened for supply chain: - Pinned to the v0.36.0 commit SHA (ffe1f89), not the movable tag — the repo was recently transferred to a new, unverified org (The-PR-Agent), though it's the genuine original pr-agent (repo id 662766482, 11.5k stars). - Runs on a DEDICATED, minimal IAM key (bedrock:InvokeModel only) via PR_AGENT_AWS_* secrets — never the app's general AWS credentials. - Only /review runs automatically; /describe and /improve are disabled so PR descriptions are never overwritten. Requires three new secrets before it functions: PR_AGENT_AWS_ACCESS_KEY_ID, PR_AGENT_AWS_SECRET_ACCESS_KEY, PR_AGENT_AWS_REGION (EU region). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): handle push events + restrict push to /review PR-Agent skips synchronize (push) events by default, so the bot ran green but posted nothing. Enable handle_push_trigger and scope push_commands to /review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): fix pr_actions (event list, not commands) + add synchronize pr_actions is the list of PR event actions to handle, not slash-commands. Setting it to ["/review"] removed every real event from the allowlist, so the bot skipped everything. Restore the default events + synchronize; command selection stays on the auto_review/describe/improve booleans (review-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): raise max_model_tokens to 64k for fuller diff coverage Default ~32k input window truncated large PRs. Sonnet 4.6 has 200k context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-agent): use Claude Opus 4.8 (Sonnet 4.6 fallback) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skatteverket): scope AGI status flips by salary_run_id Bot review (swedish-compliance) caught that commitSubmitAgi flipped agi_declarations status by (company_id, period) only. A correction run sharing the period would have its still-valid declaration co-flipped to rejected/ pending_signature. Scope both updates by salary_run_id (in scope from params) — more precise than the period-only route handler, which has no run id. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(pg): fix gen_random_bytes assertion for modern pgcrypto OpenSSL-backed pgcrypto (CI Postgres image) rejects gen_random_bytes(0) with 'Length not in range' rather than returning empty bytea, so the pre-existing 'returns empty bytea' assertion fails on every pg-real run (repo-wide, not specific to this PR). Assert the real contract — exactly n bytes for a positive n — instead of the version-dependent 0-byte edge case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5aa449ad3c |
Lock down OAuth used codes table (#641)
* Lock down OAuth used codes table * test(db): lock oauth_used_codes lockdown contract + reload PostgREST cache Add a pg-real test asserting anon/authenticated are denied SELECT/INSERT on public.oauth_used_codes while the privileged (service-role) connection can still read it, per the project's requirement that RLS changes ship a *.pg.test.ts. Also append NOTIFY pgrst, 'reload schema' so PostgREST picks up the privilege change immediately (migration rule 8). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3d3f242d11 |
fix(db): migrations failing on a fresh supabase instance (#645)
* fix(db): add extension function wrappers (avoid shadowing gen_random_uuid) Signed-off-by: filip <filip.harald@gmail.com> * test: added test for extension function wrappers (uuid_generate_v4 and gen_random_bytes) in public schema, covering existence, delegation, correctness, properties, and idempotency. Signed-off-by: filip <filip.harald@gmail.com> --------- Signed-off-by: filip <filip.harald@gmail.com> |
||
|
|
0bc81d4c88 |
feat(auth): SoD acknowledge on stage+approve keys + agent:write scope for memory tools (P0-3) (#681)
* feat(auth): SoD acknowledge on stage+approve keys + agent:write scope for memory tools Segregation of duties on API keys is now warn + explicit acknowledgement (not block): minting a key with any staging write scope AND pending_operations:approve returns 409 API_KEY_SOD_CONFLICT unless the caller re-POSTs with acknowledge_sod: true. The acknowledgement is recorded (sod_acknowledged_at / sod_acknowledged_by) for an auditable risk acceptance (ISO 27001:2022 A.5.3 / BFNAR 2013:2). The create UI surfaces an inline warning and an explicit confirm dialog before submitting the ack — the default "all scopes ticked" create routes through that path. Also introduces the agent:write scope and maps the previously-UNMAPPED memory tools gnubok_remember_fact / gnubok_forget_fact to it. Because unmapped tools were callable by any key, the migration grandfathers agent:write onto every existing non-revoked key with an explicit scope list so nothing regresses; new keys must opt in. agent:write is deliberately excluded from the default grants and is NOT a staging scope (no SoD conflict with approve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): enforce both-or-neither on the SoD acknowledgement pair Review finding (Greptile P2): sod_acknowledged_at/sod_acknowledged_by were independently nullable, so a partial write could silently pass and undermine the auditable risk acceptance (ISO 27001 A.5.3 / SOC 2 CC6.1). Adds a paired-NULL CHECK constraint + pg-real coverage for both partial-write directions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(auth)+feat(auth): compliance-review round — self-attestation documented, ack logged, SoD boundary assumption captured - Migration header now states explicitly that the SoD acknowledgement is a SELF-attestation by deliberate design (enskild firma has no second person; the claude.ai approval flow needs stage+approve on one credential) — the control objective is informed consent + audit record, not dual control. - The acknowledge_sod=true path now emits a structured log.warn (api_key.sod_acknowledged with key id/prefix, conflicting scope, scopes, acknowledger, company) so the acceptance lands in the logging pipeline in addition to the sod_acknowledged_* columns (ASVS V16.1.1). - STAGING_SCOPES carries the documented system control (BFNAR 2013:2 systemdokumentation) for why agent:write is not a staging scope: memory tools write advisory agent context and cannot stage räkenskapsinformation. Dismissed as by-design/verified: hard-block and second-approver remediations (user decision: warn + acknowledge); scope-update gap (the [id] route only supports DELETE — scopes are immutable post-creation); session-auth concern (withRouteContext is cookie+MFA only; API-key auth exists only on /api/v1 and MCP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-trigger CI (Supabase Preview 502 infra hiccup) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
305f469fc3 |
harden(db): tenant backstop — payment company-consistency triggers + write-RPC guards (P0-2) (#680)
* harden(security): payment-row company-consistency triggers (tenant backstop) invoice_payments and supplier_invoice_payments are the only two child tables carrying BOTH a parent FK and their own company_id. A row whose company_id disagrees with its parent's company_id is a tenant-isolation defect that would surface a foreign tenant's payment in this company's AR/AP ledger. RLS scopes by company_id but never cross-checks the parent, so nothing at the DB layer guaranteed the invariant. - Pre-flight DO block: fail the migration loudly (listing offending ids) if any existing row already violates child.company_id = parent.company_id, rather than arm a trigger over dirty data that can never be updated again. - enforce_payment_company_consistency(): one INVOKER trigger function parameterized on TG_TABLE_NAME, wired BEFORE INSERT OR UPDATE OF (company_id, parent_fk) on both payment tables; raises on mismatch. Matches the SECURITY posture of the sibling enforcement triggers in migration 017. - pg-real coverage in tests/pg/payment-company-consistency.pg.test.ts: matching pair inserts ok; cross-tenant insert + cross-tenant UPDATE raise; both the customer and supplier side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(security): tenant guards on six SECURITY DEFINER write RPCs (backstop) bulk_book_transactions, match_batch_allocate, mark_entry_as_opening_balance, reserve_voucher_range, release_voucher_range and rotate_company_inbox are all SECURITY DEFINER and EXECUTE-able by `authenticated`, so an authenticated user could call them via PostgREST with ANOTHER company's p_company_id. Three already carried an auth.uid()-based membership check and rotate_company_inbox an owner/admin gate, but the two voucher-range RPCs had NO tenant check at all. Adds the canonical claims-based guard (mirrors 20260615120000_link_voucher_rpcs_tenant_guard.sql lines 54-69) at the top of each body: for anon/authenticated callers, membership of p_company_id (public.user_company_ids()) is required else RAISE 42501; service_role and no-claims callers (migrations, pg-harness, MCP / API-key paths whose company scoping happens in TS) bypass BY DESIGN. Each function body is otherwise copied verbatim from its latest definition; existing GRANTs re-applied. pg-real coverage in tests/pg/securitydefiner_write_rpc_tenant_guards.pg.test.ts: per RPC — userA session targeting companyB raises 42501; targeting own company passes the guard (succeeds or yields a non-42501 domain outcome, documented inline); a no-claims bare-pool cross-tenant call bypasses the new guard, proving the service-role / MCP paths are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(db): renumber tenant-backstop migrations to 20260619130000/130100 PR1 (agent attribution) claimed the 20260619120000 version slot in the same batch; Supabase migration versions must be unique across the repo, so the tenant-backstop pair moves to 130000/130100. Filename-only change plus the matching doc-comment references in the two pg tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(db): restore source comments dropped in copied RPC bodies The guarded redefinitions of bulk_book_transactions and match_batch_allocate must be byte-verbatim copies of their latest sources (modulo the inserted tenant-guard block) so the next CREATE OR REPLACE copy keeps full provenance. Restores the Round-2/Round-3 compliance-fix annotations that were lost in the copy. Verified mechanically: zero residual diff vs sources after stripping the guard block, for all six functions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): drop raise-guards from bulk_book/match_batch — they break the jsonb error contract Local full-migration replay + pg-real run surfaced that prepending the 42501 raise-guard to bulk_book_transactions and match_batch_allocate changes their error contract for authenticated cross-tenant callers: both already enforce membership in-function and return structured domain errors (BULK_BOOK_UNAUTHORIZED / BATCH_UNAUTHORIZED) that routes, MCP tools, and their existing pg tests branch on. The guard added no isolation (they were tenant-safe) but broke that contract. The migration now guards only the four RPCs where it is sound: mark_entry_as_opening_balance (P0001→42501, still an exception), rotate_company_inbox (already 42501), and the two genuinely unguarded voucher-range RPCs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): compliance-review round — log hygiene, explicit INVOKER, anon revoke, UPDATE-path test Addresses the compliance-swarm findings on this PR: - Pre-flight dirty-data check now raises with COUNTS only; the row ids move to RAISE NOTICE so error pipelines do not ingest identifier dumps (ASVS V8.2.1 / SOC 2 CC6.1). - enforce_payment_company_consistency() declares SECURITY INVOKER explicitly — the default was already INVOKER; this makes the security model self-documenting. - REVOKE ... FROM PUBLIC, anon on reserve/release_voucher_range and rotate_company_inbox, matching the mark_entry_as_opening_balance pattern. - Adds the missing supplier_invoice_payments UPDATE-path trigger probe (SOC 2 PI1.3). Dismissed as by-design: the JWT-claim trust boundary (set_config requires direct SQL access, which already bypasses by design — same model as 20260615120000). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(db): voucher-range compliance guards + FK-rerouting trigger probes Review round 2 on this PR: Swedish compliance review (both pre-existing function behaviour, hardened while the PR owns these bodies): - reserve/release_voucher_range now refuse closed/locked fiscal periods (BFL 5 kap 5§ — the sequence of a locked period is räkenskapsinformation; mirrors mark_entry_as_opening_balance). - release_voucher_range asserts no verifikat exist in the released range before rolling last_number back (BFL 5 kap 6-7§ — never re-issue or orphan posted verifikationsnummer). Neither guard can fire in the legit SIE-import flow, which only releases numbers above its highest inserted verifikat into an open period — and the import caller treats a failed release as non-fatal. Greptile P2: the UPDATE OF <parent_fk> trigger leg was never probed — added cross-tenant FK-rerouting rejection tests for both payment tables (the supplier company_id UPDATE probe landed in the previous commit). Verified: full migration replay on fresh supabase/postgres + 333/333 pg-real green on an origin/main merge (incl. merged #678). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): release-succeeds probe must persist — callBare rolls back The legit-path release test asserted last_number after calling the RPC via callBare, whose BEGIN...ROLLBACK wrapper undoes the UPDATE before the assertion reads it (caught in CI; the local pre-push replay had validated the branch's committed state, not the then-uncommitted test). Call the RPC directly on the pool, like the engine pg tests do for persisting calls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bc61862e76 |
feat(agent): telemetry + CI-gate quick wins from the "AI systems that ship" audit (#677)
* feat(agent): telemetry completeness + durability, CI gates, commit_method provenance Quick wins from the "Building AI systems that ship" audit: - mcp.tool_called gains errorMessage (message_sv, truncated 500 chars) on all failure exits; new mcp.skill_loaded event on every gnubok_load_skill (all tiers) so atom usage is finally measurable - event_log: (event_type, created_at) index; cleanup cron keeps mcp.*/agent.* telemetry 180 days (delivery events stay 30) - CI: lint ratchet (npm run check:lint — 60 legacy errors baselined, fails only on NEW errors) and a pg-real coverage gate (migrations touching trigger/RPC/RLS/DEFERRABLE require a *.pg.test.ts change; escape hatch: -- pg-test: covered-by/skip) - journal_entries.commit_method CHECK widened with 'api_key'/'agent'; the MCP approve path records 'api_key' truthfully instead of 'user_accept' (agent_first_vision §8 P0-1). 'agent' is reserved — ALL MCP traffic (incl. claude.ai OAuth, whose access_token is a minted API key) authenticates as api_key today Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(import): derive opening balances from prior-year #UB when SIE lacks #IB (#675) SIE files exported without #IB 0 rows (only #UB -1) previously imported with zero opening balances. getEffectiveOpeningBalances() now derives IB from prior-year UB for balance-sheet accounts when explicit #IB is absent, surfaces the derivation as an info issue in the import preview, and excludes share-capital vouchers from opening-balance detection. Detection regexes are shared between parser and importer so the two checks cannot drift. 507 lib/import tests pass. (Authored in a parallel session in this checkout; included per request.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): address PR #677 bot findings — RoPA entry, execFileSync, gate scope note Triage of the compliance-swarm + Greptile findings: Applied: - .compliance/ropa.yaml: new mcp.telemetry processing activity declaring the 180-day mcp.*/agent.* retention, lawful basis, data categories, and the no-args/no-results minimisation (ISO A.8.10, GDPR Art.5(1)(c) — the retention split is now formally documented, referenced from the cron) - check-pg-test-coverage.mjs: execFileSync with argv array — no shell, so a hostile base-ref can't inject (ASVS V13.2.1); verified an injection attempt exits 2 without executing - check-pg-test-coverage.mjs: documented the PR-level (not per-migration) scope of the gate so reviewers know to check coverage per migration when a PR carries several risky migrations (Greptile P2) Acknowledged, no change: - errorMessage PII risk: messages are domain-mapped strings; event_log already persists far richer delivery payloads under the same RLS; now declared in ropa.yaml - cron error envelope: errorResponse maps to the canonical safe envelope and the endpoint is CRON_SECRET-gated - two-pass delete "partial state": TTL deletes are idempotent — the next daily run sweeps whatever a failed pass left behind - skill_loaded actorLabel/sessionId: mirrors the pre-existing mcp.tool_called payload; sessionId is the join key the analytics exist for Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f538401988 |
Invoice correctness bundle: voucher-link race, agent send guards, payment-reversal restore (audit C2/C17, F-2026080) (#666)
* fix(invoices): atomic link_invoice_to_voucher RPC — close the customer voucher-link race (audit C2) linkInvoiceToVoucher() did UPDATE-then-INSERT with a manual rollback restoring a STALE pre-link snapshot: under concurrent linking on the same invoice, A's failed insert could overwrite B's successful link while B's payment row remained — corrupting paid_amount/AR. Mirrors the supplier-side link_supplier_invoice_to_voucher fix (PR #602). - New SECURITY DEFINER RPC locks the invoice FOR UPDATE, re-validates (status, posted voucher, 151x AR credit, currency, overshoot, already-linked) and applies UPDATE + INSERT in one PG transaction. Inherits the supplier RPC's remaining-amount fix (trust stored remaining_amount even at 0 — the TS '> 0' guard let rounding drift slip past FULLY_PAID). Hardened per audit A5: REVOKE from PUBLIC/anon, GRANT to authenticated + service_role. - linkInvoiceToVoucher() now delegates to the RPC — same signature, same LINK_VOUCHER_* codes, so all callers (route, pending-op executor, MCP) are unchanged. Keeps the invoice.paid event (now emitted with the post-link row, mirroring the supplier wrapper) and the best-effort bank auto-reconcile. - pg-real tests: full/partial link, overshoot leaves the invoice untouched, ALREADY_LINKED, and the race regression (two concurrent full links -> exactly one wins, paid_amount never exceeds total, exactly one payment row). Verified locally against supabase/postgres:15.8.1.060 with all 334 migrations replayed: 10/10 pass. Two unrelated pg tests fail locally with AND without this change (pre-existing env sensitivity; green in CI). - Unit tests re-mocked to the RPC-wrapper contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): agent send path — block cancelled invoices + preflight PDF render (audit C17) commitSendInvoice (the agent/MCP path) was missing two guards the send route has: - No cancelled guard: a cancelled invoice passed the already-sent check, got re-rendered and EMAILED (a 'MAKULERAD' PDF delivered as if live), and the unguarded status flip silently re-activated it to 'sent'. Now rejected with the registry's INVOICE_SEND_CANCELLED message (400), mirroring the route. - No preflight render: the executor assigned the F-series number BEFORE rendering, so a render failure left a numbered-but-never-issued invoice (an F-series gap if the draft is abandoned). Now mirrors the route: on fresh allocation, render with an 'F-PREVIEW' placeholder first and reject with INVOICE_SEND_PDF_RENDER_FAILED before any number is consumed; retries with an existing number skip the preflight. Items/credit-note lookup moved above the preflight (it needs them); the real render and everything downstream are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): payment reversal restores invoice state and releases bank line (F-2026080) Reversing a payment voucher left the customer invoice deadlocked: status stayed 'paid' while remaining_amount stayed stale (= total), and the bank transaction kept pointing at the reversed JE so the line could neither be re-matched nor deleted. - Customer branch now recomputes remaining_amount from total (the supplier branch already did) and clamps paid_amount at 0. - Both branches delete the payment row(s) tied to the reversed voucher so a re-match doesn't double-count or trip the unique indexes. - New releaseLinkedTransactions() detaches bank transactions from the reversed JE (by journal_entry_id and by captured payment transaction ids), clearing the link/categorization columns so the line returns to the inbox. Covers every standalone storno path (reverse route, MCP reverse tool, delete-last-voucher); the match-invoice route already handled its own case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transactions): match-invoice preview double-subtracted VAT on per-item path (F-2026080) InvoiceItem.line_total is the NET line amount (it sums to invoice.subtotal, each line's vat_amount = line_total * rate), but the preview's per-item rate aggregation computed sub = line_total - vat_amount, double-subtracting VAT and producing an unbalanced previewed verifikat (revenue credit too low against the 1930 debit). The commit path (generatePerRateLines) was already correct; only the preview disagreed. Regression test mirrors the F-2026080 invoice: multi-item 25% SEK cash entry must balance, with 3001 = subtotal and 2611 = vat_amount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): address PR #666 review — supplier cash reversal, RPC tenant guard, CI fixes Review feedback fixes: - Supplier cash-payment reversal (Greptile): the supplier branch required a payment row before restoring status/amounts, so reversing a supplier_invoice_cash_payment (which books no payment row) left the invoice deadlocked at paid/remaining=0 — the same bug the customer branch fixed. Mirror the customer fallback (revert full paid_amount when no row exists). - Payment-row lookups now filter by invoice id + company_id: a batch voucher (match_batch_allocate) carries one payment row per invoice under the same journal_entry_id, so the unfiltered .single() errored out and silently yielded null. - Tenant guard on the voucher-link write RPCs (compliance V8.2.1, audit A5): link_invoice_to_voucher and link_supplier_invoice_to_voucher are SECURITY DEFINER + authenticated-executable, so any signed-in user could mutate another tenant's invoices via PostgREST. New migration applies the PR #625 claims-based membership guard to both, caps p_notes at the Zod layer's 2000 chars, and gives the supplier RPC the explicit REVOKE/GRANT it never had (was default PUBLIC execute). Covered by a new pg-real test. - releaseLinkedTransactions now logs Supabase errors (compliance V16.1) — a failed release leaves a bank line stuck on a reversed JE and must be observable. CI fixes: - naive-ore-round ratchet (core-only): payment-sync.ts converted to roundOre() from @/lib/money (-4 occurrences vs baseline). - match-batch-allocate.pg.test.ts flake (pg-real): Date.now()+random arrival numbers collided in CI; now time-component + monotonic counter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): address PR #666 review round 2 — payment attribution, batch-scoped deletes, send guard - RPC payment attribution (GDPR Art.32): user-session callers can no longer attribute invoice_payments / supplier_invoice_payments rows to an arbitrary user via p_user_id — the JWT sub is authoritative when role is anon/authenticated. service_role / direct callers keep p_user_id verbatim (their scoping happens in TS). pg-real test asserts the spoofed id is ignored. - Payment-row deletes scoped to the source invoice (SOC 2 CC6.3): a batch voucher carries sibling payment rows for other invoices whose status this sync doesn't restore; deleting them desynced paid_amount from the rows. - releaseLinkedTransactions success audit log: transactions has no write_audit_log trigger, so clearing the link/categorization columns now logs the affected transaction ids for incident reconstruction. - commitSendInvoice guard extended with partially_paid/credited (ASVS V2.3): both imply the invoice was already issued; the status flip would have regressed them to 'sent'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3e42fc6f32 |
Feat/voucher docs (#664)
* feat: implement inbox document picker and linking functionality * feat: implement self-billing invoice functionality - Added support for registering self-billed invoices received from customers. - Updated the invoice schema to include fields for self-billing metadata such as `is_self_billed`, `external_invoice_number`, `self_billing_agreement_ref`, and `received_date`. - Created API route for handling self-billed invoice submissions, including validation and error handling. - Implemented database migrations to add necessary columns and constraints for self-billing invoices. - Developed tests to ensure correct behavior of self-billing invoice creation and validation rules. - Updated Swedish localization files to include new terms related to self-billing. * feat: enforce SIE import requirement for non-Fortnox providers in migration process * feat: streamline invoice processing and enhance error logging across APIs |
||
|
|
c74b19df1b |
Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
358c25094d |
fix(security): tenant guard on the GL-line read RPCs (#625)
* fix(security): tenant guard on the GL-line read RPCs (PR #624 follow-up) get_unlinked_gl_lines and get_account_gl_lines_for_matching are SECURITY DEFINER and EXECUTE-able by anon/authenticated, so any authenticated (or anonymous) caller could invoke them directly over /rest/v1/rpc with another company's id and read its general-ledger lines — a cross-tenant read that bypasses the API routes' requireCompanyId() guard. Confirmed against the DB: anon and authenticated both hold EXECUTE, and SECURITY DEFINER sidesteps RLS. Add an in-function guard constraining anon/authenticated callers to their own companies (the same boundary user_company_ids()/RLS enforces). Trusted callers are untouched — service_role (the enable-banking reconciliation cron) and direct / superuser access (migrations, the pg-real harness) are not anon/authenticated, so the predicate is a no-op and behaviour is unchanged. A foreign company id now yields zero rows, not data. Scope: hardens the two READ RPCs that expose ledger data. The remaining company-scoped SECURITY DEFINER RPCs are writes / sequence generators with their own internal authorization; a broader audit of that set is tracked separately. pg-real coverage: a company-B member probing company A gets zero rows from both RPCs, while a company-A member and direct/superuser access still see the data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): revoke EXECUTE from PUBLIC/anon on the GL-line read RPCs Defense-in-depth follow-up to the tenant guard. The guard already returns zero rows to an anon/authenticated caller probing another company; this additionally strips the EXECUTE privilege so an unauthenticated (anon) caller cannot invoke the financial-ledger RPCs at all. Supabase grants EXECUTE to PUBLIC as well as to anon, and anon is a member of PUBLIC — revoking only anon is insufficient, so revoke both, then keep the two legitimate callers: authenticated (the API routes call via the user's session; the in-function guard scopes them to their own companies) and service_role (the enable-banking reconciliation cron). Verified on the DB: anon EXECUTE = false, authenticated/service_role = true. Adds an anon-role pg test asserting the call is rejected at the privilege layer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): read JWT role from claims object in the GL-line RPC guard The 20260611120000 guard used auth.role() to detect the caller's role, but auth.role() reads the individual request.jwt.claim.role GUC first and only some installs fall back to the claims object. PostgREST sets the claims OBJECT (the individual claim.* GUCs are deprecated), and the pg-real harness sets request.jwt.claims (+ claim.sub for auth.uid()) but NOT claim.role — so on an auth.role() without the object fallback it returns NULL and the guard's NOT IN ('anon','authenticated') branch was TRUE, skipping the membership check. A pg-real test caught it: an authenticated non-member could still read another company's GL lines (the guard failed open in that environment). Read the role straight from request.jwt.claims (exactly what auth.role() itself falls back to), so the guard enforces in every environment regardless of which JWT-claim GUCs are populated. Verified on the DB: an authenticated non-member evaluates both guard branches false → row excluded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
094bd85e81 |
fix(reconciliation): secondary-account scoping, dialog clipping, and N:1 matching (#624)
* fix(reconciliation): secondary-account scoping, dialog clipping, and N:1 matching Three follow-ups to per-account bank reconciliation (PR #623): - Secondary same-currency accounts (e.g. a 1931 savings account) double-counted the company's unassigned (NULL cash_account_id) transactions, inflating their bank total and showing a large bogus difference while 1930 still reconciled. Only the primary cash account now claims NULL rows; every other account scopes strictly to its own id. `includeUnassigned` is threaded through all status/run/list call sites from cash_accounts.is_primary. - The "Matcha mot befintlig verifikation" picker's dropdown was absolutely positioned inside the dialog's overflow-y-auto container and got clipped. Add an `inline` mode that renders the candidate list in normal flow; the dialog uses it, the reconciliation view keeps the compact overlay. - N:1 matching: several bank transactions can now settle one verifikat (a salary run paid in multiple transfers, an invoice paid in instalments). New get_account_gl_lines_for_matching RPC surfaces already-matched vouchers with a linked_transaction_count behind a "Visa även matchade verifikationer" toggle; manualLink's 1:1 guard is relaxed (the aggregate difference still catches mis-links). Tests: extended bank-reconciliation unit tests (strict scope + N:1), rewrote the cash_account_id isolation pg test to prove NULL rows land on the primary account only, and added a pg test for the new RPC. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): address PR review — accurate "att matcha mot" count - BankReconciliationView: the "N verifikationer att matcha mot" hint counted glLines (which includes already-matched vouchers when "Visa matchade" is on), overcounting the vouchers that still need a transaction. Use unmatchedGlLines so the label is correct regardless of the toggle (matches the table below). - MatchVerifikationPicker: document that `open` is overlay-only; the setOpen() writes are intentional no-ops in inline mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b5c3c3ec04 |
fix(reconciliation): surface bank tx + match to existing verifikat, delete UX, DPA links (#623)
* fix(reconciliation): surface bank transactions via robust cash_account_id scoping The per-account reconciliation scoping silently returned zero transactions for companies whose rows were NULL or mis-assigned mid-backfill (e.g. Arcim: 138 transactions, 101 unbooked, yet Bankavstämning showed "0 kr" while the 1930 GL movement and a large difference still displayed). Two causes, both fixed: - scopeTransactionsToAccount used a fragile nested or(...,and(is.null,...)) PostgREST filter. Replace it with a flat, reliable `currency = X AND (cash_account_id = id OR cash_account_id IS NULL)` and share the one implementation with /api/transactions so the status card and the lists can never drift. - The original best-effort backfill only touched NULL rows and an earlier revision mis-assigned cash_account_id (the since-fixed min(uuid) bug), which migrations cannot self-correct. Add an idempotent repair migration that re-seeds the default 1930 account and re-derives cash_account_id (correcting non-NULL mis-assignments) for booked rows and single-account companies. Also localise manualLink's user-facing errors to Swedish. Adds unit coverage for the new filter shape and pg-real coverage for the repair (incl. the Arcim single-account reproduction). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): match a bank transaction to an existing verifikat Adds a "Matcha mot befintlig verifikation" action to the Transactions inbox so a bank line that is already booked elsewhere (a salary run, a Fortnox/manual voucher, an invoice paid from the invoice page) can be linked to that existing verifikat with no new bokföring — the capability previously lived only in Reports → Bankavstämning. - Extract the searchable MatchVerifikationPicker into a shared client component. - New MatchVoucherDialog: resolves the tx's cash account, fetches candidates ranked server-side by reconciliation confidence, links via /api/reconciliation/bank/link (so the link is undoable in Bankavstämning). - unmatched-entries route gains an optional transaction_id that ranks candidates (ranking stays server-side; the recon lib is not client-safe). - Inbox row's overflow (⋯) menu gains the new action. Also fixes the Bankavstämning view: editing the date no longer auto-reloads (applies on Filtrera / account change only) and Datum till defaults to today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transactions): actionable Swedish errors when a bank tx cannot be deleted The delete route returned a hardcoded English 409 that getErrorMessage mapped to the misleading generic "En konflikt uppstod. Ladda om sidan...". Return structured bilingual envelopes instead: - TRANSACTION_DELETE_BOOKED (409) for a booked/linked row — steers the user to unlink in Bankavstämning or storna the voucher. - TRANSACTION_DELETE_HAS_AUDIT_TRAIL (409) for the real, common case where an unbooked row carries payment_match_log rows: the cascade hits the audit-immutability trigger (P0001), previously surfaced as a bare 500. Steers the user to match-to-voucher or ignore instead. Updates the DELETE test suite and adds the P0001 case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): surface DPA and privacy policy links The /dpa page (personuppgiftsbiträdesavtal, GDPR Art. 28) was complete but linked from nowhere. Add a "Sekretess och avtal" card on Inställningar → Konto linking to /privacy and /dpa, and a reciprocal link to the DPA from the privacy policy's sub-processor section. (The DPA already links back to /privacy.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): address PR #623 feedback - unmatched-entries: when transaction_id is supplied but resolves to no row in the caller's company, return an empty candidate list instead of silently falling back to the full unranked list (Compliance Swarm V8.2.1, high). - MatchVoucherDialog: preserve a manually-picked voucher when the candidate list reloads (e.g. "Visa alla datum") instead of discarding it (Greptile P2). - DELETE /api/transactions/[id]: return the 404 as the structured { error: { code, message, message_en } } envelope like the handler's other errors, for a uniform contract (Greptile P2). Test updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): allow editing notes on a committed journal entry Saving a note on a posted verifikation failed with "Committed entries are immutable": enforce_journal_entry_immutability() had no posted→posted path, so a notes-only UPDATE fell through to the final RAISE. `notes` is internal annotation metadata (not verifikation content under BFL 5 kap. / BFNAR 2013:2), so add a narrow carve-out that permits a notes-only change on a committed entry — verified with a whole-row to_jsonb() diff so any other field change still raises, and only when status is unchanged. Period-lock enforcement is unaffected. CREATE OR REPLACE in a new migration (same pattern as 20260428160000_fix_journal_entry_immutability_delete_bypass); the migration-017 protections are extended, never weakened. Covered by a pg-real test asserting a notes edit succeeds while amount/description/account edits still fail. (Already applied to production; committing the file + test for repo consistency.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sandbox): make pre-staged pending operations executor-complete The seeded pending_operations existed only as display previews — approving them failed because commit executors in lib/pending-operations/commit.ts validate required fields on "Godkänn". Seed a backing invoice_inbox_items row and fill the supplier-invoice and categorize params with every field the executors require (inbox_item_id, full items array; real uncategorized transaction_id + category), so the sandbox approval queue is actually approvable end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
953980c875 |
Per-account bank reconciliation + overdue/inbox/privacy fixes (#619)
* feat(reconciliation): scope bank reconciliation per cash account via transactions.cash_account_id A company with two same-currency cash accounts (e.g. checking 1930 + a savings account) saw every SEK transaction on every account, and the status card summed across both — reconciliation filtered transactions by CURRENCY while filtering GL lines by ACCOUNT (issue #604). Bind each bank transaction to the cash_accounts row it settled on: - New nullable transactions.cash_account_id FK (ON DELETE SET NULL — a bank transaction is räkenskapsinformation, BFL 7 kap, and must survive cash-account deletion) + a best-effort 4-pass backfill. - All reconciliation/transaction queries scope to the selected account with a NULL->currency fallback, so legacy/un-backfilled rows never disappear mid-backfill. - ingestTransactions stamps cash_account_id from the batch's settlementAccount; categorize + manualLink resolve and use it. - Bank leg now books to the transaction's actual settlement account via applySettlementAccount (no-op for 1930), so interest/fees on a savings/EUR account reconcile instead of mis-booking to 1930. - manualLink cross-checks the transaction's account and requires a voucher line on the selected account (no silent cross-account links). - BankReconciliationView: quick-book menu for any settlement account, in-flight request abort on account/date switch, 500-row truncation notice, per-account state reset. - pg-real coverage for the FK, all backfill passes, account-scoped query isolation, and cross-company isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): stop marking paid invoices and credit notes as overdue update_overdue_supplier_invoices() (the daily pg_cron job) flipped every past-due 'registered'/'approved' row to 'overdue' without looking at the outstanding balance. Credit notes — created 'registered', remaining 0, due today — got flipped the next day, surfacing as "Förfallen" with "kvar att betala 0 kr"; so did any fully-paid invoice left in 'registered'/'approved'. Guard the cron on remaining_amount > 0.005 (the "fully paid" threshold used by the payment/match paths) and is_credit_note = false, and backfill the rows already mis-flagged (credit notes -> 'registered', paid -> 'paid' with paid_at stamped only when missing). pg-real coverage for the guarded function and the one-off backfill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): refresh dokumentinkorg on realtime row changes The InvoiceInboxWorkspace only refetched on mount and on explicit in-component actions. When an inbox item was resolved out of band — the in-app agent sheet committing a staged create_supplier_invoice_from_inbox / book-direct op, the /pending page approving one, or another tab booking it — none of those paths called fetchItems(), so the booked underlag stayed in "Att göra" until a manual reload (issue #600). Add invoice_inbox_items to the supabase_realtime publication (mirrors the /pending fix in 20260520120100) and subscribe in the workspace, refetching the whole list on any change so derived status/counts/ordering stay authoritative. RLS scopes the channel to the user's company. fetchItems now preserves optimistic upload placeholders so a refetch firing mid-upload can't drop an in-flight row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(privacy): disclose EU AI inference via Amazon Bedrock (eu-north-1) Update the privacy policy and DPA to state that AI inference, when AI features are enabled, runs inside the EU via Amazon Bedrock (eu-north-1, Stockholm) using Anthropic's Claude models — no transfer to a third country, prompts not retained after the call or used for model training. Add AWS as a subprocessor row and refresh the "last updated" dates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): rename invoice_inbox_realtime to avoid version collision main's #617 shipped 20260605120000_transactions_original_description.sql — the same version this branch used for the inbox-realtime publication. The Supabase migration tracker keys on the numeric version, not the filename, so the preview branch failed with a duplicate-key error on supabase_migrations.schema_migrations (version 20260605120000 already exists). Rename to the unique version 20260605120500; the body (ALTER PUBLICATION) is order-independent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): align run guard with status; harden filter interpolation Addresses PR review (greptile + compliance swarm): - The v1 and core bank/run routes rejected an unknown account uniformly, including the default '1930', while the status routes were lenient for '1930'. A company reconciling its primary SEK account without a cash_accounts row got 200 from status but 400 from run. Make run match status: '1930' falls back to currency-only scoping (cashAccountId undefined); non-default unknown accounts are still rejected. Adds a test. - /api/transactions accepts a user-supplied `currency` query param that was interpolated raw into a PostgREST .or() filter. Reject anything that isn't a 3-letter ISO code — RLS already scopes to the company, but an unsanitized value could otherwise malform/widen the filter. Assert currency/cashAccountId shape in scopeTransactionsToAccount as well. - categorize: log (instead of silently swallowing) a cash_accounts settlement-account lookup error, so a fall-back-to-1930 mis-booking is observable in the audit log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): correct backfill UPDATE..FROM join; idempotent realtime publication Two SQL errors that only surface on real Postgres (CI pg-real + Supabase preview) — the unit suite mocks Supabase, so neither was caught locally. - Backfill pass (a): `UPDATE transactions t ... FROM journal_entry_lines jel JOIN cash_accounts ca ON ca.company_id = t.company_id` referenced the UPDATE target `t` inside the FROM join's ON clause, which Postgres rejects ("invalid reference to FROM-clause entry for table t"). Move the company match to WHERE; the JOIN now relates jel<->ca only. Semantics unchanged. - invoice_inbox_realtime: `ALTER PUBLICATION ... ADD TABLE` is not idempotent (SQLSTATE 42710 if the table is already a member). The earlier version-collision push partially applied it on the Supabase preview branch, so the re-apply errored. Guard with a pg_publication_tables existence check. Both statements validated against a real Postgres: the single-line tx binds, the two-bank-line transfer stays NULL, and the publication add runs twice cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): backfill pass (c) uses array_agg, not min(uuid) Postgres has no min() aggregate for uuid, so pass (c)'s min(id) raised "function min(uuid) does not exist" on apply (CI pg-real + Supabase). The HAVING count(*) = 1 already guarantees one row per group, so (array_agg(id))[1] returns that single id. Validated the full backfill (all four passes) and the overdue migration against a real Postgres: every pass binds / falls through as intended, and the overdue guard + backfill produce the right statuses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(compliance): add RoPA entry for Amazon Bedrock AI inference (GDPR Art.30) The privacy policy now discloses AI inference (transaction categorization + document/receipt OCR) via Amazon Bedrock as a processing activity, but .compliance/ropa.yaml had no matching Art.30 record. Add it: opt-in consent basis, EU-region (eu-north-1) inference with no third-country transfer, prompts not retained or used for model training. Mirrors the privacy-page disclosure shipped in this PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c6c86cded4 |
Mcp/template data feedback (#617)
* fix(booking-templates): scope template list to the active company GET /api/settings/booking-templates relied solely on the btl_select RLS policy, which is membership-wide (user_company_ids) and returns templates from every company the user belongs to. A user who owns multiple companies saw all their templates merged regardless of which company was active. Narrow the list in the API layer (mirroring counterparty-templates) to system + the active company + the active company's team. RLS stays the security backstop; this fixes the cross-company merge within a single user's own view (it was never a cross-tenant data leak). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): show proper message for duplicate bank file upload The bank file import page mis-parsed the structured error envelope ({ error: { code, message, details } }), so a BANK_FILE_DUPLICATE (409) fell through to the generic "Kunde inte läsa filen" fallback. The upload step also hardcoded that same string as the error heading, so duplicates were doubly misreported as parse failures. - Parse the structured envelope by error.code; surface error.message for all codes instead of rendering the error object. - Add a dedicated BANK_FILE_DUPLICATE message using the importedAt / importedCount details the route already returns. - Add an optional errorTitle prop to BankFileUploadStep (defaults to the previous text) and pass "Filen är redan importerad" for dupes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tests): add comprehensive tests for recordateEntry, inbox-linking, and external-id handling - Implemented unit tests for recordateEntry in the bookkeeping module to validate various scenarios including date changes, non-posted entries, and fiscal period restrictions. - Created tests for inbox-linking status in pending operations to ensure correct handling of invoice inbox items and supplier invoices, addressing historical bugs related to status updates. - Added tests for external-id utilities to ensure consistent handling of monetary amounts and deduplication keys across different transaction sources. - Introduced new functions in external-id.ts for stable external ID generation and normalization of imported descriptions, enhancing transaction deduplication reliability. feat(migrations): add new database migrations for transaction handling - Created migration to exclude storno and correction vouchers from unmatched GL lines, ensuring accurate reconciliation. - Added a migration to preserve original bank transaction descriptions in a new immutable column, allowing for user edits while maintaining audit trails and deduplication integrity. * feat(migrations): add function to exclude storno/correction vouchers from unmatched GL lines * feat(transactions): enhance transaction handling with improved description normalization and preloaded original entries --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
28f7cefc86 |
feat(bulk-book): manual booking mode + document inheritance (#610)
* 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> --------- 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> |
||
|
|
fc7a46c3f2 |
fix(match-batch): cross-currency allocations + widened tolerance (#607)
* fix(match-batch): cross-currency allocations + widened tolerance Reported by jakob testing PR #603's MatchAllocationDialog with a SEK bank tx + a mix of SEK and USD invoices: 1. Tally rendered "1 USD + 1 SEK = 2 kr" — summing different currencies as if they were the same. 2. The 0.005 SEK tolerance blocked confirm on any FX rounding delta. ## What changed **UI (MatchAllocationDialog.tsx)** - Per-row amount input is explicitly in TRANSACTION currency (SEK for a Swedish bank import). Cross-currency rows show an "≈ X.XX (invoice currency)" hint under the input so the user can verify the FX result. - Default amount for a cross-currency allocation is `invoice.remaining × invoice.exchange_rate` (booked SEK), so the user doesn't have to mental-math the FX. - Overshoot tolerance widened from 0.005 SEK to `max(1 SEK, 0.5% × tx)` so bank-side FX rounding doesn't block confirm. A 2 400 kr tx now accepts ~12 kr of tolerance, a 100 kkr transfer accepts 500 kr. **RPC (match_batch_allocate cross_currency migration)** - BATCH_CURRENCY_MISMATCH dropped per-allocation. Mixed currencies now accepted with the convention that the cross-currency row pays the FULL invoice remaining (matches the single-tx match-supplier-invoice behavior). Partial cross-currency is out of scope for v1. - AR/AP line is booked at `invoice.remaining × invoice.exchange_rate` (the SEK that was originally on 1510/2440). FX residual is posted to 7960 (Valutakursförluster) or 3960 (Valutakursvinster) per BAS. - Sign conventions per direction documented inline: Customer: bank > booked → Cr 3960 (gain); bank < booked → Dr 7960 Supplier: bank < booked → Cr 3960 (gain); bank > booked → Dr 7960 - New BATCH_FX_RATE_MISSING when the cross-currency invoice has no exchange_rate on file (would otherwise silently book at 0). - New BATCH_FX_DEVIATION_TOO_LARGE when the user-entered amount deviates more than 10% from booked SEK — catches typos like "140" (USD invoice currency) when they meant "1390" (SEK equivalent) without rejecting genuine rate-day FX movement. RPC patched on remote via Supabase MCP. Same-currency path is byte-identical to the previous behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-batch): PR review — strict sum, bank line = tx_abs, FX validation Round-1 review fixes on the cross-currency batch allocation flow: UI (MatchAllocationDialog): - Tighten tolerance to 0.005 SEK so the "balanced ✓" indicator matches what the server will accept. The previous widened tolerance (max 1 SEK or 0.5% × tx) created a reconciliation gap where the JE's bank line could legitimately disagree with the actual bank receipt. - Require balanced before confirm — undershoot is now a blocking state with an explicit warning, not a silent "leave unallocated". - Cross-currency default no longer caps at remainingTxBudget. Capping a USD invoice's default to the leftover SEK budget could silently trigger BATCH_FX_DEVIATION_TOO_LARGE on submit. The user re-balances the other rows to fit. - Add explicit FX-rate validation (bound check 0 < rate < 100000). - When a cross-currency invoice has no usable exchange_rate on file, leave the amount blank and surface a warning instead of guessing. RPC (match_batch_allocate): - New code BATCH_AMOUNT_BELOW_TX. Strict sum check on both sides means the server can't be coaxed by a direct API caller into the same broken state the UI now blocks. - Bank line credit/debit = v_tx_abs (the actual bank movement) instead of sum-of-allocations. Same value within rounding under the strict sum check, but it makes intent legible and lets per-row FX diff lines absorb rounding. - Defense-in-depth company_id filter on all re-queries / UPDATEs in the line-build + payment-row passes. - Drop the v_booked_sek-aliasing-for-invoice.total foot-gun. Use a dedicated v_inv_total var. - Truncate invoice_number to 32 chars in line_description. Tests: - pg-real: cross-currency happy path (USD invoice paid by SEK tx with FX loss to 7960, bank line = tx_abs). - pg-real: BATCH_AMOUNT_BELOW_TX rejection on undershoot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-batch): PR review round 2 - caller user_id verification + FX bound Compliance-swarm + swedish-compliance findings on round 1: - CC6.3 (HIGH): p_user_id was caller-supplied and written into journal_entries.user_id / payment-row user_id without verifying it equals auth.uid(). Membership covered the company; nothing covered the user attribution. Two-layer fix: explicit guard rejects when p_user_id <> auth.uid(), and all writes now resolve v_caller = auth.uid() directly so the guard cant be silently bypassed. - A.8.28 (MED): server-side FX upper-bound (0 < rate < 100000) matches the UI. Previously RPC only checked > 0, allowing the UI guard to diverge. - V1.2.5 (LOW): truncate v_tx.date when concatenated into line_description (defense alongside round 1s invoice_number trunc). - Symmetry: populate supplier_invoice_payments.exchange_rate (column existed, INSERT omitted it). Customer side already populated. Matches swedish-compliances traceability note on AP rorelseskulder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-batch): PR review round 3 - drop p_user_id, CHECK constraints, payment-day rate Genuine round-2 review findings (compliance-swarm + swedish-compliance): - V4.5: p_user_id dropped from RPC signature entirely. Round-2 added a guard; this removes the attack surface at the API boundary. Caller is resolved via auth.uid() inside the function. Route updated. - V2.2: CHECK constraint on invoices.exchange_rate and supplier_invoices.exchange_rate (0 < rate < 100000). Three layers now enforce the bound: schema, RPC, UI. - swedish-compliance traceability gap: payment_exchange_rate column on both invoice_payments and supplier_invoice_payments. Populated as v_alloc_amount / v_inv_remaining for cross-currency rows so FX diffs are reconstructible from the payment record alone (BFL 7 kap behandlingshistorik). NULL for same-currency. The existing exchange_rate column continues to store the invoicing rate. - CC6.1: extract isValidExchangeRate() to lib/utils.ts. UI's three inline bound checks now share one validator. - Dead code: drop unused leftover_note i18n key (sv + en). Tests: - pg-real signature updated (4-arg -> 3-arg) across all 9 call sites. - Added payment_exchange_rate assertion to cross-currency happy path (invoicing rate 10.0 stays, payment-day rate stored as 10.5). Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): missed 4th arg in BATCH_UNAUTHORIZED pg-real test Round-3 dropped p_user_id from match_batch_allocate. The replace_all caught the userId/companyId pattern but missed the BATCH_UNAUTHORIZED test which uses outsiderId instead of userId. CI failed with "bind message supplies 4 parameters, but prepared statement requires 3". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4da87e5e4c |
feat(transactions): bulk-book + is-booked predicate (#606)
* feat(transactions): bulk-book + is-booked predicate Closes the second of the two multi-tx ↔ multi-voucher flows from the original plan. Where PR #603's match_batch_allocate took 1 tx and spread it across N invoices (samlingsbetalning), this PR takes N bank transactions on the same day and rolls them up into ONE combined verifikat (samlingsverifikation per BFL 5 kap 6§ st 3) — the kiosk masshantering pattern the user explicitly asked for. ## Backend (Phase 3b) - **PL/pgSQL RPC** bulk_book_transactions: two branches, both atomic. 1. Link to existing posted verifikat (p_existing_journal_entry_id): no new JE. Validates the JE's 19xx net equals sum(tx.amount), inserts N transaction_voucher_links rows, and for N=1 also sets transactions.journal_entry_id (1:1 reader-path back-compat). 2. Create new combined verifikat (p_new_entry with pre-computed balanced lines): the route's applyTemplate() has already done ratio + VAT expansion per the chosen mode. The RPC validates the lines balance and the 1930 net matches sum(tx.amount), then commits via commit_journal_entry. Same security pattern as match_batch_allocate: company-member check via auth.uid(), SELECT … FOR UPDATE on each tx in id order, deterministic fiscal-period resolution (ORDER BY period_start DESC). - **Endpoint** POST /api/transactions/bulk-book — fetches template via RLS, expands per mode (one_line_per_tx | sum_per_account) using lib/bookkeeping/template-library.applyTemplate, passes the resulting lines to the RPC. On success emits one transaction.reconciled event per tx. - **22 new BULK_BOOK_* error codes** (sv + en) covering all guard paths. ## UI (Phase 5b) - **BulkBookDialog** — template picker + mode toggle (segmented control: en rad per transaktion / summera per konto) + live preview table with balance + bank-leg invariant indicators. Confirm only enabled when both pass. - **Multi-select inbox** — sticky action bar gains a "Bokför i klump" button gated by same-date + same-direction across selected txs. Tooltip explains the disabled state. ## Phase 6: is-booked predicate New lib/transactions/is-booked.ts. After multi-allocation and bulk- book, tx.journal_entry_id can be NULL even though the tx is anchored (via invoice_payments / supplier_invoice_payments / transaction_voucher_links). The helper checks all three storage locations so future readers don't falsely show multi-anchored txs as "unbooked". Companion getPrimaryJournalEntryId() resolves the best JE link to surface in UI. SQL mirror is_transaction_booked() exists from the PR #602 foundation migration. Existing readers (TransactionHistoryList, TransactionInboxCard) are not yet refactored to use the helper — that's a follow-up that touches per-tx JE links across multiple call sites. The helper is documented + tested so subsequent refactors are mechanical. ## Tests - tests/pg/bulk-book-transactions.pg.test.ts — 8 pg-real scenarios (happy path create-new with 3 txs, happy path link-existing, date mismatch, direction mismatch, amount mismatch, unbalanced lines, unauthorized). - app/api/transactions/bulk-book/__tests__/route.test.ts — 5 unit tests (schema XOR, link path, create-new with template fetch + applyTemplate, structured-error mapping). - lib/transactions/__tests__/is-booked.test.ts — 11 cases covering all three storage locations + primary-JE resolution. 26 unit tests pass on touched paths. RPC migration applied to remote via Supabase MCP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #606 review round 1 + CI fixes Closes the build failure and the two real Greptile findings. ## CI - **core-only + Vercel build fail**: I used useMemo for selectedTransactions and bulkBookEligible on the transactions page without importing it. TypeScript build (`next build`) caught it with "Cannot find name 'useMemo'". Fixed the import. ## Review findings - **(P1) Currency mismatch returned BULK_BOOK_DIRECTION_MISMATCH** whose user-facing message blames direction. Mixed SEK + EUR batches would show "All transactions must be the same direction" which is factually wrong. Introduced dedicated BULK_BOOK_MIXED_CURRENCY code (sv + en) explaining the actual constraint, and switched the route to use it. - **(P1) Branch B (create-new) N=1 missed reconciliation_method='manual'**. Branch A's N=1 UPDATE sets it alongside journal_entry_id; Branch B's didn't, leaving the reconciliation_method NULL even though the single tx was reconciled via the same flow. Downstream readers (reconciliation reports, status indicators) would treat the two N=1 paths differently. New follow-up migration patches Branch B's final UPDATE. RPC patch applied to remote via Supabase MCP. 26 unit 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> |