Commit Graph
85 Commits
Author SHA1 Message Date
f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers

Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n
namespaces and 4 unused dependencies; fold byte-identical helper copies
into one canonical home each (lib/utils chunk/sleep/utcDateStamp,
lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format,
lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body +
v1ValidationError rolled out to ~55 v1 routes, booking-template schemas).

No behaviour change: v1 bodies and status codes, MCP tool schemas, DB
writes and money math are untouched. Naive ore rounding was deliberately
not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list
of things left alone on purpose.

tsc, lint, 19588 unit tests and check:guards green; antipattern baseline
ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(transactions): import RawTransaction from @/types after the ingest re-export removal

CI's type ratchet (check:types, full tsconfig) caught the one test file
that still imported the type through lib/transactions/ingest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:16 +02:00
22f0647d6c feat(bookkeeping): make inline rattelse discoverable on the verifikat page (#1554) (#2011)
* feat(bookkeeping): make inline rättelse discoverable on the verifikat page (#1554)

A user who wanted Fortnox-style "stryk rader" went looking on the
verifikat page and concluded the feature did not exist: since #1739 every
correction action sits behind an icon-only ⋯ menu, nothing says which
correction track applies when, and the struck-line marker showed only a
date.

- Promote "Stryk rader i verifikatet" to a visible outline button for a
  posted, non-structural entry whose period the period-status endpoint
  reports as open; the ⋯ item stays so the menu remains the complete list.
- Add the convention-7 "?" after the H1 with the two-sentence track rule:
  inline rättelse while the period is open and unlocked, storno once it is
  locked, closed or declared.
- The rattelse-log route now returns an additive actor_label resolved
  from profiles via the service client (same precedent as
  behandlingshistorik); struck rows read "Struken {date} av {actor}" and
  the Rättelsehistorik rows carry the actor beside the date.

No change to the RPCs, the log table, or which corrections are legal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna

* fix(bookkeeping): address review findings on inline rättelse discoverability (#1554)

- Tie the un-awaited period-status fetch to the fetchData run that issued
  it (monotonic request ref), so an earlier response resolving last can no
  longer set periodStatus='open' for an entry in a locked period and promote
  the "Stryk rader" button the RPC would refuse.
- Align the "?" help copy with what the system enforces: storno is the only
  path once the period is locked or closed; a VAT-declared month is stated
  as a caveat (same wording as the StrikeLinesDialog explainer), not as a
  gate the product does not apply. Both sv and en.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 17:15:25 +02:00
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>
2026-08-26 15:02:26 +02:00
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>
2026-08-26 14:56:37 +02:00
MattssonandClaude Fable 5 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>
2026-08-24 15:06:30 +02:00
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>
2026-08-20 13:25:06 +02:00
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>
2026-08-20 12:29:43 +02:00
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>
2026-08-20 10:06:01 +02:00
MattssonandClaude Fable 5 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>
2026-08-17 12:02:43 +02:00
MattssonandClaude Fable 5 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>
2026-08-17 11:32:45 +02:00
c897a906df fix(ux): actions update lists in place - no more takeovers, jumps and dead air (#1629)
* fix(ux): update lists in place on actions instead of takeover spinners and jumps

Founder report: the app feels glitchy when clicking around, especially
when deleting a row or booking something. The repo-wide anti-pattern
behind it: single-row actions trigger whole-list skeleton/spinner
takeovers (layout collapse, scroll jump, full stagger-enter replay),
deletes give zero feedback then hard-jump, and the /transactions exit
"animation" was filter-only and never animated.

Per surface:

- Never take over a rendered list for a background refresh. The
  skeleton/spinner swap is now reserved for an empty (or foreign) list
  on /transactions (fetchTransactions), /pending (fetchOperations,
  covering both listed Granskning findings, one file), kundfakturor
  (fetchInvoices), leverantörsfakturor (fetchInvoices, plus
  try/catch/finally so a failed fetch can no longer stick the skeleton
  or masquerade as an empty register) and the verifikat list
  (JournalEntryList now takes a refreshToken prop and refetches in
  place; /bookkeeping no longer key-remounts it into a spinner, so
  expansion/selection/pagination/scroll survive a created verifikat).
  Quiet inline Loader2 cues near the list headers on /transactions and
  /pending signal a background reconcile.

- /transactions row exit: exiting rows (booked/ignored/deleted) stay
  rendered through the existing 350ms window with a real exit
  transition (.row-exit: fast fade, then the space closes by
  transitioning cell paddings/line metrics and a numeric max-height on
  the fixed-height cell spans) and pointer-events off. Instant removal
  under prefers-reduced-motion. Applied to the inbox cards, the
  skattekonto card and the history rows.

- /transactions delete: routes through processingId (row spinner) and
  the exitingIds path, and decrements totalUncategorizedCount when the
  deleted row was pending (the realtime echo is not guaranteed for
  DELETE on a filtered subscription).

- FyPicker double-fetch: the initial fetch now waits for FyPicker's
  onReady (fires after its persisted-scope restore), so mount does one
  correctly scoped fetch instead of racing an unscoped fetch against
  the restore refetch (list -> skeleton -> list on every visit). Period
  changes refetch background-only behind the client-filtered list.

- Pagination survives realtime echoes: background refreshes re-fetch
  range(0, pagedCountRef) instead of resetting to the first 200 rows,
  so "Visa fler" pages no longer collapse after any action.

Gates: full vitest suite green (14764 passed), tsc output byte-identical
to the origin/main baseline, eslint 0 errors on touched files,
check:guards green, package-lock untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): apply review round on action-feedback smoothness

- /pending: sequence-guard fetchOperations so a stale previous-tab
  response can't overwrite the current tab's rows, counts, or loading cues
- /pending: check res.ok on the pending fetch and both history fetches
  before applying payloads; failures keep current rows and surface the
  existing error toast
- /transactions: reset fiscal scope (fyReady/fyPeriodId/fyPeriod) during
  render on company switch so FyPicker re-runs its persisted restore and
  stale bounds never scope a fetch for the wrong company
- /transactions: drop a deleted row's id from selectedIds so the bulk bar
  can't act on a deleted row
- row exit: add the inert attribute on exiting row wrappers alongside
  pointer-events so keyboard focus and activation are blocked too
- JournalEntryList: preserve selection on refreshToken background
  refreshes (reconciled against the refreshed page); user-initiated
  reloads still clear 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>
2026-08-16 18:44:02 +02:00
MattssonandClaude Fable 5 4362bffc0c fix(skattekonto): deep-link Skapa verifikat manuellt to a prefilled, auto-linked verifikat (#1621)
* fix(skattekonto): deep-link Skapa verifikat manuellt to a prefilled, auto-linked verifikat

"Skapa verifikat manuellt" in the SkattekontoBookDialog routed to plain
/bookkeeping: the user landed on the list with no form, no prefill and no
link to the row (reported by a user for a Slutlig skatt event, which has
no booking rule by design).

The CTA now deep-links to /bookkeeping?skv_tx=... carrying the row's id,
date, text and amount. The bookkeeping page opens the Nytt verifikat
dialog prefilled (1630 on the correct side per the booking sign
convention, balanced counter line with the motkonto left to pick, date
and description set) and, once the verifikat is saved (posted or draft),
links it back to the skattekonto row via the existing match endpoint. A
failed link degrades to a destructive toast pointing at the manual
"Matcha mot verifikat" path.

The URL params are prefill convenience only: the match route re-validates
ownership, ALREADY_BOOKED and ENTRY_ALREADY_LINKED server-side. The
parse/build/line-shaping contract lives in core lib
(lib/skatteverket/manual-verifikat-prefill.ts, unit-tested) because the
bookkeeping page cannot import from the extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skattekonto): keep deep-link payload out of the URL + share the 1630 constant

Resolves the PR #1621 review findings in one pass:

- Compliance swarm (GDPR Art.5(1)(f), ISO A.8.12): the deep link no longer
  carries date, text and amount as query params, where they would persist
  in browser history, access logs and Referer headers. The row payload is
  staged in sessionStorage, consumed single-use and validated against the
  opaque skv_tx id, which is all the URL exposes. A missing or mismatched
  payload degrades to the plain /bookkeeping list; the auto-link itself is
  still validated server-side by the match route.
- Swedish accounting review note: SKATTEKONTO_ACCOUNT ('1630') is now
  imported by the extension's booking and match libs from the core prefill
  lib instead of being duplicated, so prefill and server-side booking
  cannot drift.
- CodeRabbit docstring warning: the new lib exports carry docstrings.

Storage is injectable (PrefillStorage) so the node-env tests cover the
round-trip, single-use semantics, id mismatch, malformed payloads and a
throwing privacy-mode storage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(skattekonto): record the sessionStorage staging window as accepted residual risk

The compliance swarm's remaining LOW finding (ISO A.8.12) offers
documentation as its remediation path: an XSS attacker already reads the
full ledger via the session's authenticated APIs, so the sub-second
sessionStorage staging window adds no capability worth a server-issued
token roundtrip. Recorded in the lib header and DECISIONS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 15:10:06 +02:00
9686b54b41 refactor(design): lock the border-radius ladder, one radius per role (#1607)
Seven radii were in circulation (4/5/6/8/12/16px + pill) with no rule for
which went where; one toolbar row on /transactions mixed four shape
languages. This locks a 4-tier ladder (design.md convention 16):

- pill: interactive toolbar controls (buttons, chips, pickers, segmented
  controls, toolbar search, count nubs)
- rounded-xl (12px): overlay tier: page panel, dialogs, slide-overs
- rounded-lg (8px): cards, form fields, popover/menu content, boxes
- rounded-sm (4px): nested leaves (menu items, checkboxes, kbd/code nubs)

Changes:
- New SegmentedControl primitive (pill-in-pill tablist, h-8) replaces the
  hand-rolled bg-muted/70 tablist copied across 11 files
- New ToolbarSearch primitive (pill, h-8) adopted on 9 page toolbars;
  dialog/picker searches keep the rounded-lg Input
- dialog.tsx 8px -> 12px, matching SettingsModal/slide-over/CommandPalette
- ContextPicker chips at the shared h-8 toolbar height
- ~300 rounded-md / bare rounded call sites remapped by role; auth icon
  tiles and the mobile nav sheet come down from 16px to 12px
- rounded-md, bare rounded, rounded-2xl and rounded-[Npx] are dead
  vocabulary, enforced by a new off-ladder-radius check in check:guards

Verified: lint 0 errors, 14422 unit tests pass, check:guards green, tsc
clean on all changed files, sandbox screenshots of transactions/
bookkeeping/granskning toolbars and the Ny verifikation dialog.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 08:55:37 +02:00
57d6651cfc feat(empty-states): startkort on six pages with strata imagery (#1603)
Replace the true-empty states on Kundfakturor, Transaktioner, Underlag,
Loner, Bokforing and Skattekonto with StartCard: a self-contained dark
hero (image-derived ground baked into the strata render, white primary
CTA) that says what the page can do instead of what is missing. Primary
CTAs lead with the connect/setup action per page (bank via PSD2 deep
link, mailboxes, Skatteverket, migration import); filtered/search empty
states and viewer fallbacks keep the old compact states. Design signed
off in the Startkort prototype iterations 2026-08-13.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 23:27:24 +02:00
MattssonandClaude Fable 5 05380ddf54 feat(bookkeeping): correction-chain depth guard + Bedrock stream retry (#1581)
* feat(bookkeeping): bypassable chain-depth guard on corrections and stornos

Correcting or reversing an entry that already sits 3+ links deep in a
rattelse chain (correction_of_id/reverses_id walked in the DB, never
description matching) now throws CORRECTION_CHAIN_TOO_DEEP, steering the
caller to book ONE correction expressing the chain's net effect. Agents
looped storno+rattelse 10 deep on a live company (63/193 vouchers noise).

The guard is advisory, never a dead end: allow_deep_chain bypasses it on
every surface (correctEntry/reverseEntry option, REST body, MCP tool arg
staged through pending_operations, and confirm dialogs with Ratta anda /
Aterfor anda in the web UI). MCP staging pre-flight fires the guard at
stage time so the agent reconsiders in the same turn, and the executor
re-checks at commit. tools/list payload ceiling bumped 59K -> 59.5K for
the two bypass properties (trimmed to one sentence first).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(agent): retry the Bedrock stream once on transient failures

A transient stream death (429/5xx, transport cut, or the two known
stream-corruption signatures: 'Unexpected event order' and 'request ended
without sending any chunks') killed the whole chat turn, stranding the
user mid-answer. The turn now retries once per turn after a short backoff:
safe because nothing is persisted until finalMessage() succeeds. A new
stream_restart event carries the pre-attempt text snapshot so the chat
client resets the partial bubble, drops uncompleted tool chips, and shows
'Forsoker igen...' until the retried stream produces text. Non-transient
errors (403, 400) keep the existing immediate-error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): regenerate accounted-api skill and wire allow_deep_chain through v1

apiskill:check failed: CorrectJournalEntrySchema gained allow_deep_chain,
making references/journal-entries.md stale. Regenerated (hand-applied: the
generator output is deterministic from the registry). While wiring: the v1
correct route validated allow_deep_chain but dropped it, and the v1 reverse
route's strict body schema would have rejected it outright, leaving API
clients no bypass when the chain-depth guard fires. Both now forward the
flag to the engine and document CORRECTION_CHAIN_TOO_DEEP as a pitfall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: re-trigger CI after Vercel infra hang

The preview for e527e4044 compiled in 91s then hung 40 minutes in the
TypeScript phase and was killed with no error output; a CLI redeploy of
the identical code went Ready in 5m. Empty commit to refresh the git-
triggered deployment status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bookkeeping): address CodeRabbit review on the chain-depth guard

- correction-chain: report rootVoucher only when the walk reached a
  genuine parentless root; a broken link, cycle, or hop-cap now yields
  null instead of presenting an intermediate voucher as the chain root.
- recordate: propagate allow_deep_chain end-to-end (recordateEntry
  option, route schema, and a Flytta anda bypass confirm in the dialog);
  a date move is another storno+rattelse layer and carried the guard
  with no override path.
- v1 correct/reverse: run the chain-depth guard before the dry-run
  return so a dry run gives the same verdict as the real execution.
- dashboard reverse route: 400 on malformed JSON or a non-boolean
  allow_deep_chain instead of silently reversing without the override;
  empty body stays the supported no-body case. Tests added.
- AgentChat stream_restart: discard the dead attempt's reasoning and
  re-arm the post-tool paragraph break so a retried turn doesn't render
  thinking twice or glue its continuation onto restored text.
- v1 reverse route doc comment updated for allow_deep_chain.

Not changed: the journal-list reverse flow (flagged as a dead end) can
never receive CORRECTION_CHAIN_TOO_DEEP: the list renders Aterfor only
for entries that are neither storno nor correction, and such entries
have no backward chain links, so their depth is always 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(bookkeeping): recordate route test expects the new options arg

recordateEntry now takes { allowDeepChain } as a sixth argument; the
route test's called-with assertion predates it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 19:32:41 +02:00
7881f757a9 feat(bookkeeping): show who committed a verifikat, and mark agent work in Granskning history (#1591)
Flows build plan prereq 3 (provenance display). Pure UI over columns that
have existed since migration 20260619120000:

- types: JournalEntry gains committed_actor_type/committed_actor_label
  (the detail/chain APIs already select('*'), the type just lacked them)
- voucher detail: new "Bokford av" row in the Details card, derived from
  actor type + credential label, with the Bot mark for non-user actors
- Granskning Historik rows get the same actor circle pending rows have
  (Bot vs ClipboardCheck) so agent-originated history reads at a glance
- run-turn: correct the staged_operation params comment (tool-use input
  is a superset of pending_operations.params, not the same values)

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:01:15 +02:00
e51a2c8102 refactor(design): no amber boxes, attention is one ochre sentence (#1562)
The founder wants the yellow boxes gone everywhere. The design system
already agreed: status colors are data, not chrome (convention 12) and
attention is a single ochre sentence, never a banner (convention 6).
This enforces it:

- ConfirmationDialog: the amber warning panel is now an AttnLine, and
  the hardcoded Swedish default warningText is gone: it injected an
  immutability warning into dialogs whose authors never asked for one
  (every current caller passes the prop explicitly, so no behavior
  change at any call site).
- Badge warning variant: amber fill replaced with a hairline chip and
  ochre text.
- DestructiveConfirmDialog warning variant: neutral icon disc, default
  primary confirm button (only --destructive survives as chrome).
- BankSyncStatusChip stale state: same neutral shape as the healthy
  chip, ochre text carries the signal.
- SandboxBanner: solid amber bar becomes secondary-on-border chrome.
- BankIdAuth, BankIdCompanyPicker, SessionTimeoutModal: the last three
  raw-amber (bg-amber-*) holdouts moved onto tokens, the company-picker
  banner becoming a plain AttnLine.
- Mechanical sweep of the ~58 hand-rolled bg-warning/border-warning
  boxes across 45 files: fills to bg-muted/30 (icon discs bg-muted),
  borders to border-border, text-warning-foreground to text-attn. The
  account-class dots in account-number.tsx keep bg-warning: they are
  data indicators, not chrome.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:20:16 +02:00
abff8d90f6 refactor(ui): validation as errors on attempt, help text behind the ? (#1561)
The founder flagged the app as bloated with standing instructional text.

Ny verifikation: the what's-missing lines (Ange en beskrivning, Minst
två rader...) rendered from the first frame because their only gate was
form validity, which an empty form fails: instructions dressed as
validation. The submit buttons now stay enabled and an attempt on an
incomplete form is what surfaces the lines, in destructive red, per the
error-on-submit idiom. The Enter-to-advance flow keeps the old
completeness predicate so navigation is untouched.

Matcha mot befintlig verifikation: the two-sentence explainer moved
behind a ? (HelpPopover, convention 7), the N:1 note tightened, the
Visa även matchade switch became a quiet link (switches are settings
idiom), and Stark träff, the normal auto-selected case, renders as
muted text instead of a chip (chips mark exceptions, convention 5).

Deleted always-visible paraphrase lines and their orphaned keys:
items_card_description, picker_description, references_subtitle,
sort_stack_hint, dimensions hints, document_help, and the dead
fill_balance_hint key that had no render site at all.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:20:00 +02:00
1e9f245f7c refactor(agent): keep the assistant in the nav, FAB and underlag flow only (#1557)
The founder wants the scattered per-page assistant buttons gone: the
assistant is reachable from the nav and the floating tab everywhere, so
in-page duplicates were noise. Removed the AgentSparkleButton call sites
(year-end, verifikat detail, supplier invoice detail, invoice editor)
and the now-orphaned component, the soft hand-off link in the Ny
verifikat modal (plus its i18n keys), and the transaction-row overflow
item. Kept: nav entry, floating tab, the Dokumentinkorg flow, and the
sanctioned "Skapa med assistent" split-button mode on /bookkeeping
(design.md convention 14).

The command palette's hand-off entries hardcoded the agent name "Anna";
they now use the identity from AgentSheetProvider like every other
affordance, and hide until agent onboarding is done (same gate as the
FAB).

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 11:19:45 +02:00
67febd5097 feat(ui): prev/next record navigation on detail pages (#1530)
* feat(ui): prev/next record navigation on detail pages

Customer feedback: stepping between invoices in a reskontra (56 -> 57 ->
58) required going back to the list for every record.

List pages now write their full ordered id array to sessionStorage when
a row is opened (Accounted:list-context:<scope>:<companyId>), and the
detail pages for kundfakturor, leverantorsfakturor, and verifikat show
a compact prev/next pager (chevrons + 'n av m') next to the back
control. ArrowLeft/ArrowRight step too, except while typing in a text
field or while a dialog is open. Navigation uses router.replace so
'tillbaka' returns to the list in one step. Deep links and new tabs
have no context: the pager hides and pages behave as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pager): overlay-aware arrow guard, notes-draft safety, context on Visa detaljer

Review fixes on the detail-record pager:

- The keyboard guard matched any mounted [role=dialog], so the agent
  sheet (which stays mounted display:none once opened) killed arrow
  paging for the rest of the tab session, while open dropdown menus
  did not block at all. The guard now mirrors the AgentSheet Esc
  selector (data-state="open" variants incl. alertdialog and radix
  menu/select/listbox content) and also yields while focus sits inside
  a dialog/menu/listbox container. Extracted as pure functions in
  lib/hooks/detail-pager-guards.ts so the rules are testable in the
  node test environment.
- Arrow keys could unmount the verifikat page and destroy an unsaved
  notes draft once the textarea lost focus. useDetailPager and
  DetailPager now take a keyboard flag, and the verifikat page
  disables keyboard paging while editingNotes is active; the chevron
  buttons stay live.
- The expanded-row Visa detaljer link in JournalEntryList navigated
  without writing the list context, producing stale pager snapshots;
  it now calls rememberListContext like the voucher link.
- ListContext.listPath was written and strictly validated but never
  consumed: removed from the interface, all writers, and the read
  validation. Reads stay tolerant of extra properties so contexts
  stored by older builds still parse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(pager): quiet wayfinding strip instead of buttons in the title cluster

The pager sat between the back arrow and the H1, which read as a toolbar
of three boxed buttons and made the title jump horizontally per record.
All three detail pages now share the verifikat page's pattern: a muted
back text-link on the left and the pager right-aligned on the same quiet
row. The pager itself drops to 16px glyphs, muted ink, and hides when the
list context holds a single record.

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>
2026-08-12 21:07:57 +02:00
5b0ca3d874 fix(copy): make K3 and year-end claims match what the code actually does (#1431)
* fix(copy): make K3, leasing and year-end claims match what the code does

Follow-up to the batch that removed the uppskjuten-skatt posting on
obeskattade reserver (K3 29.37 gross in juridisk person) and added the K2
asset-account gate. Six user-facing strings still described the old
behaviour or made claims the code cannot support.

1. Arsredovisning page: the K3 explainer promised an uppskjuten skatt-not
   and a materiella anlaggningstillgangar-not in every K3 document. Both
   are conditional (a 2240/8940 balance, assets in the register) and the
   first is now absent in the normal case. The kassaflodesanalys is
   dropped with a warning when it cannot be generated, so it is named
   only when the document actually carries one.

2. Regelverk settings: kassaflodesanalys was presented as following from
   K3. It follows from being ett storre foretag
   (swedish-year-end-closing/references/reporting-and-filing.md:10,
   legal-framework.md:42); the copy now says the product includes one and
   states the storre-foretag rule separately. Komponentavskrivning was
   presented as optional under K3; it is mandatory where component useful
   lives differ materially (k2-vs-k3.md:5, asset-accounting
   references/depreciation.md:33).

3. Note 1 and the Uppskjutna skatter-not no longer claim the 2240 balance
   is hanforlig till obeskattade reserver. deriveLatentTaxMovement reads
   the 2240/8940 balances only, and under K3 that account carries deferred
   tax on all temporary differences (k2-vs-k3.md:11-13).

4. The deferredTax 'unknown' branch emitted the gross-reserve statement,
   which is the denial phrased positively: the same affirmative claim
   about books that could not be read. It now emits no deferred-tax
   paragraph at all; build-data already warns on that path.

5. Capitalized-lease detection looked at 1260/1269 only. On the shipped
   BAS 2026 chart 1260 is a free inventarier account and 1269 is ack.
   avskrivningar pa datorer, so owned computers were reported as leased,
   while 1217/1227 (finansiellt leasade) were missed. Detection now reads
   the company's own account names in kontogrupp 12, which is where BAS
   keeps capitalized leases (leasing-and-disposal.md:28) and which owned
   inventarier on 1220 never matches. 1720 forutbetalda leasingavgifter
   stays out: that is the operational treatment.

6a. gnubok_year_end_readiness listed FX revaluation as a blocker (it is a
   warning) and omitted UNBOOKED_TRANSACTIONS, the common one. The
   description now names every actionable blocker kind, within the
   280-char budget, and a test pins it against YEAR_END_BLOCKER_KIND.

6b. companies.accounting_framework defaults to 'k2', so every enskild
   firma hit the K2 asset gate and was handed a BFNAR 2016:10 punkt 10.4
   citation plus a K3 remedy it cannot take: a sole trader prepares ett
   forenklat arsbokslut, not an arsredovisning (legal-framework.md:29,
   :48). entity_type now rides along on the companies read the routes
   already do, and non-AB entities get wording with no citation and no
   K3, keeping the 1090 remedy. The K1 counterpart of punkt 10.4 is not
   sourced in the repo skills, so nothing was invented in its place.

* fix(copy): close the review findings on the copy-truth sweep

Three follow-ups from the source and code reviews. (1) The K2/K3 help text had upgraded a vague sentence into a definite boundary claim ('gransen gar vid <trosklar>'), which excludes the other routes into mandatory K3 that are live right now for this control's audience: noterade vardepapper, and from fiscal years starting after 2025-12-31 also utlandsk filial, kryptotillgangar, aktierelaterade ersattningar and fastighetsbolag. An AB in one of those categories would have read the sentence and stayed on a regelverk it may no longer use. (2) hasCapitalizedLeaseAsset compared per-side cumulative totals, so a lease acquired earlier and disposed this year still claimed the balance sheet carries a leased asset; it now compares the net balance. (3) The K3 warning enumerated a kassaflodesanalys the document may not contain, contradicting the newly conditional page copy on the same screen.

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>
2026-08-06 11:54:57 +02:00
77d20287b9 fix(bokslut): unblur the pre-submission warning list on the årsredovisning page (#1415)
* fix(bokslut): revive preflight action links and unblur arsredovisning warnings

Two dead pieces of pre-flight UI wiring:

1. PreflightStep's BlockerRow matched blocker strings with English-only
   regexes (/draft journal entries/, /voucher gap/, /trial balance/,
   /continuity/), but validateYearEndReadiness emits Swedish (the wizard is
   a "stays Swedish" surface). None of the contextual action links ("Visa
   utkast", "Hantera nummerlucka", "Öppna balansrapport", "Granska ingående
   balans") ever rendered. Swedish alternates added, mirroring the MCP
   classifier, plus a link for the unbooked-transactions blocker.

2. The "Innan inlämning till Bolagsverket" warning list on the
   årsredovisning page was rendered inside the blurred/inert coming-soon
   wrapper, so in the default config (direct submission gated) users could
   not read their own pre-submission warnings while the draft-PDF button
   next to them stayed active. The warnings describe problems in the user's
   own report and apply to the paper PDF path too: they now stay sharp;
   only the digital-inlämning explainer remains blurred.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* revert: hand PreflightStep blocker links to #1420 (typed codes)

#1420 solves the same dead-link problem with stable blocker codes instead
of regex string matching, and correctly drops the /bookkeeping/voucher-gaps
branch: that path exists only as an API route (app/api/bookkeeping/
voucher-gaps), so this PR's regex fix would have made a link render that
navigates nowhere. Keeping only the unique arsredovisning warning-list
unblur here so the two PRs stop competing for the same file.

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>
2026-08-05 18:08:38 +02:00
MattssonandClaude Fable 5 f633349c8d feat(analytics): show form field labels in session replays (#1416)
* feat(analytics): show form field labels in session replays

Replays showed the sidebar after #1412 but form pages were still fully
masked, so you could not tell WHICH field a user was interacting with.
Tag the shared Label primitive (components/ui/label.tsx, used by every
form in the app) with data-ph-unmask: field labels are static i18n
chrome, and maskAllInputs keeps every typed value hidden.

The one Label whose text is user data, the user-defined dimension name
in LineDimensionFields, gets data-ph-mask, which wins even on the same
element because maskTextFn checks it first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(analytics): re-mask three Labels that render user data

Completing the audit the PR review asked for: a multiline sweep over
every Label child found three call sites whose label text is user
data, missed by the first single-line pass. Danger-zone confirm
labels interpolate the user's email (AccountDangerZone) and the
company name (CompanyDangerZone), and the periodisering auto-detect
row label is counterparty name + invoice number. All three now carry
data-ph-mask. Currency-code and row-count interpolations were
reviewed and left visible: categorical UI state, not books data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:18:07 +02:00
MattssonandClaude Fable 5 5ca64bde30 feat(bokslut): IL 18 kap pooled tax depreciation with method election (#1393)
* feat(bokslut): IL 18 kap pooled tax depreciation with method election

Rakenskapsenlig (huvudregel 30 / kompletteringsregel 20) and restvarde 25
as a company-level annual pool separate from per-asset book depreciation.
Method election persisted with immutable snapshots and book-conformity
confirmation for rakenskapsenlig.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(db): move tax depreciation migrations to coordinated versions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bokslut): keep tax depreciation view loadable when a saved election goes stale

A predecessor's changed closing value can push a saved elected deduction
above the new statutory maximum; the view now falls back to the statutory
recomputation so the snapshot is flagged stale instead of crashing.
Ratchet naive-ore-round baseline down by 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bokslut): resolve tax-depreciation period selects statically

The no-phantom-columns guard counts every select it cannot resolve
toward a hard ceiling, and the PERIOD_COLUMNS join pushed the repo
4 over (364 > 360). Inline the literal column list at the four call
sites so the guard verifies these columns instead of skipping them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bokslut): address review findings on tax depreciation election

- DepreciationPanel: gate the saving flag on a dedicated save sequence
  so a successful save (which refreshes the view and bumps the request
  version) no longer leaves the card permanently busy
- computeTaxDepreciation: refuse kompletteringsregel_20 with a positive
  basis and no acquisition cohorts instead of degenerating to a full
  write-off the cohort evidence does not support (IL 18 kap. 17 §)
- migration 227000: judge the asset-method guards on NEW.disposed_at so
  reversing a disposal cannot reactivate a grandfathered non-linear row
- migration 227200: require snapshot column completeness in the CHECK;
  SQL NULL semantics let partially populated snapshots pass the pure
  arithmetic comparisons
- depreciation route: use the string issue code 'custom' like the rest
  of the codebase instead of the Zod 3 compat enum

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:43:58 +02:00
Jakob WennbergandClaude Opus 5 108f348c84 refactor(motion): retime content entry, stop the double and one-frame animations (#1282)
The stagger is the standard content entry on every migrated page
(convention 11), so its budget matters more than any single surface. It ran
500ms per item in 80ms steps, putting the 10th row at 1220ms against a
300ms UI budget, and it only defined delays for children 1-10: on any list
longer than ten rows, children 11+ inherited delay 0 and arrived BEFORE the
middle of the list. Now 300ms per item in 40ms steps with the delay capped
at 360ms, so the tail lands at ~660ms instead of ~1220ms.

The cap is on the delay, never on the animation: the `both` fill holds a
child at opacity 0 until its delay elapses, so excluding rows 11+ from the
animation would paint them while rows 1-10 were still invisible.

Adds --ease-emphasized (the strong ease-out) rather than retiming
--ease-out, which is unlayered in :root and therefore shadows Tailwind's
own token: changing it would retime every ease-out utility in the app.

Foldout rows in JournalEntryList, periodiseringar and TransactionInboxCard
sit directly inside a staggered tbody, so expanding a verifikat fired the
inherited slideUp (with an invisible pre-roll of up to 320ms) on top of the
foldout's own transition. They opt out via data-no-stagger.

Also: the confirmed match on Hem faded at full height and then vanished in
one frame, jumping everything below it 52px; it now grid-collapses over
200ms with the gap inside the collapsing area. The assistant sheet slid
nothing while the page panel animated 300ms to make room for it; it now
arrives along the same edge on the same curve, gated to first mount so
re-expanding a collapsed session stays instant. Chat history no longer
replays 20 simultaneous 500ms page-entry slides on resume: only genuinely
new messages animate. Payroll wizard segments transition their colour.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:35:32 +02:00
Jakob WennbergandClaude Opus 5 bc5c673f7d fix(ui): reach touch-only actions, confirm before posting, drop a dead ring (#1280)
* fix(ui): reach touch-only actions, confirm before posting, drop a dead ring

Three defects from the UI craft audit where the interface is wrong, not
just inconsistent.

Unreachable on touch: 'Markera klar' on deadlines and the bulk-select
checkbox on /pending were 'opacity-0 group-hover:opacity-100'. Coarse
pointers never fire hover and DeadlineForm has no completion control, so
on a phone there was no way to mark a deadline done at all. Factored the
reveal into HOVER_REVEAL_CLASS, which adds pointer-coarse:opacity-100.

Unguarded ledger writes: 'Bokför' on the journal-entry detail page and on
the invoice detail page posted an immutable verifikat on one click, one
screen after the list confirmed the identical action. Both now open a
ConfirmDialog describing the outcome up front (convention 10), reusing
the list's indicative voucher-number prefetch.

Dead ring: SummaryCard emitted ring-1 ring-primary/40 and ring-1
ring-warning/40 on the same element, so the two set the same custom
property and one silently lost. The override is an exception, so it is a
Badge now (conventions 5 and 12).

Also corrects the design.md primitives table, which named ui/table.tsx as
the data-table primitive while 17 files use ui/dry-table.tsx; building a
list page from the documented row produced ~15% taller rows and a
different hover tint, which had already happened twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): let the post-confirm dialog close after a successful commit

ConfirmDialog calls onOpenChange(false) immediately after `await onConfirm()`
resolves, but handleCommit/handleBook clear their in-flight flag in a
`finally` block, so the guard's closure still saw isCommitting/isUpdating as
true and swallowed the close: the dialog would sit open over a booking that
had already succeeded.

The guard was redundant as well as wrong. ConfirmDialog already blocks
Radix-initiated closes while pending, via `onOpenChange={(next) => !pending
&& onOpenChange(next)}` on the Dialog itself. Passing the setter directly
matches every other dialog on both pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:33:36 +02:00
65c6d4c178 Fix/07 27 (#1271)
* fix(enable-banking): keep bank account mappings across reconnects and surface dead sessions

A PSD2 reconnect silently moved the user's ledger mapping. Account identity
came from the provider's account uid, which does not survive a
re-authorization at every ASPSP, and a fresh connect to an already-connected
bank mints a new bank_connections row regardless. Both paths looked like "an
account we have never seen", so the allocator handed out the next free 19xx
slot and a 1930/1940/1941 mapping came back as 1942-1946 on every consent
renewal, roughly quarterly per connection.

Match on the IBAN instead. resolvePsd2LedgerAccount() finds the existing
cash_accounts row by normalized IBAN before allocating, and upsertFromPsd2
promotes that row in place rather than inserting a second one, so it keeps its
id and its linked transactions and is re-pointed at the connection that just
authorized. The previous holder's connection status is deliberately ignored:
one IBAN is one physical account, and the old row often still reads 'active'
because the bank killed the session without telling us.

The allocator also stopped treating a 19xx number as free just because no
cash_accounts row holds it. A chart imported from SIE carries the company's
real bank accounts by name with no PSD2 row behind them, which is how a SEK
company account got proposed as an unrelated brokerage account. Overflow now
skips chart-occupied numbers, falling back only when nothing unnamed is left.

Dead connections kept rendering as "Aktiv": status only ever changed when a
transaction fetch failed, so a session killed bank-side stayed healthy-looking
with a stale last_synced_at while the user read old balances as current. Add
probeSessionHealth() and run it in the daily cron over every connection that
run did not prove alive, including the ones the loop skips silently
(capability gate, all accounts deselected) and the ones parked in
pending_selection that the cron never looked at. It acts only on a definite
dead answer; anything ambiguous leaves the row alone, since a wrong flip costs
a full BankID re-authorization. The all-accounts-deselected branch is
reclassified 'synced' to 'skipped' for the same reason: it never contacts the
bank, so it must not count as proof of life. The settings row warns when an
active connection has not synced in three days or has never synced.

Which company a connection belongs to was invisible. Everything was already
scoped to ctx.companyId, so there was no cross-tenant leak, but a bank
authorized while the wrong company was active looked identical to the right
one. Name the company on the connect surface and in the account picker, and
say where the connection went when the callback lands under a different active
company. Warn (bypassably) before authorizing a bank where the same user
already holds live connections in other companies: several ASPSPs allow one
active AIS session per login, so the new authorization can kill the others.

The history start date already defaulted to the fiscal-year start; the card
above it recommended a mid-year date and contradicted the selected option. It
now states the fact and offers the shortcut without presenting it as advice.

Not addressed: sharing one PSD2 session across companies. company_id is the
tenancy anchor on bank_connections and cash_accounts hangs off
(company_id, bank_connection_id), so that needs the session to become its own
entity. See DECISIONS.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(supplier-invoices): show the posted line description in the voucher preview

The "Verifikation som bokförs" preview built its expense debit lines with
description set to the raw account number, so the BESKRIVNING column showed
"5615" or "6990" where the posted verifikat actually says "Leverantörsfaktura
123, ACME AB". A hardcoded 11-entry ACCOUNT_LABELS map masked this for
2440/2641/26xx, which is why the column read as a mix of friendly labels and
bare account numbers, neither of which was the posted text.

The preview now renders exactly the line_description the engine writes: the
shared invoice-level text on expense lines and 2440, "Ingående moms {rate}%
{desc}" on 2641, and the reverse-charge pair taken straight from
generateReverseChargeLines instead of being re-derived locally.
buildSupplierDescription moves into its own dependency-free module so the
client-side preview can call it without pulling the journal engine (and its
Supabase server client) into the browser bundle. The account name stays
reachable on the AccountNumber hover card.

Picked option A from the issue, keeping the fixed invoice-level description
rather than propagating each item's own text: the customer-invoice side
already writes invoice-level descriptions, so per-item text would create an
inconsistency between the two invoice sides rather than remove one, and it
would need an aggregation-collision policy in the journal engine. Rationale
recorded in DECISIONS.md.

Refs #1258

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(bookkeeping): restore the copy icon on verifikat rows

The row-language rewrite in #1123 reused the copy icon's slot for the new
expand toggle, removing the zero-click copy affordance from the bookkeeping
list without mentioning it. The leftover orphaned copy_voucher_tooltip key
in both message files is what identifies it as collateral rather than a
product decision.

Restore a copy icon in the row's right-edge action cell, reusing that key
for aria-label and title. stopPropagation keeps the click off the row's
expand toggle. The icon is hover-revealed on md+ and always visible below
it: #1123 collapsed the desktop table and the mobile card into one
responsive table, so hover-only would leave touch users with nothing.

Copy is no longer gated on posted. The copy_from handler and the GET
journal-entries route never looked at status, so copying a draft already
worked end-to-end and only the detail-page button hid it; the two list
surfaces were already ungated. Both list affordances now respect canWrite,
which previously dropped read-only users into a dialog they could not
submit.

The repo does not render components in tests, which is why #1123 removed
this silently. Pin the source shape instead, the same way the copy-invoice
query is pinned.

Closes #1266

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(transactions): revalidate stale invoice match pointers before offering a match

potential_invoice_id / potential_supplier_invoice_id are written once, at bank
import, and never revisited. When one of several identical recurring invoices
was settled by a different transaction, every other transaction kept pointing
at the now fully paid invoice. The match dialog then measured the bank amount
against a 0 kr remaining balance and reported a "Beloppen skiljer sig ...
fakturan blir delbetald" partial payment, and the worklist offered the same
dead suggestion as a one-click confirm row.

Worse, the manual escape hatch was hidden exactly when it was needed:
TransactionInboxCard only shows "Matcha mot leverantörsfaktura" when no
suggestion exists, so a stale pointer left the user with no way at all to
reach the correct invoice.

Fixed by revalidating at read time rather than by clearing sibling pointers on
settle. Invoices are settled through many paths (both match routes, mark-paid,
MCP, bank reconciliation, SIE import), so write-time cleanup leaks the moment
one is missed, while the candidate lookup covers every route into the list.
The shared accept-lists in lib/invoices/matchable-statuses.ts mirror the CAS
guards the match routes already enforce.

  - listSuggestedMatches and the transactions page candidate fetch filter on
    status + remaining_amount, so a settled candidate yields no suggestion and
    the manual picker reappears on its own.
  - InvoiceMatchDialog blocks a settled target with a distinct message and a
    disabled confirm. Not advisory: both routes reject it outright with
    MATCH_INVOICE_ALREADY_PAID / MATCH_SI_ALREADY_PAID, so no override could
    succeed.
  - The supplier detail card now shows remaining_amount like the customer
    branch, instead of total. On a partially paid invoice it used to print
    "1 250 kr" directly beside "Differens: 1 250 kr".
  - match-supplier-invoice clears potential_supplier_invoice_id on the
    transaction it just matched, mirroring the customer route.

No bookkeeping was ever at risk: both routes already refused a settled target
before creating a voucher. The damage was confined to a misleading dialog and
a dead end.

createQueuedMockSupabase gains passive call recording (calls / findCall /
findCalls) because the proxy swallowed filter and update arguments, which made
the new assertions inexpressible.

Refs #1259, #1260

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(webhooks): dispatch on emit instead of waiting for the next cron tick (#1256)

* feat(webhooks): dispatch on emit instead of waiting for the next cron tick

The webhook dispatcher ran only on a per-minute cron, so the floor on
delivery latency was up to 60 seconds plus the request. An external consumer
that wanted to react as a transaction landed had only one alternative:
polling /api/events, which the 100 rpm per-key limit makes expensive and
which still cannot beat the tick interval.

Schedules one dispatch cycle as soon as deliveries are enqueued. The cron is
unchanged and remains the retry and sweep path; this only moves the first
attempt forward. Wired into the event-bus fanout plus the two routes that
enqueue a delivery directly: the :test verb, whose entire purpose is telling
someone whether their receiver works, and the manual delivery retry.

Three properties are load-bearing and covered by tests. The kick is never
awaited, because eventBus.emit is awaited at ~99 call sites including
journal_entry.committed and each delivery can burn a 10 s receiver timeout.
It coalesces per function instance, so a bulk booking that emits once per row
does not schedule one claim round trip per row. It claims 5 rows rather than
the cron's 50, because it runs on the tail of a user-facing request.

Double delivery is not a risk: claim_due_webhook_deliveries already claims
FOR UPDATE SKIP LOCKED and flips rows to in_flight in the same statement, so
a kick racing the cron sees disjoint rows.

Does not close #1201, which asks for a realtime stream for API consumers.
This is the cheap half.

Refs #1201

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(webhooks): stop claiming the kick makes double delivery impossible

Adversarial review of the previous commit caught an overstatement in its own
comments. SKIP LOCKED keeps a kick and the cron from claiming the same row at
the same moment, but claim_due_webhook_deliveries autocommits before any POST
is issued, so from then on ownership is only status='in_flight' and a later
cycle's recoverStuckInFlight sweep can re-arm a row still queued behind an
earlier cycle's serial loop.

Delivery is at-least-once, which is what the public docs already tell
receivers ("the same delivery id may arrive more than once ... idempotency is
on you"). The comments contradicted that.

No behaviour change. The kick does not create this window: the cron claims 50
rows serially against the same 20 s stuck threshold, which is wider than what
a batch of 5 can open.

Refs #1201

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(bokslut): add bokslut-flow depreciation (78xx) back to the bolagsskatt base (#1253)

* fix(bokslut): add bokslut-flow depreciation (78xx) back to the bolagsskatt base

sumPostedYearEndDispositions reconstructs resultat fore skatt for the tax
calculation, because generateIncomeStatement excludes every
source_type='year_end' entry. It summed class 88 and 7533 but not 78xx, so
planenlig avskrivning posted by the bokslut flow
(lib/bokslut/assets/depreciation-engine.ts) was dropped from the income
statement and never added back. The bolagsskatt base and the
periodiseringsfond 25 % cap were therefore computed on an overstated result:
tax too high by roughly 20.6 % of the depreciation.

Also exclude the period's final bokslutsverifikation from the fetch. It
carries source_type='year_end' as well and reverses every P&L account,
78xx/88xx/7533 included (verified against production closing entries), so
once the year is closed it would cancel the add-back this function exists to
produce. That hazard already applied to 88xx and 7533; the fix closes it for
all three rather than widening it.

Scope is deliberately the tax base only. Making the standalone
resultatrakning show bokslut entries is a separate, larger change: the same
exclusion is duplicated in the kpi_report_aggregates RPC, it moves displayed
profit for every company that ran the bokslut flow, and it means removing
the add-back at four call sites.

Refs #1051

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(bokslut): scope the closing-entry lookup to the company and fail loudly

Review (CodeRabbit + the compliance swarm, ASVS V8.2.1) flagged the new
fiscal_periods read in sumPostedYearEndDispositions on two counts, both fair.

It filtered only on the period id while every sibling query in the same
function carries the tenant scope. Primary key or not, service-role paths
have no RLS to fall back on and the repo's rule is to filter company_id
explicitly, so it now does.

It also discarded the query error. That mattered more than it looks: a failed
read fell through to closingEntryId = null, which silently re-admits the
closing verifikat's 78xx/88xx reversals and understates the tax base, i.e.
exactly the failure this lookup was added to prevent. It now throws, and the
surrounding catch turns it into the existing 'Failed to read posted
dispositions' error. A wrong bolagsskatt is worse than a loud failure.

Two regression tests: the lookup carries both eq filters, and a lookup
failure propagates instead of degrading to a wrong number.

Refs #1051

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(storage): drop the client-side DELETE policy on the documents bucket (#1254)

* fix(storage): drop the client-side DELETE policy on the documents bucket

20240101000024 documents this bucket as WORM: "No UPDATE or DELETE policies".
That described the repo, not production. Production carries a
users_delete_own_documents policy that exists in no migration file:

  FOR DELETE TO authenticated
  USING (bucket_id = 'documents'
         AND (storage.foldername(name))[2] = auth.uid()::text)

Under it, the uploading user can delete the storage bytes of any document
they uploaded under the legacy documents/{userId}/... layout, using nothing
but their normal browser token. That includes documents linked to a posted
verifikat, which are rakenskapsinformation under the BFL 7 kap 2 § seven-year
retention duty. deleteDocument()'s linked-check and the
block_document_deletion() trigger both guard the document_attachments ROW,
not the object: the row survives, still pointing at a file that is gone.

Reproduced against a local replay of the full migration stream: with the
policy present the uploader's own DELETE removes the object; with it dropped
the same statement matches zero rows. Company-scoped keys were never exposed
(their second path segment is the company id, not auth.uid()), so this only
ever reached the legacy layout, which is where most documents still live.

Safe because every in-app remove() on this bucket already runs on the service
role, covered by service_role_all_documents.

Deliberately narrow: users_read_own_documents and users_upload_own_documents
stay. The Phase B backfill from 20260726092000 has not run, so dropping the
legacy SELECT policy now would make existing documents unreadable. That is
Phase C.

The pg-real test asserts no DELETE and no UPDATE policy over the bucket under
ANY name: the hole arrived under a name this repo never used, so pinning a
name would not have caught it.

Refs #1208

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(storage): make the WORM ratchet see FOR ALL and WITH CHECK policies

Review caught two blind spots in the ratchet, both fair. It matched only
polcmd 'd' and 'w', but polcmd '*' (FOR ALL) grants DELETE and UPDATE just as
effectively, and FOR ALL is the shape the one legitimate policy on this table
already uses, so a hostile one would look unremarkable in the catalogue. It
also read only polqual, so an UPDATE policy carrying its bucket restriction in
WITH CHECK was invisible.

Both assertions now run through one helper that covers d/w/*, concatenates
USING and WITH CHECK, and filters by grantee so service_role_all_documents
(how the application does its authorized deletes) is excluded while every
client-reachable role is not. A policy granted to PUBLIC has an empty
polroles, which is the most permissive case there is, so it is treated as
client-reachable rather than as "no roles".

Matching on the substring rather than the exact `bucket_id = 'documents'`
shape pg_get_expr emits today: a policy written as bucket_id::text or with the
comparison reversed would slip past a stricter match, and for a WORM ratchet a
false alarm is cheap while a silent hole is not.

Adds a probe case that creates a FOR ALL policy and asserts the helper sees
it, so the main assertion cannot pass vacuously. That case earned its keep
immediately: it caught that node-postgres hands back a raw string for a name[]
column, so the role filter needed rolname::text to work at all.

Verified against a local replay of the full migration stream: red with the
original prod FOR DELETE policy present, red with a FOR ALL probe, green
without either. Full pg-real suite 933 passed.

Refs #1208

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(storage): catch a destructive policy that names no bucket at all

Adversarial review of the previous commit found the ratchet still failed
open, and reproduced it: a policy with no bucket_id predicate covers EVERY
bucket, documents included, so gating on the bucket name discarded exactly
the widest hole. The concrete shape is Supabase's own stock "Enable delete for
users based on user_id" template, USING (auth.uid() = owner), which is the
single most likely form of a future dashboard edit.

A destructive policy is now in scope unless it provably cannot reach this
bucket, i.e. only a bucket_id predicate naming some other bucket exempts it.

The behavioural assertions had the matching blind spot: fixtures were seeded
without an owner, so an owner-based policy matched NULL and the DELETE
reported 0 rows for the wrong reason. Objects now carry an owner the way
storage-api stamps them in production, so those tests fail loudly instead of
passing by accident.

Two probes pin both directions: a bucketless policy must be reported (and is
shown to really permit the delete), and a policy scoped to another bucket must
not be, so the ratchet cannot start crying wolf on receipts or sie-files and
get switched off.

Verified against a local replay of the full migration stream: red with the
stock bucketless template installed, green without it. Full pg-real suite 935
passed.

Refs #1208

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(kontoplan): make a deactivated account reachable again (#1262)

is_active=false read as "does not exist" on every read path but as "exists"
on the (company_id, account_number) unique constraint, so a deactivated
account vanished from the kontoplan with no way back and re-creating it
answered "Kontonummer X finns redan i din kontoplan."

The write side was already correct: POST /accounts/activate has a
toReactivate branch and PUT /accounts/[number] accepts is_active:true.
Both were simply unreachable, so this opens routes to them rather than
relaxing the read filters, which are load-bearing for
AccountsNotInChartError.

- Kontoplan gets a "Visa inaktiva" filter; inactive rows carry an "Inaktiv"
  chip and the existing per-row switch reactivates them in one click.
- Deactivating an account that has posted lines now warns first, using the
  usage count already loaded for the Verifikat column.
- POST /accounts distinguishes the two collisions and returns the new
  ACCOUNT_EXISTS_INACTIVE code; AddAccountDialog offers "Aktivera kontot
  istallet" rather than a dead-end 409. The stored account is left exactly
  as it was; values typed into the failed create form are not applied.
- bas-lookup consults the company's own chart before the static BAS
  reference, so a deactivated custom account reads as known and
  "Aktivera och bokfor" is no longer disabled for it. New in_chart /
  is_active fields let callers tell "will be added" from "will be revived".
- BAS-katalog stops showing "Aktiverat" for an account the company holds
  but has deactivated; it falls through to a relabelled Aktivera button,
  and the per-class counts follow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(supplier-invoices): flag foreign 0 % lines with reverse charge switched off (#1255)

* fix(supplier-invoices): flag foreign 0 % lines with reverse charge switched off

A foreign supplier charging no Swedish VAT is normally omvand
skattskyldighet. With the reverse-charge switch off,
createSupplierInvoiceRegistrationEntry emits neither the 26x4 output leg nor
the 44xx/45xx basis lines, so ruta 20-24, 30-32 and 48 all stay empty and the
momsdeklaration takes a shape Skatteverket rejects. For a fully deductible
purchase the net moms att betala is unchanged, which is exactly why this goes
unnoticed. The form already auto-ticks reverse charge for eu_business but not
for non_eu_business, so that path slips through silently.

Adds a pure helper plus a non-blocking banner cloned from the existing
rc_account_warning block. Deliberately silent for swedish_business, where 0 %
is a genuine exemption that belongs in no ruta at all, and phrased as a
question rather than an assertion: a non-EU goods purchase cleared at customs
is legitimately 0 % without reverse charge, and pushing that user into
ticking the switch would manufacture a new wrong verifikat.

Does not add the exempt/import/other picker the issue proposes:
supplier_invoices.vat_treatment is metadata that no booking or ruta mapping
reads, and the codebase cannot book import VAT at all, so an import option
would imply ruta 50/60 were handled when they are not.

Refs #1042

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(supplier-invoices): name the local-VAT case in the foreign 0 % hint

Review flagged that the most common foreign document a Swedish small company
sees is an invoice carrying the supplier's OWN local VAT, booked at 0 %
Swedish VAT with reverse charge correctly off. The banner fires there, and
the previous copy only offered "momsfri av annat skal, till exempel en
varuimport" as the way out, which does not describe that invoice at all: it
is not VAT-free, it carries foreign VAT.

Names both legitimate cases explicitly and says 0 % is correct in them, so
the hint cannot read as an instruction to tick reverse charge on a purchase
where that would produce a wrong verifikat. Title also narrowed to "utan
svensk moms" for the same reason.

Refs #1042

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sandbox): call the sandbox assistant Assistenten, not Anna (#1244)

A named persona earns its name once someone has been through onboarding and
chosen it: it is their assistant and they named it. Nobody in the sandbox chose
anything, so a first name reads as a character the product invented and implies
a relationship the visitor never opted into.

Both halves move together, which is the point. profile_summary is the agent's
own self-description inside the system prompt, so leaving it as "Du är Anna"
would have the header say one thing while the assistant introduces itself as
another in its first sentence. Nothing else in the stack checks that pairing,
so a test now does.

Scope: this changes the seed, so new sandbox companies get the new name. The
483 sandbox profiles already seeded keep 'Anna' (the seeder returns early once a
profile exists, and its caller only runs while verified_at is null). Backfilling
those is a production write on demo data and is being raised separately rather
than smuggled into a code change.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(reports): show the last posted voucher per series in report headers

Adds a "Senaste bokforda verifikat: A 214, B 37" line to the balans- and
resultatrapport, so a printed or exported report answers which vouchers are
actually in it rather than only which dates it spans (#1267).

Reads MAX(voucher_number) over posted entries, never
voucher_sequences.last_number. The sequence counter is an allocation
high-water mark that drifts from the books in both directions:
next_voucher_number burns a number when the follow-up insert fails,
delete_last_voucher decrements by one instead of resetting to the new MAX,
and pre-RPC SIE imports left it behind. Since the point of the line is
avstamning, an allocated number would send a reconciler chasing a gap that
does not exist, so the label says plainly that the number is the posted one.

Scoped to the report own date range, so a Q1 report printed in November says
something true about Q1. The balansrapport keeps the fiscal-year start as its
lower bound because it accumulates. Skipped on a dimension-filtered
resultatrapport: that report already discloses it is partial, and an
unfiltered voucher range beside a filtered result invites the wrong
conclusion.

Populated in both engines, so the JSON, PDF and XLSX routes all inherit it
without signature changes. Best-effort: a header nicety never breaks a
report. The pure formatter lives in its own module so the client view does
not pull the Supabase query path into the browser bundle. No new i18n keys;
both report views and the PDF template are hard-coded Swedish per the
"stays Swedish" report surfaces in .claude/rules/i18n.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(customers): stop rendering personnummer ciphertext, make unreadable rows editable, add a reveal path (#1263)

customers.personal_number holds AES-256-GCM ciphertext (20260726110000).
Three defects compounded into one broken surface for private customers.

The list queried Supabase from the browser with select('*') and rendered the
raw value, 76-82 chars of hex, into the nowrap identifier cell. It now reads
GET /api/customers, which already masks every row, so the ciphertext never
leaves the server. Searching by personnummer works again: the client filter
had been matching against ciphertext and could never hit.

A row whose value cannot be decrypted renders as the placeholder
'********-????'. None of the three mask checks recognised it, each having its
own '-1234'-only copy, so such a customer could not be edited in ANY field:
name and address edits 400'd on a personnummer the user had no way to
correct. All three now share one pattern from the new crypto-free
lib/customers/mask-personal-number.ts, which the client form can import.
Typing a fresh personnummer overwrites the unreadable value, which is the
only repair possible: the rejected writes failed whole INSERTs, so there is
nothing to backfill.

The value was write-only by construction. GET
/api/customers/{id}/personal-number is the deliberate drill-in, mirroring the
employee convention, gated on the write role because .compliance/ropa.yaml
listed no_full_value_read_endpoint as a safeguard for this column; that entry
is rewritten rather than left stale, and reveals log actor and customer id
but never the value.

Also: arcim-migration wrote the identity number as plaintext, which aborts
any import containing a Privatperson with 23514 since the constraint flip;
and the customer embeds on /api/invoices shipped ciphertext to the browser on
every invoice read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: enhance ruta 05 handling for dynamic revenue accounts

- Introduced `fetchDynamicRuta05Accounts` to fetch company-specific revenue accounts marked with a VAT rate, addressing issue #1261.
- Updated VAT declaration logic to include these dynamic accounts in ruta 05 calculations, ensuring accurate reporting for user-added accounts.
- Modified `ACCOUNT_RUTA` to include account 3000 for completeness in ruta 05.
- Enhanced tests to validate the inclusion of user-added revenue accounts in ruta 05 and ensure correct VAT calculations.
- Seeded default VAT rates for BAS revenue accounts to ensure proper classification in the VAT declaration.

* fix: enhance data handling and masking in customer and invoice APIs

* fix(vat): resolve the 3000 gruppkonto's rate for the ruta 05 base split

3000 "Forsaljning inom Sverige" is mapped to ruta05 by ACCOUNT_RUTA, so a
balance on it is filed in the right box already. What was missing is the
rate split: unlike 3001/3002/3003 the account number carries no sats, and
fetchDynamicRuta05Accounts skipped it because it is in ACCOUNT_TO_BOX. A
company posting to the gruppkonto therefore got a ruta 05 total that
breakdown.invoices.base25/12/6 did not add up to.

Surface those rates separately as staticRateByAccount: rate-only on
purpose, because the static map already sums the account and adding it to
the dynamic account list would double the filed figure. A test pins that
single-count property.

Also add 3000 to the MCP server's RUTA_05_ACCOUNTS, which is the display
list behind report.rutor.ruta05: without it a 3000 balance appeared in the
filed projection but not in the report the agent reads back.

The comment claiming SALES_OUTPUT_VAT_SHORTFALL reads base25/12/6 was
wrong and is corrected. That check derives its expected base from the
output-VAT rutor (ruta10/0.25 + ruta11/0.12 + ruta12/0.06); nothing reads
the per-rate bases, which are reporting metadata. So the incomplete split
never affected a filed return or a warning, only the breakdown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com>
2026-07-28 19:50:16 +02:00
Jakob WennbergandClaude Fable 5 98e1a48c2b feat(bokslut): the bokslut trio in the flat concept language (scenes 34-36) (#1161)
* feat(bokslut): Arsbokslut hub as Stegen with de-boxed linear steps (trio 1/3)

Scene 34: the six-step wizard gets the house horizontal stepper (done
checks behind the current step, click-back navigation, forward stays
with each step's continue action), the period select moves into the
header as the context control, and the progress-bar and picker cards
die. Preflight, Preview, Execute and Result are de-boxed: sans eyebrow
sections with hairlines, quiet action links, attn lines instead of
boxed alerts, muted text for normal states. Step content is centered
at reading width. Accruals/Dispositions keep their internals for now
(interactive panels; follow-up pass). All wizard logic untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bokslut): INK2 and NE-bilaga views in the flat house language (trio 2/3)

Scene 36 direction: the declaration views lose every card. Statutory
sections (Tillgangar, Eget kapital och skulder, Resultatrakning, INK2S,
Intakter, Kostnader) become sans eyebrow sections with hairlines, the
header card becomes a flat block with the SRU download beside it, the
filing instructions lose their info box, warnings render as attn lines,
and the NE R11 result becomes the emphasized document-foot row. All
ruta tables, SRU downloads and warnings logic untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bokslut): Arsredovisning page, studio and digital filing in the flat house language (trio 3/3)

De-boxes the whole scene-35 surface: the page shell (period picker,
K3 note, narrativ, flerarsoversikt, underskrifter, PDF section), the
AnnualReportStudio (workflow strip, scope form, completeness checks,
versions) and DigitalInlamning (iXBRL review, submission form,
status history) all move from Card shells to sans-eyebrow sections
with hairlines. Inner info boxes flatten to bordered text blocks;
the digital-checks warning becomes an attn line. The iframe border
and the Kommer snart overlay sign stay: one frames an external
document, the other floats above blurred content.

Statutory Swedish-only surface: no new i18n keys.

* polish(bokslut): align trio controls with the house language

Founder feedback: buttons and selects still read old-style next to the
bookkeeping page. Sweeps all trio surfaces:

- Drop every min-h-11 override on Buttons and Inputs (44px chunky
  controls) so the compact house pill and h-10 input apply.
- Replace all native <select> elements (BooleanQuestion, currency,
  group size, signing method, signer role, versions, AGM outcome,
  roll) with the shadcn Select primitive used system-wide.
- Step navigation matches the merged Moms Stegen: white outline
  size-sm pills labelled 'Nästa: <steg> →' and '← Tillbaka'. Booking
  commits (Verkställ, Bokför valda dispositioner) stay primary.
- Year-end period picker becomes the ContextPicker chip (the house
  context-picker idiom), and the stepper row widens to max-w-4xl so
  step 6 'Klart' no longer clips.
- font-sans on two uppercase h3 eyebrows that rendered serif via the
  global h1-h3 display rule.

* fix(vat): one toolbar row and the FyPicker chip on every fiscal-year surface

The prod momsdeklaration (helårsmoms) broke into two sparse rows:
Exportera alone, then a labelled 280px FiscalYearSelector below it,
plus a serif uppercase worklist heading and internal check codes in
the UI.

- The VAT toolbar is one flat row: periodicity picker, fiscal-year
  chip, black Exportera, all h-9 aligned.
- FyPicker (the rounded ContextPicker chip from UI-migration PR 3)
  replaces FiscalYearSelector on every page-level surface it had
  left: the VAT toolbar, FocusedReport's header (all report detail
  pages) and Kassaflödesanalys. Dialog/settings forms keep the
  labelled select, which is a form field, not a context picker.
- 'Verifikationer som saknar basbelopp' becomes a sans eyebrow with
  hairline; internal codes (RC_BASIS_MISSING et al) no longer render
  in check rows, the Swedish message already cites the SKV felkod.
- font-sans on the SkatteverketPanel validation eyebrow.

* polish(vat): periodicity, year and quarter/month pickers as ContextPicker chips

Founder feedback: the Arsvis dropdown should also be the rounded
style. The cadence choice now lives behind a settings-style 'Period'
chip (Manadsvis/Kvartalsvis/Arsvis with a check on the active one),
and the year and quarter/month selects become chips too, so the
whole momsdeklaration toolbar is chip-shaped: Period, 2026,
Kvartal 3 (jul-sep) or Rakenskapsar 2026, then the black Exportera
pill. The concrete period chip already implies the cadence, so the
Period chip can stay label-only.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:20:00 +02:00
Jakob WennbergandClaude Fable 5 2bec5acedb feat(ui): rest-of-nav 1 — Viktiga datum, Skattekonto, Periodiseringar, Import to the concept language (#1140)
* feat(ui): concept scenes 17/24/32/33 for Viktiga datum, Skattekonto, Periodiseringar and Import (rest-of-nav 1)

Viktiga datum: thread rows with type-icon circles behind a type seg
(Alla/Skatt/Fakturering/Egna), Narmast countdown pane, Ny deadline lifted
to the page header, both banners replaced by one AttnLine, mark-done via
ConfirmDialog. DeadlineCard/DeadlineFilters die; DeadlineRow is the row.

Skattekonto: card-less saldo hero with OCR + quiet copy, shortfall AttnLine
computed from the next drain date with a betalningsuppgifter dialog
(bankgiro 5050-1055 + OCR), one dry-table with Kommande/Forfallna/
Genomforda band rows, chips only on unbooked genomforda rows, quiet
hover actions. Tabs and per-row badge noise are gone; the concept's
Saldo column is dropped because SKV stores no per-row running balance.

Periodiseringar: banner becomes an AttnLine with inline Bokfor forfallna
(now confirm-first), house seg with Aktiva count, dry-table with muted
normal states and animated RowFoldout for installments, Los upp nu as a
quiet hover link through the shared ConfirmDialog.

Import: tabs collapse into one two-column row list (Importera | Exportera)
in the concept row language; SIE export moves into a small dialog and
Molnsynkronisering folds the CloudBackupCard open in place. The
/import?view=export#sie-export and /import#cloud-backup deep links keep
working. Sandbox notice is an AttnLine.

All four pages get stagger-enter, a help popover behind ? and sv+en keys
for every new string. 9189 tests green, lint clean, guards pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(ui): align dashboard loading skeletons with the migrated page silhouettes

Folds in the parallel WIP from this checkout at the founder's request:
every loading.tsx under (dashboard) now mirrors its migrated page
row-for-row (24px title block, pill actions, borderless table heads,
single-line rows), and the shared (dashboard)/loading.tsx takes Hem's
greeting + Att gora silhouette. Also lands the pending DECISIONS.md
lines (onboarding swap plan note + rest-of-nav 1 deviations).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* polish(ui): authority logos, stat-tile skattekonto hero, quieter type on rest-of-nav 1

Viktiga datum: statutory deadlines wear the receiving authority's mark
(Skatteverket for tax dates, Bolagsverket for arsredovisning/arsstamma)
as a small badge on white; other deadlines keep the neutral type icons.
Row dates go muted, titles drop font-medium, the Narmast countdown
steps down to the house text-4xl display scale.

Skattekonto: the 32px serif hero becomes two compact metric tiles in
the KPIHeroCards idiom (saldo with the Skatteverket mark + OCR meta,
nasta dragning with date and event count), matching how numbers read
on the migrated pages.

Periodiseringar: chevron column dropped; rows expand on click exactly
like the verifikat list.

Import: provider logo chips return on Hamta fran annat system (live-
version parity) and Koppla bank carries the Enable Banking mark.

Adds skatteverket(_color), bolagsverket, enable-banking plus claude/
anthropic marks (for future use) under public/logos/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* polish(ui): keep Importera and Exportera as separate tabs on the import page

Founder feedback: the merged two-column landing goes back to the
familiar split. The house seg switches between the Importera rows and
the Exportera rows (SIE 4 dialog + Molnsynkronisering fold), ?view=export
selects the export tab again and the hash deep links flip to it before
opening their surface. Row language and logo chips stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): readable Enable Banking mark and full-width skattekonto

The enable-banking.webp is the full stacked logo in white-on-transparent:
invisible on the light chip and mush at 16px. The chip now uses a cropped
368px icon square (enable-banking-icon.png) with the marketing site's
grayscale+brightness treatment in light mode and a white lift in dark.

Skattekonto loses its max-w-3xl cap so the table stretches the content
column exactly like Bokforing and Transaktioner; the saldo tiles take
KPI-card width (lg:grid-cols-4). Import's tab columns stretch too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): address review-bot findings on rest-of-nav 1

CodeRabbit triage, all three confirmed real: the Bokfor forfallna attn
action is hidden for read-only users instead of rendering a no-op link;
a failed deadline edit rethrows so the form stays open with the user's
input; authority marks are reserved for statutory (system-generated)
deadlines: a manual tax-category deadline keeps the neutral icon.

Compliance swarm's two high findings verified clean, no change needed:
/api/bookkeeping/accruals/:id/dissolve and /api/deadlines/:id (+ /complete)
all run through withRouteContext with company_id scoping and 404 on miss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:36:08 +02:00
MattssonandClaude Fable 5 d840257c0c Add/stripe connect transactions (#1139)
* fix(mcp-oauth): allow ChatGPT connector callbacks and resume OAuth after login

Add chatgpt.com/connector/oauth/* (per-instance) and the legacy
chatgpt.com/connector_platform_oauth_redirect to the built-in OAuth
redirect allowlist so ChatGPT MCP connectors can register and authorize.

Fix the login page dropping the ?next= destination: an OAuth-initiated
visit that required login previously ended on the dashboard and the
connection flow silently died. Login now resumes to the sanitized next
path (hard navigation, since the consent page is route-handler HTML),
carries it through the MFA step-up as returnTo, and /mfa/verify
hard-navigates for /api/ destinations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): dedup incoming feed rows against booked hand-entered twins

Users who bookkeep via MCP/chat first and connect their bank afterwards got
the same movement twice: the synced row's external_id lives in a different
namespace, the free-form manual title never text-bridges the bank's raw
string, and the cross-channel mirror deliberately excluded manual/mcp rows.

Extend the mirror with a booked-hand-entered track: an incoming feed row is
skipped when a BOOKED manual/mcp row shares its (date, ore) bucket count-
symmetrically. Gates beyond the feed-vs-feed mirror: stored row must be
booked (staged rows never consume an import), currencies must not contradict
(bucket key is date+ore only), the cash-account guard applies to the count
exactly as to consumption, and symmetry uses the Layer-1-unmatched incoming
count so an already-stored row cannot inflate it. Consumption stamps the
batch cash_account_id onto an account-unbound hand row, so one hand row can
never consume feed rows on other accounts in later syncs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bookkeeping): inline verifikat rattelse (strike lines + text/date edit)

Second sanctioned correction track under BFL 5 kap 5/9 pp, Fortnox-style:
strike lines inside a posted verifikat with replacements in the same
voucher, and correct description/entry_date without an andringsverifikat.
Envelope: posted entries, open unlocked periods, company lock date,
same-period date moves, structural/FX/doc-linked lines excluded, and a
reconciliation guard preserving per-account net on bank/reskontra sides of
externally linked entries. Every rattelse writes an immutable who/when row
(journal_entry_rattelse_log, WORM, archived as rakenskapsinformation) and
struck originals render struck-through in the verifikat; list rows and the
detail header carry a Rattad marker. CLAUDE.md hard rule 1 and the
swedish-accounting-compliance skill are amended to state the two-track
rule. Staging carries the DDL; prod gets it on merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: live saldo in booking form, prior-year window comparison, hideable assistant FAB

- Manual journal entry: saldo column now shows before -> after computed
  from the typed debit/credit amounts (direction feedback while booking)
- Resultatrapport: a narrowed date range now compares against the same
  window shifted one year back (#862), merged across fiscal periods for
  brutet rakenskapsar; P&L rows report window activity instead of
  rolled-forward YTD closing
- Assistant FAB: per-user hide toggle (user_preferences.hide_assistant_fab,
  settings > assistant), sidebar entry unaffected; collapsed sessions keep
  their reopen handle

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(stripe): sync balance transactions as a bank feed on 1686

Import the connected Stripe balance into the transactions inbox, opt-in
per connection (transaction_sync_enabled on stripe_connections):

- Balance transactions map to feed rows with the two-row gross+fee split
  and frozen external_id formats (stripe_{acct}_{txn} / _fee), dated on
  created, bound to a provisioned "Stripe-saldo" cash account on 1686 so
  booking settles against the clearing account by construction.
- Double-booking protection: settled payment-link charges import
  pre-linked to their settlement entry; payout rows import pre-linked to
  the payout entry; processPayoutPaidEvent claims the payout's fee rows
  at booking time (linkPayoutFeedRows, idempotent from both directions).
- Cursor last_balance_txn_synced_at with 24h overlap; first run
  backfills 90 days floored at the day after the company lock date.
- Nightly cron /api/extensions/stripe/transactions/cron (03:30),
  transaction-sync toggle route, "Synka nu" covers both feeds, settings
  panel toggle with last-synced/backfill note, sv+en strings.
- Migration 20260723200000 (applied to staging).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): offer match-to-voucher on unbooked history rows

Unbooked transactions with is_business already set (e.g. left behind when
a voucher was removed without a full uncategorize) land in the history
list instead of the inbox, where the match-against-existing-voucher
action did not exist, leaving them with no path back to voucher
matching. Add the same menu item to the history list for unbooked rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(transactions): enhance ownership checks and error handling in journal entry routes

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 01:16:20 +02:00
Jakob WennbergandClaude Fable 5 aa72a75dfc feat(bookkeeping): concept toolbar, template booking, confirm-first posting (UI migration PR 4) (#1123)
* feat(bookkeeping): concept toolbar, template booking, confirm-first posting (UI migration PR 4)

The Bokforing page adopts the concept (scene 9) on top of the PR 3 kit:

- Toolbar in concept order with the FyPicker chip far right replacing the
  "Visar:" scope selector (same persisted scope, one-click change)
- "Nytt verifikat" is a SplitButton with three remembered modes: Tomt
  verifikat (existing editor, voucher-number hint kept), Bokfor fran mall
  (new centered TemplateBookDialog: existing booking_template_library
  data MRU-ordered, date + editable amount recomputing the kontering
  live via applyTemplate, Balanserar row, direct booking + MRU touch),
  and Skapa med assistenten (existing agent-sheet path; suggestion lands
  in Granskning). Last-used mode persists via ui_state.create_mode
- Draft posting goes through ConfirmDialog describing the outcome
  ("Bokfors som verifikat A-218: ...") with an indicative next-voucher
  preview; the success toast still shows the real number
- "Underlag saknas" becomes the row's only warning chip (Badge warning)
  instead of the bare triangle icon; exempt rows keep the muted glyph
- New lib/hooks/use-ui-state.ts: client read of ui_state to seed the
  split button's initial mode

No backend, migration or RPC changes. VAT-split math is applyTemplate,
already unit-tested in lib/bookkeeping/__tests__/template-library.test.ts;
split-button persistence is tested in lib/ui-state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bookkeeping): use roundOre in TemplateBookDialog money math

The antipattern ratchet caught two hand-rolled Math.round(x*100)/100;
route them through lib/money roundOre like the rest of the codebase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: PR 4 decisions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bookkeeping): concept dry-table verifikat list (scene 9)

The list itself adopts the concept, verified against the artifact's
scene 9 markup: a borderless table (Verifikation / Datum / Beskrivning /
Belopp) with hover-revealed selection checkboxes, hover-revealed chevron,
and an animated grid-rows row expansion whose kontering renders as the
concept's vlines sub-table (uppercase hairline heads, Summa row).
Expansion actions become quiet underlined links (Visa detaljer, Skapa
andringsverifikation, Aterfor (storno), Kopiera); posting keeps its pill
+ ConfirmDialog. Drafts get a row-level Bokfor button like the concept.

All functionality preserved: batch "Inget underlag kravs" bar (above the
table), attachment counts + preview, no-doc-required toggle, out-of-
period + status badges, FX line amounts, sum footer, pagination. The
density toggle is dropped: the table has one density by design.

Fixes from verification: the list's i18n lives in the journal_list
namespace (new keys moved there; they rendered as raw keys otherwise),
and the sidebar brand Image gets explicit dimensions (Next dev warning).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bookkeeping): bulkbar appears only when a verifikat is selected

Concept behavior: no standing "Markera alla (62) / Markera alla utan
underlag" bar. The batch bar is hidden until the first row is selected
via its hover checkbox, then pops in with the count, the reason input,
Undanta underlagskrav, and quiet actions for Markera alla, the
filter-scoped bulk mark, and Avmarkera. All batch functionality kept,
just no chrome until it is needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:37:37 +02:00
Jakob WennbergandClaude Fable 5 6b506a9ca8 feat(ui): frame-layout shell, pill buttons, 24px page titles (UI migration PR 1) (#1117)
* feat(ui): frame-layout shell, pill buttons, 24px page titles (UI migration PR 1)

The visual shell from the concept, zero behavior change:

- New --frame token pair (40 18% 96% light / 0 0% 5% dark); the dashboard
  wrapper is bg-frame and <main> becomes a rounded 12px panel with its own
  inner scroll (md-gated; mobile keeps document flow + bottom nav)
- Sidebar goes borderless/transparent on the frame
- Buttons are pills app-wide (radius 99px, default 7px/16px padding, 13px
  text, icon buttons become circles), set once in components/ui/button.tsx
- PageHeader locked at exactly 24px/32px Hedvig serif
- MainContainer resets panel scroll on route change (Next's window
  scroll-to-top never fires for an inner scroll container)
- chat layout + extension workspaces switch viewport-height formulas to
  h-full so they fill the panel instead of overflowing it by 20px
- .claude/rules/design.md rewritten with the 14 locked UI-migration
  conventions from dev_docs/ui_migration_plan.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): clamp hand-rolled page titles to the locked 24px/32px

Transaktioner (TransactionStatusBar) and 13 other pages hand-roll their
h1 instead of using PageHeader, so they kept text-3xl/4xl after the
shell change. Clamp them all to font-display text-2xl leading-8.
Onboarding heroes and headline numbers are intentionally untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ui): keep the frame strip above the panel on banner-less accounts

<main>'s 10px top margin was the first in-flow margin inside the shell
wrapper, so it collapsed through the wrapper and pushed the whole shell
down, showing white body background above the panel instead of the warm
frame strip (only visible on real accounts: the sandbox banner blocked
the collapse). Flex containers never collapse child margins, so the
shell wrappers become md:flex md:flex-col.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:19:12 +02:00
Mattsson e11f70b347 Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
2026-07-21 23:00:15 +02:00
MattssonandClaude Fable 5 4e47335308 feat(year-end): administrative undo of executed year-end closing + skatteverket scope fixes (#1081)
* fix(skatteverket): request the ska scope for skattekonto v2

The skattekonto v2 API rejects skahmst-only tokens with 403 "The required
scopes are not authorized" (observed in prod 2026-07-20; no company has
synced since 2026-05-10). The requested `skattekonto` scope is silently
dropped from every grant, while `ska` appears in one real May grant, so
request it too: SKV grants the intersection, so this is harmless if wrong.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skatteverket): correct the skattekonto scope model around ska

Root cause of the May 10 skattekonto outage, confirmed via git history and
prod token data: the `ska` scope (the interactive skattekonto API's actual
scope, requested since the extension's first commit in March) was removed
by the "remove unused scopes" cleanup in the #431 series. Every token
issued after that hour lacks it and the API answers 403 "The required
scopes are not authorized"; no company has synced since. The May 15 repair
re-added skahmst, which per its tjanstebeskrivning is a different bulk
E-transport service and does not substitute; `skattekonto` is not a real
SKV scope name and is silently dropped from grants.

Follow-up to the ska re-request (cd8f7a30):
- document the confirmed scope model in oauth.ts so ska is never
  "cleaned up" again
- panel missing-scope warning and reconnect-button now gate on ska,
  not skahmst/skattekonto
- scope badge labels: ska takes the saldo & transaktioner label,
  skahmst relabeled as the E-transport file service
- consent-page note covers both terse scope names and says ska is
  required

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(year-end): warn on untaxed profit at verkstall, Swedish readiness messages, always-visible period selector

An aktiebolag could execute year-end with a profit and zero bolagsskatt
booked without any warning (support case: closing moved 592k to 2099
untaxed). The preview now computes bolagsskattMissing (AB + profit + no
89xx account among closed accounts, 8999 excluded) and both the preview
and execute steps render an advisory, bypassable warning.

validateYearEndReadiness messages are now Swedish (the bokslut wizard is
a stays-Swedish surface); the MCP year_end_readiness classifier matches
both the new Swedish strings and the legacy English ones.

The wizard period selector now always renders, keeps a selected-but-
ineligible period selectable, and resets a stale ?period= id from
another company instead of leaving the user stuck on the wrong year.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(year-end): administrative undo of an executed year-end closing

Storno-only reset used when a bokslut was executed prematurely (e.g.
without bolagsskatt) and no arsredovisning exists yet: reverses the next
period's result_appropriation and opening_balance entries, reopens the
period, reverses the closing entry, and detaches closing_entry_id.
Resumable if interrupted midway; attribution per BFL 5 kap 6.

Migration 20260720140000 adds the trigger escape hatch: closing_entry_id
may only change once set when the old closing entry is reversed with a
posted storno chain (status flag alone is forgeable via PostgREST), and
a non-NULL replacement must be a posted year_end entry in the same
period. Covered by a pg-real test.

planResultAppropriation idempotency is now posted-only: a reversed
omforing no longer blocks the re-run from posting a fresh 2099 -> 2098
reclassification (it previously returned null silently, leaving the new
year's equity polluted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): address CodeRabbit, PR-Agent and compliance findings

- undo script: company_id filters on verify queries, period-scope the
  arsredovisning precondition checks, validate service-key format,
  escalate audit_log insert failure to a hard error (BFNAR 2013:2)
- detach migration: company-scope the storno chain EXISTS, replace the
  em dash in the new error message

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): address round-2 compliance swarm and Swedish review findings

- undo script: require --confirm-url with --commit so an env swap fails
  loud; retry the audit_log insert 3x and direct the operator to insert
  the behandlingshistorik row manually on final failure (BFNAR 2013:2)
- year-end preview: document why resultAccountSummary is a complete 89xx
  scan; warning text now also names periodiseringsfond and
  overavskrivningar as legitimate zero-tax reasons

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:17:43 +02:00
Jakob WennbergandClaude Fable 5 da859d7236 refactor(ui): design-system consistency pass over dense pages + i18n de-bloat (#961)
* refactor(ui): normalize dense pages to the locked design system

Sweep of the info-dense surfaces against .claude/rules/design.md; no
behavior changes, classNames and primitive adoption only.

- Replace hand-rolled h1s with PageHeader (import, suppliers, kpi,
  salary/employees, skattekonto, settings layout) and drop the one
  double title (SalarySettingsContent under the settings h1)
- Replace hand-rolled empty states with EmptyState (skattekonto,
  banking/api-keys/oauth/counterparty settings) and hand-rolled
  pulse divs with Skeleton (deadlines, report view loaders)
- Remove semantic colors used as chrome: amber/emerald banners in
  AGIPanel and SkatteverketPanel, success/warning tints in
  kassaflodesanalys, arsredovisning and import become neutral
  surfaces with the tint kept on the icon only
- Full-opacity borders everywhere (border-border/30-60,
  border-destructive/20-40, border-foreground/30, text-destructive/80)
- Snap off-scale spacing (p-5 to p-6, p-2.5 to p-3, gap/mt-x.5 to
  scale values); KPI metric tiles p-6 to p-4 per the tile rule
- Remove the mobile Select that duplicated the invoices status Tabs
  (TabsList already scrolls horizontally); single Tabs now serves
  both breakpoints
- supplier-invoices: shared formatCurrency instead of a local
  formatAmount helper; skattekonto: formatDate/formatDateLong/
  formatDateTime instead of raw dates and toLocaleString
- arsredovisning flerarsoversikt converted to the Table primitive
  with right-aligned tabular-nums cells
- Settings: CardTitle text-base on section cards, one heading idiom
  in AccountSettingsContent, h3 to h2 in CompanyProfileView

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(i18n): trim text bloat and fix an untranslated sv string

- Fix invoice_credit.create_failed_fallback: sv catalog carried the
  English "Failed to create credit note"; now "Kunde inte skapa
  kreditfaktura". Translate new_user_checklist.step3_title in en
- Drop descriptions that paraphrase their own title (design.md
  forbidden pattern): invoice_detail.credited_description,
  invoice_credit.original_card_description, invoice_editor
  customer/notes card descriptions (keys deleted from both
  catalogs, zero remaining usages); the transaction booking
  DialogDescription becomes sr-only so screen readers keep it
- Trim redundant sentences from settings_salary.info_payroll_scope,
  settings_backup.intro, ext_cloud_backup_long_description,
  settings.name_description, salary_payments.open_payments_note and
  shorten invoice_credit.reason_card_description; statutory BFL/tax
  prose untouched
- Normalize toast punctuation (dimensions/self_billing
  created_description lose the trailing period like their siblings)
- common.delete "Radera" to "Ta bort" (zero live call sites; Radera
  stays reserved for irreversible account/company deletion)

Catalogs verified key-identical (4795 keys each) and JSON-parseable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:56:30 +02:00
Jakob WennbergandClaude Opus 4.7 573feea890 perf: cross-system snappiness batch (middleware, loading states, bundle) (#909)
* perf: cross-system snappiness batch (middleware, loading states, bundle)

Middleware: resolve the active company at most once per request and run
the user_preferences + first-membership queries in parallel, cutting 1-2
sequential DB round trips from every authenticated page load.

Loading states: add loading.tsx skeletons for the six highest-traffic
dashboard routes, render the real salary page header during load instead
of a full-page skeleton, replace the blank fallback={null} Suspense
flashes on customers/articles, and reshape the settings skeleton to
match the actual form layout.

Bundle: defer recharts chart components via next/dynamic on the KPI page
and report views, replace the import page's framer-motion marching-ants
border with a CSS keyframe, and enable optimizePackageImports for
recharts/date-fns/framer-motion.

Transactions: extract the potential-match lookups into a shared parallel
helper; a single-query PostgREST embed is blocked until the
potential_supplier_invoice_id FK exists in prod (see DECISIONS.md).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(transactions): log potential-match query failures instead of dropping them

A DB error in the invoice/supplier-invoice hint lookups previously
surfaced as "no potential match"; log it so failures are diagnosable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-06 16:16:33 +02:00
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00
Jakob WennbergandClaude Fable 5 9f7c842a33 feat(skatt): setup gates + auto-loading momsdeklaration across Skatt & bokslut tabs (#885)
* feat(skatt): setup gates + auto-loading momsdeklaration across Skatt & bokslut tabs

Every tab in the Skatt & bokslut nav group now tells an unconfigured user
what is missing and where to fix it, instead of dead-ending or rendering
zeros:

- Momsdeklaration: gates on vat_registered with a settings CTA; auto-fetches
  the configured period on load and on every period change (no more "Hämta"
  button); one period control (räkenskapsår picked inline for helårsmoms,
  shell selector + back link dropped via new ReportDescriptor.standalone);
  raw <select>s replaced with the Select primitive; banner when moms_period
  is missing; BankID connect returns to the page instead of the report
  library.
- Deadlines: callout (sv+en) when no system-generated tax deadlines exist —
  they are derived from tax settings, so point at /settings/tax rather than
  presenting an empty manual todo list.
- Årsbokslut: "no räkenskapsår yet" (CTA to bookkeeping settings) is now
  distinguished from "nothing to close yet".
- Skattekonto: non-auth fetch failures render an error card with retry
  instead of the misleading "inget saldo hämtat ännu" empty state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skatt): address bot review — res.ok guard, blocking moms_period gate, panel hidden while räkenskapsår unresolved

- Auto-fetch treats non-2xx or unparsable responses as errors (with retry)
  instead of rendering undefined data.
- momsPeriodMissing now blocks the declaration (EmptyState + settings CTA)
  rather than fetching a guessed quarterly period behind a banner — a
  declaration submittable for the wrong period type is a hazard, not a
  convenience.
- SkatteverketPanel is not rendered while yearly mode awaits a fiscal
  period, so its actions can never target an unconfirmed period.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:12:51 +02:00
Jakob WennbergandClaude Fable 5 816b1769c8 feat(dimensions): PR6 retro-tagging — audited retag carve-out, BulkTagWorkbench, staged MCP tool (#867)
* feat(dimensions): PR6 retro-tagging — audited retag carve-out, workbench, staged MCP tool

Tier-2 retro-tagging (founder decision №1, approved 2026-07-02): posted
entries in OPEN periods can have their dimension tags changed through ONE
audited path — everything about the verifikat itself stays immutable.

Carve-out (migration 20260702170000): the line-immutability trigger gains a
single narrow branch — while the transaction-local GUC set by the RPC is
active, an UPDATE of a posted line is admitted iff every non-dimension
column is unchanged, enforced by a whole-row to_jsonb diff (any future
column is protected by construction; mirrors cost_center/project are in the
changeable set because they are derived views of dimensions['1']/['6']).
Precedent: mark_entry_as_opening_balance (20260613120000).

retag_line_dimensions RPC: tenant guard (20260619130100 pattern), writer
gate (viewers rejected), posted-only, open period + company lock date
enforced, every code validated against the ACTIVE registry, immutable
dimension_retag_log row (before/after/actor/reason, INSERT-only via its own
trigger, no FKs so the trail survives hard-deletes) written BEFORE the
carve-out UPDATE. Idempotent no-op without a log row. Untag ({}) supported.
Legal position per the plan: dimensions are internredovisning metadata, not
BFL 5 kap 7§ verifikat content — this is strictly more conservative than
Fortnox/Visma (dimension-only diffs, open periods only, immutable log,
storno past locks — Tier 3 has no exceptions).

Mandatory pg suite (11 tests): GUC-less updates still blocked; amounts/
description can never change even under the GUC (transaction-local);
closed/locked/lock-date, role, registry, draft and cross-tenant rejections;
log immutability; gnubok.allow_delete bulk path unaffected.

UX (all writes through the ONE RPC): pencil on posted-voucher lines in
bookkeeping/[id] ("Påverkar endast internredovisningen, inte verifikatet")
+ retag-history card; BulkTagWorkbench at /dimensions/tagging (filters,
shift-select, merge vs "Ersätt tagg" replace mode, reversal-pair warning
with "Inkludera motverifikat" auto-selection, per-line failure display).

MCP: gnubok_tag_journal_lines (bookkeeping:write) — filter block resolved
via resolve-don't-select, ≤500 lines, staged via pending_operations (new
op type migration 20260702171000, medium risk tier, shared Zod validation
boundary between staging and commit; executor loops the RPC per line with
partial-success aggregation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dimensions): address #867 review — SQLSTATE classification, blocking storno confirm, documented divergence

- Retag route classifies RPC errors by SQLSTATE instead of message-regex:
  P0001 (every rule violation in the RPC) → 409 verbatim, 42501 (tenant
  guard) → 403, anything else → logged 500 with a generic message. No more
  substring sniffing.
- The workbench's storno-pair warning escalates to a BLOCKING confirmation
  naming the unselected counter-vouchers before apply (Srf U 14 gross
  reporting — one-legged retags silently skew project P&L; the banner alone
  was advisory).
- The empty-bag divergence is now documented on both schemas as intentional:
  the direct dialog/workbench path allows {} (human untags phantom codes,
  logged with reason), the MCP staged path rejects it (agents never
  bulk-clear history).

Triage notes: the log's missing FKs are the point (behandlingshistorik must
survive undo_sie_import hard-deletes — a cascade would erase the trail);
SIE exports are generated fresh on demand, never cached, so post-retag
exports carry the new object lists automatically; date-scoped registry
values are deliberately not enforced at retag because entry creation does
not enforce them either — enforcing in one path only would be incoherent
(both belong to the PR10 rules engine).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:02:34 +02:00
Jakob WennbergandClaude Fable 5 01dbef4015 feat(dimensions): PR4 reports — dimension-filtered P&L + Resultat per projekt/kostnadsställe (#862)
* feat(dimensions): PR4 reports — dimension-filtered P&L everywhere + Resultat per projekt/kostnadsställe

The Project P&L milestone of the dimensions plan (dev_docs §7 PR4).

One choke point lights up everything: generateTrialBalance gains
options.dimensions (SIE dim → code map) pushed down as jsonb containment
(dimensions @>, served by idx_jel_dimensions_gin) on both line queries, with
company-wide opening balances dropped when filtered (they cannot be
dimension-scoped; P&L-safe by whitelist). Resultatrapport, resultaträkning,
huvudbok, monthly-breakdown and the TB drill-down inherit the filter; the
KPI route filters only its P&L-side inputs (income statement, months,
expense composition) — never cash/VAT.

New report lib/reports/dimension-pnl.ts — "Resultat per projekt/
kostnadsställe" (Fortnox Resultatrapport projekt): value-as-column matrix
over one dimension with an explicit "(Utan dimension)" bucket computed as
the residual against the same trial-balance pass resultatrapport uses, so
every row and the Totalt column reconcile with the unfiltered
resultatrapport by construction. Registered in REPORT_CATALOG (visible only
when dimensions_enabled), slug-routed view + xlsx export.

UI: DimensionFilter (dimension + value picker, persistent "Filtrerad — ej
fullständig rapport" chip) mounts in FocusedReport for catalog entries
flagged dimensions: true; huvudbok rows show line dim codes.

Statutory exclusion pinned by TEST, not convention:
lib/reports/__tests__/dimension-statutory-guard.test.ts fails if the filter
parser leaks into balance sheet, balansrapport, kassaflöde, VAT, SIE or
full-archive routes/generators, or if the catalog whitelist widens.

MCP: new gnubok_get_dimension_pnl (reports:read); dimensions filter arg on
get_trial_balance/get_income_statement/get_general_ledger with
resolve-don't-select (names → registry codes, resolution echoes);
query_journal totals fixed to aggregate the FULL match set (was silently
slice-scoped while claiming otherwise) with an honest totals_scope field,
plus group_by / group_by_dimension aggregation.

Also: voucher-detail dim-6 badge now uses the registry name instead of the
non-standard "PR" abbreviation (#859 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dimensions): address #862 review — export disclosure, prior-column suppression, period-label honesty, route hardening

- Filtered XLSX/PDF exports now carry the partial-view disclosure past the
  file boundary (BFNAR 2013:2): filename suffix (-dim6-p001), a
  "Filtrerad … — ej fullständig rapport" row on every sheet, and a header
  note/title line in the PDFs.
- Resultatrapport drops the prior-year column when a dimension filter is
  active — project codes are time-limited under K2/K3, so "this code last
  year" may be a different project (same rule as narrowed date ranges).
- dimension-pnl no longer accepts fromDate: the matrix is cumulative from
  period_start by design (closing-balance semantics), and the period label
  now states exactly that instead of echoing a lower bound that was never
  applied. Routes/MCP tool updated to toDate-only.
- dimension-pnl routes 404 on an unknown/foreign period id and cap dim_no
  to 4 digits (matching the MCP tool's PostgREST-path guard, which the
  generator now also enforces itself).
- Statutory-guard test's generateTrialBalance call-site scan is paren-aware
  instead of a 300-char window; added fully-untagged and injection-guard
  test cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:20:47 +02:00
Jakob WennbergandClaude Fable 5 11126d6d56 feat(dimensions): PR3 tagging — voucher-form pickers, MCP dimension tools with resolve-don't-select, engine soft validation (#859)
Phase 3 of dev_docs/dimensions_implementation_plan.md. Companies with
dimensions_enabled=false see zero change; existing free-text API writers keep
working (validation is toggle-governed).

Engine (soft validation):
- validateEntryDimensions() in dimension-resolver: zero queries for untagged
  entries; toggle off → passthrough; toggle on → one settings fetch + two
  registry queries, rejects unknown dims/codes and archived values with
  Swedish per-code messages (DimensionValidationError, 400, details.issues).
  Wired into createDraftEntry + updateDraftEntry before any insert; reversal/
  storno paths untouched (verbatim copies). Fails open on transient registry
  errors — soft validation must never block bookkeeping.

MCP (agent write path):
- New tools: gnubok_list_dimensions, gnubok_list_dimension_values (fuse.js
  fuzzy), gnubok_create_dimension_value (STAGED via pending_operations —
  agents never silently mint reporting values; new op type + CHECK migration
  + executor with duplicate-idempotency).
- create_voucher/correct_entry: per-line dimensions bag + default_dimensions,
  resolve-don't-select server-side (code OR natural-language name; exact →
  fuzzy ≤0.30 with ≥0.15 runner-up margin; non-exact resolutions echoed with
  confidence; ambiguous → ranked candidates, no auto-create).
- gnubok_get_agent_briefing gains a dimensions block (enabled, dims, top
  values) — omitted when registry empty.
- TOOL_SCOPE_MAP entries; risk tier low for staged value creation.

UI:
- JournalEntryForm (manual voucher + TransactionBookingDialog embed): header
  "+ Kostnadsställe/Projekt" progressive disclosure (gäller alla rader with
  documented inheritance rule) + per-row tag popover + compact KS·PR badges;
  gated on dimensions_enabled.
- Voucher detail: display-only dimension badges with registry-name resolution.
- EditDraftEntryDialog carries line dimensions so editing a draft no longer
  strips tags.

categorize/bulk_book dims deferred to PR7 (needs the bulk_book RPC migration).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:36:45 +02:00
MattssonandClaude Opus 4.8 f63d3e3100 Bug/open banking flow (#854)
* fix(enable-banking): pin Mobile BankID (decoupled) auth_method so Handelsbanken corporate connects

We never sent auth_method to Enable Banking, so it fell back to the ASPSP's
visible default — REDIRECT for Handelsbanken. For Handelsbanken *corporate*
PSUs the redirect flow does not support Mobile BankID, so authorization failed
right after the user approved in the BankID app. Mobile BankID at Handelsbanken
is a DECOUPLED method flagged hidden_method=true, which Enable Banking only uses
when requested explicitly.

Resolve the bank's preferred auth method before /auth: query the ASPSP's
auth_methods and pick the DECOUPLED (Mobile BankID) method when present,
otherwise leave auth_method unset so banks that already work are untouched.
The method name is read dynamically per psu_type, so it is robust across
sandbox/production naming.

- api-client: add approach/hidden_method to AuthMethod, fix ASPSP.auth_methods
  field name (was available_auth_methods, never populated), add
  getPreferredAuthMethod(), thread optional authMethod through startAuthorization
- index: resolve authMethod in /connect and pass it on both fresh + reconnect
- tests: cover method selection and request-body shaping

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(invoice-inbox): clean up bulk-selection toolbar UI

Redesign the selection toolbar shown when inbox items are checked:
one solid primary "Bokför valda" button with outlined secondary
actions ("Fråga assistenten", "Ta bort") and a plain selection
count. Removes the redundant "Avmarkera" button (users uncheck the
still-visible box), fixes label clipping, and gives the toolbar more
breathing room.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(entitlements): bypass paywall in local development

Add isPaywallBypassed() so all gated capabilities are testable locally
without a subscription. Fires only on NODE_ENV=development (npm run dev)
or an explicit DISABLE_PAYWALL=true escape hatch — production builds run
under NODE_ENV=production and the entitlement suite runs under 'test',
so both keep exercising the real gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tic): resolve enskild firma bolagsuppgifter via 12-digit personnummer

TIC's Lens search is fuzzy and only resolves an enskild firma from the 12-digit (century-prefixed) personnummer; a 10-digit form fuzzy-matched an unrelated entity. Expand personnummer to 12 digits before querying and reject hits whose registration number is unrelated to the request. Add a "Hämta" action to the settings Bolagsuppgifter panel to (re)fetch on demand.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(transactions): implement categorize core for bank transaction categorization

- Added `categorize-core.ts` to handle categorization of bank transactions, supporting single and bulk operations.
- Introduced `categorizeMatchedTransaction` and `bulkBookMatchedInboxItems` functions for transaction processing.
- Implemented fiscal period validation and duplicate booking detection.
- Enhanced logging and error handling for transaction categorization.

feat(scripts): add diagnostic script for Handelsbanken ASPSP metadata

- Created `check-handelsbanken-aspsp.mjs` to fetch and display available authentication methods for Handelsbanken.
- Outputs metadata for business and personal PSU types, including default authentication methods.

fix(migrations): increase statement timeout for SIE bulk delete operations

- Updated `20260629160000_sie_bulk_delete_statement_timeout.sql` to set a longer statement timeout for bulk delete RPCs to prevent cancellations during large imports.

feat(migrations): add bulk book inbox items to pending operations

- Expanded `pending_operations` table to include `bulk_book_inbox_items` operation type in `20260630120000_pending_operations_add_bulk_book_inbox_items.sql`.
- Supports bulk booking of matched inbox items against bank transactions.

test(pg): add tests for replace_period_opening_balance_link RPC

- Implemented tests in `replace-period-opening-balance-link.pg.test.ts` to validate the functionality of the opening-balance correction flow.
- Ensured immutability of opening balance links and proper handling of posted vs. non-posted entries.

* fix(sie-export): update journal entries and lines handling in SIE export tests

* fix(migrations): resolve version collision on 20260629160000

The SIE bulk-delete statement_timeout migration shared version
20260629160000 with journal_entries_list_series_filter (merged from
main via #798/#823), causing a schema_migrations_pkey duplicate key
error on apply. Rename the branch's migration to 20260629160100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(compliance): resolve compliance-swarm + review findings

- opening-balance/correct: compensating rollback for the non-atomic
  storno+rebook so a mid-sequence failure never leaves two posted OB
  entries (ASVS V2.3); durable audit event on every failure path
  (V16); reference the original verifikationsnummer in the corrected
  entry per BFL 5 kap 5§; document that requireWrite already enforces
  write-role + membership (V8.2.1 was a false positive)
- reports sources routes: validate the cursor date component as ISO
  (/^\d{4}-\d{2}-\d{2}$/) before use, 400 on malformed (ASVS V1.2),
  applied to both the VAT-declaration and trial-balance routes
- AgentSessionList: await the rename PATCH, revert the optimistic
  title and toast on failure (ASVS V4.5)
- bank booking: exclude same-batch siblings from the booking-time
  duplicate guard so bulk-booking distinct same-(date,amount)
  transactions no longer false-positives; pre-existing duplicate
  detection is preserved
- BulkBookInboxDialog: drop the unsafe currency-based reverse_charge
  default, add an omvänd skattskyldighet advisory, and type VAT
  options to the backend VatTreatment union
- OpeningBalanceRowEditor: hold onChange in a ref (synced in effect,
  not during render) so an unstable callback can't cause a render loop

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:13:00 +02:00
Jakob WennbergandClaude Opus 4.8 2da9c71eb3 UI badge cleanup + /chart-of-accounts route + loop skills (#850)
Bundles three separable concerns:

- style(ui): badge audit + cleanup across 45 files — real-status chips use Badge variants (raw Tailwind colors dropped), non-status count/type/label chips demoted to muted text, clustered badges consolidated; 20 unused imports removed.
- feat(bookkeeping): Kontoplan moved to a dedicated /chart-of-accounts route (nav + command palette wired); /bookkeeping shows the journal list only.
- chore(skills): loop-* automation skills + design-scan workflow under .claude/.

fix(reports): restored the destructive count badge on blocking errors in the periodisk sammanställning (EC Sales List) — a genuine status cue the audit had wrongly flattened; flagged by the PR reviewer and Swedish compliance bot, now clean.

All CI green; compliance bots report no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:29:23 +02:00
Jakob WennbergandClaude Opus 4.8 d63d2aecf0 feat: UI slop cleanup, invoice icon/header polish + year-end in Rapporter, journal-list DataList refactor (#847)
UI cleanup: removed AI-slop (redundant suppliers subtitle, decorative Sparkles glyph), decluttered the article-detail header (single status badge + muted type · #number), standardized the invoice icon Receipt→ReceiptText (no $ in a SEK app), and matched ReportExportMenu trigger size to the primary CTA on list pages.

Bookkeeping: surfaced year-end closing in Rapporter (catalog descriptor) and dropped the redundant header button; refactored JournalEntryList to DataList primitives + chunked /api/documents/counts in 50-ID batches (large pages previously 400'd); added optional fraction-digit overrides to formatCurrency. The fiscal-year lock indicator is preserved as a labeled Låst/Stängt badge in FiscalYearSelector.

All PR-bot findings triaged as false positives (unused import, formatCurrency öre, lock indicator) or intentional design (year-end placement, empty-state messaging). CI green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:42:56 +02:00
Jakob WennbergandClaude Opus 4.8 b800dcd403 style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup (#835)
* style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup

Multi-agent scan of all 404 UI files against the locked design system, then
141 verified surgical fixes across 109 files (net -32 lines):

- Remove forbidden elevation/motion: shadow-* and rounded-xl on cards, active:scale
  bounce, hover:shadow on list items, transition-all -> transition-colors.
- Drop font-medium from single-weight Hedvig display headings/numerals.
- Replace raw rainbow Tailwind status colors with Badge variants / brand tokens /
  neutral surfaces (achromatic chrome, semantic colors stay data-only).
- Route raw dates through formatDate(), hand-rolled currency through formatCurrency(),
  add tabular-nums to financial figures; text-gray-* -> text-foreground tokens.
- Swap hand-rolled skeletons for the Skeleton primitive; off-scale spacing -> token scale.
- Fix copy: mislabeled "Leverantörsfakturor" -> "Utgifter" on bank-import outflow total,
  collapse no-op identical-branch ternaries, broken Swedish diacritics (mojibake),
  correct mismatch-password toast, correct supplier currency-field label.
- Remove PII-leaking debug console.log on register, stray console.logs.

Verified: tsc clean on all changed files, eslint clean, production build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(auth): sanitize residual error logs in register flow

Follow-up to PR review (compliance swarm V16 / GDPR Art.5(1)(f)): the
remaining console.error calls in the register flow passed raw error
objects, which Supabase may populate with PII (email) in nested fields.
Log only sanitized message strings instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 13:14:13 +02:00
MattssonandClaude Opus 4.8 60e33c4b51 Fix/cus fee 28 (#820)
* feat(invoices): add Plusgiro input to bank details settings

Plusgiro was already persisted, validated by the API schema, rendered on
the invoice PDF and toggleable via "Visa plusgiro" — but the settings UI
had no field to enter the number, so plusgiro-only users could not fill
it in. Add the input next to Bankgiro with Luhn validation and hyphen
formatting, include it in the save payload (normalised on save so raw
digits still match the dashed schema format), and add sv/en strings.

Adds validatePlusgiroNumber/formatPlusgiroNumber helpers + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(invoices): respect non-VAT-registered seller in PDF preview + portal tooltips

Two user-reported bugs:

- PDF preview (/api/invoices/preview-pdf) ignored company.vat_registered and
  fell back to the customer-driven 25% rate, so a non-momsregistrerad seller
  saw VAT in the review step even though the created invoice books none. Mirror
  the server-side write gate (build-invoice-write.ts): force 0% when
  vat_registered is false (delivery notes excepted).

- InfoTooltip rendered TooltipContent without a Portal, so tooltips were
  clipped by the scrollable DialogContent (overflow-y-auto) in the send-invoice
  journal-entry review. Wrap in TooltipPrimitive.Portal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(transactions): book library mall from its literal lines, not a lossy fallback

Booking a bank transaction with a user-created booking-template (mall) via the
convertible "QuickReview" fast path reduced the template to a single category +
one account_override, silently discarding the chosen debit/credit. A
kundinbetalning mall (D 1930 / K 1510) booked as a generic cost (D 6991 / K 1930),
or with a VAT line as D 1930 / K 1930 / K 2611 — and the result flipped with the
direction inferred from the business/settlement line tags, so visually-identical
templates produced different verifikationer.

Route every library template through the journal-entry editor (applyTemplate ->
/book), which posts the literal lines, regardless of convertibility. Add
regression tests locking the contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bookkeeping): make the booking-time duplicate guard bypassable

TRANSACTION_BOOK_POSSIBLE_DUPLICATE told users they could "book anyway" but
the UI dead-ended on a toast with no way to do so. Add a shared
DuplicateBookingDialog that surfaces the already-booked sibling and lets the
user review it or book anyway (force bound to the reviewed candidate, which
the server re-detects so a stale id cannot wave the guard away).

- Wire the dialog into the /transactions categorize flow and the manual
  booking dialog (JournalEntryForm -> /api/transactions/[id]/book)
- Bind the override to expected_duplicate_transaction_id OR
  expected_duplicate_journal_entry_id so ledger-only vouchers (paid invoice,
  salary run) can be confirmed too
- Extend the guard to the pending-operations commit path and the MCP server
- Tests for book/categorize routes, detection, and the commit guard

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bookkeeping): log duplicate-guard bypass to behandlingshistorik in the agent commit path

The web /book and /categorize routes append a durable
BankTransactionDuplicateDismissed event when a user books over a detected
possible double-booking. The agent commit path (commitCategorizeTransaction,
commitMarkInvoicePaid) skipped the guard silently on allow_duplicate=true,
leaving no behandlingshistorik — an auditor could not reconstruct why the
duplicate was allowed (BFNAR 2013:2 kap 8).

When allow_duplicate=true, re-detect the candidate and append the dismissal
event (BankTransactionDuplicateDismissed for the bank-line path,
InvoiceDuplicatePaymentDismissed for mark-paid). Best-effort — a logging
failure never blocks a legitimate booking. Payloads stay PII-safe (ids,
amounts, dates only — no customer or merchant name).

Also fix the misleading DuplicateBookingDialog JSDoc: the retry binds
expected_duplicate_journal_entry_id, not candidate.transaction_id, so the
systemdokumentation matches the actual control (BFL 7 kap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(mcp-server): stub booking-duplicate guard in receipt-matcher categorize tests

The gnubok_categorize_transaction tool runs the booking-time duplicate guard
before staging; its detection queries consumed the queued supabase mock
results, so the staging assertions saw a thrown duplicate error instead of a
staged op. Mock detectBookingDuplicate to "no duplicate" since these tests
don't exercise that path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(transactions): use roundOre for duplicate-guard öre rounding

Replace naive Math.round(x*100)/100 with roundOre() from @/lib/money in the
booking-time duplicate guard (detection lib, commit executor, MCP categorize
tool), satisfying the no-new-antipatterns ratchet guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sie-export): paginate journal entries and lines to prevent truncation

* fix(bookkeeping): keep the Verifikat/Utkast toggle reachable on an empty list

The journal entry list early-returned a pristine empty card whenever the visible list was empty and no filter was active, returning before the Verifikat/Utkast toggle rendered. This stranded users with only drafts (no posted entries) and users who emptied the drafts list, who then had to use the main menu to get back to posted entries.

Narrow the early return to a genuinely empty ledger (committed view, no drafts, no filters); make the in-list empty placeholder context-aware (no drafts / no filter matches / no posted entries yet); resolve the draft count before clearing loading on an empty committed list to avoid a toggle flicker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(enable-banking): persist psu_type and reuse it on reconnect

Reconnecting a bank connection re-derived psu_type from the company entity_type every time (aktiebolag -> 'business'), silently overriding the type the user actually authorized with. A connection that only signs as 'personal' — common for AB owners who use a personal Mobile BankID, notably at Handelsbanken — flipped back to 'business' on every consent renewal and failed at the bank's signing step.

- Add nullable bank_connections.psu_type column (idempotent migration)
- Persist psu_type on connect; on reconnect reuse the stored value (explicit client override still wins)
- Let users switch account type (Företag/Privat) from the reconnect button
- Tests for persistence, reuse, and override

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(import): set maxDuration=300 on bank-file execute to prevent timeout

A full-year bank file (300+ rows) runs a sequential per-row ingest that takes ~85s of server time. The execute route set no maxDuration, so it inherited the platform default and was killed mid-run — the import "spins then aborts" for the user. Match the SIE import route and give it a 5-minute budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(transactions): add assistant entry point on transaction rows

The agent ("Lena") could only be reached from Dokumentinkorgen, and only once an underlag was matched to a transaction. Transaktioner is the most common starting point for booking, so users could not start a booking with the assistant from there at all.

Add a per-row "Fråga [namn]" button on unbooked transaction rows that opens the existing transaction.categorization intent with the row's transaction_id. The intent already reads any linked underlag, so it works whether or not a receipt is attached. No new logic — only the missing entry point.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(invoices): enable Swish payment QR on invoices

Flip SHOW_SWISH_ON_INVOICE on so the Swish row and payment QR render on the invoice PDF, and make the "Visa Swish" settings toggle live (it was hardcoded disabled). The preview-pdf route now builds the QR too, so it shows in forhandsvisning. Position the QR in the top-right of the payment box. No Swish API integration -- the QR is generated offline and prefills the customer Swish app; reconciliation stays via bank matching.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bookkeeping): scope verifikat list to current year, add storno action, clarify correction preview

Three UI fixes from user feedback; no engine logic changed.

- List defaults to the current räkenskapsår instead of all years. Voucher
  numbers run per fiscal year (one A42/year), so showing every year at once
  made them look like duplicates. New resolveCurrentPeriodId helper.
- Add 'Återför (storno)' action on the entry detail page and list row, wiring
  the existing reverseEntry — a pure reversal (BFL 5 kap 5§) with no
  replacement, distinct from 'Rätta'.
- Correction 'Effekt per konto' preview now labels a removed account 'tas bort'
  (vs a bare dash) and warns when the proposal is unbalanced; dialog explains
  the rows are the full new verifikat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bank_connections): add psu_type column to persist chosen authorization type

* feat(errors): add CannotReverseStornoError for handling reversal of storno or correction entries

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:34:51 +02:00
MattssonandClaude Opus 4.8 241959513b Fix/mcp and req (#753)
* feat(api): test-mode API keys force dry-run on the v1 REST API

A key created with mode='test' (prefix gnubok_sk_test_) binds to the real
company, but the v1 wrapper forces dry_run on every write so nothing is
persisted or sent. Mutations on endpoints that can't be simulated
(dryRunSupported=false or unregistered) are refused with 403
TEST_KEY_WRITE_BLOCKED — fail-closed. Reads pass through unchanged and every
test-key response carries X-Gnubok-Mode: test. Live keys are unaffected
(mode defaults to 'live').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(invoices): company default "Vår referens" + per-line sales-account override

Add company_settings.default_our_reference (settings form, schema, type); the
invoice editor pre-fills our_reference from it on new invoices only, never
overwriting an edited draft. Separately, add an optional per-line
försäljningskonto (class-3) override in the editor — left blank, the engine
still derives the revenue account from the VAT rate, and reverse-charge/export
lines ignore the override.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(invoices): render a Swish payment QR on invoice PDFs

Build the Swish "Type C" QR payload offline (no Swish API call) and embed it as
a PNG in the invoice PDF payment box when Swish display is enabled, the invoice
is in SEK, and the amount is positive. Also surface the invoice number in the
payment box. Wired through every PDF render path: send, mark-sent and pdf
routes (both legacy and v1), the recurring-schedule sender, and the staged-send
commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bookkeeping): draft exclusion + correction-chain collapse on verifikationslista

Extend list_fiscal_period_entries_with_related with two opt-in params:
p_exclude_draft (keep drafts off the committed list — they get their own
surface) and p_collapse_corrections (render a correction group as the single
live correction, hiding the mechanical storno and the reversed original).
Both default false; nothing is deleted, every voucher keeps its number, and a
"show all" toggle exposes the full chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reports): link multi-year SIE periods so resultatrapport shows the prior year

SIE import now sets fiscal_periods.previous_period_id in both directions when
creating a period, so multi-year files chain correctly regardless of #RAR order.
A backfill migration repairs periods imported before this (idempotent; only
touches NULL links on first-of-month periods). generateResultatrapport falls
back to the date-adjacent prior period when the chain is still null, so the
comparison column works for legacy data too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(articles): hide the VAT field for non-momsregistrerade companies

The article form reads company_settings.vat_registered and, when false, hides
the moms field and forces vat_rate to 0 on submit — mirroring the invoice
editor so a non-VAT-registered company never sets a rate it can't charge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(import): allow file-based imports in the sandbox

Bank-file, CSV/Excel and SIE imports run entirely on uploaded data with no
external service, so they're now reachable in the sandbox. Only the API-backed
options that need live third-party credentials (PSD2 bank connection, provider
migration) stay disabled. Updates the sandbox notice copy to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bookkeeping): add edit draft functionality for journal entries

* feat(database): add default "Vår referens" column to company_settings for invoicing

* fix(tests): set SHOW_SWISH_ON_INVOICE to false in PDF template mocks

* @
fix(payments): use roundOre for Swish amount formatting

Replace naive Math.round(x*100)/100 with roundOre from @/lib/money to
satisfy the antipattern guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:49:33 +02:00
MattssonandClaude Opus 4.8 43925bc2d3 fix(import): SIE bulk-delete on service client + provider/reporting/b… (#724)
* fix(import): SIE bulk-delete on service client + provider/reporting/banking fixes

Rebuilt branch onto main as a single commit.

- import: run SIE bulk-delete RPCs on the service client to escape the 8s
  statement_timeout; undo_sie_import now takes an explicit actor (p_user_id)
  so its owner/admin gate works when auth.uid() is NULL on the service
  client (migration 20260624120000) + pg-real regression test
- providers: distinguish missing Fortnox license from expired connection;
  provider_consent_tokens PK regression test
- reports: include unmapped BAS expense groups in the income statement
- enable-banking: reconnect closed/expired bank sessions in place
- bookkeeping: surface linked invoices as underlag on the verifikat view
- scripts: track BL cleanup/diagnostic tooling; data files (*.csv) are
  git-ignored and consentId is now a required arg with no silent default

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(import): add Cache-Control header to journal entry references response

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:40:26 +02:00
MattssonandClaude Fable 5 db8983ba9e Add/bokslut (#718)
* feat(arcim-migration): Briox provider with SIE-over-API import

- Briox auth via account ID + application token (no app-level
  credentials); both tokens rotate on refresh and are persisted
- New sie-fetcher pulls the general ledger as SIE through the
  provider API for Fortnox, Briox and Bjorn Lunden
- Wizard stops on a failed SIE import and surfaces the real errors
  instead of proceeding to the misleading migrate-guard message
- PROVIDER_SIE_ONLY_FORTNOX renamed to PROVIDER_SIE_NOT_SUPPORTED;
  new PROVIDER_TOKEN_INVALID for rejected provider credentials

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bookkeeping): per-line accruals (periodisering) on invoices and supplier invoices

Defer revenue/costs per invoice line to 29xx/17xx interim accounts with
automatic monthly dissolution (nightly cron + catch-up at registration),
schedule cancellation on credit, year-end auto-detect exclusion for
already-scheduled invoices, invoice-inbox service-period extraction for
prefill, and an MCP tool to list schedules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bokslut): iXBRL arsredovisning generation and Bolagsverket digital filing

Generate the annual report as iXBRL from a generated taxonomy registry
(K2 element lists, taxonomy:generate/check scripts + CI guard), expose it
via the fiscal-period API, and add the bolagsverket extension for digital
submission to eget utrymme with webhook-driven status tracking
(submissions table + pg tests, lifecycle events, year-end wizard UI).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(mcp): raise origin-guard test timeout to 20s

The dynamic import pulls in the full server module; the parse alone
flirts with the 5s default under full-suite parallel load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add new scripts and documentation for K2 AB taxonomy generation and validation

- Introduced `generate-taxonomy-registry.ts` to automate the generation of the iXBRL taxonomy concept registry from official element lists and tuple models.
- Added `validate-ixbrl.mjs` for validating generated iXBRL reports against the official taxonomy package using Arelle.
- Included new documentation files:
  - `k2-ab-arsredovisning-elementlista-2024-09-12_rev20250312_sv.xlsx`
  - `tuple-innehallsmodell-arsredovisning-k2-2024-09-12.xlsx`
  - `taxonomi-paket-2024-09-12_rev20250312.zip`

* Add tests for bookkeeping accruals dissolution and supplier invoices

- Implement tests for the POST /api/bookkeeping/accruals/[id]/dissolve route, covering success and error scenarios.
- Add tests for the DELETE /api/supplier-invoices/[id] route, including authentication checks and validation of invoice deletion conditions.
- Introduce tests for the Arcim migration provider client, ensuring token handling and error classification.
- Create tests for the Bolagsverket extension, validating submission role enforcement and environment settings.
- Add Zod schemas for Bolagsverket response payloads to ensure proper validation.
- Implement tests for MCP server's list accrual schedules, confirming registration and scope mapping.
- Add consistency tests for IXBRL document generation, ensuring duplicate facts and XML escaping are handled correctly.
- Introduce typed domain errors for accrual schedules to improve error handling in the service.
- Add tests for resolving consent with Briox token refresh concurrency, ensuring proper token management and error handling.

* fix(tests): update payload size guard comments to reflect recent changes in tool descriptions and ceiling adjustments

* fix(gitattributes): mark generated JSON files in bokslut taxonomy as linguist-generated

* feat(migrations): add backfill for invoices.journal_entry_id and fallback for next_voucher_number user_id

* feat(bokslut): enhance compliance and financial processing features with new submission details and security measures

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:35:30 +02:00
Jakob WennbergandClaude Opus 4.8 4dfd790de5 feat(bookkeeping): Ny verifikat modal, ledger-style list, SIE no-underlag exemptions (#698)
* feat(bookkeeping): Ny verifikat modal, ledger-style list, SIE no-underlag exemptions

Verifikat UX
- "Ny verifikat" opens in a modal (NewJournalEntryDialog) instead of an inline tab;
  the review step renders inline in the dialog rather than stacking a second dialog.
- JournalEntryForm: konteringsrader are the focus, with a compact pre-filled metadata
  bar (datum/serie/text/valuta/period) on top; verifikationstext auto-fills from the
  first row's account.
- JournalEntryList: belopp shown on collapsed rows; expanded view is an aligned
  Konto/Benämning/Debet/Kredit table.

SIE imports no longer flood "Att hantera: saknade underlag"
- Import gains an opt-in (off by default) toggle to mark imported verifikat as "Inget
  underlag krävs"; a "Rekommenderas vid migrering" badge nudges it for historical years.
- Multi-select batch-mark in the list for selective cleanup.
- Filter-scoped bulk mark (POST /api/bookkeeping/no-doc-required/bulk-missing): marks
  every missing-doc verifikat matching the active filters across all pages, with a
  dry_run count to confirm scope — the scalable remedy for a post-import flood.
- Shared helper markEntriesNoDocRequired + per-entry batch route.

Tests: no-doc helper, batch route, bulk-missing route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bookkeeping): address PR #698 review findings

- JournalEntryForm: restore the explicit "no underlag" acknowledgement in the
  modal's inline review. When no document is attached, the confirm button reads
  "Bokför utan underlag" (BFL 5 kap 6-7 §§), equivalent to the blocking dialog the
  non-bare flow shows — the bare path no longer posts behind only a passive banner.
- batch no-doc route: guard the ownership query with source_type IN
  NEEDS_DOC_SOURCE_TYPES so a crafted request can't exempt non-document-requiring
  entries (defense in depth on top of company + posted scoping).
- bulk-missing route: resolve doc/exemption status by querying only the candidate
  ids (chunked) instead of loading the company's full document_attachments and
  journal_entry_no_doc_required tables into memory — data minimisation + bounded
  memory for large migrations (the most-repeated reviewer finding).

Triaged as non-issues (left as-is): partial-import exemption (gated on
result.success == zero errors), reason write-back (sidecar row is FK-linked and
carries the reason), and "bulk-exempting manual entries" (consistent with the
existing per-entry NoDocRequiredToggle). No DB migration — reuses the existing
journal_entry_no_doc_required table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bookkeeping): centralize bulk-missing date/series validation in Zod

Move the ISO-date and verifikationsserie format checks into the Zod schema so
malformed input is rejected with a clean 400 instead of being silently nulled
(or, for a shaped-but-invalid date, throwing a 500 via fetchAllRows). The date
refinement rejects values like 9999-99-99 / 2026-02-30 that a bare
/^\d{4}-\d{2}-\d{2}$/ regex lets through. Addresses the PR #698 reviewer nit on
split schema-vs-runtime validation. +2 route tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:41:30 +02:00