64ea0fef0200e38fdbd142ec2a646d2db5cec6f4
223 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
528c53ffe7 |
fix(transactions): stop defaulting supplier-invoice payment account to a stale private-funds setting (#985)
* fix(transactions): stop defaulting supplier-invoice payment account to a stale private-funds setting match-supplier-invoice (POST + preview) defaulted the credited cash account from company_settings.last_supplier_payment_account, a sticky setting written by the manual mark-paid "betald med privata medel" flow. Once that setting held 2893 (skuld till aktieägare) from an unrelated private payment, every later match against a real bank transaction reused it instead of the transaction's actual bank account, silently booking genuine bank payments as shareholder-loan repayments. Resolve the credit account from the matched transaction's own cash_account_id -> cash_accounts.ledger_account instead (falling back to 1930 when unlinked), mirroring the existing settlement-account lookup in transactions/[id]/categorize/route.ts. last_supplier_payment_account is no longer read by either route. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * refactor(transactions): extract shared settlement-account resolution helper Dedupe the identical cash_account_id -> ledger_account lookup across match-supplier-invoice (POST + preview) and categorize into resolveSettlementAccount, per CodeRabbit's nitpick on PR #985. Pure extraction, no behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(transactions): cover settlement-account lookup-error and preview parity gaps Adds the two test cases CodeRabbit flagged as missing on PR #985: - POST match-supplier-invoice: cash_accounts lookup errors, falls back to 1930 and warns (previously unexercised). - preview match-supplier-invoice: linked cash account other than 1930 (parity with the equivalent POST-route test). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(transactions): thread resolved settlement account into FX/cash-method supplier-payment branches Closes the remaining items from the Swedish-accounting-compliance bot review on PR #985: - match-supplier-invoice/route.ts computed paymentAccount via resolveSettlementAccount but only passed it into the pure-SEK clearing branch; the FX branch (createSupplierInvoicePaymentEntry) and cash-method branch (createSupplierInvoiceCashEntry) still defaulted to 1930 internally even though both already accepted the parameter. - resolveSettlementAccount now also warns (and falls back to 1930) when cash_account_id resolves to a row with no ledger_account, not just on a hard query error. - Documents company_settings.last_supplier_payment_account's scope via a column comment: it must never be read to resolve a matched transaction's settlement account. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * fix(bookkeeping): abort instead of silently defaulting to 1930 when settlement-account lookup errors Compliance-bot finding on PR #987 (applies equally to #985/#986, shared helper): resolveSettlementAccount treated "no cash_account_id" and "lookup threw a real DB error" the same way -- warn and fall back to 1930. An explicit cash_account_id almost certainly resolves to a non-1930 account, so a transient failure masking it risked the exact class of misbooking this whole PR series exists to fix, just triggered by infra flakiness instead of a stale setting. Now throws BookkeepingDatabaseError on a genuine query error; every caller already runs under withRouteContext/withApiV1 (or the pending- operations dispatcher), whose existing catch-all already converts any isBookkeepingError() throw into the correct structured 500 -- no caller changes needed. The "row found but ledger_account empty" case stays warn+fallback (data-integrity gap, not a query failure). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * test(bookkeeping): use rejects.toBeInstanceOf for settlement-account error assertion Addresses CodeRabbit nitpick from the 2026-07-12 review round: matching BookkeepingDatabaseError via a `constructor` key in toMatchObject is non-idiomatic; toBeInstanceOf is the standard vitest assertion for this. Signed-off-by: Jonas Flodén <jonas@floden.nu> * docs: scope FX/cash-method paymentAccount gap note to /api/v1 and MCP routes CodeRabbit flagged the #1000 reference on PR #985 as ambiguous — the main match-supplier-invoice route's FX/cash-method branches already thread paymentAccount (per the prior entry), so the still-open gap only applies to the /api/v1 and MCP-facing route. Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.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> |
||
|
|
7d7f604e00 |
Add/stripe invoice link (#998)
* feat(supplier-invoices): show registered invoices under "Att betala" with inline approve Registered supplier invoices are already booked as debt (2440) but were hidden from the "Att betala" tab until approved, which confused users. The tab now shows registered invoices too, marked "Ej godkand" with a compact inline approve button. Approval remains the gate for payment, not visibility; status model and approve API untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reports): add date range filter to huvudbok (kontoanalys) Mounts the existing ReportDateRange control on /reports/huvudbok so the ledger can be narrowed to any date range within the fiscal year, matching Fortnox kontoanalys. Lines before the range roll into each account's opening balance so running balances stay correct at the range start; lines after the range are dropped. Applies to the XLSX export too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): add optional payment link on invoices (paste-link MVP) The user pastes a payment link created in their PSP dashboard (e.g. a Stripe Payment Link) onto an invoice. The recipient gets a "Betala online" button in the invoice email and a QR code + clickable link in the PDF payment box. No PSP integration server-side: this is the demand probe; a future Stripe Connect integration would auto-fill the same column. - invoices.payment_link_url (migration 20260709090000), https-only + 2048-char cap enforced in CreateInvoiceSchema; empty string normalises to undefined and build-invoice-write always writes a concrete value so clearing the field on a draft edit NULLs the column - editor field (real invoices only) with one-link-per-invoice hint; strings in sv+en (messages landed via e0e11066) - email button (customer.language, hidden for credit notes/proforma/ delivery notes, URL escaped for the href attribute) + URL in the plain-text part - PDF QR + link row following the Swish QR pattern; wired into send, download and preview routes - derived documents (credit note, proforma convert, recurring) do NOT copy the link: it encodes one amount for one specific invoice - MCP gnubok_create_invoice accepts payment_link_url (validated at staging and re-checked in the commit executor); v1 API exposes the column; tools/list token ceiling bumped 45K -> 45.5K (ledger entry in payload-size.bench.test.ts, headroom was <10 tokens) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): show oresavrundning on editor/form totals, supplier list and invoice email The rounding logic (getDisplayTotal) was correct but only applied on the PDF, invoice list/detail and review dialog. The invoice editor summary, the supplier invoice form totals and the supplier invoice list showed the raw ore total right next to the toggle, and the invoice email said "Att betala" with the unrounded invoice.total while the attached PDF showed the rounded amount (and the email also ignored the ROT/RUT deduction). Extract the PDF's Att betala block into getAmountToPay (lib/invoices/rounding.ts) and point PDF + email at it so they cannot drift; behavior-identical refactor for the PDF. Booked amounts stay ore-exact; display-only as designed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): adapt huvudbok date-range tests to the two-step entry-lines fetch The date-range tests (0969168f) mocked the old single-query shape with the parent entry embedded on each line; main's refactor (fetchEntryLines) queries journal_entries first and reattaches. Queue entry rows like the other tests so the merge of the two features is actually exercised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): fetch full invoice projection in v1 send so ROT/RUT deduction and payment link reach the PDF and email The v1 send route's hand-rolled column list omitted deduction_total, deduction_personnummer_last4, payment_link_url and the item-level ROT/RUT fields, so invoices sent via the public API overstated 'Att betala' and dropped the deduction box. Reuse the shared INVOICE_FULL_COLUMNS/INVOICE_ITEM_FULL_COLUMNS so the send row can never drift from the GET shape again. Also harden the supplier-invoice inline approve: a thrown fetch left the button stuck spinning; failures now refetch the true server state. 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> |
||
|
|
abe9ac9d8c |
Fix/attributes config (#926)
* fix(git): pin LF on generated extension registry and vitest snapshots setup:extensions and vitest write these files with LF; with core.autocrlf=true git expects CRLF and flags them as phantom modifications on every dev/build run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): enforce MFA on mcp-oauth consent and gate viewer storno route mcp-oauth/authorize renders an HTML consent page and issues 303 redirects that withRouteContext cannot express, so it kept raw getUser() and thereby skipped the AAL2 gate: a password-only (AAL1) session could approve consent that mints a long-lived, MFA-bypassing API key. Add a route-local requireAal2() step-up on GET and POST; AAL1 sessions redirect to /mfa/verify, BankID users are exempt. Separately, POST /api/reports/vat-declaration/rc-basis-gaps/fix calls correctEntry() (storno of a posted entry) but lacked requireWrite, so viewer-role members could trigger it. Add { requireWrite: true }. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route transactions endpoints through withRouteContext Migrate the transactions routes off hand-rolled supabase.auth.getUser() onto the MFA-enforcing withRouteContext wrapper; add requireWrite on mutating handlers (book, uncategorize, attach-document, ignore, batch-match, create-from-document). Behavior and response shapes preserved; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route SIE import and bank reconciliation through withRouteContext Migrate import/sie and reconciliation/bank routes onto the MFA-enforcing wrapper; requireWrite on mutations (import execute, create-accounts, mappings write verbs, link/unlink/run/mark-opening-balance). Reads (status, unmatched-entries) stay ungated. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route salary endpoints through withRouteContext Migrate salary employees and runs routes (plus ku, payroll-config, tax-tables) onto the MFA-enforcing wrapper; requireWrite on mutations. Personnummer masking/encryption untouched; file downloads (AGI XML, payslip PDF, payment files) keep their headers. Two payment-file GETs retain requireWrite because they stamp *_file_generated_at and previously gated viewers. Tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route report endpoints through withRouteContext Migrate the read-only report routes (trial balance, balansrapport, resultatrapport, income statement, ledgers, KPI, VAT declaration, salary journal, monthly breakdown, journal register, continuity check, full archive, etc.) onto the MFA-enforcing wrapper. All read-only, no requireWrite. JSON/XLSX/PDF/ZIP response bodies and headers preserved byte-for-byte; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route invoices, skatteverket, agent and extension endpoints through withRouteContext Migrate invoices, supplier-invoices, skatteverket tax-payments, and dynamic extension routes onto the MFA-enforcing wrapper with requireWrite on mutations. The two NDJSON streaming agent routes (invoke, onboarding/stream) use requireAuth() directly (the wrapper can't wrap a streaming response) so MFA is still enforced. skatteverket payment-file GET keeps requireWrite (stamps a generated-at field). Response shapes and file headers preserved; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route documents, events, team and account endpoints through withRouteContext Migrate documents, events, kpi/preferences, vat/validate, support/contact onto the MFA-enforcing wrapper with requireWrite on mutations. account/password, team/accept and team/members use requireAuth() directly (user-level or pre-membership flows with no active company context) so MFA is still enforced. events keeps its dual API-key-or-session auth. Document retention guard untouched; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route settings and pending-operations endpoints through withRouteContext Migrate settings (api-keys, oauth-clients, booking-templates, counterparty-templates, logo, company settings) and pending-operations (commit, bulk-commit, reject, edit-before-approve) onto the MFA-enforcing wrapper with requireWrite on mutations. Credential-guarding routes keep their per-user ownership filters. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(guards): ratchet raw-route-auth baseline 119->1 after A1 migration Lock in the withRouteContext migration so the count cannot regress. The single remaining entry, mcp-oauth/authorize, is a documented exception (HTML consent + redirects, MFA enforced via route-local step-up). Record the campaign and requireWrite decisions in DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vat): add eSKD momsdeklaration file export for "Deklarera via fil" Generate the Skatteverket eSKDUpload v6.0 XML file so users can file VAT by upload instead of typing every ruta into the form. Extract buildFiledAmounts() as the shared whole-krona source of truth (öre truncated per SFL 22 kap 1 §) so the XML file and the manual-filing PDF can never disagree. Adds the /eskd API route, an XML option in the report export menu, and the upload button on the manual-filing card. Strings in sv + en. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(vat): add 'vat_settlement' source type and update related components * fix(booking): adjust search input layout and enable autofocus * fix(vat): support 12-digit org numbers and adjust emission order for eSKD file * fix(migration): add 'vat_settlement' to journal_entries.source_type CHECK --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
63e05c4eec |
fix(import): company-scope the bank_file_imports dedup key (#925)
* fix(import): company-scope the bank_file_imports dedup key The bank_file_imports unique constraint was (user_id, file_hash), predating multi-tenancy: a user importing the same statement file into a second company hit an upsert that resolved onto the first company's row, which RLS rejected (42501). Widen it to (company_id, file_hash) - the swap 20260330130000 made for sie_imports but missed here - and drop the now-obsolete BANK_IMPORT_DUPLICATE_OTHER_COMPANY cross-company pre-check from the v1 route (the structured-error code stays for API compat). The migration was already applied to prod; committing it reconciles the orphan (prod schema_migrations had 20260707130000 with no matching repo file). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(import): fix stale unique-constraint comment (CodeRabbit) The completion-update comment still described the old (user_id, file_hash) constraint; it is (company_id, file_hash) since 20260707130000. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fca75df3e2 |
chore: update snapshot for endpoint counts and keys (#912)
* chore: update snapshot for endpoint counts and keys * fix(migrations): update pg-test comment for reconciliation migration clarity |
||
|
|
3a88b53fd9 |
Add/api and invoice (#911)
* feat(salary): validate employee clearing/kontonummer at entry Bank details on the "Anställda" form had no structural validation, so a typo in clearing/kontonummer was saved silently and only surfaced at Bankgirot LB generation (or never, on the SEPA path). Adds a shared validator (lib/salary/payment/bank-account.ts) wired into the create dialog, edit page, CreateEmployeeSchema, and the PATCH route: 4-digit clearing or 5-digit Swedbank (8xxxx), 5-11 digit account, both-or-neither. Mirrors encodeReceiverAccount so entry-time validation matches what the payout layer can encode. Update validates only when a bank field actually changes, so legacy free-text data stays editable. Includes a conservative clearing to bank-name hint (null for unknown ranges). Per-bank mod10/mod11 checksum deferred to a soft-warning follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(chart-of-accounts): styled delete warnings and bulk select-all Replace the native window.confirm() on single-account delete with the styled DestructiveConfirmDialog, and add to the prune dialog a master 'select all unused accounts' checkbox plus an explicit confirmation step before bulk deletion. New sv/en strings for the confirm titles and actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): encrypt personnummer on v1 employee create; tolerate legacy plaintext on read The v1 REST create route stored personnummer unencrypted, which then threw ERR_CRYPTO_INVALID_AUTH_TAG on every decrypt-on-read path and 500'd the employees roster. Encrypt on write in v1 create, decrypt on read in the v1 list/detail/patch responses, and make decryptPersonnummer pass a raw 12-digit value through with a warn so a legacy plaintext row can't take the roster down. Encrypt seeded personnummer. Add a gated, idempotent backfill for existing rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): save a manual entry as a reusable template Add a "Spara som mall" action to the manual journal-entry form next to the existing "Anvand mall" picker, so users can capture a booking pattern the moment they work it out. Opens the shared TemplateForm (create mode) pre-seeded from the current lines via deriveTemplateLinesFromBooking, and saves through the existing POST /api/settings/booking-templates. Rendered in both the mobile and desktop layouts and in create + edit modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(pending): label all staged operation types The Granskning list rendered the raw snake_case operation_type (e.g. create_supplier_invoice_from_inbox) for any type missing from the label map, which hogs the meta row and wraps awkwardly on mobile. Add short sv/en labels for all operation types in OPERATION_RISK_TIERS, plus a humanized fallback for future ones, and simplify the label map to a plain operation_type -> i18n-key record (the icon/variant fields were dead). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reports): let users file moms without a Skatteverket connection The momsdeklaration was never gated on the Skatteverket connection (it renders from the bookkeeping), but the not-connected "Anslut med BankID" card read as a wall. Make manual filing a first-class path: - Add a "Lämna in din momsdeklaration" card under the report with a PDF download (SKV 4700 layout, hela kronor) and a skatteverket.se link. - Add a momsdeklaration PDF route + template; buildManualFilingRows() rounds each ruta to whole kronor and recomputes ruta 49 per the SKV 4700 formula so it ties out. The PDF is a read/record copy, not a submission file (moms has no upload channel). - Offer PDF alongside Excel in the report's export menu. - Reframe the not-connected SkatteverketPanel to "Skicka direkt till Skatteverket (valfritt)". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(salary): compact new-employee dialog and warn on bad account check digit Redesign NewEmployeeDialog into a compact layout: borderless sections split by hairline dividers (no per-section cards), a fixed header + scrolling body + solid footer (fixes content showing through the old sticky bar), and denser grids. EmployeeTaxCard gains a `flat` variant so the dialog can host it without card chrome; the edit page keeps the boxed version. Add non-blocking Swedish account check-digit validation (lib/bankgiro/account-number.ts): mod10 (reuses luhn) + mod11, with a clearing->method table from the Bankgirot "Bankernas kontonummeruppbyggnad" spec, cross-checked against jop-io/kontonummer.js and verified against a real account (Forex 9420/4172385). Surfaced as a soft warning in both employee forms; unrecognised clearings return 'unknown' so we never warn on a valid but unmapped account. Never blocks saving. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(invoices): configurable send time + editing for recurring invoices Re-register the accidentally-removed recurring cron (now hourly) and add a per-schedule send hour (Europe/Stockholm, DST-aware). The cron never sends for a past date, and the enabling migration pauses every existing schedule on deploy so nothing auto-sends behind a user's back; users reactivate consciously (with a confirm) or click "Skapa faktura nu" to send this month on demand. Automatic sending now requires a customer email. Adds a full edit flow (row click opens the prefilled form, PATCH), fixing the row-click 404. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): configure självfaktura via the invoice API Add an optional is_self_billed flag (plus external_invoice_number, self_billing_agreement_ref, received_date) to the public invoice-create endpoint so callers can register a received self-billing invoice (mottagen självfaktura, ML 17 kap 15§) via the API. It was previously only reachable from the internal dashboard route, so it was missing from the API docs. Extract the booking into a shared service (lib/invoices/self-billed-sale.ts) and refactor the internal /api/invoices/self-billed route to a thin wrapper over it, so the dashboard and the API cannot drift. Books as a sale (Debit 1510 / Credit 30xx+26xx) with the counterparty's number; no own number is consumed. Fields are plain optionals (no schema refine) so UpdateInvoiceSchema.omit() keeps working; required-when-self-billed is enforced in the route. Documented in the endpoint registry. No migration (columns already exist). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(settings): allow a partial voucher-series-per-source-type map In Zod 4 an enum-keyed z.record is exhaustive (every source_type required), so saving a default_voucher_series_per_source_type map that omits a source type (e.g. the newly added result_appropriation) failed with "expected string, received undefined". Use partialRecord so the map can be sparse; the engine falls back to series 'A' for any unmapped key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(salary): resolve employer name via getCompanyDisplayName Payslip PDFs, the payslip email, AGI, KU10, and the BG/LB + SEPA payment files now resolve the employer name through getCompanyDisplayName (company_settings.company_name, falling back to companies.name), matching how invoices already display it. Read-side coalesce, so no migration or backfill: companies.name is write-once at onboarding and not authoritative for these surfaces. The sidebar company switcher uses the same coalesce for the non-active companies in the list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(kontoplan): index-only account usage counts + lighter reference load Add a covering index on journal_entry_lines (journal_entry_id, account_number) so get_account_usage_counts becomes an index-only scan (prod worst case ~440ms). Slim /api/bookkeeping/accounts/reference to return only the company's activation rows and merge against the client-bundled BAS_REFERENCE instead of re-sending the full ~1,300-account catalog every load, and defer the BAS catalog + usage counts off the first-paint critical path in ChartOfAccountsManager. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(salary): add bank-account checksum warning string sv/en strings for the employee bank-account (clearing/kontonummer) soft checksum warning shown by the create/edit forms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: update decision log Append the 2026-07-06/07 decision entries (salary employer-name coalesce, sidebar switcher, employees API personnummer fix, kontoplan load optimization, momsdeklaration manual filing, recurring invoices resend + reactivation + editing, "spara som mall", voucher-series partial map, and självfaktura via the invoice API). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: address compliance-review findings on recurring invoices + moms filing - recurring cron: close the double-send window with an atomic compare-and-set claim on last_run_at (release-on-failure) so two overlapping hourly runs can't both spawn from the same stale batch row - recurring edit dialog: force auto_send=false whenever the effective customer has no email, so a disabled-but-checked box can't PATCH auto_send=true after the async customer load - momsdeklaration manual-filing: truncate rutor to whole kronor (öretal faller bort per SFL 22 kap 1 §) instead of round-to-nearest, matching the SRU path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <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> |
||
|
|
678f2ccffd |
feat(mcp): P2 hygiene — honest category suggestions, skill-reference lint, cadence copy (#882)
* feat(mcp): counterparty-tied category suggestions + no_signal (P2-1)
suggest_categories padded every transaction with a company-wide
category-frequency fallback at <=0.5 confidence — an identical four-way
spread on 20+/24 items that agents correctly reported as pure noise
(agent.feedback). Real signal came from memory atoms and query_journal.
- History is now counterparty-keyed: buildMerchantHistory groups past
categorized transactions by normalized merchant; the engine only
surfaces history for THIS transaction's merchant, with provenance
('Bokförd N gånger tidigare för denna motpart') and occurrence-scaled
confidence (0.56 at 1x, capped 0.85). No global padding — an empty
list is the honest answer.
- The MCP tool returns no_signal_transaction_ids for transactions where
NO source matched, steering agents to investigate (query_journal)
instead of pattern-matching on unrelated rows.
- Both callers (REST suggest-categories route + MCP tool) share the new
helpers, so web UI and agents improve together.
Part of dev_docs/mcp_optimization_plan.md (P2-1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): dangling-reference validation in skills:check + fix 10 dangling links (P2-2)
skills:generate/check now fail when an atom SKILL.md links a
references/*.md that does not exist on disk — a dangling pointer ships
a 404 to every agent that follows it (the weekly-booking-check
incident, agent.feedback).
The validator immediately caught 10 live dangling links in 4 atoms,
three distinct flavors:
- filename typo: swedish-asset-accounting/references/depreciaton.md
renamed to depreciation.md (the link was right, the file misspelled)
- link mismatch: swedish-e-invoicing linked market-providers-pricing.md;
the file is market-provider-pricing.md (link fixed)
- unauthored plans: single-shareholder-ab-fmb TODOs and reklambyra's
'planerad utbyggnad' section used resolvable references/ paths for
files that were never written — rephrased as plans without paths
Seed migration regenerated (4 atoms bumped, renamed reference child).
Part of dev_docs/mcp_optimization_plan.md (P2-2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(events): align agent-feedback review cadence copy (P2-4)
gnubok_feedback replies 'we aggregate signal weekly'; the event-log
handler comment said quarterly. One of them was lying — weekly wins
(the mcp_optimization_plan triage is the living example).
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> |
||
|
|
163fbd8222 |
feat(dimensions): PR8 salary — employees.default_dimensions, per-employee cost lines, aggregation re-key (#869)
Employees carry a default dimensions bag and the salary booking puts each
employee's cost on their kostnadsställe/projekt
(dev_docs/dimensions_implementation_plan.md PR8):
- employees.default_dimensions (migration 20260702220000; jsonb DEFAULT
'{}' + object CHECK)
- salary-entries: the one-line-per-account aggregation is re-keyed to
account+bag — P&L cost lines (löner incl. line items + base remainder,
arbetsgivaravgifter, semesteravsättning + dess avgifter, pension, SLP)
split per employee bag while every balance-sheet/settlement leg (2710,
1930, 2731, 29xx, 2740, 2514) stays aggregated; liability credits equal
the sum of the rounded debit buckets so entries balance by construction;
dimension-less runs book byte-identically to before. Replaces the dead
SalaryRunEmployee.cost_center/project pair (never wired)
- both book routes (dashboard + v1) read the bag via the employees join —
read-at-book, so the run review shows exactly what will book
- employee form (new + edit) gets a gated Kostnadsställe/Projekt card;
run review shows per-employee dims chips; run GET + v1 employee
routes/schemas + MCP list_employees carry the field
- pre-merge audit: all salary reports (salary-journal, AGI,
avgifter-basis, vacation-liability) read salary_run_employees — not
journal lines — and every ledger consumer sums per account, so the
line split breaks nothing; SIE export + dimension P&L pick the split
up as intended
8 new engine propagation tests + book-route dims flow test.
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>
|
||
|
|
fb3fe82a56 |
feat(dimensions): PR5 SIE round-trip — lossless dimension import + undo lockstep (#866)
* feat(dimensions): PR5 SIE round-trip — lossless dimension import, registry upsert, undo lockstep
SIE import previously parsed and silently DISCARDED all dimension data
(object lists at sie-parser.ts:651-654, #DIM/#OBJEKT in the ignore list at
:689). Import is now lossless — the dimensions plan PR5 milestone.
Parser: #TRANS object lists ({1 "KS01" 6 "P001"}) land on the line as an
SIE-dim-no → code map (canonical numeric keys, quoted codes, malformed
pairs warn); #DIM/#UNDERDIM/#OBJEKT parse into registry records. OIB/OUB
stay ignored (dimension reporting is P&L-only in v1).
Importer (lib/import/sie-dimensions.ts): upserts missing dimensions/
dimension_values rows — never renames existing ones (ON CONFLICT DO
NOTHING); undeclared reserved numbers synthesize their SIE-standard names
(mirroring the export's orphan synthesis); codes violating the registry
CHECK are skipped with a warning but survive verbatim on lines (documented
legacy-free-text exception). Bulk voucher insert now writes the dimensions
jsonb + cost_center/project mirrors via the sanctioned dual-write helpers
(no trigger suppression needed — the immutability trigger guards
UPDATE/DELETE, not INSERT). Import auto-enables dimensions_enabled with a
result-card notice (pre-authorized by the column comment). arcim-migration
provider syncs inherit all of it via the shared parser/importer.
Undo lockstep (migration 20260702154500): created_by_import_id provenance
on both registry tables (ON DELETE SET NULL); undo_sie_import deletes the
values/dimensions the undone import introduced when no remaining
posted/reversed line references them — user-created rows and rows other
bookkeeping references are untouched. The registry guard triggers act as
backstop. replace_sie_import deliberately skips the lockstep (re-import
re-upserts the same codes). Six pg-real tests cover the lockstep.
Round-trip pinned by test: parse → import state → export → parse preserves
declarations (#UNDERDIM parent links included), values, and per-line object
lists — including synthesis of referenced-but-undeclared values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): restate function-local statement_timeout on undo_sie_import
CREATE OR REPLACE resets proconfig, so the 290s timeout from 20260629160100
was silently dropped — regressing service-client bulk deletes to the
authenticator role's 8s limit. Caught by sie-import.replace.pg.test.ts in CI.
Full pg-real suite green (483/483, TZ=UTC).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): surface OIB/OUB drops and dimension presence as parse-level info (#866 review)
Dropping object-balance records must never be silent — one info issue counts
the skipped #OIB/#OUB rows (object-level balances are P&L-out-of-scope in
v1), and a second announces dimension data before the user executes the
import (the preview step renders parse issues), so the auto-enable notice is
no longer purely post-hoc.
Triage notes for the remaining findings: the RPC's opening SELECT is the
company-ownership check the swarm asked for; registry writes are RLS-bound;
line-verbatim codes are the documented legacy-free-text exception; export
emits no #KSUMMA so there is nothing to recompute; SIE dims 3–5 are
"reserved for future use" with no standard names, so generic synthesis is
spec-correct; ON DELETE SET NULL is deliberate — provenance is operational
metadata for undo, not räkenskapsinformation (the guarded journal lines
are), and RESTRICT would block legitimate post-retention housekeeping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
11126d6d56 |
feat(dimensions): PR3 tagging — voucher-form pickers, MCP dimension tools with resolve-don't-select, engine soft validation (#859)
Phase 3 of dev_docs/dimensions_implementation_plan.md. Companies with dimensions_enabled=false see zero change; existing free-text API writers keep working (validation is toggle-governed). Engine (soft validation): - validateEntryDimensions() in dimension-resolver: zero queries for untagged entries; toggle off → passthrough; toggle on → one settings fetch + two registry queries, rejects unknown dims/codes and archived values with Swedish per-code messages (DimensionValidationError, 400, details.issues). Wired into createDraftEntry + updateDraftEntry before any insert; reversal/ storno paths untouched (verbatim copies). Fails open on transient registry errors — soft validation must never block bookkeeping. MCP (agent write path): - New tools: gnubok_list_dimensions, gnubok_list_dimension_values (fuse.js fuzzy), gnubok_create_dimension_value (STAGED via pending_operations — agents never silently mint reporting values; new op type + CHECK migration + executor with duplicate-idempotency). - create_voucher/correct_entry: per-line dimensions bag + default_dimensions, resolve-don't-select server-side (code OR natural-language name; exact → fuzzy ≤0.30 with ≥0.15 runner-up margin; non-exact resolutions echoed with confidence; ambiguous → ranked candidates, no auto-create). - gnubok_get_agent_briefing gains a dimensions block (enabled, dims, top values) — omitted when registry empty. - TOOL_SCOPE_MAP entries; risk tier low for staged value creation. UI: - JournalEntryForm (manual voucher + TransactionBookingDialog embed): header "+ Kostnadsställe/Projekt" progressive disclosure (gäller alla rader with documented inheritance rule) + per-row tag popover + compact KS·PR badges; gated on dimensions_enabled. - Voucher detail: display-only dimension badges with registry-name resolution. - EditDraftEntryDialog carries line dimensions so editing a draft no longer strips tags. categorize/bulk_book dims deferred to PR7 (needs the bulk_book RPC migration). 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> |
||
|
|
a8072f6423 |
feat: realtime updates for dashboard transactions (rebased reimplementation of #757) (#833)
* Add realtime transaction subscriptions to dashboard and transactions page Introduce a shared browser Supabase hook so client components can create a stable realtime-capable client once and reuse it across multiple dashboard surfaces. Dashboard navigation now keeps the uncategorized transactions badge in sync through a company-scoped postgres_changes subscription on public.transactions, and the transactions page now subscribes to the same table so it can refresh its list and uncategorized count live without a full page reload. The transactions page keeps the initial server-driven load path intact, but once mounted it listens for inserts, updates, and deletes on the active company's transactions. When a change arrives it refetches the list and total uncategorized count using the same RLS-scoped filters that already power the page, then hydrates the visible rows in the same way as the initial load. The refresh path is intentionally coalesced so bursts of realtime events do not trigger overlapping database reads. The dashboard nav and the transactions page both use the new shared hook instead of creating browser clients ad hoc. This keeps the realtime client setup consistent, avoids duplicate client construction logic, and makes it straightforward to add more realtime dashboard consumers later without re-implementing the same Supabase plumbing. A Supabase migration is included to add public.transactions to the supabase_realtime publication. Without that publication entry the browser subscription would be correct but silent, so the migration is required for hosted environments as well as local resets. Also included in this commit is the current package-lock.json drift present in the staged set. Signed-off-by: Esaias Westberg <esaias@westbergs.se> * fix(migration): retimestamp transactions realtime publication to clear collision 20260628120000 collided with 20260628120000_ef_no_owner_employee.sql on main. Renamed to a unique timestamp after main's latest. SQL unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(dashboard): fix indentation in collapsed-nav badge block --------- Signed-off-by: Esaias Westberg <esaias@westbergs.se> Co-authored-by: Esaias Westberg <esaias@westbergs.se> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7cd1a7388f |
fix(customers): add columns the system-migration importer needs (address_line2, default_payment_terms, vat_number_validated) (#780)
* fix(customers): add address_line2 + default_payment_terms columns The customers table lacked address_line2 and default_payment_terms, yet the suppliers table has both and three code paths write/read them on customers: the arcim-migration importer, the built-in customer CSV import, and the customer CSV export. Inserts failed with PostgREST "Could not find the 'address_line2' column of 'customers' in the schema cache", so customer imports landed 0 rows while suppliers imported fine. Add the two columns to match suppliers (text, integer default 30). Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(customers): add missing vat_number_validated boolean column The customers table was missing the boolean vat_number_validated flag that types/index.ts and ~40 code sites depend on (getVatRules/getAvailableVatRates, the customers-list validated badge, the v1 customers API select), yet no migration ever created it — only the vat_number_validated_at timestamp exists. It went unnoticed because normal customer creation writes the flag only when a VIES check runs, and reads degrade to undefined when the column is absent. The arcim-migration importer writes it unconditionally (false), so customer and sales-invoice-stub inserts failed with PostgREST "Could not find the 'vat_number_validated' column of 'customers' in the schema cache". Same class of gap as the address_line2/default_payment_terms columns added in the previous commit on this branch. Signed-off-by: Jonas Hagberg <jonas@lindan.se> * fix(customers): retimestamp migrations to clear version collision 20260628120000 collided with 20260628120000_ef_no_owner_employee.sql (merged to main via #797) and with #757. Renamed to unique timestamps ordered after main's latest (20260629160000). SQL unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Jonas Hagberg <jonas@lindan.se> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
663e1232a4 |
fix(bookkeeping): push grundbok series filter into the RPC so pagination total is honest (#798) (#823)
The verifikationslista filtered by voucher series in the API route, AFTER the list_fiscal_period_entries_with_related RPC had already paged — recomputing `count = entries.length` from the current page only. That clobbered the RPC's window-function total_count, so whenever a user filtered the grundbok by series the paginator's total collapsed to the matching subset of one page and 'next page' disabled early — the list appeared to stop after ~one page. - New migration adds p_series to the RPC and applies the filter inside the matching CTE, so COUNT(*) OVER () reflects the filtered set. DROP+CREATE (a new param changes the function identity); p_series DEFAULT NULL keeps existing callers unaffected. Re-GRANTs EXECUTE to authenticated to match the original definition, since DROP+CREATE drops prior grants. - The route passes p_series and drops the post-filter recompute; entries and count now come straight from the RPC. - Extends list-filters.pg.test.ts: series=B over a 2A/3B period returns the 3 B entries with total_count=3, and a short page (limit 2) still reports total 3 — the regression that broke pagination. Note: hosted DB does not auto-apply migrations on merge — apply 20260629160000 to prod after merge. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
60e33c4b51 |
Fix/cus fee 28 (#820)
* feat(invoices): add Plusgiro input to bank details settings Plusgiro was already persisted, validated by the API schema, rendered on the invoice PDF and toggleable via "Visa plusgiro" — but the settings UI had no field to enter the number, so plusgiro-only users could not fill it in. Add the input next to Bankgiro with Luhn validation and hyphen formatting, include it in the save payload (normalised on save so raw digits still match the dashed schema format), and add sv/en strings. Adds validatePlusgiroNumber/formatPlusgiroNumber helpers + tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): respect non-VAT-registered seller in PDF preview + portal tooltips Two user-reported bugs: - PDF preview (/api/invoices/preview-pdf) ignored company.vat_registered and fell back to the customer-driven 25% rate, so a non-momsregistrerad seller saw VAT in the review step even though the created invoice books none. Mirror the server-side write gate (build-invoice-write.ts): force 0% when vat_registered is false (delivery notes excepted). - InfoTooltip rendered TooltipContent without a Portal, so tooltips were clipped by the scrollable DialogContent (overflow-y-auto) in the send-invoice journal-entry review. Wrap in TooltipPrimitive.Portal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transactions): book library mall from its literal lines, not a lossy fallback Booking a bank transaction with a user-created booking-template (mall) via the convertible "QuickReview" fast path reduced the template to a single category + one account_override, silently discarding the chosen debit/credit. A kundinbetalning mall (D 1930 / K 1510) booked as a generic cost (D 6991 / K 1930), or with a VAT line as D 1930 / K 1930 / K 2611 — and the result flipped with the direction inferred from the business/settlement line tags, so visually-identical templates produced different verifikationer. Route every library template through the journal-entry editor (applyTemplate -> /book), which posts the literal lines, regardless of convertibility. Add regression tests locking the contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): make the booking-time duplicate guard bypassable TRANSACTION_BOOK_POSSIBLE_DUPLICATE told users they could "book anyway" but the UI dead-ended on a toast with no way to do so. Add a shared DuplicateBookingDialog that surfaces the already-booked sibling and lets the user review it or book anyway (force bound to the reviewed candidate, which the server re-detects so a stale id cannot wave the guard away). - Wire the dialog into the /transactions categorize flow and the manual booking dialog (JournalEntryForm -> /api/transactions/[id]/book) - Bind the override to expected_duplicate_transaction_id OR expected_duplicate_journal_entry_id so ledger-only vouchers (paid invoice, salary run) can be confirmed too - Extend the guard to the pending-operations commit path and the MCP server - Tests for book/categorize routes, detection, and the commit guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): log duplicate-guard bypass to behandlingshistorik in the agent commit path The web /book and /categorize routes append a durable BankTransactionDuplicateDismissed event when a user books over a detected possible double-booking. The agent commit path (commitCategorizeTransaction, commitMarkInvoicePaid) skipped the guard silently on allow_duplicate=true, leaving no behandlingshistorik — an auditor could not reconstruct why the duplicate was allowed (BFNAR 2013:2 kap 8). When allow_duplicate=true, re-detect the candidate and append the dismissal event (BankTransactionDuplicateDismissed for the bank-line path, InvoiceDuplicatePaymentDismissed for mark-paid). Best-effort — a logging failure never blocks a legitimate booking. Payloads stay PII-safe (ids, amounts, dates only — no customer or merchant name). Also fix the misleading DuplicateBookingDialog JSDoc: the retry binds expected_duplicate_journal_entry_id, not candidate.transaction_id, so the systemdokumentation matches the actual control (BFL 7 kap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp-server): stub booking-duplicate guard in receipt-matcher categorize tests The gnubok_categorize_transaction tool runs the booking-time duplicate guard before staging; its detection queries consumed the queued supabase mock results, so the staging assertions saw a thrown duplicate error instead of a staged op. Mock detectBookingDuplicate to "no duplicate" since these tests don't exercise that path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(transactions): use roundOre for duplicate-guard öre rounding Replace naive Math.round(x*100)/100 with roundOre() from @/lib/money in the booking-time duplicate guard (detection lib, commit executor, MCP categorize tool), satisfying the no-new-antipatterns ratchet guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sie-export): paginate journal entries and lines to prevent truncation * fix(bookkeeping): keep the Verifikat/Utkast toggle reachable on an empty list The journal entry list early-returned a pristine empty card whenever the visible list was empty and no filter was active, returning before the Verifikat/Utkast toggle rendered. This stranded users with only drafts (no posted entries) and users who emptied the drafts list, who then had to use the main menu to get back to posted entries. Narrow the early return to a genuinely empty ledger (committed view, no drafts, no filters); make the in-list empty placeholder context-aware (no drafts / no filter matches / no posted entries yet); resolve the draft count before clearing loading on an empty committed list to avoid a toggle flicker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(enable-banking): persist psu_type and reuse it on reconnect Reconnecting a bank connection re-derived psu_type from the company entity_type every time (aktiebolag -> 'business'), silently overriding the type the user actually authorized with. A connection that only signs as 'personal' — common for AB owners who use a personal Mobile BankID, notably at Handelsbanken — flipped back to 'business' on every consent renewal and failed at the bank's signing step. - Add nullable bank_connections.psu_type column (idempotent migration) - Persist psu_type on connect; on reconnect reuse the stored value (explicit client override still wins) - Let users switch account type (Företag/Privat) from the reconnect button - Tests for persistence, reuse, and override Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): set maxDuration=300 on bank-file execute to prevent timeout A full-year bank file (300+ rows) runs a sequential per-row ingest that takes ~85s of server time. The execute route set no maxDuration, so it inherited the platform default and was killed mid-run — the import "spins then aborts" for the user. Match the SIE import route and give it a 5-minute budget. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(transactions): add assistant entry point on transaction rows The agent ("Lena") could only be reached from Dokumentinkorgen, and only once an underlag was matched to a transaction. Transaktioner is the most common starting point for booking, so users could not start a booking with the assistant from there at all. Add a per-row "Fråga [namn]" button on unbooked transaction rows that opens the existing transaction.categorization intent with the row's transaction_id. The intent already reads any linked underlag, so it works whether or not a receipt is attached. No new logic — only the missing entry point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): enable Swish payment QR on invoices Flip SHOW_SWISH_ON_INVOICE on so the Swish row and payment QR render on the invoice PDF, and make the "Visa Swish" settings toggle live (it was hardcoded disabled). The preview-pdf route now builds the QR too, so it shows in forhandsvisning. Position the QR in the top-right of the payment box. No Swish API integration -- the QR is generated offline and prefills the customer Swish app; reconciliation stays via bank matching. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): scope verifikat list to current year, add storno action, clarify correction preview Three UI fixes from user feedback; no engine logic changed. - List defaults to the current räkenskapsår instead of all years. Voucher numbers run per fiscal year (one A42/year), so showing every year at once made them look like duplicates. New resolveCurrentPeriodId helper. - Add 'Återför (storno)' action on the entry detail page and list row, wiring the existing reverseEntry — a pure reversal (BFL 5 kap 5§) with no replacement, distinct from 'Rätta'. - Correction 'Effekt per konto' preview now labels a removed account 'tas bort' (vs a bare dash) and warns when the proposal is unbalanced; dialog explains the rows are the full new verifikat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bank_connections): add psu_type column to persist chosen authorization type * feat(errors): add CannotReverseStornoError for handling reversal of storno or correction entries --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4f0a7b1db0 |
feat(entitlements): per-company capability paywall — gate, trial seeding, UI upsells, Stripe checkout (#815)
* feat(entitlements): capability-grant gate substrate (paywall + modularity) Two-axis capability primitive behind the SaaS paywall and the per-tenant modularity/marketplace vision: - migration: capability_grants (entitlement axis, polymorphic company/firm scope), company_capability_config (enablement axis), metered_events (append-only), company_has_capability() RPC reusing the 20260619130100 tenant guard; SELECT-only RLS (writes service-role only, no self-grant). - lib/entitlements: hasCapability/requireCapability gate (mirrors guardSandbox, fail-closed, NEXT_PUBLIC_SELF_HOSTED bypass), capability key namespace, metering helper. - unit (11) + pg-real tests (RPC/RLS/tenant-guard incl. no-self-grant). Gate not yet wired into call sites (follow-up commit). Paid keys: ai, bank_sync, skatteverket, email_send. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): enforce capability gate at paid external-service chokepoints Wire the gate into the paid surfaces (keys: ai, email_send, bank_sync, skatteverket): - AI routes (agent invoke/composer/onboarding stream): requireCapability(ai) - Invoice send (web + v1): requireCapability(email_send) - document-extraction event handler: skip Bedrock extract if ai not entitled - enable-banking + skatteverket crons: per-company hasCapability skip in loop - colocated send-route test mocks updated (requireCapability -> null) Free per founder decision: TIC org lookup, VIES VAT validation, FX auto-fetch, cloud backup, BankID login, all internal bookkeeping. DEPLOY ORDER: fail-closed by design — do NOT deploy before trial/comp grant seeding lands, or companies without grants lose these features. Seeding + Stripe checkout/webhook are the next steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): seed trial + comp capability grants Makes the fail-closed gate safely deployable — nobody is locked out at cutover: - AFTER INSERT trigger on companies grants every NEW company a 30-day trial on the PAID keys (ai, bank_sync, skatteverket, email_send), on ALL creation paths (RPC/MCP/direct) — so a new signup can use onboarding AI immediately. - one-time backfill for EXISTING companies: created <=2026-06-07 -> trial ends 2026-07-07; created later -> created_at + 30 days. - permanent comp grants for Arcim/Mattsson (matched by name, no hardcoded UUIDs). - pg tests: clearGrants() for controlled resolver tests + trigger coverage. Trigger fn is SECURITY DEFINER so it writes grants regardless of caller RLS (table has no INSERT policy for authenticated — no self-grant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): client capability visibility + billing page Non-payers get a clean upsell instead of broken/empty features: - CompanyContext gains capabilities[] + useCapability(key); resolved once server-side in the dashboard layout via getCompanyCapabilities (batched, 2 queries), all three provider branches wired. - /settings/billing upgrade page — the destination upsells point to (Stripe Payment Link via NEXT_PUBLIC_STRIPE_PAYMENT_LINK; degrades to 'coming soon' until automated checkout lands). - ChatEmptyState: non-payer sees an Uppgradera CTA (mirrors the sandbox state). - SendInvoiceDialog: email send disabled + upsell note when email_send missing (extends the existing sandbox-disable pattern). Fast-follow: chat input/FAB + document-inbox empty state + bank/skatteverket/ AI-suggest buttons + a shared capability_blocked->toast backstop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): gate remaining paid UI surfaces with upsell (fast-follow) disable-with-upsell across the rest of the paid surfaces (keys: bank_sync, skatteverket, ai): - BankSyncNowButton: sync/reconnect disabled + note when !bank_sync (CSV/SIE stays free) - AGIPanel: AGI submit-to-Skatteverket disabled + note when !skatteverket - SkatteverketConnectPanel: BankID connect/reconnect disabled + upsell - ApprovalCard: AI re-propose (correction) gated; manual approve/reject stay free - InvoiceInboxWorkspace: upsell when extraction empty AND !ai (deterministic parse + manual entry unaffected) - AgentTrigger FAB: routes to /settings/billing when !ai (no dead chat) - settings nav: 'Abonnemang'/'Subscription' link to /settings/billing (sv/en) TaxPaymentPanel + TransactionInboxCard intentionally untouched — only local/ deterministic actions there, nothing paid+external to gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(entitlements): automated Stripe subscription checkout + webhook Self-serve revenue wired to the same capability-grant primitive: - migration: company_subscriptions (company<->Stripe link/status) + stripe_webhook_events (idempotency) - lib/stripe: getStripe singleton, plan->price mapping, subscription-sync (statusGrantsAccess / subscriptionToState / applySubscriptionState / handleStripeEvent). Active sub -> upsert source='stripe' grants for PAID keys (expiry = period_end + 3d grace); canceled/unpaid -> remove ONLY stripe grants (freeze-and-retain). - routes: POST /api/billing/checkout (hosted subscription Checkout, company_id metadata), POST /api/billing/portal (Customer Portal), POST /api/stripe/webhook (raw-body signature verify, event-id dedup; handles checkout.session.completed + customer.subscription.*) - billing page: real plan-toggle Checkout CTA / manage-subscription portal, gated on isStripeConfigured() - adds stripe@22; unit tests for sync logic Provisioning is webhook-driven (never trusts the success redirect). Needs env: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(entitlements): validate UUIDs in capability filter + log webhook errors Addresses PR review (Superagent Security / PR Agent): - has-capability.ts: validate companyId/teamId as UUIDs before interpolating into the PostgREST .or() filter (fail-closed) — removes the latent injection vector flagged in the entitlement gate. Unit tests updated to use UUIDs. - stripe/webhook: log processing failures with event id + type before the generic 500, so a failing webhook is visible to operators. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): always-free AGI XML download for manual filing; only direct API submit is paid Per founder decision on the swedish-compliance-review finding: AGI is a mandatory statutory filing, so producing/downloading the AGI XML must never be paywalled. Adds a free 'Ladda ner AGI-fil' button (generates + downloads the XML for manual upload to Skatteverket's e-service) on all tiers; the gated 'Skicka in underlag' stays the paid convenience (direct API submission — which also requires the paid BankID connection). Upsell reworded to point to the manual path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(entitlements): harden comp-grant match after prod verification Verified Arcim/Mattsson in prod (pwxtzglxptnnvjrpixpg): the name match was case-sensitive (missed the active 'Arcim technology AB' lowercase variant) and would have granted 3 archived dupes. Now match by org_number (5595386219 / 5595719864) OR case-insensitive name, active companies only — hits exactly the 3 active comp companies, excludes archived dupes and the unrelated 'Amnäs Mattsson, Emil' enskild firma. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
55ba66908b |
feat(salary): let an enskild firma employ staff while blocking owner/board payroll (#797)
An enskild firma that hires staff should get the payroll module, but its owner or board can never be on payroll (owner compensation is egna uttag / BAS 2013, not lön). - Migration 20260628120000 adds the enforce_ef_no_owner_employee trigger (BEFORE INSERT OR UPDATE OF employment_type) as the all-paths backstop. - lib/salary/employment-rules.ts is the app-layer mirror (forbidden set kept byte-identical to the trigger); getCompanyEntityType() resolves the same company_settings -> companies precedence. - The two UI salary routes and the v1 POST guard before insert/update for a clean 400 with guidance. - Payroll nav + Lön settings now show for any employer (aktiebolag OR company_settings.pays_salaries), wired through the dashboard layout. Fixes #782. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5bacda4839 |
fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14 (#796)
* fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14
Onboarding derived the VAT number as SE${orgNumber}01. For an enskild firma the
org number is a 12-digit personnummer, producing SE + 14 digits, which fails the
^SE\d{12}$ validation — the pre-filled value is re-submitted on save and the tax
settings page becomes unsavable.
New shared helper lib/vat/vat-number.ts (normalize/validate/derive, reusing
normalizeOrgNumber to drop the century + Luhn-validate). UpdateSettingsSchema,
the onboarding wizard, the onboarding upsert in lib/company/actions.ts, and the
arcim-migration provider import all route through it. Backfill migration repairs
existing SE+14 rows to SE+12 (idempotent, scoped to ^SE\d{14}$ only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(arcim): warn when a provider VAT number is dropped as malformed
The provider VAT guard silently discarded a value that doesn't normalise to a
valid SE+12 momsregistreringsnummer. Emit a structured warn (provider +
company, no raw value — it can embed a personnummer) so consistently-bad
provider data is observable rather than invisible. Addresses the OWASP V16
logging finding on the arcim VAT-normalisation change 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>
|
||
|
|
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> |
||
|
|
2a8bf9b42e |
Bug/year end numbers (#744)
* fix(bookkeeping): allow creating a fiscal year that fills an interior gap Fiscal-period creation only allowed chaining a new räkenskapsår before the earliest or after the latest existing period, so a company with a gap between years (e.g. 2024 + 2026 from an SIE import, missing 2025) could not create the missing year — it failed with "New period must chain before the earliest or after the latest existing period". Generalise forward chaining onto the new period's immediate predecessor, which covers both appending a new latest year and filling an interior gap. The "prior year must be locked" guard now applies only to true appends, not gap fills (a backfill, like backward chaining). previous_period_id is set to the predecessor and the successor is relinked so the BFNAR 2013:2 continuity chain stays intact. The create dialog suggests the missing year (capped so it never overlaps the next period), the settings page seeds the dialog at the earliest gap, and the default suggested name is now "Räkenskapsår <year>". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): omföra föregående års resultat (2099 → 2098) at year-end Year-end closing posts the result to 2099 "Årets resultat" and the opening balance carried it forward on 2099 every year, so 2099 accumulated across years and the prior result never moved off "Årets resultat". executeYearEndClosing now posts a separate "Omföring av föregående års resultat" verifikat (Dr 2099 / Cr 2098 for a profit, reversed for a loss) into the new period after the continuity check passes, so 2099 starts each year at zero. Kept as a standalone entry rather than folded into the opening balance so the IB stays a faithful mirror of the prior UB and IB/UB continuity still holds. Aktiebolag only; idempotent; no-op when 2099 is flat. The 2098 → 2091/2898 disposition (bolagsstämma decision) is intentionally left to a separate step. - new source_type 'result_appropriation' (migration + type + Zod enum) - generateResultAppropriation helper (planner + poster) wired as step 11 - ResultStep surfaces the omföring voucher - unit tests + pg-real invariant - scripts/repair-result-appropriation.ts: retroactive catch-up (dry-run default) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): shadow-detect date-drift duplicate bank transactions The content-dedup bridge buckets on exact (date, ore), so the same transaction re-imported with a booking date that drifted a day lands in a different bucket and slips past every dedup layer. Add a measure-only ("shadow") detector that flags would-be +/-1-day duplicates and counts them, without changing what is inserted - so the gap can be validated on real data before any enforcement, mirroring the scope-drift shadow. - shiftIsoDate(): pure, deterministic adjacent-date helper - ingest: DEDUP_DATE_DRIFT_MODE flag (default on), pre-loop bucket snapshot, per-row gate with desc-bridge + cross-channel-symmetry signals; logs shadow_date_drift_candidates, never alters inserts - fail-safe date guard so the measurement can never abort an import - regression tests for both signals, account/window/distinct guards, no-double-count, and the malformed-date fail-safe Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(bookkeeping): anonymize a customer reference in fiscal-period tests Remove a real customer name ("AXMD AB") from regression-test comments; no logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(workflows): enhance Docker image scanning and caching mechanisms * fix(bookkeeping): enhance year-end result appropriation handling and error reporting --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
274fe0743f | fix(migrations): restore Swedish diacritics in chart of accounts names (#740) | ||
|
|
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> |
||
|
|
e5f1d7a916 |
fix(reports): count reversed entries in bank/supplier/AR reconciliation (#734)
A reversed (storno-corrected) journal entry must be summed together with
its storno + correction, exactly as the trial balance and balance sheet
already do (.in('status', ['posted','reversed'])). The reconciliation
paths used posted-only and manufactured phantom differences.
- bank-reconciliation getReconciliationStatus: gl_1930_balance now counts
posted+reversed (so it equals the balansräkning for the account). Dropped
the correction_adjustment subtraction and the reversed-linked-tx drop — a
corrected/amount-corrected bank receipt now reconciles. The prior model
broke once correctEntry began re-pointing the bank transaction to the live
correction (the two changes were mutually inconsistent and produced a
difference equal to the corrected amount).
- supplier-reconciliation (2440) and ar-reconciliation (1510/1513): same
posted -> posted+reversed fix. Removes the false "Ej avstämd" gap a fully
paid, fully corrected company shows — the books net to 0 over posted+reversed
while a posted-only query double-counted the payment legs.
Also in this change:
- MCP gnubok_get_reconciliation_status: add account_number param (was hardcoded
to 1930; the lib already supported per-account reconciliation).
- counterparty-templates normalizeCounterpartyName: strip trailing month /
personal-initials tokens so "ngrok JW" / "Ngrok Mars" learn as one merchant.
- pending-operation reject 409 (route + MCP tool): clarify a resolved op was
approved explicitly (no auto-commit path exists) instead of a bare
"already committed".
- create_voucher: normalize description with String().trim() for consistency
with line_description.
- migration 20260625120000: backfill stripped diacritics on ~830 companies'
seeded chart-of-accounts names (Foretagskonto -> Företagskonto, etc.); the
seed function was fixed for new companies in 20260516130000 but never
backfilled. UPDATE-only and idempotent. Already applied to production.
Tests: 899 passing across the touched suites; lint + typecheck clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
43925bc2d3 |
fix(import): SIE bulk-delete on service client + provider/reporting/b… (#724)
* fix(import): SIE bulk-delete on service client + provider/reporting/banking fixes Rebuilt branch onto main as a single commit. - import: run SIE bulk-delete RPCs on the service client to escape the 8s statement_timeout; undo_sie_import now takes an explicit actor (p_user_id) so its owner/admin gate works when auth.uid() is NULL on the service client (migration 20260624120000) + pg-real regression test - providers: distinguish missing Fortnox license from expired connection; provider_consent_tokens PK regression test - reports: include unmapped BAS expense groups in the income statement - enable-banking: reconnect closed/expired bank sessions in place - bookkeeping: surface linked invoices as underlag on the verifikat view - scripts: track BL cleanup/diagnostic tooling; data files (*.csv) are git-ignored and consentId is now a required arg with no silent default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): add Cache-Control header to journal entry references response --------- 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> |
||
|
|
e978136210 |
fix(supplier-invoices): payment-match integrity — no more paid-without-voucher half-states (#711)
* fix(transactions): abort supplier-invoice match when payment voucher fails The match route caught a payment-JE creation failure and proceeded anyway: invoice marked paid with payment_journal_entry_id NULL, a payments row with no voucher, and the bank line linked but unbooked. That half-state is unrecoverable from the UI — mark-paid rejects 'paid' invoices and the match route rejects already-linked transactions (the "user can re-book" comment was wrong). The v1 route was already strict; this aligns the cookie route. A failed voucher now fails the whole match before any state mutation, with bookkeeping errors mapped to their structured codes and a new MATCH_SI_JE_FAILED fallback. Incident: Arcim 2026-06-11 — invoice 20250928 marked paid with no payment voucher because account 3740 was missing from the chart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): bank-sync supplier-invoice match is a suggestion, not a hard link A high-confidence (>=0.85, unambiguous) supplier-invoice hit at sync time set transactions.supplier_invoice_id directly — without booking a payment or touching the invoice. The half-link then BLOCKED the match route (MATCH_SI_TX_ALREADY_LINKED), stranding the bank line with no path to a payment voucher and the invoice stuck on 'registered'. Sync now always writes potential_supplier_invoice_id; the hard link is reserved for completed matches where the payment voucher is booked. High-confidence hits still drain the matching pool and skip the mapping engine. Incident: Arcim 2026-06-11 — RosholmDell 18299 (29 890 kr) auto-linked at sync, unmatchable afterwards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): seed standard BAS accounts on demand in the engine A minimal company chart routinely lacks accounts that legitimate engine flows reach — 3740 (öres- och kronutjämning) the first time a Bankgiro payment lands a sub-krona off the invoice, 6580 on a first legal invoice. createDraftEntry threw AccountsNotInChartError and turned a standard account into a dead end. The engine now backfills missing accounts from BAS_REFERENCE (full metadata incl. SRU code) before failing. Conservative by design: unknown numbers still throw, and deactivated accounts are never resurrected — deactivation is a deliberate user choice. Concurrent seeding (23505) counts as success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): require explicit expense account, drop the 5010 seed Every new line item (and every AI-prefilled line) was silently seeded with account 5010 Lokalhyra. AI extraction deliberately never suggests accounts, so any invoice saved without touching the field was misbooked as premises rent — legally wrong verifikat that need rättelse to fix. Lines now start with an empty account: the supplier's default_expense_account fills empty rows when set, and submit blocks with a clear toast until every row has an account. Incident: Arcim 2026-06-11 — a legal-services invoice (should be 6580) and a SaaS subscription (should be 5420) both posted to 5010. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(bookkeeping): clarify voucher description suffix to (ankomstnr N) "(ankomst 2)" read as "arrived twice" / a duplicate marker; it is the company-internal sequential arrival counter for supplier invoices. "(ankomstnr 2)" says what the number is. Existing posted vouchers keep their old description (immutable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): cancel orphaned payment voucher when match loses the CAS race When the payment JE posts but the invoice CAS update matches 0 rows (a concurrent request settled it first), both match routes returned MATCH_SI_NOT_OPEN and left the voucher orphaned in the ledger. mark-paid has always compensated for exactly this case; the compensation is now a shared helper (cancelOrphanedPaymentEntry: cancel + voucher-gap explanation per BFNAR 2013:2) used by all three routes. Flagged by the compliance swarm and the Swedish compliance review on PR #711 — the one finding both converged on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): next_voucher_number user_id fallback for service-role contexts Mirrors 20260421170500 (commit_journal_entry got this fix; its twin did not). Under a service-role client auth.uid() is NULL and the voucher_sequences upsert fails its user_id NOT NULL check before ON CONFLICT can arbitrate — even when the sequence row exists. Every non-interactive caller of the storno/correction path (getNextVoucherNumber → correctEntry) was broken. Fallback: companies.created_by (same source seed_chart_of_accounts uses). Interactive flows still record auth.uid(); DO UPDATE never touches user_id on existing rows. Also restores SET search_path = public, lost when 20260330 recreated the function after the 20260304 hardening. pg-real: new test exercises the RPC on the superuser connection (auth.uid() IS NULL) and asserts sequential numbers + owner attribution. Found live: the Arcim repair script booked payment vouchers fine (commit_journal_entry) but failed on corrections (next_voucher_number). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): harden cancelOrphanedPaymentEntry — never throw, breadcrumb before mutating Two hardenings from the PR #711 review round: - Whole body wrapped in try/catch: the caller is returning the correct CAS-conflict response, so an unexpected client rejection must not replace it with a 500 (best-effort is now a hard guarantee). - The gap-recovery data (series, number, period, explanation) is logged BEFORE the cancel: the cancel and gap insert are separate statements, and a crash between them would otherwise leave a cancelled voucher with no BFNAR 2013:2 gap explanation and no way to reconstruct it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |