cd9fb5b717efdd34c6d3a46301e4e730edf4e88e
460
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dcb5d329a3 |
fix(skatteverket): show a banner when the connection is dead (#1925)
* fix(skatteverket): make a dead Skatteverket connection impossible to miss A company whose SKV session had expired got no usable signal anywhere. /skattekonto/saldo answers 200 for a token already flagged needs_reconsent (it keeps the stale snapshot visible on purpose), so the page looked healthy on load, and the reconnect AttnLine lived inside the `data` branch: a company that had never completed a sync had no snapshot, so clicking "Synkronisera nu" set the reconnect message and rendered absolutely nothing. The 401 branch returns before the toast, so the click was a silent no-op. On /transactions the same banner was gated on the 'skatteverket' source filter, which most users never select. Skattekonto page: - probe /status on load through the shared skvStatusNeedsReconnect predicate, so the prompt appears before the user clicks anything - render the reconnect line at page level, regardless of `data` - swap the "Synkronisera nu" header action for "Anslut igen": the sync cannot succeed while the session is dead, and offering it is what made the failure look like nothing happening - give the expired case its own StartCard instead of the onboarding one, which reads as "you never set this up" to someone who did - drop the "klicka pa Synkronisera nu" hint while a reconnect is pending Transactions page: show the reconnect line on every source filter. Max one attn line per page is preserved: reconnect outranks the imported-rows line and the saldo shortfall, the latter because a shortfall computed from a stale snapshot must not outrank "the data is stale". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): show a banner when the connection is dead Cut back to a banner and nothing else, after three skeptics refuted the first cut. /skattekonto: the reconnect line lived inside the `data` branch of the saldo section, so a company with no snapshot (never completed a sync) saw nothing at all when the sync it just clicked died on an expired session. The 401 path returns before the toast, so the click was a silent no-op. Move the line to page level and probe /status on load so it also appears without clicking, since /skattekonto/saldo answers 200 for a token flagged needs_reconsent and nothing else in the payload reveals a dead session. /transactions: show the same line on every source filter. Nothing is hidden or removed while the flag is true: the saldo tiles, the Kronofogden line, "Synkronisera nu", the empty state and the shortfall warning all stay exactly as they were. Prod has 151 of 162 connected companies with every token dead, so a flag that hides anything hides it from nearly everyone, and the shortfall warning's action is the page's only route to the bankgiro and OCR. Also: a healthy probe clears a message left by an earlier failed sync (previously only syncNow's success path did, so a transient 401 kept the banner up until a remount), and the existing visibility refetch now covers the reconnect state so the banner does not survive the consent that fixed it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skatteverket): guard status probe against stale responses A fire-and-forget /status probe from an earlier reload could resolve after a later one and overwrite needsReconnect with an outdated result, restoring the reconnect banner right after a successful sync. Each reload now bumps a probe sequence and a result is applied only while it is still the latest. Also puts the 2026-08-26 decision record in the required [date] decision: why format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4ac7b45c8a |
perf(layout): dashboard layout in two waves, nav flags as one RPC, local JWT verification (#1946)
The dashboard layout runs on every hard load, hard refresh, company switch and the 16 router.refresh() sites, and loading.tsx cannot paint until it resolves. It cost ~20 network calls in 4 sequential waves: a third getUser() round trip to Supabase Auth (after the proxy's and the route guard's), the company resolution, then 16 reads including four limit-1 probes whose only job is to decide whether to render the Webshop and Körjournal nav rows, and an entitlements read that itself ran two waves. - lib/auth/claims.ts: claimsPinned/userFromClaims extracted from require-auth.ts (unchanged) so the dashboard request context shares the exact pinning + mapping. getDashboardAuthContext verifies the JWT locally and falls back to getUser() only when claims are missing, unpinned or unverifiable: the proxy already performed the per-request revocation check before the layout runs (same semantics approved for routes on 2026-07-23). - Wave 1 (user-keyed, parallel with the company resolution): team membership, profile, user preferences and the memberships join, which now also supplies the active company's row and role, so the separate companies and company_members reads are gone. - Wave 2 (company-keyed): settings, agent profile, the switcher's settings names, entitlements in ONE wave (getCompanyEntitlements takes the team_id the join already carries and runs the grants read alongside config + subscription), and get_dashboard_nav_flags(). - supabase/migrations/20260826120000_get_dashboard_nav_flags.sql: SECURITY INVOKER, STABLE, EXECUTE for authenticated only; RLS applies inside. lib/dashboard/nav-flags.ts wraps it with the pre-RPC four-probe fallback on PGRST202/42883/42501 (self-hosted not yet migrated, deploy ordering) and degrades to hidden rows on any other error. - tests/pg/dashboard-nav-flags-rpc.pg.test.ts (6): fresh company, active vs pending WooCommerce, active Shopify, mileage trips, RLS for a member of another company, EXECUTE grants. Unit tests for the wrapper (RPC row, single-object payload, each fallback code, other errors) and for the entitlements teamId option. ~20 calls / 4 waves -> ~12 calls / 2 waves, 0 auth network calls. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
41424a1650 |
feat(ux): one silhouette from route fallback to detail content (#1944)
Opening a row on the customers, invoices, verifikat, supplier-invoice, supplier, article and salary-run lists flashed three unrelated layouts: the segment's list-shaped loading.tsx (or, for suppliers and articles, the Hem-shaped dashboard fallback) during the RSC round trip, then the client page's bare centred spinner in an h-64 box while it fetched, then the content with a full layout change. Two flashes per click on the most travelled drill-down path. - components/common/DetailPageSkeleton.tsx: back link + title row + card grid + line block, the silhouette of a document/register detail page; InvoiceEditorSkeleton for the editor routes (same shape the Ny faktura dialog shows while its chunk loads). - loading.tsx for every [id] segment (customers, invoices, invoices/edit, invoices/credit, bookkeeping, supplier-invoices, suppliers, articles, salary/runs) and for the two list segments that had none (suppliers, articles, cloned from customers/loading.tsx). - The client pages render the same skeleton while they fetch instead of the centred Loader2, so the RSC fallback to client fallback handoff is invisible. The reference-data gates are already gone (A1 to A6); this only covers the primary-entity fetch. - app/(dashboard)/__tests__/detail-loading-states.test.ts pins both. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3ee3565d6d |
perf(reference-data): sweep the remaining raw reads onto the session cache, ratchet to 0 (#1941)
Final consumer migration of the responsiveness plan: the 35 files still fetching fiscal periods, settings, accounts, cash accounts, dimensions or templates on their own now read lib/reference-data, and every client write site invalidates the shared cache instead of refetching locally. Settings and registries: FiscalYearsManager, FiscalPeriodEditor (period snapshotted once per company so a revalidation cannot reset dates being edited), BookingTemplatesPanel, ChartOfAccounts, ChartOfAccountsManager, EditAccountDialog, CorrectionEntryDialog, StrikeLinesDialog, InvoicePaymentAccountsSettings; the dimensions registry (DimensionsManager, DimensionCombobox, LineDimensionFields, DimensionFilter, bookkeeping/[id]) reads useDimensions and the ad-hoc fetchDimensions/fetchDimensionsCached helpers are deleted. Pages and pickers: CashAccountSelector (FyPicker-shaped restore, once per company load), use-account-names, FiscalYearGapNotice, OpeningBalancePeriodStep, BankFileConfirmStep, ImportReviewStep, the import page (invalidates accounts + periods after a SIE execute), customers list, invoices list + detail, pending, salary employee, asset dispose, year-end and periodisering pages (invalidate periods after closing), reports DimensionPnlView (its pivot picker read the wrong payload key and was always empty; it now populates), SkatteverketPanel, TemplatePicker, ArticleForm (vat_registered). Invoice dialogs and extensions: SendInvoiceDialog, PaymentBookingDialog (init reduced to the credit-note lookup + catalogue, proposal and voucher preview fire on open when cached; a local getSession replaces the network getUser for the fallback CC), InvoiceInboxWorkspace, TicWorkspace, ArcimMigrationWorkspace (invalidates after each SIE import step), enable-banking AccountPickerDialog. raw-reference-fetch ratchet: 35 -> 0 files. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4560ccbfc9 |
perf(forms): supplier-invoice form, register forms and review dialogs read the session cache (#1938)
The supplier-invoice editor issued four requests on every mount (suppliers, accounts, settings, fiscal periods) and defaulted vatRegistered=true, entity type and rounding until /api/settings landed, so the moms controls visibly flipped. The register forms fetched the whole chart of accounts to fill one konto combobox, and each transaction review dialog refetched accounts, cash accounts or settings per open. - use-supplier-invoice-data: thin composition of useSuppliers, useAccounts, useCompanySettings and useFiscalPeriods; the settings-driven gates come from a pure deriveSupplierInvoiceDefaults() (tested) instead of state that flips when the fetch returns; the per-invoice öresavrundning toggle is the one local override. Inline supplier create invalidates the shared list instead of patching local state. - SupplierForm, ArticleForm (posting accounts), QuickReviewDialog, InvoiceMatchDialog, supplier-invoices/[id] (payment dialog chart): useAccounts; ArticleForm's inline account create invalidates the chart. - BulkBookDialog, MatchVoucherDialog, DuplicateBookingDialog: cash accounts from useCashAccounts (resolveAccount over the cached list; an empty list still resolves to 1930 with the fallback note). - QuickReviewDialog, BulkBookDialog, NewEmployeeDialog, customers list (default payment terms), salary run page (payment format, bank, IBAN, dimensions): derived from useCompanySettings; the salary page's post-settings-modal refetch becomes a cache invalidation. raw-reference-fetch ratchet: 45 -> 35 files. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9a56b7aff9 |
perf(bookkeeping): fiscal-year pickers and cash accounts read the session cache (#1934)
First consumer migration onto lib/reference-data. FyPicker and
FiscalYearSelector (14 consumer surfaces, 47 fiscal-period fetch sites
before this series) now read useFiscalPeriods(); with the layout seed the
restore of the persisted scope runs in the first effect tick and onReady
fires on mount instead of after a round trip. Their restore rules are
extracted into a pure resolveInitialFiscalScope() (lib/reference-data/
fiscal-scope.ts) so the two pickers cannot drift apart again, and the
restore runs once per company load, not on every background revalidation.
- /reports: the static catalog renders immediately; only the "no fiscal
year" empty state waits for the picker (previously six skeleton bars
until /api/bookkeeping/fiscal-periods resolved).
- JournalEntryList (/bookkeeping): resolves its initial scope from the
cached list instead of its own fetch; the saved-scope shortcut still
unblocks the entries fetch first when nothing is cached, and resolution
is guarded to once per company so a revalidation can never snap a
deep-link "all years" visit back to the stored year.
- /transactions: the account chooser reads useCashAccounts({ enabledOnly })
(seeded) instead of fetching /api/cash-accounts on every visit; the bank
sync button invalidates that entry after a sync.
- STORAGE_KEY_PREFIX / ALL_YEARS_VALUE move to a dependency-free
fiscal-year-storage.ts (re-exported from FiscalYearSelector) so lib/ code
can import them without a React component.
raw-reference-fetch ratchet: 55 -> 51 files.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
47fe193c48 |
feat(perf): session-cached reference data layer, server-seeded, with a raw-fetch ratchet (#1932)
* feat(perf): session-cached reference data layer, server-seeded, with a raw-fetch ratchet
Customer report (2026-08-26): "it takes time before all fields load when
clicking around". The cause is on the client: fiscal periods, settings,
accounts, cash accounts, dimensions and templates are fetched raw from 47 /
27 / 14 / 8 / 12 / 5 independent call sites, uncached, on every mount and
every dialog open, each request paying the auth proxy and route wrapper
before its own query. SWR was adopted for exactly this on 2026-07-13 but
reached only three files.
This PR adds the layer; consumers migrate in the follow-ups.
- lib/reference-data/keys.ts: one key builder per data set, company id in
position 1, null without a company; company_settings keeps the shape
useCompanySettings already uses so that hook is seeded without a change.
- lib/reference-data/fetchers.ts: browser Supabase for fiscal periods and
cash accounts (mirroring period.list and listForCompany ordering, pinned
by tests), /api for the lists whose routes do real work (accounts RPC,
dimensions ensure, template scoping, customer masking).
- lib/reference-data/hooks.ts: useFiscalPeriods, useCashAccounts,
useAccounts, useDimensions, useBookingTemplates, useCustomers,
useSuppliers, useArticles (+ re-exported useCompanySettings); one-minute
dedupe, keepPreviousData, background revalidation kept on so writes from
MCP/agents/other tabs surface.
- lib/reference-data/invalidate.ts: invalidateReferenceData(kind) for the
success path of every client write.
- lib/reference-data/seed.ts + components/providers/ReferenceDataSeed.tsx:
the dashboard layout fetches fiscal periods and cash accounts in its
existing batch and hands them, with the settings row it already had, to
SWR as fallback, so the first form of a session renders its period, bank
account and settings-driven fields on first paint. getDashboardSettings
now selects the full row for that (its other consumers read a subset).
The chart of accounts is not seeded (hundreds of KB for large charts).
- scripts/checks/raw-reference-fetch.mjs, wired into check:guards as a
per-file ratchet: GET-shaped fetch('/api/<reference path>') anywhere in
client-facing code and .from('<reference table>').select( in 'use client'
files. Baselined at 55 files; new sites fail CI; at 0 the entry goes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(checks): anchor every optional whitespace run in the raw-reference-fetch regex
CodeQL js/redos flagged the `\s*,?\s*\)` tail: two adjacent optional
whitespace runs around an optional comma backtrack polynomially on a long
near-miss. The URL and init-object pieces are now named fragments and
every whitespace run is followed by a literal, so there is one way to
match. Behaviour unchanged (same 7 fixtures + baseline count of 55);
a worst-case timing test pins the linear scan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(checks): make the use-client directive regex unambiguous (CodeQL js/redos)
An unclosed /* let the lazy comment body be re-split at every later /*.
The body is now (?:[^*]|\*(?!\/))* which cannot cross a */, so the outer
repetition has one parse. Pinned with a 3000-comment worst-case test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: re-trigger checks for the rebased head
No workflow ran for dd560a7af (nor after close/reopen); an empty commit
gives the pull_request event a fresh head. No code change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(checks): single-character whitespace alternative in the use-client detector (CodeQL js/redos)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a8980a3a41 |
perf(hem): stream the home page in three sections and trim its query plan (#1945)
Hem was one ~33-query render behind a single fallback: the greeting waited for the slowest worklist scan, and the request also paid a sequential bank_file_imports read after the batch, a second scan of the suggested matches (getWorklistCounts counted the same 200 rows the pane listed) and an awaited stale-dismissal delete on the read path. - page.tsx awaits only what the greeting shell and the redirects need (settings, profile, agent profile, the Skatteverket flag); the notice line, the setup checklist and the Att göra + Fortsätt panes are async server components behind their own Suspense (hem-sections.tsx). RSC streaming applies to client navigations too, so the greeting paints first on every visit and each block fills in as its queries land. - DashboardContent becomes the shell with three slots; HemNotices keeps the one client-side action (the wrong-account sign-out); HemSkeletons are the two fallbacks. - getWorklistCounts accepts the suggested matches the caller is already fetching (a promise, so it stays parallel); listSuggestedMatches runs once at the scan cap and the pane shows the first five. - countInboxDocuments runs its id chunks in one wave instead of N sequential round trips. - getCompanyNotices takes deferReap; Hem passes Next's after() so the stale-dismissal delete runs after the response. - bank_file_imports joins the checklist section's batch. Tests: worklist aggregate (precomputed matches skip the rescan), notices aggregate (deferReap receives the reap; the delete does not run inline and runs when the task does). Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f338850bd0 |
fix: hide API-archived customers and suppliers from lists and pickers (#1927)
* fix: hide API-archived customers and suppliers from lists and pickers The v1 API soft-archives customers and suppliers (archived_at, plus is_active=false on suppliers) and its own list routes hide those rows behind ?include_archived=true. No other surface filtered archived_at, so an archived counterparty stayed a normal row in the dashboard rosters, the internal /api/customers and /api/suppliers list routes, the MCP list tools and every customer/supplier picker. Apply the same canonical `archived_at IS NULL` filter on every non-v1 list and picker path: - /api/customers GET, /api/suppliers GET (feeds the customers page and the supplier-invoice form) - suppliers dashboard page (reads suppliers via browser Supabase) - InvoiceEditor and NewRecurringScheduleDialog customer pickers; an invoice or schedule being edited keeps its current customer visible (archiving does not refuse on drafts, so a draft can point at one) - deadlines page and CalendarWorkspace customer pickers - InvoicePreviewCard sample customer - gnubok_list_customers and gnubok_list_suppliers: hidden by default, optional include_archived boolean mirroring the v1 flag; rows now carry archived_at so an agent can tell them apart when opted in Detail routes and by-id lookups are untouched: an archived row still opens. The delete-vs-archive semantics are unchanged. The tools/list payload guard moves 60.7K to 60.8K: main had ~6 tokens of headroom, so even the bare boolean contract crossed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(schema): raise the unresolvable-expression ceiling by 2 for the archived-customer picker filters The two .or('archived_at.is.null,id.eq.<uuid>') filters keep an edited draft's archived customer selectable. The uuid is a runtime value, so the scanner cannot resolve the expression; both columns exist and the filter is covered by the archived-counterparty tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c634430677 |
feat(woo): select multiple orders and book them with one template sweep (#1900)
* feat(woo): select multiple orders and book them with one template sweep Adds bulk booking to the orders page (issue #1880): hover-reveal checkbox column, a bulkbar with select-all/clear, and a confirm dialog that books every selected order with the standard order template (per-store payment- method mapping, optionally one override account for the whole selection). Server side, POST /api/webshop-orders/bulk-book books each order as its OWN verifikat through the exact same flow as the single-order endpoint: the guards, FX retry and race-free draft -> claim -> commit sequence are extracted to lib/webshop-orders/book-order.ts and shared by both routes, so nothing added to the single path can miss the bulk path. Partial failure is reported per order and never aborts the batch. Fixes #1880 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(woo): replace mangled NUL byte in bulk dialog grouping key with a pipe The account-group key template literal picked up a raw 0x00 byte during generation (known escape-mangling hazard), making git treat the file as binary. Same grouping semantics, plain '|' separator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(woo): bulk sweep only books derived lines, never guessed ones (skeptic findings) The sweep has no reviewing user, so everything the single dialog relies on a human to catch is now refused per order or aborted: - empty vat_breakdown: the ratio-inferred fallback split (a 25%+6% mixed sale classified as 12%, refunds reversing zero moms via 3004) is only allowed as the single dialog's editable prefill; bulk refuses with WEBSHOP_ORDER_VAT_BREAKDOWN_MISSING - invoice-mode payment methods: booking would foreclose Skapa faktura and post a wrong clearing leg; refused with WEBSHOP_ORDER_INVOICE_MODE_METHOD (the account override does not bypass the merchant's configured flow) - 3740 residual above ore scale (gift-card gaps booked as 'oresavrundning'): refused with WEBSHOP_ORDER_RESIDUAL_TOO_LARGE - settings-fetch failure now aborts the sweep instead of silently rebooking every order to 1686 against the confirmed dialog - maxDuration 300 so a platform kill cannot strand an order between claim and commit - per-order guard details (e.g. journal_entry_id) survive into the failure envelope The dialog mirrors the skip rules up front (named order numbers, not an anonymous count) so the confirmation describes exactly what will book. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(woo): refuse non-Swedish VAT-rate buckets in the bulk sweep A foreign OSS bucket (e.g. German 19%) passes the non-empty breakdown gate with zero residual, but the rate-to-account maps would fall back to the 25% accounts and book foreign VAT as Swedish utgaende moms 2611 (skeptic finding). The sweep now refuses such orders per order with WEBSHOP_ORDER_UNSUPPORTED_VAT_RATE (details.rates names the offending rates); the dialog mirrors the rule and names the skipped orders. Only the single dialog may show that prefill, as an editable guess. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
79013cf092 |
feat(bookkeeping): guarded fiscal-year reset + findable Angra import (#1883) (#1897)
* feat(bookkeeping): guarded fiscal-year reset + findable Angra import (#1883) Two deliverables from the community report where a bad SIE test import left no way out short of deleting the company: A) Discoverability: the voucher list shows one attn line linking to /import?history=sie whenever the page contains import-sourced vouchers, and /import?history=sie deep-links straight into the fold-open SIE import history where per-import Angra already lives. B) Reset of an UNLOCKED fiscal year regardless of how the entries arrived: new reset_fiscal_year RPC (same gnubok.allow_delete escape hatch as undo_sie_import; no enforcement trigger touched) behind GET/POST /api/bookkeeping/fiscal-periods/[id]/reset and a typed type-the-year-name confirmation dialog on the fiscal years settings list. Refuses on: locked/closed year, company lock date over any part of the year, executed year-end, arsredovisning state, later year depending on this year's UB, VAT-declared evidence (vat_settlement verifikat, SKV lock/submit audit rows, extension workflow keys, fail closed) and AGI-declared months. Entries referenced by RESTRICT/NO ACTION FKs abort the whole reset (all-or-nothing). Documents are detached, never deleted (BFL 7 kap); every delete is audit-logged plus one behandlingshistorik summary row. Fixes #1883 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): harden fiscal-year reset after skeptic review (#1883) Blocking skeptic findings on PR #1897, one consolidated pass: - New snapshot blocker cross_year_reference: an entry outside the year whose correction_of_id / reverses_id / reversed_by_id points into the year made the delete crash with an uncaught P0001 (immutability trigger refusing the ON DELETE SET NULL referential UPDATE) after an eligible:true preview, and silently severed draft chains. 12 such chains exist in prod today. - New snapshot blocker rot_rut_state: a begaran om utbetalning that reached Skatteverket (submitted/paid/partially_paid/rejected) was silently unlinked via SET NULL, erasing the bokforing behind a filed and possibly decided myndighetsarende. - Rakenskapsinformation preservation (BFL 7 kap): line-level trigger audit rows carry no company_id and header rows no amounts, so a reset destroyed konton/belopp with no company-readable trace. The RPC now archives the full content of every verifikat in company-scoped RESET_SNAPSHOT audit rows before deleting (action added to audit_log_action_check, NOT VALID), and behandlingshistorik renders them. - Dimension registry lockstep on reset (mirrors undo_sie_import): flipped imports can never be undone again, so their dimensions/values would have been orphaned forever. - EXCEPTION WHEN raise_exception now returns a typed FISCAL_YEAR_RESET_LINKED_ENTRIES envelope instead of a bare 500; gnubok.allow_delete is cleared before leaving the guarded block. - Voucher-list attn line fires only for source_type 'import': opening_balance is also written by year-end closing and the manual IB flows, which mislabelled every year-2+ company as SIE-imported. - /import?history=sie now scrolls the SIE history into view. - Reset dialog copy (sv+en) discloses that linked invoices, payments and bank transactions become unbooked; new blocker strings in both locales. - pg fixture fix: document_attachments seeded without company_id (23502); new pg tests for both blockers, RESET_SNAPSHOT rows and the lockstep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0bb482bf6e |
feat(bookkeeping): edit the lines of a proposed kontering (Andra rader) (#1894)
* feat(bookkeeping): edit the lines of a proposed kontering via Andra rader Proposal views (AI suggestion, static template, counterparty template with or without a line pattern) previously offered only accept-or-start-over: the verifikation preview was pure rendering and the only line-editable path was library templates. This adds an "Andra rader" affordance to the proposal view in QuickReviewDialog that hands the COMPUTED lines (accounts, SEK amounts, VAT legs, exactly what the preview shows) into TransactionBookingDialog / JournalEntryForm as an editable prefill, reusing the same initialLines mechanism library templates already use. - lib/bookkeeping/proposal-lines.ts: line computation extracted from JournalEntryPreview into computeProposalLines() (single source for preview and prefill, so they cannot drift) plus proposalLinesToFormLines() mapping to the JournalEntryForm prefill shape. The settlement leg is flagged so the booking dialog swaps in the transaction's resolved cash account and stamps currency metadata, mirroring buildInitialLinesFromTemplate. - JournalEntryPreview now renders computeProposalLines() output unchanged. - TransactionBookingDialog accepts proposalLines (takes precedence over preselectedTemplate); the booking still goes through JournalEntryForm's normal manual validation and the engine, no validation bypassed. - Ore rounding funnels through roundOre(); guard baseline ratcheted down. - New strings in messages/sv.json and messages/en.json (tx_quick_review). - Unit tests for all three proposal branches incl. VAT legs, reverse charge, multi-line patterns, 3740 rounding diff and FX metadata. Fixes #1878 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): make the Andra rader prefill engine-exact (skeptic findings) Three skeptics refuted the first cut of #1878: the extracted preview math was a lossy approximation of the engine, and making it bookable made every loss a real booking defect. This commit closes each refuted scenario by mirroring the exact engine path per proposal branch: - Balance: VAT is single-rounded and the net leg is gross minus that VAT (transaction-entries.ts semantics). Independently rounded net+VAT went off by 1 ore for 12% grosses at 14 mod 28 ore (e.g. 102.06, 100.94), prefillling an unbookable verifikat. - 'Ingen moms' deviation: the dialog resolves the UI 'none' sentinel via resolveExplicitVat before computing lines, so an explicit no-VAT choice prefills no VAT line instead of re-deriving the 25% category default into a bookable 2641 leg (ruta 48 inflation on e.g. loan repayments). - Ore parity: engineRound (plain Math.round(x*100)/100, matching the engine) replaces roundOre where the engine is naive; roundOre kept only where the engine uses it (category VAT leg). No more 1-ore drift between preview, prefill and the booked verifikat (8.62 RC, 34.30@12%). - Legacy counterparty pairs: new counterpartyLegacy mode mirrors the legacy booking path: reverse charge emits the 2645/2614 fiktiv-moms pair (previously dropped: an RC expense would have booked without fiktiv moms, understating rutor 30/48), VAT on expenses only, income gross, and sign-mismatched matches mirrored like buildLegacyMismatchResult. - Pattern mirror: sign-mismatched line patterns flip learned sides like buildMultiLineMappingResult; ratio allocation filters business/tax types. - Entity accounts: static template accounts resolve debit/credit_account_ab for aktiebolag (resolveTemplateAccountsForEntity), so an AB no longer previews or books EF-only accounts like 2013. - Settlement swap: only a literal-1930 settlement leg is swapped to the resolved cash account (applySettlementAccount parity); learned non-1930 money legs (1510/2440/2890/19xx) stay authoritative. - FX: QuickReviewDialog hands its enriched transaction row to the booking dialog so the settlement leg's exchange_rate metadata matches the rate the SEK amounts were computed with. 34 unit tests incl. every skeptic counterexample; guard baseline ratcheted to 622 (below main's 626). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): line-pattern settlement leg uses the learned legacy pair (skeptic refutation) Two independent skeptics refuted the pattern branch: the engine books the money leg on the counterparty template's learned legacy account (credit for an expense, debit for an income, mirror-swapped, falling back to 1930), while the preview/prefill defaulted to 1930. A SIE-learned pattern settling on 2440 showed kredit 1930 in the preview but booked kredit 2440 on confirm. QuickReviewDialog now passes the learned pair raw (no entity resolution, engine parity) and computeProposalLines selects the settlement account exactly like buildTransactionEntryLines; the literal-1930 swap to the resolved cash account is unchanged. CodeRabbit findings declined deliberately (see DECISIONS.md): the 3740 rounding line keeps the engine's business-side placement for both diff signs (parity contract; an unbalanced set is rejected at commit), and the naiveOreRound baseline stays at 622 (engineRound is a documented parity exception). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1f9578ca76 |
feat(woo): mark an order as already booked outside the integration (#1895)
* feat(woo): mark an order as already booked outside the integration Orders booked by hand before the store was connected sat under Att bokfora forever: the only exits were the book and create-invoice routes. - Migration: manually_booked_at/_by + optional manually_booked_journal_entry_id on webshop_orders (informational link, no financial freeze; the mark produced no accounting objects). - POST/DELETE /api/webshop-orders/[id]/mark-booked: mark with optional posted-verifikat reference (validated per company), conditional claim against concurrent booking/invoicing; unmark is a plain revert. - book and create-invoice routes refuse marked rows (409 WEBSHOP_ORDER_MANUALLY_BOOKED) and exclude them in their atomic claims. - List route: booked/unbooked filters treat a manual mark as a closed exit, so marked rows leave the Att bokfora tab and join Bokforda. - Orders page: row overflow menu with Markera som bokford / Angra markering, MarkOrderBookedDialog with a searchable candidate list of posted entries near the order date, muted status text linking to the referenced verifikat. Fixes #1879 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(woo): close skeptic findings on the manual-booked mark - mark-booked applies the same open-twin gate as book/create-invoice: an OPEN legacy feed transaction blocks the mark (409 WEBSHOP_ORDER_LEGACY_TRANSACTION_OPEN); ignored or booked feed rows unlock it, so no open path to a duplicate remains. - ingest treats manually marked rows as frozen for drift purposes: remote financial deltas set remote_changed_after_freeze (same badge as booked rows) instead of silently refreshing the row under the user's assertion. - re-marking with a journal_entry_id updates the informational link instead of silently dropping it. - dialog: candidate amount computed from the returned lines (the list API does not return total_amount), newest-first ordering, cap hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(migrations): bump webshop manual-booking migration past freshly merged 20260825120000 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(woo): resolve PR review findings in one pass - freeze v3 migration: financial fields are frozen at the DB level while a row is manually marked as booked (review finding: the mark's freeze lived only in ingest.ts, so any other write path could silently mutate a marked row); unmark stays the escape hatch. pg test added. - pass the active locale to getErrorMessage in the orders page and MarkOrderBookedDialog (CodeRabbit: English users got Swedish errors). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cd46d936f3 |
fix(bokforing): account search results in the booking dialog search field (#1877) (#1889)
* fix(bokforing): account search results in the booking dialog search field (#1877) The search field in the "Bokfor transaktion" dialog only matched template metadata, so typing an account number or name (e.g. active 5460 Forbrukningsmaterial) gave zero hits and no path to booking; the real account search was only reachable via the discreet "Bokfor manuellt" link. - TemplatePicker now also searches the company's active chart of accounts (reusing lib/bookkeeping/account-search.ts) and shows hits as a "Konton" result group; picking one routes into the same manual booking flow as "Bokfor manuellt" with the account prefilled on the counter line. - New buildActiveAccountIndex helper indexes chart rows active-only, so deactivated accounts never surface as bookable results. - searchTemplates additionally prefix-matches all-digit tokens against a template's business account (debit for expense, credit for income, both legs for transfers, AB variants included); the settlement leg is deliberately excluded so "1930" does not light up every template. - Library template search prefix-matches line accounts on all-digit queries. - Empty search results show a hint pointing at "Bokfor manuellt" for accounts outside the active chart; placeholder now says "Sok mall eller konto...". New strings in both messages/sv.json and messages/en.json. Fixes #1877 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokforing): match only business lines when digit-searching library templates (#1877) CodeRabbit review on PR #1889: the all-digit library-template match also hit settlement and VAT lines, so searching "1930" lit up every user template with a bank settlement leg. Restrict the predicate to business lines, mirroring the static catalog's settlement-leg exclusion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9ebb2e518f |
feat(reconciliation): absorb the last bank-view tools and retire /reports/bank-reconciliation (#1871)
The old Bankavstämning report page was the only place a user could still tag a bank row as ingående balans or move it to another bank account, so the new /reconciliation page kept linking out to it and the reconciliation lived in two places. Both row tools now live on the account overview (hover-revealed, same endpoints), the ?autorun=1 deep link from the transactions inbox runs the matcher on the new page, and the report slug redirects: old links, ⌘K, the bokslut readiness wizard and the ignore toast all land on /reconciliation. BankReconciliationView and its FocusedReport branches (own range preset, help popover, autoRun plumbing) are removed; the source-grepping parity test for its quick-book path goes with it. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f75ea2384d |
feat(calendar): enable calendar sync for Viktiga datum (#1853)
Turns on the calendar extension (built Feb 2026, stripped in the 2026-03-02 production readiness deploy, never re-enabled): ICS feed settings, calendar workspace, subscribe button on the Viktiga datum page. Hardening before first real use: feed serve route now requires the creator to still be a company member (offboarding stops the feed); stable pagination (due_date + id, dedupe) on feed queries; fetches inside the logged try block; invoice events limited to sent/paid/partially_paid/overdue; event UIDs rebranded to accounted.se while zero feeds exist; APP_URL fallback fails closed in production; mobile stacking for the deadlines header; settings note that Google Calendar needs default notifications on subscribed calendars; calendar workspace aligned with the design system. Skeptic reviewed (3 refutations, all fixed) plus one compliance swarm finding (fixed). No migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cef8206f83 |
feat(salary): skattekonto payment file as ISO 20022 pain.001 (#1845)
The tax payment file (skatt + arbetsgivaravgifter to Skatteverket BG 5050-1055) was Bankgirot LB only; banks that take pain.001 for salary, like SEB via file communication agreements, refuse the LB .txt. The route now accepts ?format=pain001 and generates the payment through the supplier-payment pain.001 generator, whose Swedish giro dialect (BG payee + SCOR OCR, Validex-validated) is exactly this payment shape. The TaxPaymentPanel gets the same format selector as the salary PaymentFilePanel, seeded from company_settings.preferred_payment_format, with the missing-sender warning per format (bankgiro for LB, IBAN for pain.001). A migration widens the tax_payment_file_format CHECK to admit 'pain001'; bg_lb stays the default for old clients. Claude-Session: https://claude.ai/code/session_01DUB7L8DbVP2icn8ZpoPkoE Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0676f5a564 |
feat(bookkeeping): dashboard deep link filters verifikat utan underlag server-side (#1840)
* feat(bookkeeping): dashboard deep link filters verifikat utan underlag server-side The dashboard "Verifikat utan underlag" card (and the push-notification link) pointed at /bookkeeping?missingUnderlag=true, but nothing read the param: the user landed on the plain unfiltered ledger. The existing "Visa saknade underlag" toggle also only filtered the already-fetched page, so it could not represent the badge count across pages. - lib/bookkeeping/missing-underlag.ts: shared resolver of "posted verifikat lacking underlag" (document-requiring source types, no current-version document, no anchored supplier-invoice reference per BFL 5 kap 7 §, no exemption), extracted from the bulk "Inget underlag krävs" route so list, bulk remedy and dashboard badge share one predicate. - GET /api/bookkeeping/journal-entries?missing_underlag=true: resolves the full missing set server-side, applies the active sort stack, pages it, and returns the full-set count, fetching page rows in id chunks so the "Alla" page size cannot blow the PostgREST URL limit. - JournalEntryList: the toggle is now server-backed (refetch on change, honest count in the dialog badge); client-side re-filtering against late-arriving attachment counts removed. Deep-link arrival turns the filter on and scopes the visit to all fiscal years in memory only, matching the all-years badge count without touching the saved preference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): harden the saknade-underlag filter after skeptic review Three skeptic subagents refuted the first cut; this fixes every confirmed finding in one pass: - FyPicker: new suppressAutoRestore prop. The deep-link visit opens as "Alla räkenskapsår" in memory, and FyPicker's on-load restore of the persisted year (value === null) snapped the scope back right after load, desyncing the list from the all-years badge that launched it. Manual picks still persist as usual. - Voucher-label search: the resolver now carries the same parseVoucher OR-branch as the direct list path, so searching "A209" with the filter on finds verifikat A209 instead of silently returning 0 rows. - Staleness while the filter is on: batch exempt, the single-row "Inget underlag krävs" toggle, and a row gaining its first underlag now refetch in place so fixed rows leave the filtered list and the count stays honest (the pre-server-filter behavior). The attachment-driven refetch is guarded per entry id against predicate-disagreement loops. - Drafts view: the filter switch is disabled there; the predicate is posted-only and the badge would mislabel the draft count. - Perf: the bulk-exempt route resolves ids only, skipping the per-row total_amount computed column on its full post-import candidate scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): keep the underlag resolver statically checkable The skeptic-fix commit tripped the phantom-column scanner ceiling (tests/schema/no-phantom-columns.test.ts, 382 > 380): a computed select() string and a runtime-built .or() are expressions the scanner cannot resolve against the schema. Restructured instead of raising the ceiling: the idOnly/full column choice is two literal select() calls behind a lazy branch, and a voucher-label search fans out to two statically-checkable candidate queries (description ilike, series+number eq) unioned by id, same semantics as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f40795896f |
feat(reconciliation): sign-off, period picker, Hem row and the three doors for it (#1835)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): the Avstämning page, one body for every account with an outside truth /reconciliation in Arbeta (after Transaktioner), on the approved layout: an account rail on the left (bank accounts and the skattekonto, logo or monogram, last fetch, status dot, URL-owned selection), and for the selected account four tiles (outside, ledger, difference, unexplained), the bridge that explains the difference, an actions row (link the proposed pairs, book the unbooked skattekonto events, run the bank matcher) and a full-width table banded by bucket with proposal rows linkable one by one. Every read and write goes through the PR 2 dashboard routes, so the page shows exactly what the v1 API and the MCP tools see. Also: nav item, command palette entry, sv/en strings. Period picker, manual match mode and sign-off are deliberately not here (PR 4/5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): sign-off, period picker, Hem row and the three doors for it "Markera som avstämd t.o.m. <datum>" as an append-only attestation: account_reconciliations (who signed which account through which date, with the numbers as they stood; reopen stamps instead of deletes; RLS members write as themselves, viewers read). Policy in one place (lib/reconciliation/signoff.ts): refused with an unexplained difference unless forced with a note, refused past today or past the skattekonto snapshot, refused at or before an active sign-off; reopen is the undo. Every status read now carries the latest active sign-off and the rail shows "avstämt t.o.m.". Three doors: dashboard routes (GET/POST .../signoff, POST .../reopen), v1 (same, scope reconciliation:signoff, Idempotency-Key, dry-run, registry + regenerated API skill), MCP gnubok_reconcile_signoff (search catalog, stages reconciliation_signoff after a policy dry run; executor + risk tier + op-type CHECK migration pair). Events reconciliation.signed_off / reconciliation.reopened, and the four reconciliation events join the public webhook set (additive; API version unchanged, changelog section added). Page: räkenskapsår + range picker in the header (own preset memory, opens on this month) scoping the bridge, the items and the default sign-off date; sign-off dialog with the forced-with-note path; reopen on hover. Hem: worklist category reconciliation_due ("Konton att stämma av"), zero until the company has signed anything off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): classify reconciliation:signoff as a tenant write for the MCP role guard gnubok_reconcile_signoff carries the deliberately separate reconciliation:signoff scope; the central viewer guard keys on the :write/:approve/:manage suffixes, so a viewer could reach the tool (RLS would still refuse the row, but the guard is the intended layer). Add :signoff to the classifier; the strictness test that caught it now passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(providers): serve local rate-limiter waiters in arrival order Two callers that both found the in-memory bucket empty each set their own timeout; the timeouts expired at the same instant from different timer lists and which woke first was platform-dependent. hydrateInvoices relies on "started first, requested first" to serve open invoices before paid ones, so lib/providers/__tests__/hydrate-invoices.test.ts flipped on CI (twice on #1817) while holding locally. A promise queue makes the local waiters FIFO without changing the rate; the Upstash path is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 14a7599bf2c6fa7f97de6ffab3dc4cf4d0e1827d) --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c609dbb228 |
feat(reconciliation): the Avstämning page: one body for every account with an outside truth (#1834)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): the Avstämning page, one body for every account with an outside truth /reconciliation in Arbeta (after Transaktioner), on the approved layout: an account rail on the left (bank accounts and the skattekonto, logo or monogram, last fetch, status dot, URL-owned selection), and for the selected account four tiles (outside, ledger, difference, unexplained), the bridge that explains the difference, an actions row (link the proposed pairs, book the unbooked skattekonto events, run the bank matcher) and a full-width table banded by bucket with proposal rows linkable one by one. Every read and write goes through the PR 2 dashboard routes, so the page shows exactly what the v1 API and the MCP tools see. Also: nav item, command palette entry, sv/en strings. Period picker, manual match mode and sign-off are deliberately not here (PR 4/5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
62135c0c02 |
fix(invoices): settle öresavrundning in batch match and close stuck partially_paid invoices (#1829)
A whole-krona payment (Bankgiro/Swish/kort) of an öre-bearing invoice left the invoice hanging: the match_batch_allocate RPC (samlingsbetalning dialog + MCP path) had no öre handling, so a sub-krona overshoot was rejected as BATCH_OVERSHOOT and a sub-krona shortfall parked the invoice in partially_paid forever. Invoices already stuck that way had no exit: the mark-paid dialog proposed clearing the full total (rejected with MATCH_AMOUNT_EXCEEDS_REMAINING) and the route refused partially_paid outright. Fix, both halves: 1. New migration 20260824120000 replaces match_batch_allocate with the same öresavrundning band every single-payment path already uses (ORE_ROUNDING_SETTLEMENT_MAX = 1.00 kr, lib/money.ts): overshoot rejected only at >= 1 kr; a 0 < |remaining - allocation| < 1 kr diff clears the FULL remaining off 1510/2440, books the residual to 3740 with correct polarity per side, records the full remaining as paid and flips the status to paid. >= 1 kr diffs keep today's behaviour. 2. proposePaymentLines is remaining-aware: a partially_paid SEK accrual invoice gets a proposal clearing the actual remaining, and a sub-krona remaining gets a bank-less Dr 3740 / Cr 1510 write-off so one click closes a stuck invoice. The mark-paid route and the invoice-detail button now accept partially_paid (the settle layer's CAS guard always did). Forward-only: already-stuck invoices are not auto-repaired; they are closed via the new dialog proposal. Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
78525bd391 |
fix(invoices): make self-billed invoices creditable and their dates visible (#1827)
A self-billed invoice has invoice_number null by design (the counterparty's number lives in external_invoice_number), which broke the whole credit flow: the confirm input was disabled and compared against null, the API minted the literal number 'KR-null', and the credit-note PDF dropped its ML 17 kap 22 reference to the original. The editor also hid fakturadatum inside the collapsed Forval panel, so self-billed invoices silently registered with today's date and, being immutable, could not be corrected. - creditConfirmNumber() falls back to external_invoice_number; the credit page uses it for reason default, subtitle, original row, preview, confirm label/placeholder/disabled, mismatch check and submit gate - createCreditNote numbers 'KR-<external>' for self-billed originals and refuses with typed 400 INVOICE_CREDIT_NO_NUMBER when no number exists - mark-sent and send select external_invoice_number and fall back for the credit-note PDF's reference to the original - the Forval chip line now shows the invoice date in every mode, and self-billed mode renders fakturadatum + mottagningsdatum uncollapsed as transcription fields next to the external number Fixes #1820 Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2fd58c4125 |
feat(pending): queue order toggle, entry date + notes in review, account names everywhere (#1812)
* feat(pending): queue order toggle, entry date + notes in review, account names everywhere Four review-queue gaps reported by a customer approving bokslut batches: - Oldest-first toggle: /api/pending-operations accepts order=asc|desc (default desc); the queue header gets an Äldst först / Nyast först button, remembered per browser (localStorage pending.sortOrder). - Fiscal year visible: categorize previews now carry the transaction date (preview_data.date) and render a Datum row, so two open years are distinguishable. - The agent's `notes` (audit-trail context) is shown in the detail panel as Anteckning; before, it was stored in params and never rendered. - Account names: VoucherLinesTable and PreviewKonteringTable fall back to the chart name from AccountNamesContext (6110 Kontorsmateriel · AMAZON PRIME instead of the bank text alone); useAccountNamesSource moves to a shared hook so the chat ApprovalCard provides the same names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): call useAccountNamesSource in ApprovalCard The provider referenced accountNames without the hook call; the core build (tsc) caught it. Local tsc had not, so this also re-runs the full check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
13b69a2056 |
fix(customers): personnummer via MCP lands in personal_number, masked everywhere; MCP payment terms follow settings (#1788)
* fix(customers): personnummer on the MCP path lands in personal_number, masked everywhere; MCP payment terms follow settings Follow-up to #1724 (Discord kalletoxic): the fix reached the web form and the v1 REST API, but not the MCP path, and the web customer list still showed a personnummer raw when it sat in org_number. Personnummer (MCP + every write path): - gnubok_create_customer gets a personal_number input. Until now it had none, so an agent creating a private person either dropped the number or put it in org_number, which nothing masks. Encrypted at staging (personal_number_encrypted + personal_number_masked; personal_number is now a forbidden staging key in staging-pii-guard), the approval preview shows ********-1234, commitCreateCustomer stores the ciphertext as-is. Idempotency hashes the masked preview (new StageOptions.idempotencyParams) because the random-IV ciphertext would make identical retries look like payload changes. - A personnummer-shaped org_number on customer_type=individual is the personnummer in the wrong field: it is moved into personal_number (encrypted) and org_number cleared, on CreateCustomerSchema (web POST, v1 POST, v1 bulk), both PATCH routes, MCP staging, and commitCreateCustomer for in-flight ops. Only a DIFFERENT personnummer next to personal_number is refused (new CUSTOMER_PERSONAL_NUMBER_CONFLICT). The business-type guard from #1724 is unchanged and now also fires at MCP staging, so the user never approves an operation that fails at commit. - Read side: the web customer list and gnubok_list_customers mask a legacy individual row's org_number personnummer instead of showing it raw; list_customers exposes personal_number_masked and never the ciphertext. - scripts/repair-customer-personal-number-in-org-number.ts moves the existing rows (dry run: 134 rows across 10 companies on prod); run by hand with --confirm after deploy. - customer-onboarding skill: EF customers follow the #1724 decision (individual + personal_number); ROT/RUT section names the real field. Payment terms (MCP): - gnubok_create_customer staged `payment_terms || 30`, so resolveDefaultPaymentTerms at commit always saw 30 and the company's invoice_default_days never reached MCP customers. Resolved at staging now, so the preview shows the value the row will get. tools/list payload ceiling 59.75K to 59.85K (descriptions trimmed first, rationale in payload-size.bench.test.ts). apiskill regenerated; no migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbLqn9bgZ9NJ5qnZMeC1Bk * fix(scripts): literal update payloads in the personnummer repair script The no-phantom-columns scanner counts a runtime-built update payload as unresolvable and the ceiling (379) had no headroom; two literal payloads keep the guard able to resolve both branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CbLqn9bgZ9NJ5qnZMeC1Bk --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3ac80edc96 |
feat(peppol): gate Peppol per company: request access, operator enables with a sending cap (#1794)
* feat(peppol): gate Peppol per company: request access, operator enables with a sending cap Peppol is no longer available to every company by default. Each transmission is billed per document by the access point and each receiving identifier consumes a contracted tenant slot, so the product now works like this: - peppol_access (new table, RLS read-only for members, service-role writes): status requested | enabled | disabled, max_sends (null = no cap), receive_enabled as a separate grant, who asked and who enabled. - POST /api/settings/peppol/access: the company asks from Settings > Fakturering; the row is written and the operators are e-mailed (best effort, the row is the source of truth). - scripts/peppol/access.ts list | enable <company|orgnr> [--max-sends N] [--receive] | disable | show: the operator side. - POST /api/invoices/[id]/peppol/send refuses PEPPOL_ACCESS_REQUIRED / PEPPOL_SEND_LIMIT_REACHED before touching the invoice; the invoice page's send item says so instead of pretending. Registration for receiving refuses PEPPOL_ACCESS_REQUIRED / PEPPOL_RECEIVING_NOT_ENABLED. - Settings UI: access status row with "Begär åtkomst", sends used of cap, receiving switch only once receiving is granted. Refs #546 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ * test(peppol): pass route params to the settings handlers; baseline-align the access row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ * fix(peppol): revoke default table privileges from authenticated on the access and receiving tables Supabase grants ALL on new tables to authenticated by default; the earlier REVOKE covered PUBLIC and anon only, so a member's UPDATE on peppol_access was an RLS-filtered no-op instead of a permission error (pg-real caught it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
05c3c6ebd9 |
feat(peppol): Qvalia access-point adapter, send flow and delivery webhook (#1780)
* feat(peppol): Qvalia access-point adapter, send flow and delivery webhook Qvalia is the contracted Peppol Access Point (signed 2026-08-21). This fills the provider-neutral PeppolTransport seam from #1595 with a real adapter and turns the disabled "Skicka via Peppol" menu item into a working send flow. Adapter (lib/invoices/transports/qvalia.ts): partner-scoped recipient lookup, XML submission to /invoices/outgoing with integrationId correlation, 409 recovery only when the stored copy carries the same seller endpoint, tolerant mapping of Qvalia's free-text webhook statuses onto the 11-state lifecycle, constant-time shared-secret webhook verification (Qvalia does not sign webhooks), and evidence retrieval of the message-log status plus Qvalia's stored XML copy. Registered from the environment in lib/init.ts; switched on per deployment with PEPPOL_TRANSPORT_PROVIDER=qvalia. POST /api/invoices/[id]/peppol/send: stage the exact XML, look up the recipient, record recipient_verified and submitting, submit, record submission_accepted, then issue a draft with the mark-sent semantics (issueAndBookInvoice) only after the network accepted it. A sync rejection is a terminal failed event so the identical document is never re-sent; an operational failure is retryable; an already-submitted XML replays idempotently. POST /api/webhooks/peppol/qvalia resolves the delivery by integrationId, persists the verified event via the service-role RPC and stores evidence best-effort; unknown submissions answer 200, our own persistence failures 500. UI: the send item is availability-driven with a confirm dialog, the invoice page shows the latest Peppol status, and drafts can be sent (the number is assigned server-side). Probe script for the first sandbox contact under scripts/peppol/qvalia-probe.ts. Refs #546 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ * fix(peppol): Qvalia sandbox facts from first live contact: bare-key auth, api-test host, SMP-URL document types The onboarding mail and a live probe against the sandbox (partner SE5595386219) corrected three assumptions from the public docs: the key is accepted bare in the Authorization header (the ApiKey prefix answers 401), the sandbox host is api-test.qvalia.com, and the recipient lookup returns document types as SMP service URLs, so capabilities are now normalized to bare Peppol document type ids before comparison. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ * feat(peppol): probe commands to inspect and configure the Qvalia webhook subscription Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ * fix(peppol): decode UBL entities in one pass (CodeQL js/double-escaping) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqFpxeWqbpR7bcwUJLRERQ --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8249fcab5e |
feat(mileage): suggest driving distance from the from/to addresses (#1778)
* feat(mileage): suggest driving distance from the from/to addresses When both endpoints are typed in the trip form (create mode), a debounced lookup geocodes them via Nominatim and fetches the driving distance via OSRM, both proxied through /api/mileage/distance so addresses leave only our server, without user identifiers. The suggestion renders as a click-to-apply hint under the distance field, never auto-fills, and stays fully editable. Tooltip shows what the geocoder matched. In-instance caching (24h hits, 10min misses) plus 1.1s politeness spacing keep usage inside the OSM public-endpoint policies. OSMF is disclosed as a data recipient on the privacy page. Requested by a beta user: first-time routes had to be measured by hand; route memory (PR #1657) only helps from the second trip onward. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): resolve skeptic findings on the distance suggestion Compliance: routing switched from router.project-osrm.org (demo server, non-commercial use only) to FOSSGIS's routing.openstreetmap.de; the lookup is now click-triggered ("Foresla stracka") instead of as-you-type, per Nominatim's no-autocomplete policy; visible OpenStreetMap attribution next to the applied suggestion; privacy page reworked to name OSMF and FOSSGIS e.V. as independent recipients outside the sub-processor table, with an honest note that typed addresses can themselves be personal data. Correctness: suggestion-cache key separator changed from '|' (collidable by address text) to newline; routes rounding to 0.0 km are no longer suggested (the form rejects 0); the Nominatim politeness queue is bounded at 3s wait and bails to null instead of holding request handlers open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): resolve CodeRabbit findings on the distance suggestion A generation counter invalidates in-flight lookups when the route or km field changes or the dialog closes, so a slow response can never write an old route's distance into a changed form. Privacy page now states each recipient's actual payload (Nominatim gets address texts, FOSSGIS only coordinates), discloses the 24h in-memory server cache, and carries today's revision date. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
47c039453c |
feat(import): undo a bank file import including ignored transactions (#1764)
* feat(import): undo a bank file import including ignored transactions (#1672) A mis-parsed bank CSV could not be cleaned up: re-importing dedup-skips the bad rows, the single-row DELETE refuses imported rows by design (TRANSACTION_DELETE_IMPORTED), and there was no bulk action. Transactions also never recorded which import batch inserted them, so a strictly scoped undo was impossible. - transactions.bank_file_import_id: batch link stamped at ingest by both bank-file import paths (dashboard execute route, v1 REST route). PSD2/ manual/MCP rows stay NULL. No retroactive backfill: fuzzy attribution could delete rows belonging to a different import. - undo_bank_file_import RPC: owner/admin-only bulk delete of the batch's unbooked rows, ignored INCLUDED. Booked rows (journal link, payment rows, voucher links) and rows with append-only payment_match_log history are skipped and reported, mirroring the single-row route's guards. Marks the import 'undone' (re-import reuses the row via the company_id+file_hash upsert), writes one audit_log summary row, and hardens the actor gate like undo_sie_import: p_user_id honored only for service_role callers, 42501 otherwise, no anon EXECUTE. - DELETE /api/import/bank-file/[id]/undo returns the deletion report; RPC 42501 maps to BANK_FILE_UNDO_FORBIDDEN (403). Closes #1672 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(import): return 404 when the bank-file undo target does not exist An unknown or out-of-company import id answered 400 BANK_FILE_UNDO_FAILED, hiding the not-found semantics the SIE import routes already expose ('Import not found', 404). Flag the case in undoBankFileImport (notFound) and map it to a new BANK_FILE_UNDO_NOT_FOUND structured error (404); status-refusals and RPC failures keep the 400 envelope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * feat(import): show bank file import history with undo on the import tab The undo shipped for issue #1672 was API-only: no surface listed a company's bank_file_imports, so neither users nor founders could reach DELETE /api/import/bank-file/[id]/undo, and the deletion report existed only in JSON. Mirror the SIE pattern (SIEImportHistory, #1574): - GET /api/import/bank-file: list the company's imports newest-first, same { data, count, limit, offset } shape as GET /api/import/sie. - BankFileImportHistory: fold-open 'Tidigare bankfilsimporter' row on the Importera tab with filename, date, format, imported count and status per import, plus an undo action on completed rows behind a DestructiveConfirmDialog. The undo stays owner/admin-only via the undo_bank_file_import RPC's actor gate, like the SIE one. - After undo the toast shows the full report: transactions removed, booked rows skipped, rows with match history skipped, so nothing disappears silently from the ledger's surroundings. - i18n strings in messages/sv.json and messages/en.json following the sie_history_* key style; list-route test mirroring the SIE list test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * chore(migrations): move undo_bank_file_import after main's 2026-08-19 migrations Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(import): validate bank-file list params, fail closed on undo lookup, log lost batch attribution Review findings on #1764 (CodeRabbit): - GET /api/import/bank-file rejects non-integer/negative/oversized limit and offset and unknown status with a mapped 400 (BANK_FILE_LIST_INVALID_QUERY), limit capped at 100; boundary and invalid-input tests added. - undoBankFileImport distinguishes PGRST116 (zero rows -> notFound/404) from other lookup failures, which now return an error instead of masquerading as a permanent 404. - The v1 import route no longer discards the bank_file_imports upsert error: kept non-fatal by design (an unattributed batch imports fine and never appears in undo history), but the failure is now logged loudly. - Route test beforeEach clears the event bus (repo convention). Signed-off-by: Emil <emilmattsson14@gmail.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e733ab7c43 |
fix(arsredovisning): unblock the signing flow, accept foreign parent org nr, explain Fortnox underlag failures (#1738)
Batch from a real migration walkthrough (Fortnox -> Accounted, 2026-08-20): - Årsredovisning: the "Låst version" select was empty with no explanation because the only version was a draft and "Lås version för underskrift" is disabled while the four Lagstadgade upplysningar checkboxes and the content confirmation count as blockers. The select is now disabled with a hint that names the blocker count and links to Fullständighetskontroll, the four AR-NOTE-*-UNCONFIRMED issues carry remediation text, the lock button explains why it is grey, and "Markera som signerad" says what it still needs (locked version, bevisreferens, date). - Moderföretagets org.nr accepts a foreign registration identifier (CHE-123.456.789, HRB 12345, 923 609 016); personnummer shapes stay out. - Fortnox underlag discovery: log status, body and Fortnox's message on failure, show the message in the UI, treat a 400 with behörighet/scope text as scopes-required, and fall back to an unfiltered voucherfileconnections list when the financialyear filter answers 400. - Kontomapping: the Momskod column had min-w only; table-fixed collapsed it and its selects overflowed into Konfidens. Real w-72 now. - SIE import warnings pluralise correctly for one skipped voucher; the Verifikationsserie option says the source series is preserved. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a92c492dbe |
refactor(ui): record detail pages as documents, not card piles (#1739)
Bring every record detail page onto the register-detail document grammar from #1624 (DetailSection/DefRow, one status element per the list pages' chips-mark-exceptions rule, one primary next step plus Förhandsgranska visible and everything else behind a ⋯ overflow menu, line tables on the dry-table idiom with the headline total in the serif): - invoices/[id] (11 cards, 13-button toolbar): Kund | Detaljer rows, Fakturarader table + totals, Anteckningar, Betalning, Påminnelser, Utskickshistorik (InvoiceDeliveryHistory flattened); title carries the doc type, related documents become link rows - supplier-invoices/[id], bookkeeping/[id] (serif title instead of font-mono, JournalEntryAttachments variant="section", CorrectionChain flattened), invoices/[id]/credit, assets/[id]/dispose (form as Fönster rows), salary employees/[id] (edit form behind Redigera in a dialog, Ingående saldon collapsed), salary runs/[id] + run panels (Betalfil, Skattebetalning, AGI, förmåner, override) and the payslip page - DetailSection gains an optional help slot (convention 7) Styling/structure only: no API, fetch, validation, state, dialog or permission change; every action stays reachable. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e6c4fe2cf8 |
fix(customers): personnummer guard + personal_number on v1 + payment terms from settings (#1724)
* fix(customers): stop personnummer landing unmasked as org_number, persist personal_number on v1, default payment terms from settings
Closes #1707. Closes #1708.
Personnummer (#1707, Discord kalletoxic):
- CreateCustomerSchema rejects an org_number shaped like a Swedish
personal identity number on business customer_types. Only
customer_type=individual rows are masked in lists, so accepting one
stored an unmasked personal identifier (GDPR art. 5.1 c). The shape
check uses the month-position rule (legal-entity orgnr always
carries >= 20), so real orgnr can never false-positive.
- The v1 create, v1 PATCH and bulk-create endpoints accepted
personal_number through the shared schema but silently dropped it.
They now store it encrypted, expose it masked (********-1234) on the
single-customer surfaces, and treat the masked form as unchanged,
mirroring the internal routes.
- Route-level guards on both PATCH routes (new 400
CUSTOMER_ORG_NUMBER_IS_PERSONAL) plus a client-side message in
CustomerForm (sv + en).
Payment terms (#1708, Discord kalletoxic):
- New resolveDefaultPaymentTerms: provided value, else
company_settings.invoice_default_days, else 30. Wired into the UI
new-customer dialog, the internal POST, v1 create (incl. dry-run),
bulk-create and the MCP staged create_customer.
apiskill regenerated; no migrations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: record what the CI build OOM actually was
main raised the build heap to 8192 in parallel with this branch, so the
fix itself is already in and this keeps it untouched. What was missing
is the diagnosis.
Measured with tsc --noEmit --extendedDiagnostics, type-checking the repo
needs 4 192 550 K at
|
||
|
|
9fc05c383f |
feat(notices): one aggregated notice line instead of stacked degraded-state banners (#1733)
* feat(notices): lib/notices aggregator + single notice line on Hem
Degraded-state surfaces (broken/expiring bank connections, Skatteverket
reconnect, failing cloud backups, wrong-account hint) each hand-rolled
their own detection and stacked independently on the dashboard. This adds
lib/notices, mirroring lib/worklist, as the single owner of every health
predicate, and de-clutters the surfaces:
- lib/notices/{types,predicates,categories,aggregate}: five documented
categories with a fixed priority order; every predicate soft-fails to
null; pure decision helpers live in predicates.ts so 'use client' pages
can import them without pulling server-only modules. Broken supersedes
expiring for the same bank connection by construction (status filter).
- GET /api/notices + POST /api/notices/dismiss (withRouteContext), and a
notice_dismissals table (per company+user+notice_id, RLS user-scoped).
Notice ids embed a state discriminator, so a dismissal hides exactly
the state the user saw and a NEW failure surfaces again.
- Hem renders only the highest-priority notice as ONE AttnLine where the
boxed BackupHealthBanner card sat (banner deleted; its multi-provider
sentence logic moved into the backup_failing predicate), with a quiet
"+N till" inline expander. otherAccountHint joins the same list as the
lowest-priority category instead of an unconditional extra line.
- transactions and skattekonto keep their own AttnLine copy/CTA but source
the reconnect decision from the shared skvStatusNeedsReconnect /
skvAuthErrorNeedsReconnect predicates; Hem's Bevaka row imports the
expiring-consent day-math instead of duplicating it.
- design.md convention 6 addendum: max one global notice line + max one
page-domain attn line (locked convention: needs founder sign-off).
- i18n: new notices namespace in sv+en; moved banner/hint keys deleted.
- notice_dismissals classified as archive-excluded (UI state, not
räkenskapsinformation) to satisfy the full-archive contract.
SkatteverketPromoCard keeps its localStorage dismiss for now; migrating it
to notice_dismissals is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(notices): stable dismissals with reaping, bounded ids, unnamed-bank copy
Review fixes on the notice aggregator:
- Migration renamed 20260819080000 -> 20260819190000_notice_dismissals.sql
(version collision with another in-flight PR; content unchanged).
- backup_failing dismissal stability: the id no longer embeds
last_auto_sync_at / needs_reauth_at, which the cron re-stamps while the
SAME incident persists and so resurrected a dismissed notice daily. The
id is now stable per (provider, reason), and the opposite direction is
kept correct by stale-dismissal reaping in getCompanyNotices: when a
category is currently healthy, the caller's stored dismissals for that
category (matched on the 'category:' id prefix) are best-effort deleted,
so error -> dismiss -> healthy (reaped) -> new error resurfaces. Audit of
the other ids: bank ids embed connection id + status/expiry and skv
embeds the incident's first-error/expiry timestamp (markNeedsReconsent
only fires post-connect), all stable per incident; they get the same
reaping as hygiene. Contract documented on Notice.id in types.ts.
- NULL bank_name no longer interpolates the Swedish fallback 'banken' into
the English message: a bank_broken_one_unnamed message variant (sv + en)
is selected instead of a name param.
- Bounded notice ids: folding several connections into one discriminator
now collapses to count + first 8 hex of a sha256 over the sorted parts
(node:crypto, server-only) instead of concatenating uuids; single
connection ids stay human-readable. Dismiss schema cap tightened to 200
with an updated rationale.
- Tests: persisting failure stays dismissed across two aggregations,
healthy state reaps, new failure after reap resurfaces, hint never
reaped, failed reap swallowed, 30-connection id under 200 chars and
stable across orderings, unnamed-bank variant, sorted backup id stable
across cron re-stamps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(notices): pg-real coverage for the notice_dismissals policies
The coverage gate is right to flag the migration: every policy on this table
binds company membership AND auth.uid(), and nothing exercised it. The suite
pins the property that makes the table different from the rest of the schema:
a dismissal is personal, so a colleague in the same company keeps seeing a
notice the other member hid. It also covers the upsert re-stamp (which needs
the UPDATE policy), cross-tenant refusal, dismissing on behalf of another
user, the caller-scoped DELETE that reaping relies on, and the composite key.
Falsification-verified against a real Postgres: weakening the SELECT policy
to company-only scoping fails the colleague-isolation test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4cf227001d |
fix(skattekonto): bound the sync to the first räkenskapsår, add an ignore path, EF-aware avdragen skatt (#1729)
* fix(skattekonto): scope the sync and the avdragen-skatt rule for enskild firma
Two EF problems on the skattekonto surface:
1. Stuck pre-company rows. The sync never passed datumFrom, so SKV's
~555-day default lookback imported the owner's PERSONAL skattekonto
history from before the company existed. Those rows can never be
booked (no fiscal period covers them), never deleted (external
mirror), and had no ignore path: visible forever.
- syncSkattekonto now bounds the fetch at the company's earliest
fiscal_periods.period_start (new getEarliestFiscalPeriodStart in
period-service; no bound when no period exists yet). Applied
uniformly to EF and AB.
- New skattekonto_transactions.is_ignored column (migration
20260819080000, copies the transactions.is_ignored precedent:
CHECK that an ignored row has no journal_entry_id, partial index;
the existing company-scoped UPDATE policy already covers it) plus
PATCH /skattekonto/transaktioner/:id/ignore (409 on booked rows,
race-guarded on journal_entry_id IS NULL). Ignored rows leave the
default GET buckets; ignored_count is always reported and
include_ignored=1 returns the rows, surfaced as a count line +
"Ignorerade" band on /skattekonto and an Ignorera affordance with
confirm + Ångra on both /skattekonto and the /transactions inbox.
- PERIOD_LOCKED for a date before the first fiscal period now says
the row predates the company's bookkeeping and can be ignored,
instead of "lås upp perioden" (a dead end for those rows).
2. "Avdragen skatt" auto-mapped to 2710 for every entity type. For an
EF without employees that line is almost always A-skatt an outside
employer withheld from the owner's private salary, not the firm's
payroll liability. New data-driven skattekonto_rules.requires_employer
column (migration 20260819080100, set on the avdragen-skatt seed and
its per-company clones); the matcher gates such rules for an
enskild_firma unless company_settings.employer_registered is true
(the existing AGI gate signal, fetched in the same settings query).
Gated rows take the NO_COUNTER_ACCOUNT path with a distinct hint;
AB and employer-registered EF keep 2710 unconditionally. Regression
guard pins EF preliminärskatt to 2013.
The nightly sync upsert excludes is_ignored so it can never silently
un-ignore a row. New pg tests for the CHECK + RLS need a test:pg run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skattekonto): clamp datumFrom to the SKV window, gate ignored rows, widen the employer signal
Review fixes on the EF-scoping PR:
- sync: clamp datumFrom to max(earliestPeriodStart, today - 555 days); a
bookkeeping start older than SKV's 555-day default is omitted entirely,
since sending it would widen the window past the default and anything
older than ~915 days fails the whole sync with felkod 2. The misleading
"no-op for AB" comment is corrected and boundary tests added.
- booking/match: an ignored row now throws a typed ROW_IGNORED error
(409) before any draft is created or link is written, in both
bokforSkattekontoTransaction and matchSkattekontoToEntry.
- page: the Nasta dragning / shortfall math re-includes ignored upcoming
charges (SKV draws them regardless of our ignore flag) while the
work-list buckets keep excluding them.
- employer gate: treat employer_registered ?? pays_salaries as the
signal (same fallback as lib/tax/deadline-config.ts), so an EF that
attested pays_salaries keeps 2710 for avdragen skatt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(skattekonto): assert the is_ignored RLS toggle inside the rolled-back transaction
withUserContext always rolls back (tests/pg/setup.ts), so the previous test
wrote inside it and read the pre-write value back on the pool connection:
it failed against a correct policy and would have passed against a missing
one only by accident. The assertions now live inside the same transaction,
pin rowCount=1 (an RLS-filtered UPDATE silently matches zero rows), and a
new test pins the negative: a non-member's UPDATE matches zero rows.
Falsification-verified against a real Postgres: dropping the UPDATE policy
makes both tests fail; with the policy they pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b77af371c4 |
fix(transactions): persist the source filter per company and stop the reset race (#1726)
The source filter was persisted under a browser-wide v1 key (#1105), but the stale-filter guard added in #1124 compared the restored value against sourceItems before cash accounts, skattekonto rows, and transactions had loaded, so every mount reset the in-memory filter back to 'Alla källor' while storage kept the old choice: restore-then-reset on every visit. - New pure helper components/transactions/source-filter-storage.ts: per-company v2 key, isSourceFilter moved out of the page, read/write helpers (read removes the retired v1 key once), and resolveEffectiveSourceFilter. - page.tsx keeps sourceFilter as the WANTED filter, restored per company (with a state-only ?source= URL override that is never written to storage); the guard effect is replaced by a derived effectiveSourceFilter memo used by every consumer, so a source that is still loading or went stale shows 'all' without destroying the choice. - ?highlight= deep links widen to 'all' in memory when the wanted filter would hide the highlighted row. - Unit tests for the helper; no i18n changes, no migrations. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
64fc7c783d |
fix(periodisering): stop overselling automatic periodization to enskild firma (#1730)
* fix(bokslut): honest periodisering for enskild firma (K1) Stop mis-selling automatic periodisering to sole traders and give the auto-detect a materiality floor: - Remove the inert PeriodiseringAutoDetectToggle (write-only localStorage, no reader anywhere); the settings row is now a plain link to the periodisering wizard, with new i18n keys in sv+en. - Auto-detect tags suggestions under 5 000 kr as low confidence with the reason 'Under 5 000 kr: behöver normalt inte periodiseras', citing K1 (BFNAR 2006:1) for enskild firma and K2 for aktiebolag; the wizard only pre-ticks high-confidence rows, so under-floor posts land unticked. Personnel-cost lines (7xxx) are exempt: they must always be accrued. - The accruals GET route resolves companies.entity_type and threads it to the detector. - Per-line accrual hint in the invoice editors is entity-aware: new accruals.k1_hint (K1, förenklat årsbokslut) for EF, k2_hint stays for AB. - Periodisering wizard and year-end AccrualsStep relabel Revisionsarvode to Bokslutsarvode for EF, default the liability account to 2991 instead of 2992, and show a muted K1-floor intro line. All copy stays advisory (behöver normalt inte, never får inte): entity_type is a proxy since no förenklat-vs-full-årsbokslut flag exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): SEK-correct materiality floor, entity-type via settings, narrower personnel exemption Review fixes on the K1 periodisering branch: - The 5 000 kr floor now compares a SEK amount: queries select currency and subtotal_sek, the floor uses the periodisation share of subtotal_sek for foreign-currency invoices, and is skipped entirely when no SEK amount is resolvable (accrual-k2-hint precedent, DECISIONS.md 2026-07-26). - The accruals route resolves entity type via getCompanyEntityType (company_settings-primary, companies fallback) instead of reading companies.entity_type directly. - The personnel-cost exemption from the floor is narrowed from startsWith('7') to /^7[0-6]/: 78xx/79xx are not personnel costs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c402421908 |
feat(billing): make the expired-trial state visible with a clear upgrade path (#1725)
getCompanyEntitlements now derives an entitlementState (trial / trial_expired / lapsed_subscription / paid / none) plus trialExpiredAt from the grants it already fetches, reading company_subscriptions.status inside the existing Promise.all so churned payers get 'abonnemang' copy instead of 'provperiod'. The state threads through CompanyContext and the dashboard layout. Two new surfaces, both hidden in sandbox: - SubscriptionTouchpoint replaces the sidebar trial pill: countdown while the trial runs, a persistent muted upgrade link to /settings/billing once it lapses (visible even collapsed, icon-only with aria-label), and the first mobile bottom-sheet touchpoint. - TrialExpiredDialog: one-time on-entry notice with 'Se abonnemang' and a ghost dismiss; acknowledgement persists per user+company in user_preferences.ui_state.trial_expired_ack (read server-side, no flash), set on dismiss and click-through alike. Narrows the 2026-07-11 'no trial-expired nag' decision at the founder's direction after a user could not find the upgrade path at all; see DECISIONS.md. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4e20c9dec4 |
fix(import): bulk-confirm the VAT-treatment review gate in account mapping (#1723)
* fix(import): bulk-confirm the VAT-treatment review gate in account mapping A Fortnox chart routinely puts 70+ class 3/4 accounts behind the vat-treatment review gate, and the only way through was one Bekräfta click per row across paginated 50-row pages. A live migration (2026-08-18) died exactly there, stuck at 50 kvar with Continue disabled and no way to see why. One outline button next to Continue now accepts the suggested default for every remaining row, with the exact semantics of the per-row button batched (defaults kept, rows marked reviewed). Wired in both the import wizard and the Arcim migration workspace. Strings in sv+en. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(guards): two naive-ore-rounds that stacked past the ratchet baseline #1700 and #1705 each added one Math.round(x*100)/100 and each passed CI alone against baseline 630; the first branch containing both trips the ratchet at 631. Convert both to roundOre (629, below baseline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: place the roundOre import on its own line The previous commit inserted it inside a multi-line import block, breaking parsing in pdf-template.tsx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: give next build an explicit 8 GB heap The build worker OOMs on the runner's default Node heap since the bundle crossed the default old-space ceiling (first branch containing all of 2026-08-19's merges). Public-repo runners have 16 GB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6b9338f17e |
feat(invoices): Betald-stämpel i faktura-PDF + betalningsbekräftelse till kund (#1700)
* feat(invoices): Betald-stämpel i faktura-PDF + betalningsbekräftelse till kund Closes #1693. A paid faktura re-renders with a BETALD banner (paid date and amount) and "Betalt: X" followed by "Att betala: 0"; partially_paid gets the Betalt / Att betala (remaining) rows without a banner. Credit notes and proformas are unchanged. Labels in sv and en. The paid copy is its own document, a betalningsbekräftelse, never the archived original: GET /api/invoices/[id]/pdf?variant=paid refuses anything but status paid (409 INVOICE_PAYMENT_CONFIRMATION_NOT_PAID), names the file Betalningsbekraftelse-<nr>.pdf and never reads or replaces the delivery archive. invoice-pdf-source gains the 'payment_confirmation' re-render reason so the UI caveats it like any re-render. POST /api/invoices/[id]/send-payment-confirmation emails the paid PDF with a dedicated subject/body through the existing email service and recipient routing, without touching status, sent_at, journal entries or invoice_deliveries (no kind column there; logged via the route logger instead). Detail page: the two actions sit inside the Betald card (download paid copy, send confirmation with an up-front confirm dialog), not in the header row. No migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): one-line hint for the betalningsbekräftelse actions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
eb0df1722b |
fix(salary): show the sysselsättningsgrad product next to the run salary input (#1702)
A Discord report had a 10 % employee: typing 4 531 in the run gave a 453,10 kr gross, so the user typed 45 310 to get it right. The engine was correct (grundlön = månadslön × sysselsättningsgrad / 100) but nothing on the row said so; the formula only lived in Beräkningsdetaljer. Below 100 % the row now prints "× 10 % = 4 531 kr" under the monthly salary (input and read-only shapes), and both employee forms explain under Sysselsättningsgrad that the base salary is monthly salary × degree. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9d59e509ab |
fix(invoices): fold the ROT/RUT card into Detaljer and mask personnummer as YYYYMMDD-XXXX (#1699)
* fix(invoices): fold the ROT/RUT card into Detaljer and mask personnummer as YYYYMMDD-XXXX Founder review of #1690 (2026-08-18), two decisions. Declutter (design B): the separate Skattereduktion card on the invoice detail page duplicated the totals block. It is gone; what it carried beyond the amounts now lives in Detaljer as plain rows, only for invoices with a claim: Personnummer (masked, or "Saknas"), Fastighet (ROT only: fastighetsbeteckning or BRF, with lagenhetsnummer inline), and Skattereduktion with the begaran lifecycle ("Ej begard" + inline "Skapa begaran" link when paid and unclaimed; otherwise the rot_rut_status_* label, date and decided amount), styled like the neighbouring Bokforing row. Totals block unchanged. Per-line subtext shortened to "<RUT|ROT> · <arbetstyp> · <n> tim" (desktop + mobile). Personnummer mask: invoice surfaces now show YYYYMMDD-XXXX (birth date visible, last four hidden), the payroll convention (maskPersonnummer), instead of XXXXXXXX-<last4>. Computed on read from the stored AES-GCM ciphertext by lib/invoices/deduction-personnummer.ts: no schema change, nothing stored, never throws (bad ciphertext logs and renders no personnummer). InvoicePDF derives it itself when given the stored row so no render call site can drop it; the preview route passes an already-masked value (it only has the typed plaintext or the kundkort fallback). The v1 pdf/send routes fetch the ciphertext for the render only; INVOICE_FULL_COLUMNS / INVOICE_PDF_COLUMNS stay as pinned. The detail page and the editor's kept-hint read the mask from the new GET /api/invoices/[id]/rot-rut (withRouteContext, company members), which never returns the last four alongside the mask. v1 REST and MCP keep deduction_personnummer_last4 for compatibility. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): stack the ROT/RUT claim state and action in Detaljer At the sidebar card width "Ej begard" and "Skapa begaran" wrapped mid-word side by side (seen in the sandbox on a paid invoice). Same shape as the Bokforing row now: state on top, the action under it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
72603abfd6 |
feat(invoices): show the ROT/RUT claim on the invoice detail page (#1690)
The claim was visible only on the PDF (audit item 8, reported by kalletoxic 2026-08-18): the detail page showed the unreduced total and nothing about the deduction. Now: - Totals: Totalt stands, a Skattereduktion ROT/RUT row follows and the bold line is Att betala, computed with the same getAmountToPay() as the PDF and the invoice email so the three never disagree. - Each claimed line shows kind, arbetstyp, hours and its deduction amount under the description (desktop + mobile). - New Skattereduktion card: customer share vs Skatteverket share, masked personnummer, fastighetsbeteckning/BRF/lägenhet for ROT, and where the begäran om utbetalning stands (reads rot_rut_payout_request_items; paid invoices without a begäran get the one-line CTA to /invoices?rot-rut=1). Verified in the sandbox against a RUT invoice in draft, sent and paid state. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bc357531cc |
feat(shopify): port the order sync from the transactions feed to webshop_orders (#1676)
* feat(shopify): port the order sync from the transactions feed to webshop_orders Shopify orders now land as rich rows on the Orders page (platform 'shopify'), the same surface WooCommerce uses, instead of opaque bank-feed rows on the 1584 cash account: - order-sync.ts writes through the shared upsertWebshopOrders service; the 1584/ensureManualCashAccount wiring is gone (prod has zero Shopify feed rows). Cursor/overlap/dedup, revoked classification and the frozen external_id formats are unchanged. - vat_breakdown is reconstructed from the order-level taxLines (net = tax/rate, remainder as a 0%-bucket, refuse on unusable data); refund VAT is prorated from the parent order's mix. The line-item snapshot is stored only when it reconstructs the charged total to the ore, else the invoice conversion falls back to one aggregate line. - GraphQL query gains createdAt, taxesIncluded, taxLines, lineItems and shippingLines (all non-PII; page size 100 -> 25 for query cost). - Nav gate counts active shopify_connections; the Orders empty-state CTA goes to the platform-neutral /import hub; panel/manifest copy now points at the Orders page (sv + en). - Paid-only qualification and the 90-day backfill stay; the bookkeeping-lock row filter is dropped (lock is enforced at booking, parity with WooCommerce). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(shopify): carry the prorated parent tax on refunds when per-rate bucketing is refused A refund whose parent vat_breakdown was refused (unreported rates) stored total_tax 0 and prefilled a 0%-refund with no moms reversal. The parent's total tax is now prorated into the refund row, so the booking dialog's ratio-inference fallback presents an editable bucket with the reversal instead (CodeRabbit + Swedish review + skeptic finding). Adds the mixed-rate line and truncated shipping-page tests CodeRabbit asked for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cfdddb2d7e |
feat(mcp): customer_number on create_customer + Beta tags on webshop surfaces (#1677)
* feat(mcp): accept customer_number on gnubok_create_customer Parity with gnubok_update_customer: a customer number no longer needs a create-then-update two-step with two approvals. The staged params carry the trimmed number, commitCreateCustomer inserts it, and the payload-size ceiling is bumped 59.7K to 59.75K with a documented entry (the property has no description; name + maxLength are the whole contract). Requested by a user on Discord 2026-08-16. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): mark webshop integrations and orders tab as Beta WooCommerce and Shopify rows on the import page get a quiet Beta chip next to the title, and the webshop /orders sidebar item sets the existing betaBadge flag. Chip recipe matches the nav beta badge so Beta reads identically everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): enforce customer_number invariants and show it on the approval card Consolidated resolution pass for PR #1677: - skeptic (correctness): maxLength 32 was advertisement-only on the create path; now enforced with a runtime guard in gnubok_create_customer execute (clean errors for non-string and >32) and a 400 guard in commitCreateCustomer, matching the web/v1 routes and commitUpdateCustomer. - skeptic (correctness): CustomerPreview never rendered the staged customer_number, leaving the approver blind to the new field; added a conditional Kundnr row. - CodeRabbit: reset the event bus in create-customer.test.ts beforeEach. - Tests cover both new guards at the tool and executor layers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dc311726de |
feat(mileage): prefill distance and purpose from earlier trips on the same route (#1657)
* feat(mileage): prefill distance and purpose from earlier trips on the same route Christoffer's beta feedback: recurring routes meant retyping the same km every time. Fran/Till now autocomplete from earlier trips, and when the pair matches a previous trip the one-way distance and purpose prefill from the latest match. Only empty fields are filled, edit mode is untouched, and a hint under the km field shows when a value came from route memory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): invalidate route prefill when the route changes Skeptic + review findings on the first push, resolved in one pass: - Stale prefill: km/purpose filled from a matched route survived onto a different route (typing past the match, or switching Fran), with the hint still claiming same-route provenance. Prefill state now tracks the route key and the exact prefilled strings; when the key changes, fields still holding those strings are cleared and the match re-derives for the new route. User-typed values are never touched. - Purpose was filled with no indicator: the hint now renders under both km and purpose, each cleared independently by manual edits. - Prefill now uses the unrounded half of a stored round trip (21.25, as the copy flow does) so the round-trip toggle re-doubles to the exact stored km. - Per-keystroke O(n log n) sort replaced with a WeakMap-cached sorted order per trips array. - Docstrings on all route-memory exports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mileage): offer a route's prefill at most once and drop stray NUL byte Second skeptic pass on the rework found two issues, both fixed: - Same-key refill rebuilt the prefill record from scratch, dropping the other field's live tracking and re-filling a field the user had deliberately emptied. applyRoutePrefill now matches only when no record exists for the current route key; the record survives as an offered-marker even fully disowned, so per-field tracking is stable and an emptied field stays empty until the route actually changes. - routeKey embedded a raw 0x00 byte as separator, which made git treat the file as binary and killed diff review. The separator is now an explicit String.fromCharCode(10) newline, which normalizeLocation can never produce, keeping keys collision-free and the source printable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
79240cb2ed |
fix(articles): article ROT/RUT prefill was dead for every dashboard-created article (#1651)
* fix(articles): article ROT/RUT prefill was dead for every dashboard-created article Follow-up to #1634. The user re-tested and picking a RUT article still left the line on "Ingen": the article form has always stored the bare kind ('ROT'/'RUT'), while the prefill only recognised Skatteverket work-type codes (BYGG, STAD, ...). On prod every dashboard-created ROT/RUT article holds the bare kind, so the fix in #1634 never fired for a real user, and worse, since the helper returned null for those values, picking such an article CLEARED a deduction the user had set manually on the row. - rot-rut-rules: parseArticleHouseworkType() understands both vocabularies (code -> kind + arbetstyp; bare ROT/RUT -> kind only), plus normalizeHouseworkType()/HOUSEWORK_TYPE_VALUES/workTypeLabel(). - InvoiceEditor.applyArticle: kind-only articles pre-fill the deduction and keep a same-kind arbetstyp already chosen on the row; "Spara som artikel" round-trips the code or, lacking one, the kind. - ArticleForm: the ROT/RUT select now offers the real Skatteverket arbetstyper in ROT/RUT groups (its own hint always promised "förifyller arbetstyp"); legacy kind-only values stay selectable as "RUT (arbetstyp ej vald)" so an edit never silently drops the flag. Article detail renders "RUT · Städning" instead of the raw code. - API + MCP commit schemas normalize housework_type (case-insensitive code or ROT/RUT, '' clears) and reject anything else; the CSV article import normalizes the column the same way. Prod holds 178 articles with '0'/'1' from a boolean "Rot" column that the keyword detector mapped straight through; those now read as no flag everywhere and can no longer be created. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(articles): review follow-ups on #1651 - InvoiceEditor: switching a row's skattereduktion ROT<->RUT clears an arbetstyp from the other list, and Spara som artikel only round-trips a work type that belongs to the row's kind (CodeRabbit). - MCP update_article: null / '' / whitespace now clear housework_type (commit drops only undefined keys, so the old undefined mapping made the flag un-clearable); create keeps treating them as unset. Tests. - Article CSV import warns when a non-empty ROT/RUT value is dropped as not-an-arbetstyp instead of dropping it silently. Test. - Hint wording: arbetstyp is pre-filled only when the article carries one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
93541d7186 |
fix(ux): smoothness follow-ups - detail pages, batches, toasts, and the last edges (#1633)
* fix(ux): smoothness follow-ups - detail pages, batches, toasts, and the last edges Follow-up batch to #1629: the six documented deferred items from dev_docs/loading_states_analysis.md, in the same vocabulary (first-load-only takeovers, background reconcile behind mounted content, row/button-level pending, sequence guards). - Invoice detail pages: kundfaktura and leverantorsfaktura detail no longer blank the whole page for one-field changes. fetchInvoice shows the blocking spinner/skeleton only before the first paint (or when the pager steps to a different invoice); Bokfor / status / finalize / payment / send / Attestera / Markera betald / kreditera refetch behind the mounted page, the acting button shows a spinner-in-button, and the handlers await the refetch so pending covers until the content reflects the new state. The supplier detail's single isProcessing boolean became processingAction so the spinner lands on the clicked button only. (The leverantorsfakturor LIST try/catch/res.ok item was already fixed by #1629.) - useDestructiveConfirm: confirm(opts, action?) can now carry the destructive operation, so the dialog's existing isLoading spinner actually shows while it runs, dismissal is blocked meanwhile, and confirm resolves false if the action throws. Adopted at the /transactions row delete and the supplier- invoice detail delete (which previously permitted duplicate DELETEs with zero feedback). - Batch parallelization: new lib/concurrency.ts mapWithConcurrency (bounded worker pool, order-preserving, tested). /transactions batch categorize / ignore / delete run per-row requests 5 at a time instead of strictly sequentially; the bulkbar counter ticks per completed row. - Toast-spam reduction: batch categorize rows run silent (exit animation, count decrement and state patch stay; no per-row Bokford or generic failure toast) and ONE aggregate toast reports "N bokforda[, M misslyckades]" with a single Angra alla action that pools the same /uncategorize endpoint over every booked row (per-row undo is feasible today, so the aggregate is too). Interactive escalations (SI/CI match suggestions, duplicate warning, activate-account) deliberately keep their dialogs. - Underlag row-click flash: InvoiceInboxWorkspace handleSelect seeds the detail pane synchronously from the clicked list row and starts the document load in parallel with the detail GET (which hydrates on arrival), so a row click never flashes the onboarding/empty state, and a stale-response guard keeps a slow fetch from overwriting a newer selection. - #1629 round-2 edges: /pending holds the loading state when a fetch for a not-yet-loaded tab FAILS (never renders the previous tab's rows under the new tab's header, and never fakes an empty state); /transactions clears transactions/skvRows (+ count/paging) and bumps both fetch sequences on company switch, and loadSkvRows got the same sequence-guard pattern as fetchTransactions. Gates: full vitest suite green (14772 passed), tsc byte-identical to the origin/main baseline (stash-diffed), eslint 0 errors on touched files (warnings identical to baseline), check:guards green, package-lock untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): harden action feedback against stale responses and failures Address the seven CodeRabbit findings on #1633: - invoices/[id] + supplier-invoices/[id]: latest-request guard in fetchInvoice (sequence token) so a mutation refresh overlapping pager navigation can never commit invoice A's state under invoice B's URL; the deferred related-document writes are guarded too - supplier-invoices/[id]: try/catch/finally in approve/book/mark-paid/ credit/uncredit so a rejected fetch()/json() clears processingAction instead of leaving every invoice action disabled until reload - transactions: extend the skattekonto sequence guard to the connection-status write so a status response started under the previous company cannot flip the reconnect banner for the new one - transactions: runCategorize resolves { ok, journalEntryId } so the batch aggregate counts a 200-with-null-journal-entry booking (flag flip) as success instead of narrating it as misslyckades; Angra alla only targets rows with an actual verifikat, since the storno endpoint rejects rows without one - transactions: shared undoneIdsRef lets "Angra alla" cancel a pending finishBooking state patch; a fresh booking clears its row's entry so re-booked rows still get their delayed patch - InvoiceInboxWorkspace: monotonic request tokens for the detail and document reads so a same-item reload cannot resolve out of order and paint a stale snapshot or document URL - messages: ICU plural for the success part of both partial batch descriptions in sv and en (1 bokford, not 1 bokforda) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4921d1da5e |
feat(import): import skattekontoutdrag files into the skattekonto pipeline (#1637)
* feat(import): import skattekontoutdrag files into the skattekonto pipeline Users can now upload the kontohändelse export from Skatteverket's skattekonto e-service (current CSV layout, verified against a real 2026-08 export, plus legacy .skv files) instead of needing the paid API connection. Parsed rows land in skattekonto_transactions as booked file_import rows and inherit the existing 1630 rules engine, bulk booking, match-to-verifikat and both UIs unchanged. - Core parser lib/import/skattekonto-file/ with strict detection (orgnr header + saldo markers, or two distinct SKV vocabulary terms plus row shape), sum-integrity check (opening + rows must equal closing) and a wrong-company guard against company_settings. - computeDedupKey moves to core (lib/skatteverket/skattekonto-dedup); the extension re-imports it. File rows hash-key; content-signature partitioning skips rows already booked (either key form) and promotes matching upcoming rows in place. - syncSkattekonto gains a takeover step: an id-keyed API row adopts a matching hash-keyed imported row in place, so journal links survive connecting the API after a file import. Upcoming rows can no longer clobber a booked row on hash collision. - New skattekonto_file_imports table (company-scoped file-hash dedup) plus source/file_import_id provenance columns on skattekonto_transactions. - /import gains a Skattekontoutdrag wizard (upload/preview/result, deep link ?mode=skattekonto); the bank-file flow detects skattekonto files and redirects instead of importing them as bank rows. - /skattekonto renders imported rows for unconnected companies (attn line + import CTA) instead of discarding them behind the StartCard. - Free for everyone: the local-data booking/match routes were already ungated; only API sync/saldo stay capability-gated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skattekonto): align the EF F-skatt rule with the 2012 -> 2013 decision 20260810120000 established that 2012 is not standard BAS and moved the booking templates to 2013 (owner taxes in an enskild firma are an eget uttag), but the skattekonto_rules seed still booked EF preliminarskatt against 2012. The file importer makes this rule fire for every EF F-skatt row, so bring it onto 2013 too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(import): apply review findings on the skattekonto file import - Fix the takeover candidate comparator: the single-argument sort was an inconsistent relation and could adopt a stale upcoming row ahead of the booked file row in a 3+ candidate queue (regression test added), and page the candidate scan with fetchAllRows so a multi-year window is not silently capped at 1000 rows. - Fail parsing when a statement HAS saldo markers but not both readable balances: a file cut off before "Utgående saldo" previously skipped the sum check entirely. sum_valid stays null only for marker-less legacy files. - Count a promotion only when the UPDATE matched a row, so a concurrent sync cannot inflate promoted_count; log a failed finalize of the import record instead of discarding the error. - Migration (unshipped, edited in place): user_id is nullable with ON DELETE SET NULL so import records and their file-hash dedup survive user deletion, and the INSERT policy binds user_id to auth.uid() so a member cannot attribute an import to a colleague. pg tests cover both. - Make the upload drop zone keyboard-reachable (role, tabIndex, Enter/ Space) and give the six count-bearing strings ICU plural forms in both locales. Skipped with reasons on the PR: binding execute rows to file bytes and re-checking orgnr in execute (same client-trust model as the shipped bank-file execute; Zod + RLS scope writes to the caller's own company), a 404 test (the route has no not-found path), event-bus clearing in the route test (the route touches no events), and FK NOT VALID (new column referencing a brand-new empty table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dfb34a01d9 |
feat(invoices,year-end): four byrå-feedback fixes (validation feedback, moms gate, klarmarkera, article search) (#1641)
* fix(invoices): surface validation errors instead of a silent dead submit button A missing unit (or any other Zod failure) blocked both Granska & skapa and Spara som utkast with zero feedback: handleSubmit had no onInvalid callback, the buttons stayed enabled, and the unit field rendered no inline error. Reported by a byra user whose client could not save any invoice. - onInvalid handler on all three submit paths: destructive toast plus scroll to the first inline error - inline error text under the unit select and quantity input (the only line fields that had none) - same treatment in NewRecurringScheduleDialog, including inline errors on its item rows Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): stop defaulting 25 % moms for icke momsregistrerade companies The registration form hard-coded vat_rate 0.25 on the initial line, added rows, AI prefill fallback and konto defaults, regardless of company_settings.vat_registered. A non-VAT-registered business that missed the prefilled rate booked ingaende moms (2641) it has no right to deduct (ML 8 kap. 3 \u00a7). The customer-invoice side already gates on the same flag; the supplier side ignored it. - form: read vat_registered from /api/settings; when false, all moms controls (rate cells, per-line moms, totals rows) are hidden and every line is forced to 0 %, including late AI prefills - reverse charge keeps its rate controls: self-assessment is a separate obligation from deduction - route: 400 SI_CREATE_INVALID_INPUT when a non-registered company posts a line with vat_rate/vat_amount > 0 (API/MCP defense in depth), and an omitted vat_rate now defaults to 0 instead of 25 % for those companies - tests: guard rejection, reverse-charge pass-through, 0-default; existing POST tests updated for the new settings lookup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(year-end): klarmarkera imported years already closed in a previous system SIE-imported historical fiscal years land with is_closed = false and no closing entry, so the year-end page lists every migrated year as pending bokslut even though the bokslut was done in the old software. There was no sanctioned way to mark them done: closePeriod hard-requires locked_at and closing_entry_id. - migration: fiscal_periods.closed_externally boolean (audit clarity: distinguishes a year-end run here from a close done elsewhere) - markPeriodClosedExternally(): closes + locks without a closing entry; refuses already-closed periods, periods with their own closing entry, periods that have not ended, and periods with unbooked bank transactions (same stranding guard as lockPeriod); writes the immutable audit_log entry - POST /api/bookkeeping/fiscal-periods/[id]/close-external (requireWrite) - year-end page: one attn line on the preflight step with a confirm dialog describing the outcome; the marked year drops out of the eligible list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): searchable article picker on invoice lines The article field was a plain Radix Select whose only matching is label-prefix typeahead: for numbered articles that means number-only lookup, and typing "skruv" found nothing. Byra feedback: name search would help a lot for users with real article catalogs. New ArticleCombobox (input-trigger dropdown, same pattern as AccountCombobox): free-text search over name + article number, diacritics-folded via foldText, keyboard navigation, pinned "Egen rad" free-text option, browse-all on focus like the Select it replaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log klarmarkera pg-test decision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address skeptic and compliance-review findings on PR #1641 - ArticleCombobox: keyboard focus no longer auto-opens the list, opening highlights the committed selection, typing highlights the first match, and re-selecting the current value is a no-op. Previously Tab+Enter silently detached the article and wiped its revenue-account override. - Supplier invoice prefill for icke momsregistrerade: the zeroing effect now grosses the net amount up by the extracted rate before forcing 0 %, so the booked cost and 2440 keep the full att-betala amount instead of understating both by the moms. - markPeriodClosedExternally: only migrated periods qualify (must contain SIE-imported verifikat or no verifikat at all); the update carries an is_closed=false predicate so a concurrent normal close cannot be overwritten; confirm dialog now names the reporting consequences. - Route comment: honest scope (this route only; v1/inbox/MCP sweep is a follow-up) and current-law citation (13 kap. ML 2023:200). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: use roundOre for the icke-momsregistrerad gross-up (ratchet guard) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f8db38f989 |
fix(analytics): mask session replays by default, chrome-only unmask (#1639)
* fix(analytics): mask session replays by default, chrome-only unmask Invert PostHog session-replay masking from visible-by-default with pattern masking to deny-by-default: every input value is masked wholesale (rrweb maskAllInputs, no maskInputFn) and every text node is masked unless it sits under data-ph-unmask chrome or a table column header (th). Chrome tags live on the shared UI primitives (PageHeader, Label, Button except combobox triggers, TabsTrigger, Badge, Card/Dialog/Sheet titles, tooltips, help popovers, empty states, settings labels), and tagged chrome is still pattern-scrubbed for amounts and person-/organisationsnummer. data-ph-mask beats data-ph-unmask, so call sites that interpolate user data into chrome stay masked; a very-thorough audit swept every unmasked primitive and each found site got a call-site mask. Confirm-dialog wrappers and toasts stay masked centrally: their copy describes user objects by design. Untagged new UI over-masks instead of leaking. Privacy policy, RoPA and decision log updated in the same change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(analytics): tag detail-section chrome merged from main The register-detail primitives landed on main after the replay-masking audit ran: kickers and DefRow labels are static i18n chrome, values stay masked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(analytics): close skeptic and review findings on replay masking Explicit data-ph tags now resolve before the th chrome fallback, so a th nested inside a data-ph-mask container masks correctly (regression test added). Seven missed text-leak sites get call-site masks: delete-invoice and credit-page invoice numbers, IB-correction voucher reference, TIC orgnr (served unnormalized, so the separator-based scrub cannot be relied on), articles search-term empty state, dimension segment labels, and activate-account buttons. The attribute channel is closed with rrweb's blockClass: inputs whose placeholder carries an effective user value (salary overrides, correction description, danger-zone confirms, credit confirm) get ph-no-capture, removing the element from recordings while the prefill UX stays intact; the pivot-th title attribute is dropped. Privacy-policy effective date bumped to 2026-08-17. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1bb423b2b3 |
fix(salary): surface missing sender bankgiro/IBAN before betalfil download (#1640)
* fix(salary): surface missing sender bankgiro/IBAN before betalfil download Users see a bankgiro under BANKUPPGIFTER in settings (Bolagsverket snapshot, display only) while the payment-file routes read company_settings.bankgiro, so the LB download failed with an error that pointed at a page that looked correct. 153 companies have a registry bankgiro but an empty settings field. - PaymentFilePanel warns up front when the sender bankgiro (bg_lb) or IBAN (pain001) is missing, linking to Installningar -> Fakturering - betalkonton form offers a one-click prefill of the bankgiro from companies.tic_snapshot (Luhn-validated, user still saves) - bg-lb and skattekonto payment-file error copy now names the exact place to fix instead of 'foretagsinstallningar' Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): harden bankgiro prefill and warning per skeptic review - bankgiroFromTicSnapshot now requires the snapshot's orgNumber to match companies.org_number before suggesting anything: stale fuzzy-matched snapshots can hold another entity's profile, and this field becomes the payee account on invoices and Peppol e-invoices - salary run page refetches settings when the URL returns from the intercepting settings modal, so a bankgiro/IBAN saved there clears the missing-sender warning instead of leaving it stale Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |