f87277393d6c80eac271fd7eca2ca2eca1a5fb87
650 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f87277393d |
fix(events): retry transient event_log persistence failures and stop racing function suspension (#966)
Production logs (2 months) show ~28 event_log inserts dying with TypeError: fetch failed. Root cause: the MCP server emits telemetry fire-and-forget and returns the JSON-RPC response immediately, so the insert races Vercel function suspension; supabase-js surfaces the dead fetch as a network error which was logged at error level. Two-part fix: - persistEvent (event-log-handler.ts) retries the insert once after 250ms when the error message contains "fetch failed" (network class only; constraint violations and other Postgres errors are never retried). On final failure, telemetry event types (mcp.*, agent.*) log at warn; business events (journal_entry.*, invoice.*, etc., which feed webhook delivery) stay at error. - The mcp-server telemetry emit sites (tool_called, tools_list_called, resource_read, next_hint_followed, skill_loaded, workflow_started, agent.feedback) now schedule the emit via after() from next/server, which keeps the function alive past the response until the emit settles. Falls back to plain fire-and-forget when no request scope exists (direct handler invocation in tests). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7c739529d6 |
fix(documents): make the nightly integrity-verify cron finish and surface missing objects (#965)
The nightly verify cron was killed by the platform every run: with a 500-document batch at ~0.8s/doc it hit the function timeout around item 250, so the tail of the queue (1506 current documents) was never checked. Worse, a document whose storage object could not be downloaded threw before last_integrity_check_at was stamped, so it sorted back to the head of the nulls-first queue and re-failed every night without ever surfacing as an incident. - Declare maxDuration = 300 and lower the default batch to 200 (named constant, env-overridable) so a full run fits the budget with headroom. - On download failure, write an INTEGRITY_FAILURE audit row marked DOCUMENT_OBJECT_MISSING (description prefix + new_state.reason; the DB check constraint audit_log_action_check allows only a fixed action set, so a brand-new action value is not possible without a migration), then stamp last_integrity_check_at so the row stops head-blocking the queue. If the audit insert fails the stamp is skipped so the incident write is retried next run. - Fix the stale route comment: the schedule is nightly 03:00 UTC per vercel.json, not weekly Sunday. - seed-demo-account.ts now uploads a tiny valid PDF for the AWS inbox demo document and stores its real SHA-256 and byte size, instead of inserting a fabricated hash with no storage object (the seeded row that tripped the cron every night). - Add route tests: cron auth 401, happy-path stamping, hash mismatch, missing-object incident + stamp, audit-failure retry, batch size, and maxDuration. From the 2026-07-09 production log triage. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b4a21b1029 |
fix(documents): RFC 5987 Content-Disposition so NFD filenames stop crashing inline view (#964)
* fix(documents): RFC 5987 Content-Disposition so NFD filenames stop crashing inline view macOS/iOS uploads carry NFD-decomposed filenames (base letter + combining diaeresis U+0308, char code 776). undici Headers require ByteString values (every code unit <= 0xFF), so splicing the raw filename into the Content-Disposition header threw while building the response and the inline document route 500ed. 122 prod documents across 35 companies hit this; last crash 2026-07-09T16:17. Add lib/api/content-disposition.ts emitting the RFC 6266 dual form: an ASCII quoted fallback (NFC-normalize, then replace anything outside printable ASCII plus quote and backslash with _) and filename*=UTF-8''<percent-encoded> per RFC 5987 (encodeURIComponent on the NFC name, additionally escaping ! ' ( ) * which it leaves bare). Use it in the inline document route and in the two latent same-shape sites that embed raw employee names in payslip PDF headers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): sanitize lone surrogates before percent-encoding Content-Disposition (CodeRabbit) Unpaired UTF-16 surrogates survive normalize('NFC') and make encodeURIComponent throw a URIError, so replace them with U+FFFD via String.prototype.toWellFormed() before encoding so the helper always returns a valid header value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5b505bb4a7 |
fix(entitlements): stop passing a component across the RSC boundary on the extension upsell page (#962)
The paywall branch in app/(dashboard)/e/[sector]/[slug]/page.tsx resolved the extension icon server-side and passed the resulting forwardRef component into the 'use client' EmptyState. React cannot serialize a component across the server-to-client boundary, so non-payers opening a gated extension (e.g. /e/general/invoice-inbox) got a 500 error page instead of the upgrade CTA (digest 1621801304, 2026-07-08). Fix: new client wrapper components/extensions/ExtensionUpsellState.tsx accepts only plain string props (iconName, title, description, ctaLabel, ctaHref) and resolves the icon client-side via resolveIcon, the same pattern DashboardNav and the command palette already use. The server page now passes definition.icon as a string. The two other resolveIcon call sites in server pages render the icon inside the server component, which is valid RSC, and are left untouched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a8801430f4 |
fix(reports): stop driving report queries from the unfiltered journal_entry_lines side (#971)
* fix(reports): drive report line queries from journal_entries, not the unfiltered lines side
Every report generator fetched journal_entry_lines with a
journal_entries!inner(...) embed and put the tenant filter on the
embedded side (.eq('journal_entries.company_id', ...)). PostgREST
compiles that to a correlated INNER JOIN LATERAL with a parameterized
LIMIT inside, which blocks join reordering: Postgres walked the ENTIRE
journal_entry_lines table (603k rows, all tenants) per report query.
Measured in production: 13.6 s vs 2.7 ms for the equivalent plain join,
against Supabase's 8 s statement_timeout; nightly cloud backups failed
for 5 of 11 companies on 2026-07-09 and a GL report 500'd.
Introduce lib/bookkeeping/entry-lines.ts with a shared two-step fetch:
1. fetch matching journal_entries (id + caller-selected columns)
filtered by company_id / fiscal_period_id / status / entry_date /
source_type, paginated via fetchAllRows;
2. fetch journal_entry_lines with .in('journal_entry_id', chunk) in
chunks of 100 ids (URL-length safety), paginated per chunk;
3. reattach the parent entry to each line under the embed's key shape
(line.journal_entries = {...}, aliasable) and sort lines by id
ascending to preserve the old .order('id') semantics.
Converted call sites (selected columns and filters preserved):
trial-balance (x2), general-ledger, journal-register, sie-export
(reuses its existing entry list via fetchLinesByEntryIds),
vat-declaration, dimension-pnl, opening-balances, monthly-breakdown,
periodisk-sammanstallning, rc-basis-gaps (sibling-line fetch now also
chunked), ar-reconciliation, supplier-reconciliation,
bank-reconciliation, asset-service (x2), bolagsskatt-calculator,
sarskild-loneskatt-calculator.
Tests: unit tests for the helper (chunk size, reattachment shape,
forced id/journal_entry_id columns, empty result, cross-chunk sort,
error propagation); existing report/reconciliation/bokslut test mocks
updated to the two-step query shape, preserving every assertion about
report output.
From the 2026-07-09 production log triage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(reports): stop echoing raw error messages from the general-ledger route
The catch handler returned err.message to the client in
details.reason; internal error strings (SQL fragments, table names,
timeout messages) must not reach the browser. The error is already
logged server-side with the request id, so the client envelope keeps
only the REPORT_GENERATION_FAILED code.
From the 2026-07-09 production log triage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
da859d7236 |
refactor(ui): design-system consistency pass over dense pages + i18n de-bloat (#961)
* refactor(ui): normalize dense pages to the locked design system Sweep of the info-dense surfaces against .claude/rules/design.md; no behavior changes, classNames and primitive adoption only. - Replace hand-rolled h1s with PageHeader (import, suppliers, kpi, salary/employees, skattekonto, settings layout) and drop the one double title (SalarySettingsContent under the settings h1) - Replace hand-rolled empty states with EmptyState (skattekonto, banking/api-keys/oauth/counterparty settings) and hand-rolled pulse divs with Skeleton (deadlines, report view loaders) - Remove semantic colors used as chrome: amber/emerald banners in AGIPanel and SkatteverketPanel, success/warning tints in kassaflodesanalys, arsredovisning and import become neutral surfaces with the tint kept on the icon only - Full-opacity borders everywhere (border-border/30-60, border-destructive/20-40, border-foreground/30, text-destructive/80) - Snap off-scale spacing (p-5 to p-6, p-2.5 to p-3, gap/mt-x.5 to scale values); KPI metric tiles p-6 to p-4 per the tile rule - Remove the mobile Select that duplicated the invoices status Tabs (TabsList already scrolls horizontally); single Tabs now serves both breakpoints - supplier-invoices: shared formatCurrency instead of a local formatAmount helper; skattekonto: formatDate/formatDateLong/ formatDateTime instead of raw dates and toLocaleString - arsredovisning flerarsoversikt converted to the Table primitive with right-aligned tabular-nums cells - Settings: CardTitle text-base on section cards, one heading idiom in AccountSettingsContent, h3 to h2 in CompanyProfileView Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(i18n): trim text bloat and fix an untranslated sv string - Fix invoice_credit.create_failed_fallback: sv catalog carried the English "Failed to create credit note"; now "Kunde inte skapa kreditfaktura". Translate new_user_checklist.step3_title in en - Drop descriptions that paraphrase their own title (design.md forbidden pattern): invoice_detail.credited_description, invoice_credit.original_card_description, invoice_editor customer/notes card descriptions (keys deleted from both catalogs, zero remaining usages); the transaction booking DialogDescription becomes sr-only so screen readers keep it - Trim redundant sentences from settings_salary.info_payroll_scope, settings_backup.intro, ext_cloud_backup_long_description, settings.name_description, salary_payments.open_payments_note and shorten invoice_credit.reason_card_description; statutory BFL/tax prose untouched - Normalize toast punctuation (dimensions/self_billing created_description lose the trailing period like their siblings) - common.delete "Radera" to "Ta bort" (zero live call sites; Radera stays reserved for irreversible account/company deletion) Catalogs verified key-identical (4795 keys each) and JSON-parseable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ca8a89324c |
build(deps): bump the npm group minus major bumps (from #949) (#959)
Kept bumps: - @radix-ui/react-checkbox ^1.3.6 -> ^1.3.7 - @radix-ui/react-dialog ^1.1.18 -> ^1.1.19 - @radix-ui/react-dropdown-menu ^2.1.19 -> ^2.1.20 - @radix-ui/react-progress ^1.1.11 -> ^1.1.12 - @radix-ui/react-select ^2.3.2 -> ^2.3.3 - @radix-ui/react-switch ^1.3.2 -> ^1.3.3 - @radix-ui/react-tabs ^1.1.16 -> ^1.1.17 - @radix-ui/react-toast ^1.2.18 -> ^1.2.19 - @radix-ui/react-tooltip ^1.2.11 -> ^1.2.12 - @supabase/supabase-js ^2.93.1 -> ^2.110.1 - mailparser ^3.9.8 -> ^3.9.14 - resend ^6.9.1 -> ^6.17.2 Deferred majors, left at main's values: typescript (stays ^5, not 7), eslint (stays ^9, not 10), @types/node (stays ^20, not 26), and lucide-react (stays ^1.22.0; main already took the 1.x line via #639). @anthropic-ai/bedrock-sdk remains pinned at 0.29.1, untouched. Lockfile regenerated with npm 10 (CI pin), validated with npm ci --dry-run. Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c9d9c5fe99 |
fix(billing): block demo/sandbox accounts from Stripe checkout (#948)
* fix(billing): block demo/sandbox accounts from Stripe checkout An anonymous demo user on a sandbox company reached POST /api/billing/checkout and created a live Stripe customer. Neither the checkout nor the portal route checked is_anonymous or is_sandbox, and withRouteContext lets anonymous users through (they are authenticated, just anonymously). Guard both routes on both conditions before any Stripe call: refuse anonymous users (identity truth, cheap in-memory check) and sandbox companies (matches the existing lib/sandbox/guard.ts "never charge a token" doctrine). Surface isDemo on GET /api/billing/status so the client hides the upgrade CTA instead of showing a button that 403s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(billing): redact tenant/customer IDs from incident note (CodeRabbit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0626bb6326 |
feat(app): prompt to reload when a newer deploy is live (#951)
Long-open tabs keep running the JS bundle they first loaded, so a shipped change can look "missing" (e.g. a new settings field appearing only after a full reload) until the whole app is reloaded. Add a small, unobtrusive prompt that detects a newer deploy and offers a one-click reload. - next.config: inline the deploy's commit SHA into the client bundle as NEXT_PUBLIC_BUILD_ID (empty in dev / self-hosted, which disables the check). - /api/version: public, no-store route returning the running deployment's SHA at request time. - DeployReloadPrompt: compares the two on load, on tab focus, and on a 30-min backstop; shows a bottom banner with "Ladda om" on mismatch. Mounted once in the root layout. No service worker, degrades to a no-op with no build id. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
82c66739d7 |
fix(transactions): steer duplicate guard to matching for ledger-only vouchers (#958)
When the booking-time duplicate guard flags a ledger-only voucher candidate (transaction_id null: a verifikat from an SIE import, a paid invoice, a salary payout), the dialog's primary action is now "Matcha mot verifikatet": it links the bank transaction to the existing voucher via /api/reconciliation/bank/link (the same path MatchVoucherDialog uses) instead of double-booking the same affarshandelse. "Bokfor anda" stays available but demoted to an outline button. Sibling-transaction candidates keep today's layout: matching a second bank line onto a voucher that already has one is the N:1 edge case, not the default. Wired through both call sites: the transactions page (runCategorize) reuses handleVoucherLinked's refresh, and TransactionBookingDialog (JournalEntryForm, /api/transactions/[id]/book path) reuses the booked flow so in-dialog attached documents land on the matched verifikat and the row leaves the list, with a "Bankhandelsen kopplad" toast instead of "Bokford". No lib change: the candidate payload already carries the transaction_id discriminator, covered by existing detection tests. Closes #919. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d7e110b6b2 |
build(deps): regenerate lucide-react 1.22 lockfile on current main with npm 10 (#639)
Rebuilt on top of post-#884 main so the two lockfile rewrites do not clobber each other; npm 10 used to match CI (node 20). Full production build verified locally; all imported icon identifiers exist in 1.22.0. Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> 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> |
||
|
|
11400b4dec |
fix(banking): base sync date suggestions on booked coverage and the real fiscal period (#956)
* fix(enable-banking): base sync date suggestions on booked coverage and actual fiscal period The "start after your bookkeeping" suggestion in the account picker now uses the latest posted verifikat date (journal_entries, status posted) instead of sie_imports.fiscal_year_end: the fiscal period end can lie months past the last actually booked transaction, so the old suggestion made users skip every unbooked transaction in between. Companies with no posted entries get no suggestion instead of a misleading one. The suggested start date (day after the last posted verifikat) is clamped to today (UTC): the PATCH handler rejects non-past initial_lookback_from_date values, so a company whose latest verifikat is dated today would otherwise be suggested tomorrow and get a 400 when saving. "Sedan raekenskapsaarets boerjan" now resolves from the fiscal_periods row containing today, falling back to the recurring fiscal_year_start_month setting only when no period row exists, so an extended or shortened first fiscal year (e.g. 2025-10-01 to 2026-12-31) resolves to its real start date instead of the recurring-year date. Date logic extracted to lib/date-suggestions.ts with regression tests covering both issue scenarios. Fixes #917 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(banking): keep the fiscal-year date masked when the settings fetch fails (CodeRabbit) A failed company_settings or fiscal_periods query silently fell back to the calendar-year default, the exact misleading suggestion issue #917 removes. On error the date now stays masked and the request-side fallback remains the recurring-setting derivation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2c5e1ce317 |
fix(enable-banking): release ledger claims on disconnect so reconnect lands on the original account (#916) (#955)
Disconnecting a bank left its cash_accounts rows pointing at the revoked connection, so the BAS slot (e.g. 1930) looked taken forever: reconnecting the same bank was shunted to 1939 and the picker save was rejected with a 400 the user never saw. Four coordinated fixes: - DELETE /disconnect now demotes the connection's cash_accounts rows to manual (bank_connection_id = null) after marking the connection revoked. Rows are never deleted: transactions.cash_account_id and ledger history reference them, and upsertFromPsd2 promotes manual holders in place on reconnect. - findFreeLedgerAccount and the PATCH /accounts collision guard no longer count claims held by revoked connections (new getRevokedConnectionIds helper). This is the self-heal path for rows orphaned before this fix: no manual data repair needed. - upsertFromPsd2 promotes a holder row owned by a revoked connection in place (same as the manual seed row), keeping the row id stable so the ledger's transaction history stays attached. A duplicate row for the same connection+uid on an overflow slot (mirrored there by the callback while the slot was wrongly blocked) is merged: deleted when it has no linked transactions, demoted to manual otherwise. Either way a primary duplicate hands the flag to the promoted row, so the __PRIMARY_SEK__ sentinel never resolves to a deleted or stale manual row. The linked-transactions probe is company-scoped (defense in depth on the service-role client). - AccountPickerDialog surfaces rejected saves inline in the picker with the picks intact instead of routing them into the sync-progress modal. It also stops signaling the parent to close before the request resolves: the parent unmounts the whole component on close, which tore down the progress modal mid-flight and made every save outcome (including the 400) invisible. Tests: allocator revoked-exclusion + promote/merge unit tests in lib/cash-accounts, PATCH self-heal case in accounts-route.test.ts, and a new disconnect-route.test.ts covering claim release and its failure mode. Fixes #916 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
15e5dc1a01 |
fix(csp): allow self-hosted Supabase Realtime WebSocket in connect-src (#954)
connect-src listed the https Supabase origin plus wss://*.supabase.co, but never the wss variant of a self-hosted Supabase URL. Supabase Realtime opens wss://<host>/realtime/v1/websocket, which CSP blocked; WebKit throws synchronously on a CSP-blocked new WebSocket(), so Safari unmounted the dashboard into the error boundary (Chromium only logs). - next.config.ts: add supabaseWsUrl (NEXT_PUBLIC_SUPABASE_WS_URL, or the Supabase URL with https to wss / http to ws) to connect-src - Dockerfile: bake a __NEXT_PUBLIC_SUPABASE_WS_URL__ sentinel, since the CSP is fixed at build time and only sed-substituted at runtime - docker-entrypoint.sh: derive the wss origin from NEXT_PUBLIC_SUPABASE_URL unless overridden, substitute the sentinel - .env.docker.example: document the optional override Hosted is unaffected: the wss form of *.supabase.co was already allowlisted, so the added token is redundant there. Fixes #893 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
982fe77f72 |
fix(import): actionable hint when a bank CSV lands in the opening-balance importer (#953)
* fix(import): hint when a bank statement is uploaded as opening balances Uploading a bank statement CSV to the opening-balance importer produced the generic 'Inga konton med belopp hittades' error with no clue that the file belongs in the bank-transactions importer (#918, users got stuck together with #915). When the opening-balance parse yields zero account rows, the parser now runs the registered bank-file format detectors over the CSV content (the generic CSV fallback never auto-detects, so any match is a real bank format) and reports the matched format name as detected_bank_format on the parse result. The upload step then shows an actionable Swedish error naming the bank plus a button that routes to the bank-transactions importer (/import?mode=bank). Closes #918 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(import): use the standard bank-import CTA wording (CodeRabbit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b21aa84268 |
fix(import): parse the real 2026 Lunar CSV export (issue #915) (#952)
The Lunar parser was written against an assumed format. The actual 2026 export is: Date,Time,Title,Amount,Balance,Transaction ID with quoted amounts using a comma decimal and a SPACE thousands separator, UTF-8 BOM. Defect A (silent data corruption): parseLunarAmount only stripped '.' as a thousands separator, so parseFloat stopped at the space and "12 345,00" parsed as 12. Now strips all whitespace (including NBSP U+00A0 and narrow NBSP U+202F) plus periods, converts the comma decimal, and guards with Number() + Number.isFinite so garbage rows are skipped instead of partially parsed. Legacy period-thousands files still parse correctly. Defect B (auto-detection miss): detect() required the header token "text" but the real header uses "Title", so the file fell through to "Unknown format". detect() and the description column lookup now accept title (2026) with text as the legacy fallback. Regression tests cover 2026 header auto-detection with BOM, space thousands amounts and balances, Title-column descriptions, stats and date range, and legacy format backward compatibility. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c1ea0d9bf2 |
fix(salary): make pain.001 betalfil generatable (company IBAN + BIC) (#950)
The ISO 20022 pain.001 salary payment file could never be generated: the route required company_settings.iban/bic, but no settings screen wrote those columns, so every request returned 400. The specific reason was also swallowed by getErrorMessage (isSwedishUserMessage did not know "krävs"/"saknar"), surfacing only the generic "Förfrågan innehåller ogiltiga uppgifter" (issue #945). - Add IBAN + BIC inputs to Settings > Fakturering > Bankuppgifter. BIC auto-derives from the clearing number / bank already entered, so in practice only the IBAN is typed. Validated client- and server-side. - Route requires the company IBAN (canonical debtor form every Swedish bank accepts) and derives the BIC, with clear actionable errors. - Employees are unchanged: domestic clearing + account (BBAN), which is what Swedish payroll collects. Only the company (debtor) uses IBAN. - getErrorMessage recognizes "krävs"/"saknar" so payment-file reasons surface instead of the generic 400. Fixes #945 Co-authored-by: Claude Opus 4.8 (1M context) <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> |
||
|
|
b1f85bc33e |
fix(inbox): stop the onboarding card rendering twice below xl (#944)
* fix(inbox): stop the onboarding card rendering twice below xl The Dokumentinkorg workspace is a 3-pane master-detail grid that collapses to a single stacked column below the xl breakpoint. With an empty inbox the "Så funkar dokumentinkorgen" onboarding card rendered in both the list pane (compact) and the main preview pane, because the preview pane was never hidden when stacked. So on viewports under 1280px (narrow or split windows, smaller laptops) the same card showed twice. Hide the preview and fields panes below xl when the inbox is empty so the list's compact card is the single onboarding surface. At xl+ the 3-pane layout is unchanged: quiet empty list plus one centered card. Also align both loading skeletons (WorkspaceSkeleton and the e/[sector]/[slug] route skeleton) to the same xl breakpoint; they previously collapsed at md/lg, causing a 3-pane to 1-pane snap between 768 and 1280px while the workspace loaded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(inbox): use existing hasAnyItem for empty-state checks Address CodeRabbit review: the empty-inbox condition was re-derived as `items.length === 0` in three spots (list pane, preview pane, fields rail) while `hasAnyItem = items.length > 0` already exists. Reuse `!hasAnyItem` in all three so "empty" has a single source of truth. Behavior-preserving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- 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. |
||
|
|
c068638d24 | fix(dependencies): downgrade @anthropic-ai/bedrock-sdk to version 0.29.1 (#940) | ||
|
|
3a3c4adbc6 |
Bug/ai assistant config (#939)
* revert(agent): restore plain AWS_* Bedrock credential handling
Undoes the credential-name change from #937 (
|
||
|
|
ae489cfdcb | fix(client): update AWS credential handling to prefer BEDROCK_AWS_* environment variables (#937) | ||
|
|
b10cf2ec23 |
fix(banking): harden PSD2 loading, error, and refresh states (#934)
Fixes loading/error/refresh-state gaps across the Enable Banking (PSD2) surfaces: - Delete the dead post-OAuth bank_connected sync flow + 9 orphaned i18n keys. - Surface fetch failures instead of empty/false-healthy states (settings panel error card, BankSelector non-OK guard, chip hidden on error). - Stop the full-panel spinner flash on refetch; clear the spinner in finally. - Client-side sync/backfill timeouts + live elapsed counter with a grace-period unlock so a slow bank can't trap the modal. - Broadcast a bank-sync signal so the transactions chip refetches after a manual sync. - Consolidate the import page onto the shared banking panel (also removes the core -> @/extensions import-rule violation) and standardize spinners. - Surface previously silent chart/fiscal-year fetch failures in the account picker. - Review fixes: cancellation guard on the deferred bank_error microtask; key the sync-progress dialog per attempt to reset its elapsed timer. |
||
|
|
c3764e8987 |
feat(mcp): point agents at ledger_context before booking (openwiki discovery) (#933)
The ledger-context digest rides inside gnubok_get_agent_briefing, but the server-instructions block never told agents to USE it before categorizing or creating vouchers. This adds that pointer, the AGENTS.md-injection analogue from the openwiki pattern (dev_docs/ledger_context_resource.md, Discovery): consult how THIS company books each counterparty/supplier and prefer the observed patterns over guesses, with explicit mapping rules outranking them, and historical frequency framed as evidence, not permission to auto-post. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
19cbb0094b |
fix(entitlements): gate the AI-only invoice-inbox for non-payers (#924)
The Dokumentinkorg (invoice-inbox) leaked past the paywall: visible in the sidebar, command palette, and home "Att gora" list, its page directly reachable, and every non-AI HTTP route open. Its whole value is AI field extraction (Claude Sonnet 4.6 via Bedrock), already the paid chokepoint elsewhere, so gate the whole surface on CAPABILITY.ai. - EXTENSION_REQUIRED_CAPABILITY map + resolvers (keys.ts, sectors.ts) as the single source the nav item, the page, and the API dispatcher all read. - Hide the sidebar item, command-palette entry, and home inbox row for non-payers; subtract inbox_document from the "Att gora" total via one shared visibleWorklistTotal helper (KPI tile + header cannot drift), clamped to >= 0. - Block the /e/[sector]/[slug] page (fail-closed) with an upsell EmptyState. - Enforce the capability in the extension API dispatcher (the single chokepoint that already enforces MFA), so every company-context inbox route 403s. The skipAuth /inbound webhook stays open (freeze-and-retain). - FORCE_PAYWALL=true override so the real gate is exercisable in local dev. - Tests: gating resolver, FORCE_PAYWALL, dispatcher 403/allow/webhook-exempt, visibleWorklistTotal, and enable-banking /connect + /sync 403. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a566a42aec |
fix(entitlements): gate remaining paid-feature UI, drop stale beta pricing copy (#921)
* fix(entitlements): gate remaining paid-feature UI and drop stale beta pricing copy Second post-cutover sweep. Non-payers still hit live controls that 403 at the server gate, plus dead plan names in the inbox guide: - document-inbox guide: remove "Gratis under beta för Open-användare / Pro-planen / Se priser" (plans that no longer exist); inbox stays free, AI extraction is pitched as part of the subscription - payslip send button (RunProgressBar): disabled + tooltip without email_send; the free PDF download next to it is untouched - skattekonto "Synkronisera nu": disabled + tooltip without skatteverket - invoice post-create send-now dialog: skipped without email_send (the invoice page's SendInvoiceDialog carries the upsell) - recurring-invoice auto_send checkbox: disabled + UpgradeNote without email_send, and force-unchecked so edit mode can't PATCH it back on - AgentChat composer: replaced with UpgradeNote without ai, covering deep links to /chat/* and conversations opened before expiry - /onboarding/agent server page: hasCapability(ai) redirect to /settings/billing before the gated composer stream starts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(entitlements): address review findings on the gating sweep - AgentChat: the mount effect auto-fired the first /api/agent/invoke on a fresh start regardless of capability; now returns early without ai, and Regenerate/Correction re-invokes are guarded too (real find by review bot) - recurring auto_send upsell copy moved to i18n (auto_send_requires_subscription, sv+en) matching the rest of the dialog - disabled-button tooltips wrapped in a span (payslip send + skattekonto sync): browsers suppress title on disabled elements Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ed7a178ac6 |
fix(import): scope bank account picker to the active company (#922)
The chart_of_accounts query in BankFileConfirmStep had no company_id filter, so RLS returned bank accounts from every company the user is a member of, duplicating 1930/1940/etc. in the dropdown and making the selected value ambiguous across companies. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
cbffcd7292 |
fix(invoices): accept empty self-billing fields on invoice create (#920)
#911 added self-billing fields to the shared CreateInvoiceSchema with external_invoice_number: z.string().min(1), but the invoice form has always sent that field (plus self_billing_agreement_ref and received_date) as '' on every normal invoice. The empty string failed min(1), so every invoice create returned 400. Normalise the empty optional self-billing strings to undefined in the schema (matching the existing optionalIsoDate / deduction_brf_org_number patterns), and strip the unused empty carriers client-side before the form POSTs. Required-when-self-billed is still enforced post-parse in the v1 route, so the self-billed path is unaffected. Adds schema regression tests. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
27b88426e2 |
fix(entitlements): show paid features as gated upsells instead of dead ends (#913)
* fix(entitlements): show paid features as gated upsells instead of dead ends Post-cutover, non-payers still saw fully interactive UI for paid features (bank picker, agent-build hero, SKV VAT submission) that silently failed or 403'd on the server gate. Every surface now stays visible as a conversion surface but is explicitly gated: - new shared components/billing/UpgradeNote (lock icon + billing link) - agent-build hero (dashboard + new-user checklist): routes to /settings/billing with upgrade copy when the ai capability is missing - bank connect: BankingSettingsPanel and the import-page PSD2 wizard swap the bank list for an upgrade note; the import selection card swaps the "Rekommenderat" chip for "Kräver abonnemang" - VAT report SkatteverketPanel: gated state renders before the connection check, so trial-connected companies see the upsell instead of action buttons that would 403; manual-filing framing kept - SkatteverketConnectPanel: skahmst consent note hidden while gated (irrelevant until the consent page is reachable) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(ne-bilaga): fix time-of-day flake in SRU field-code assertion The bare substrings '7310'/'7350' also match the #SKAPAD HHMMSS timestamp when CI runs at 07:31/07:35, so the assertion now requires the full '#UPPGIFT <code>' prefix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <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> |
||
|
|
7f24ede6c0 |
fix(entitlements): close paywall leaks on interactive bank, SKV, and email routes (#910)
* fix(entitlements): close paywall leaks on interactive bank, SKV, and email routes The capability gate covered crons, MCP tools, and invoice send, but four interactive server paths still bypassed it ahead of the 2026-07-07 trial cutover: - enable-banking POST /connect and /sync (bank_sync) - skatteverket authorize/validate/draft/lock/submit/spara/las/kontrollera/ skattekonto-sync (skatteverket); unlock and all reads stay free, and the AGI/VAT file download path remains ungated per the manual-filing decision - salary payslip email send (email_send) - recurring-invoice auto-send (email_send); the invoice is still created, only the email is withheld (freeze-and-retain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(entitlements): pin unlock routes as paywall-free recovery paths Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
573feea890 |
perf: cross-system snappiness batch (middleware, loading states, bundle) (#909)
* perf: cross-system snappiness batch (middleware, loading states, bundle)
Middleware: resolve the active company at most once per request and run
the user_preferences + first-membership queries in parallel, cutting 1-2
sequential DB round trips from every authenticated page load.
Loading states: add loading.tsx skeletons for the six highest-traffic
dashboard routes, render the real salary page header during load instead
of a full-page skeleton, replace the blank fallback={null} Suspense
flashes on customers/articles, and reshape the settings skeleton to
match the actual form layout.
Bundle: defer recharts chart components via next/dynamic on the KPI page
and report views, replace the import page's framer-motion marching-ants
border with a CSS keyframe, and enable optimizePackageImports for
recharts/date-fns/framer-motion.
Transactions: extract the potential-match lookups into a shared parallel
helper; a single-query PostgREST embed is blocked until the
potential_supplier_invoice_id FK exists in prod (see DECISIONS.md).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(transactions): log potential-match query failures instead of dropping them
A DB error in the invoice/supplier-invoice hint lookups previously
surfaced as "no potential match"; log it so failures are diagnosable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
cdac1808c9 |
feat(api): ROT/RUT, articles, and project lifecycle on the v1 API (#904)
* feat(api): ROT/RUT + articles + dimensions on the v1 invoice surface (#895) - v1 invoice POST now routes through buildInvoiceWriteData, the same builder as the dashboard: ROT/RUT deduction lines (server-side compute, personnummer encryption), article_id + revenue_account linkage, accruals, and line_type no longer get silently dropped on the wire. - v1 invoice PATCH accepts default_dimensions so integrations can tag a draft with a project/cost centre after creation. - New PATCH/DELETE /dimensions/:id/values/:valueId: rename, archive, set end_date on project codes; delete unreferenced values (409 with an archive hint when the BFL retention trigger blocks). - New GET /articles: read-only artikelregister list (incl. housework_type) so callers can resolve article_id before composing invoice lines. - Invoice GET/POST projections now expose deduction fields and full item columns; dry-run previews never echo the encrypted personnummer. Closes #895 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): address review on #904 - Extract shared v1 invoice projections to lib/api/v1/invoice-columns.ts so create/detail/patch responses can't drift; PATCH now returns deduction_total + deduction_personnummer_last4 like GET/POST. - Narrow the v1 create customer fetch back to the three fields the builder reads instead of select('*'). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
31b244acf5 |
fix: stop dropping VAT on MCP inbox-converted supplier invoices (#896)
gnubok_create_supplier_invoice_from_inbox sourced the invoice's header vat_amount from the OCR-extracted totals.vat field instead of summing the per-line vat_amount values. That header field is never reconciled with the line items, so per-line VAT customization (or a mis-extracted document total) could leave it at a stale or zero value. createSupplierInvoiceRegistrationEntry (and the cash/privately-paid variants) then gated the entire 2641 ingående moms posting on invoice.vat_amount > 0, so a stale header silently suppressed a correct per-line VAT split with no error. Confirmed via the ledger: this exact defect hit Glesys 623884, DNB 9664449205, and ComputerSalg 500265968 (all already manually corrected via storno+correction). Fix: derive vat_amount from summed lineItems in the MCP tool, and switch the three registration-entry gates from the header field to itemsHaveVat(items), so the engine itself can no longer be fooled by an unreconciled aggregate regardless of which caller populates it. Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> |
||
|
|
a1fad3193e |
build(deps): bump the minor-and-patch group (38 updates) (#884)
Regenerated on top of current main with an npm 10 lockfile (CI runs node 20). Includes @supabase/ssr 0.8 to 0.12, supabase-js transitives to 2.110, zod 4.4.3, next 16.2.10, react 19.2.7, vitest 4.1.9, sharp 0.35, plus Radix/framer-motion/recharts minors. One type fix: supabase-js 2.110's stricter .insert() typing required narrowing toInsert to NonNullable rows in accounts/activate. Full production build verified locally. |
||
|
|
5149caebbf |
fix(transactions): use the resolved cash account name as bank line description (#899)
The booking dialog's bank line description was always the generic "bank_line_description" translation regardless of which cash account was actually resolved via resolveAccount(). This was accurate while the bank line was hardcoded to 1930, but now that the dialog resolves the real cash_account_id (#769) it can pick a different account whose own name should show up on the line instead. Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
185b6f799c |
fix(build): pin turbopack.root, update comment for middleware to proxy rename (#897)
Pins turbopack.root to the project directory so a parent-directory lockfile cannot make Turbopack misdetect the workspace root, and updates a stale comment reference (middleware.ts is now proxy.ts). The rename itself already landed on main. |
||
|
|
8094f288fa |
fix(ci): quote workflow name with embedded colon in compliance-swarm.yml (#898)
The unquoted "compliance: full swarm" value contains a colon+space, which YAML parses as the start of a nested mapping, causing GitHub Actions to reject the whole file as invalid. Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
9b126d22b9 |
fix(reconciliation): server-side confidence floor for unattended auto-apply (#903)
* fix(reconciliation): server-side confidence floor for unattended auto-apply runReconciliation applied every greedy match, including auto_fuzzy at confidence 0.75, with no server-side threshold. The UI is checkbox-gated, but the unattended callers (enable-banking nightly sync cron, the extension's post-sync sweep, and the v1 run endpoint) had no guardrail. - Add ReconciliationOptions.confidenceThreshold (0..1, clamped): the apply loop skips matches below it. Skipped matches stay in the result's matches array and are counted in the new skippedBelowThreshold field, so they are reported for review rather than silently dropped. Dry runs are unaffected; omitting the threshold preserves current behavior. - Both enable-banking sync callers now pass DEFAULT_UNATTENDED_CONFIDENCE_THRESHOLD (0.9, mirroring the gnubok_auto_match_period MCP default), so unattended runs never commit fuzzy (0.75) or date-range (0.85) matches. - v1 POST /reconciliation/bank/run accepts confidence_threshold (optional, 0..1, mirroring the MCP tool naming) and returns skipped_below_threshold; registry docs/pitfalls updated. Closes #880 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): surface skippedBelowThreshold in unattended sync logs Review finding on the first pass: the floor's 'reported, not silently dropped' guarantee never reached the two unattended callers, which discarded or under-logged the result. Also logs the DECISIONS.md line for the deliberate no-default choice on the v1 route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b700108107 |
feat(mcp): add invoice_date_override to create_supplier_invoice_from_inbox (#848)
Unblocks inbox items where OCR missed the invoice date: priority is override, then extracted invoiceDate, then the existing hard error. Both date overrides now reject non-ISO input at staging time instead of failing opaquely at booking. Includes dry_run and validation tests. |
||
|
|
ac1529e413 |
fix(ui): block illegal VAT rates, honest opening-balance state, surface account-save errors (#902)
* fix(ui): block illegal VAT rates, honest opening-balance state, surface account-save errors - Supplier invoice form (#863 item 1): onSubmit now blocks any non-reverse-charge line whose VAT rate is outside the legal Swedish set {25, 12, 6, 0} with a destructive toast naming the line and the legal rates. Server-side schema tightening stays out of scope. - Opening balance import (#837): the summary derives isBalanced from the running totals (0.01 epsilon) and renders the AlertCircle destructive pattern with the differens amount when unbalanced; canExecute includes isBalanced so the commit button is disabled instead of funneling users into a server-side rejection. - EditAccountDialog (#838): a failed account save now shows a destructive toast with the server-provided message instead of being silently swallowed; the dialog stays open for retry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): compare opening-balance totals in whole ore, map account-save errors to Swedish Review findings on the first pass: the < 0.01 epsilon misclassified exact 1-ore imbalances as balanced (0.03 - 0.02 evaluates just under 0.01), and the save-failure toast surfaced raw English server text instead of routing through getErrorMessage like the sibling handlers in the same file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <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> |
||
|
|
b5f568004f |
fix(ci): fail loud when the compliance diff artifact is missing (#832)
Follow-up to #830: the review script now throws if DIFF_FILE is set but the artifact file is missing, instead of silently reviewing a base-vs-base diff and posting a misleading 'No diff detected' comment. The fallback filename parser also captures deleted files, and the Bedrock job gets a 10-minute timeout. |