Commit Graph

927 Commits

Author SHA1 Message Date
Jakob Wennberg 47938520d6 fix(bookkeeping): allow a first rakenskapsar shorter than 6 months (#1165)
The validator enforced a 6-month minimum on the FIRST fiscal period,
citing BFL 3 kap. The law says the opposite: BFL 3 kap 3 par expressly
allows a rakenskapsar shorter than 12 months, with no floor, when
bokforingsskyldigheten begins (Bolagsverket: the first year may be
"hur kort som helst", max 18 months). The floor only applied to
isFirstPeriod, exactly the case the law exempts, and blocked an
autumn-registered AB from shortening its first year to Dec 31 to file
an early arsredovisning.

Drop the minimum, keep the 18-month cap and the day-boundary rules,
and remove the now-dead Swedish error mapping.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 11:23:18 +02:00
Jakob Wennberg 98886e68d8 fix(vat): keep the RC-basis worklist visible until every voucher is fixed (#1164)
Correcting a single voucher cleared the momsdeklaration's RC_BASIS_MISSING
error and the whole per-voucher worklist with it: the check tested mere
presence of ruta 20-24 basis, the stepper re-derived its landing step and
yanked the user to Granska mid-work, and the remounted checks card never
refetched gaps once the aggregate check stopped firing. The declaration
then claimed "klart" while the remaining vouchers still under-reported
rutor 20-24 (FK004).

- Make RC_BASIS_MISSING/RC_OUTPUT_MISSING proportional: compare reported
  basis against the basis the per-rate output boxes imply (moms/sats),
  with a 0.5% + 1 kr tolerance for per-voucher ore rounding.
- Fetch the rc-basis-gaps worklist once per period, ungated from the
  aggregate check, so remaining rows survive remounts.
- Latch the automatic stepper landing once per period so a refetch after
  a korrigering cannot navigate the user off Kontrollera.
- Resolve rc-basis-gaps against the rakenskapsar (fiscal_period_id) for
  helarsmoms, matching the declaration totals; a calendar span hid gap
  vouchers in the tail of an extended first year.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:10:20 +02:00
Jakob Wennberg 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 Wennberg 49e86e2e67 fix(transactions): share bank-sync busy state across surfaces (#1163)
* fix(transactions): share bank-sync busy state across surfaces

useBankSync() kept busyId/syncingAll/connections as hook-local state, but
the header "Synka bank nu" split-button row and the footer "Synka nu"
button each call the hook independently: a sync started from one surface
left the other enabled and spinner-less, so a second concurrent sync of
the same connection could be started.

Hoist the state into a module-level store (lib/transactions/
bank-sync-store.ts) consumed via useSyncExternalStore, so every instance
shares busy state and the connection list:

- both surfaces spin and disable while either one syncs
- runFor/syncAll re-check the live snapshot before firing, so a click
  racing a sync from the other surface is a no-op instead of a second
  paid PSD2 call
- the bank_connections query runs once per company instead of once per
  surface (first mounted instance claims the fetch; failures release the
  claim so a later mount retries)
- a sync that hits a dead PSD2 session flips the connection to expired
  on every surface at once, not just the one that ran it

Fixes #1162.

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

* fix(transactions): discard stale connection loads after company switch

publishConnections now requires the caller to still own the load claim:
a fetch resolving after the active company switched (and re-claimed the
slot) no longer clobbers the newer company's published list.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:10:58 +02:00
Jakob Wennberg 8a7fd567bd fix(settings): validate share-capital pair before saving (#1137) (#1160)
* fix(settings): validate share-capital pair before saving (#1137)

Entering aktiekapital without antal aktier (or vice versa) died on the DB
pair constraint company_settings_share_capital_pair as a raw 500 with the
generic 'Vardet uppfyller inte de tillatna kraven' toast. The pair rule
(ARL 5 kap 14 $: the aktiekapital note needs both values) now surfaces as
a clear 400 in the PUT route, checked against effective body-or-stored
values so partial API updates are covered too. The form additionally marks
each field required when its sibling is filled, so the browser blocks a
one-sided submit before the request is sent.

Fixes #1137

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

* docs: decision-log entry for share-capital pair validation placement

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

* docs(settings): correct the share-capital note citation to ÅRL 5 kap 34 §

5 kap 14 § is ställda säkerheter; the antal aktier/kvotvärde note is 5 kap 34 §
(flagged by the Swedish compliance review bot, verified against the
swedish-financial-reporting skill).

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

* test(settings): assert the pair message on the one-sided-clear rejection

CodeRabbit review on #1160. Its second suggestion (assert the update
payload on the partial-update test) is skipped: createQueuedMockSupabase
proxies away builder args, so payloads are not recordable, same as every
other test in this suite.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:07:24 +02:00
Jakob Wennberg 8a9162b948 polish(ui): loading feedback on sync/refresh buttons system-wide (#1156)
The "Synka bank nu" row in the transactions Importera split button fired
syncAll() with zero visual feedback. SplitButton now takes busy/busyLabel
per option: the primary face and the menu row swap to a spinning Loader2,
show the busy label and go inert until the action resolves. useBankSync
holds isBusy across the whole syncAll loop so the spinner does not
flicker between per-connection syncs.

Sweep of the rest of the system for async buttons missing the same
feedback (convention: disabled + Loader2 animate-spin + label swap):

- bokslut DigitalInlamning "Uppdatera status" (Bolagsverket poll): had no
  feedback at all; now disabled + spinner + "Uppdaterar ..." while polling
- Stripe settings "Synka nu": had disabled + label swap but a static icon
- Skatteverket "Verifiera": had disabled + label swap but no spinner
- AgentMemoryPanel "Dolj"/"Aterstall" row actions: static icons on async
  patch; now swap to spinner for the busy row

Checked and intentionally unchanged: Arcim migration "Synka igen" and
"Ateranslut" (the whole step flips to a spinner view synchronously on
click), skattekonto "Forsok igen" (page flips to loading view), AgentChat
"Generera om" (streaming indicator is the feedback).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:37:34 +02:00
Jakob Wennberg bb551d1d59 polish(ui): founder feedback batch - inset sidebar hairline, calmer dialogs, Discord mark, chat skeleton (#1158)
- Sidebar: the hairline above the user block is inset to the content
  edges instead of running edge-to-edge (concept language).
- User menu: the Discord community link renders the actual Discord mark
  (inlined simple-icons path, CC0) instead of lucide MessagesSquare.
- Dialogs: open/close animation toned down, zoom 98 instead of 95 and
  150ms instead of 200ms.
- Settings modal: switching tabs no longer remounts the intercepted
  route (and replayed the whole open animation on every click). The rail
  now swaps sections via history.replaceState, which Next syncs into
  usePathname(), so the dialog stays mounted and tab switches are
  instant. Verified via Playwright: dialog DOM node survives three tab
  switches, URL tracks the section, Esc still closes back.
- /chat loading: the shared dashboard skeleton stretched edge-to-edge in
  chat's full-bleed wrapper (chat's own layout is what suspends, so a
  chat/loading.tsx cannot catch it). The shared fallback is now route-
  aware and renders a two-pane chat silhouette for /chat.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:36:45 +02:00
Jakob Wennberg 15300aa8e2 fix(invites): accept invite on BankID signup, recover missed invites on onboarding surfaces (#1157)
An invited user who registered via BankID was funneled into creating a
company instead of joining the one they were invited to: the register
page's BankID path never processed the gnubok-invite-token cookie
(unlike the login, MFA-verify, and auth-callback paths). Observed in
production 2026-07-24.

- register: BankID signup now accepts the pending invite before routing
  to /select-company, mirroring the login page's BankID path.
- lib/company/pending-invites: acceptPendingInviteByToken retries a
  missed acceptance from the cookie (pending + unexpired + email match,
  same rules as POST /api/team/accept); hasPendingInviteForEmail detects
  a stranded invitee whose cookie is gone.
- /onboarding and /select-company retry acceptance from the cookie and
  redirect to the dashboard on success, making the auth callback's
  long-promised fallback real; with no cookie but a pending invitation,
  both surfaces show a 'join via the link in the invitation email' hint
  instead of silently asking the invitee to create a company.
- No new accept path without the token: the hint deliberately points
  back to the mailed link, so mailbox possession stays required and no
  company name is leaked to unverified emails.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:36:09 +02:00
Mattsson d3c37dac08 feat(stripe): enable Stripe integration and update UI components (#1159)
* feat(stripe): enable Stripe integration and update UI components

* refactor(import): streamline user ID and sandbox status fetching logic
2026-07-24 19:19:49 +02:00
Jakob Wennberg cb7b31819b feat(vat): Momsdeklaration as Stegen - stepper, flat steps, house toolbar (#1154)
* feat(vat): Momsdeklaration as Stegen (horizontal stepper, one step at a time)

The founder-picked concept variant for Moms: the four pipeline sections
(kontrollera, granska, bokfor, lamna in) become a clickable horizontal
stepper with honest per-step status subs (fel/varningar from the
pre-flight checks, att betala/aterfa from ruta 49, bokford/utkast lifted
from the settlement proposal via a new optional onStatus callback on
VatBookingCard) and one step's content rendered at a time with quiet
Nasta-links. Errors land on step 1, otherwise Granska. SkatteverketPanel
moves inside steg 4 next to the manual filing card when a declaration
exists; the no-data states keep it standalone. A period switch resets the
step choice. All checks, booking, drilldowns, exports and the SKV submit
flow are untouched.

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

* polish(vat): de-box the Stegen step contents to the concept language

Founder review: the stepper was new but the step contents kept the old
card chrome. Now flat on the panel throughout: the period picker is a
quiet toolbar row (no card, no labels), Granska renders as a centered
document column with sans eyebrow group heads and ruta 49 as an
emphasized document foot, the pre-flight checks are hairline rows with
quiet Korrigera links instead of boxed alerts and outline buttons,
VatBookingCard and VatManualFilingCard lose their cards (notes become
flat muted/attn lines), and SkatteverketPanel's card shells become flat
sections with sans uppercase eyebrows. All logic, flows and dialogs
untouched.

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

* polish(vat): black Exportera, no page-level Fraga Anna, clear Nasta buttons

Founder feedback on Stegen: the toolbar keeps only Exportera and it
wears the primary pill (ReportExportMenu gains an optional variant prop,
outline stays the default everywhere else); the AgentSparkleButton
leaves the page (the global assistant bubble remains); the Nasta step
links become white outline pill buttons so the forward path reads as
clearly as the export action. All other buttons on the page already
follow the house variants (primary/outline/ghost/quiet links).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:32:53 +02:00
Jakob Wennberg 5c42852d7e fix(skatteverket): classify 400 expired-refresh-token as SESSION_EXPIRED (#1151)
The AGI kvittenser cron error-logged 'Skatteverket token refresh failed
(400): access_denied / Refresh Token status is expired' every 15 minutes
(19+ times on 2026-07-24 alone) and kept re-attempting the dead token
against SKV forever. Only the 404 id_not_found dialect of a dead refresh
session was classified as SESSION_EXPIRED; the 400 variants escaped as
raw Errors, so the cron's needs_reconsent flagging never ran.

Extend the classifier in refreshTokenForUser to also match the 400
'Refresh Token status is expired' body and OAuth2's standard 400
invalid_grant. With a typed SESSION_EXPIRED, the cron marks the token
row needs_reconsent on the first failure and resolveReadAuth skips the
connection on every later run until the user reconnects via BankID.

Config-shaped 400s (invalid_client, invalid_scope) deliberately stay
raw errors: a reconnect cannot fix those, and mislabeling them would
re-create the self-perpetuating reconnect banner from the 2026-07
MISSING_SCOPE incident.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:06:02 +02:00
Jakob Wennberg d0fb72dc63 feat(kpi): Nyckeltal as Instrumentbrädan — instrument panes, monthly bars, cost list (#1148)
* feat(kpi): Nyckeltal as Berattelsen (serif month hero + metric rail + quiet cost rows)

The founder-picked concept variant: the month's result as a serif hero
with a +/- delta sentence against the previous month, a single sage net
area chart (income/expenses ride in the hover tooltip), and a hairline
metric rail on the right still driven by the user's KPI preferences
(Anpassa, formula tooltips, all seven definitions supported). The cost
story renders as quiet bar rows: expense classes 4xxx-7xxx and top five
suppliers. Replaces the four-tile + three-Recharts-card layout;
KPIHeroCards, KPITrendChart, KPIExpenseMixChart and KPITopSuppliersChart
are deleted. FyPicker replaces FiscalYearSelector; help behind ?.
No API changes: everything derives from the existing KPIReport.

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

* feat(kpi): switch Nyckeltal to Instrumentbradan (founder pick v2)

Berattelsen replaced by the instrument-pane grid on founder review:
monthly result bars as plain SVG (muted months, latest in sage or
terracotta when negative, compact endpoint label, per-bar tooltips)
plus one bordered pane per visible preference KPI, with the
receivables pane carrying a two-segment not-due/overdue strip. The
cost story rows below are unchanged. Recharts leaves this page
entirely (KPIResultChart deleted). Anpassa, formula tooltips and all
seven KPI definitions still supported.

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

* feat(kpi): concept-true cost list and cash runway note

Founder review against the concept: the report now carries
topExpenseAccounts (top five BAS 4-7 accounts for the period, computed
from the trial-balance rows the route already holds) and the page
renders them as the full-width Storsta kostnaderna rows with account
numbers, exactly like the concept. The Kassa pane derives its 'Tacker
cirka N dagars utgifter' note from the period's daily burn so far.
Class-composition and supplier columns leave the UI (data stays on the
API). Route test extended for the new field.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:43:42 +02:00
Jakob Wennberg 6911f657e9 feat(reports): catalog as one dry table with band groups and Senast öppnad (#1147)
The founder-picked Tabellen variant from the rest-of-nav 2 concept:
the report library becomes a single dry table where band rows carry the
accounting taxonomy, each report is one clickable line with its
description in muted ink, and a Senast oppnad column replaces the
recents shelf (RecentReportsShelf deleted). useRecentReports now stores
slug+timestamp pairs (legacy plain-slug entries parse as undated).
FiscalYearSelector swaps to the house FyPicker chip, help moves behind
the ? popover, catalog footnote as pgnote. Entity gating, dimension
gating, route-owning reports and the persisted FY choice all unchanged.

9341 tests pass, lint clean, guards pass. sv+en keys added.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:41:09 +02:00
Jakob Wennberg 9dfa6c6708 feat(home): first-run block as a numbered four-step thread with partner marks (#1149)
* feat(home): first-run block as a numbered three-step thread with partner marks

The founder-picked stepped shape for 'Hur vill du komma igang?':
1 Fa in din bokforing (primary Flytta bokforingen + Fortnox/Visma/Bokio
marks + '+ SIE'; Starta fran borjan as an inline alternative that just
checks the step off), 2 Koppla banken (Enable Banking mark only),
3 Bygg din bokforingsassistent (Beta chip, no vendor logo). Dots walk
number -> filled active -> sage check; the persisted state machine is
unchanged, but choosing a path no longer auto-completes the setup:
the block retires when all three steps are done (or via Dolj).
DashboardContent's build-assistant hero now waits until the checklist
is gone so the assistant is not pitched twice. initial_setup i18n
rewritten for the stepped copy (sv+en), unused selected-state keys
removed.

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

* feat(home): Skatteverket as step three, assistant last, ticked steps collapse

Founder feedback on #1149: the thread is now four steps: 1 Fa in din
bokforing, 2 Koppla banken, 3 Anslut Skatteverket (with the SKV mark,
BankID authorize link; skipped entirely in builds without the
skatteverket extension), 4 Bygg din bokforingsassistent. A completed
step drops its description and actions and keeps only the checked
muted title, so the fresh-start pitch never lingers after the books
are in. The heading counts honestly ({count} steg) and completion now
requires all four steps.

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

* polish(home): one-line checklist header

Founder feedback: the title and sub-line said the same thing twice;
only '4 steg sa ar bokforingen igang' remains (Dolj stays beside it).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:37:51 +02:00
Mattsson 63fd5311ed Bug/tic unlink (#1153)
* fix(tic): allow BankID link/unlink without a company context

/bankid/link and /bankid/unlink are user-level actions, but the extension
dispatcher resolved an active company for them, so a zero-company user
(fresh BankID signup, pre-onboarding) got a 500 'No company context' when
managing the connection from /settings/account. Mark both routes
skipCompanyContext and resolve the caller in-handler via requireAuth(),
which preserves the dispatcher's MFA/AAL2 enforcement.

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

* fix(tic): return 409 account_exists instead of 500 on BankID signup with taken email

The signup guard pre-checked profiles.email, but the authoritative store
is auth.users: anonymized account tombstones (and any profile drift) hold
the email in auth.users while profiles.email is NULL. The guard missed,
createUser failed with email_exists (422), and the route surfaced a
dead-end 500 'Kunde inte skapa kontot. Forsok igen.' where retrying can
never succeed.

Drop the profiles pre-check and let createUser's own uniqueness check be
the guard: map email_exists to the existing 409 account_exists response
(Swedish message), which the register page already handles with a toast
and a redirect to login. Also removes the TOCTOU window between the old
pre-check and createUser.

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

* fix(account): actually scrub auth.users metadata on account deletion

The delete route passed user_metadata: {} / app_metadata: {} to
auth.admin.updateUserById assuming replace semantics, but GoTrue MERGES
metadata maps, so the wipe was a silent no-op: the ~100-year tombstone
kept the user's full name in raw_user_meta_data (verified on production
2026-07-24).

Move the scrub into anonymize_user_account (migration 20260724150000):
raw_user_meta_data is cleared entirely, raw_app_meta_data drops the
app-specific keys (bankid_linked, has_password) while GoTrue's
provider/providers stay, and auth.users.email is still retained as the
documented legitimate-interest tombstone. The migration also repairs
existing tombstones (guarded by profiles.anonymized_at). The route keeps
only the ban, which the DB function cannot set.

Migration content already applied to staging; pg-real test extended.

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

* docs: log BankID signup guard decision

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

* fix(account): address PR review findings on anonymize scrub

- anonymize_user_account now rejects repeat invocations against an
  already-anonymized tombstone (SQLSTATE P0002) instead of re-churning
  the scrubbed row
- note that the tombstone repair UPDATE runs atomically inside the
  migration transaction
- tic signup failure log hashes the email (sha256 prefix, matching the
  pnrHashPrefix pattern) instead of logging the raw address
- pg-real test for the double-invocation guard

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

* fix(anonymization): ensure raw_app_meta_data is not null before scrubbing keys

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:36:35 +02:00
Jakob Wennberg 213d611e54 feat(onboarding): journey PR D — wizard removal, /companies/new mode='add', searchable BankID picker (#1150)
* feat(onboarding): journey PR D — wizard removal, /companies/new mode='add', searchable BankID picker

Closes the onboarding migration (plan PR D). The journey is now the only
onboarding; the flag conditional is gone.

- /companies/new: server page rendering OnboardingJourney mode='add'
  (quiet escape link back to the app). Improvement over the old page: the
  add-company path now persists the TIC lookup snapshot too.
- Delete WelcomeOnboarding + Step1-4 + the three dead variants
  (Step2SectorSelection, Step3ExtensionSuggestions, Step4PreliminaryTax).
  onboarding-illustrations stays (backdrop uses it).
- BankIdCompanyPicker restyled to the journey's searchable list (founder
  decision: list at ANY count): filter with single-match Enter, roster
  rows with name/form/roll/orgnr, member companies under their own
  section opening directly. Contract unchanged: picks still route to
  /onboarding?org_number= and this page still makes zero TIC calls.
- i18n: prune 108 wizard-only onboarding keys and the whole companies_new
  namespace (no consumers left); add journey_cancel_add + three picker
  keys. sv and en in lockstep.

Verified: full vitest suite 9339 passed, eslint 0 errors, guards pass,
production build compiles with /onboarding, /companies/new and
/select-company routes.

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

* chore(onboarding): address PR review — drop unused hasExistingCompanies plumbing, prune stale select_company keys

Restores error_no_access/error_switch_failed (used via ternary inside
t(), which the pruner's regex missed); full pruned-key set re-verified
as bare strings against the whole codebase.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:03:18 +02:00
Mattsson 53e343ee92 Bug/invalid imports (#1146)
* feat: add Accounted MCP namespace

* fix(bookkeeping): stop flagging verifikat whose underlag lives on a referenced supplier invoice

The missing-underlag surfaces only accepted a document directly linked to
the entry, so payment verifikat for supplier invoices (doc on the
registration entry per design) and entries whose doc was pinned to the
bank transaction before matching were falsely flagged; opening the entry
showed the referenced doc and cleared the warning client-side, and it
came back on reload.

- verifikat_without_documents + transactions_without_documents now treat
  an entry as covered when a supplier invoice referencing it (registration
  or payment FK, or a supplier_invoice_payments row) carries a document
  anchored to a journal entry (BFL 5 kap 7 paragraf hänvisning till
  underlag; anchoring required because the WORM deletion guards key on
  document_attachments.journal_entry_id)
- match-supplier-invoice routes (dashboard + v1) propagate the
  transaction's pinned document onto the payment verifikat, mirroring the
  categorize route; migration backfills rows already written (open
  unlocked periods, company-guarded, never steals a linked doc)
- /api/documents/counts, the transactions-page badges, the bulk "Inget
  underlag krävs" count and the push-notification scheduler share the
  same reference-aware predicate, so every surface agrees with the RPC
- counts route validates journal_entry_ids as UUIDs (they are
  interpolated into a PostgREST or-filter)

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

* fix(transactions): align table columns flush with page edges

Collapse the checkbox gutter column to zero width and hang the
hover-revealed checkbox/expand chevron in the page margins, drop the
outer padding so DATUM sits flush left and STATUS flush right, and
tuck the overflow-menu dots under the middle of the STATUS header.
Applied to both the inbox and history tables so they stay identical.

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

* fix(arsredovisning): tie anlaggningstillgangar note to booked depreciation

The ARL 5:8 roll-forward note recomputed depreciation from its own
day-based linear formula (365.25/12 month length, non-inclusive day
count, linear only), drifting ~20 kr per year per asset from the
ledger-driven resultat- and balansrakning and misstating non-linear
methods entirely. Note figures now come from posted
depreciation_schedules rows (the same source disposeAsset reverses),
falling back to the engine's computeAnnualDepreciation when nothing is
posted; pre-onboarding opening balances iterate prior years through
the engine. Adds a note-vs-trial-balance tie-out warning (accounts
1000-1299, over 1 kr) surfaced before download.

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

* refactor(stripe): move connect and sync surface from settings to import page

Stripe's transaction feed is a continuous import source in the same
category as the PSD2 bank connection, so its connect/sync surface now
lives on the import page as a source card (mode=stripe), gated
"kommer snart" on hosted like before; self-hosted keeps the full panel.

- Import page: Stripe card after Koppla bank, renders the existing
  StripeSettingsPanel via the settings-panel registry
- OAuth callback and panel cleanup return to /import?mode=stripe
- Settings > Betalningar retired: nav item removed, route redirects,
  PaymentsSettingsContent deleted, legacy ?tab=payments mapped
- New import.stripe_* strings in sv+en; dead settings_nav.payments removed

Crons and sync logic unchanged; payment-link settings stay in the
invoicing section.

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

* fix(underlag): paginate missing-underlag cron and harden doc-surface queries

Resolve PR review findings on bug/invalid-imports:
- notification-scheduler: fetchAllRows on all 5 global reads; past 1000 rows
  the capped reads produced false "saknade underlag" notifications
- bulk-missing: LOOKUP_CHUNK 300->150 so the twice-embedded .or() id list
  stays under the PostgREST URL limit
- bulk-missing + transactions page: UUID-guard the .or()-interpolated id
  lists, matching documents/counts
- match-supplier-invoice (dashboard + v1): log documentId/journalEntryId on
  the non-fatal doc-link warning
- well-known/oauth-protected-resource: document the tool_namespace allow-list
- messages/en: reword stripe_description
- DECISIONS.md: record the asset ibAck tie-out and Tailwind !important calls

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

* fix(tic): convert registrationDate from Unix seconds to millisecond epoch in lookup and profile tests

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:03:50 +02:00
Jakob Wennberg 51ca574ca4 feat(onboarding): journey PR C — flow component + /onboarding swap behind flag (#1143)
* refactor(onboarding): extract first-year defaults + shared TIC lookup client (journey PR A)

First of four PRs replacing the onboarding wizard with the journey flow
(dev_docs/onboarding_migration_plan.md, local). No UI change.

- Move deriveFirstYearDefaults + parseStartMonthDay out of
  WelcomeOnboarding into lib/company/first-year-defaults.ts and unit-test
  them (11-vs-13-months boundary, UTC month seeding, malformed input).
- Add the missing computeFiscalPeriod unit tests (calendar year, brutet
  ar, first year short/extended, EF calendar-year rule, period names,
  BFL 3 kap. 6-18 month window errors).
- New shared fetchCompanyLookup() client: the single client path to the
  Lens-backed /lookup, typed outcomes (found / not_found / disabled /
  error / aborted), never throws. Fixes the 403/404 conflation: the
  dispatcher's 404 ("Extension not found") and feature-flag 503
  (EXTENSION_DISABLED) now degrade silently instead of rendering as
  "company not found"; only the TIC handler's own 404 does.
- Step2CompanyDetails consumes the helper; identical UX otherwise.

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

* feat(onboarding): journey state machine reducer with full branch coverage (journey PR B, 1/3)

Pure reducer for the journey onboarding: owns every transition and every
CompanySettings write; the component layer only renders steps, runs the
single TIC lookup, and calls the server action.

Encodes the plan's invariants: entry-snapshot history (Back rolls answers
AND stations), lookupRan gates fact-vs-question per field (BankID prefill
without lookup degrades to questions), vat_registered is never defaulted
without lookup data or an explicit answer, entity change wipes downstream,
org_number_invalid bounces to the Företaget station, station jumps rewind
to a station's first step.

34 unit tests: AB/EF found, not-found manual, ceased, BankID prefill
(found + degraded + disabled), first year, brutet år, moms nej, Back from
every step, station jumps, server-error bounces.

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

* feat(onboarding): journey visual primitives + sandbox gallery (journey PR B, 2/3)

Ports the founder-approved concept (artifact c82c9358) to React:

- JourneyOrb: 320-particle canvas sphere with comet travel, check morph
  and the monogram finale (glyph sampled live from --font-display). Own
  component per plan, NOT thinking-orbs. rAF pauses on document.hidden;
  reduced motion renders static frames.
- JourneyTrack: five stations with inked answers; completed stations are
  keyboard-accessible jump-back buttons; answers mirrored to an aria-live
  region.
- Question primitives: Question (ink title + "?" popover, Esc closes),
  ChipRow (fly-to-orb ghost), YearBand (springy fiscal-year preview),
  JourneyDatePicker (year -> month by name -> day), AddressFields
  (Enter-chained, skippable).
- journey.css: concept stylesheet namespaced under .jny on app tokens,
  incl. the no-scroll composition (100dvh + optical-centering balance
  spacer) and the dawn layer.
- /sandbox/journey: internal primitive gallery (auth-free sandbox path),
  demo data only: this page makes ZERO TIC calls.

i18n note: primitives are copy-agnostic (strings via props); the real
flow's sv/en keys land with their consumer in PR C.

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

* docs: log journey reducer location decision

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

* fix(onboarding): annotate ENTITY_PICKED settings as Partial<CompanySettings>

The wipeDownstream return narrows against the inferred initializer type;
tsc strict rejects the reassignment without the explicit annotation.

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

* feat(onboarding): journey flow component behind NEXT_PUBLIC_ONBOARDING_JOURNEY (journey PR C)

OnboardingJourney wires the PR B reducer and primitives into the real
flow and swaps /onboarding behind the flag (wizard remains the default).

Data + error handling parity with the wizard, structurally enforced:
- identical settings payload to createCompanyFromOnboarding (incl.
  ticLookup snapshot, derived vat_number, first-year fields through
  computeFiscalPeriod), same /api/log error logging, same
  org_number_invalid bounce (now to the Foretaget station), period
  validation before submit, generic failure -> retry on the method step.
- lookup degradation: disabled surface silent, transient error shows the
  advisory line; either way every fact the lookup could not provide is
  asked as a question (address, F-skatt, fiscal year, VAT).
- advisory dup check rides the org submit (internal endpoint, never
  blocks), rendered as a quiet fact-line note.

TIC budget: exactly ONE fetchCompanyLookup per confirmed orgnr: Enter on
the manual path, or the auto-submitted BankID deep link (which replaces
the wizard's preverified suppression per the plan addendum). No
debounce-per-keystroke; the journey strictly reduces Lens volume.

Finale per the approved concept: narrated real server steps while the
action runs, check morph, company-initial monogram, Foretagsprofil card,
conditional notes, dawn progression; sv+en strings (128 keys each).

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

* chore: trigger preview with NEXT_PUBLIC_ONBOARDING_JOURNEY=true (branch-scoped)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:33:39 +02:00
Jakob Wennberg b771c1f923 feat(onboarding): journey PR B — reducer, orb, track, question primitives (behind /sandbox demo) (#1145)
* refactor(onboarding): extract first-year defaults + shared TIC lookup client (journey PR A)

First of four PRs replacing the onboarding wizard with the journey flow
(dev_docs/onboarding_migration_plan.md, local). No UI change.

- Move deriveFirstYearDefaults + parseStartMonthDay out of
  WelcomeOnboarding into lib/company/first-year-defaults.ts and unit-test
  them (11-vs-13-months boundary, UTC month seeding, malformed input).
- Add the missing computeFiscalPeriod unit tests (calendar year, brutet
  ar, first year short/extended, EF calendar-year rule, period names,
  BFL 3 kap. 6-18 month window errors).
- New shared fetchCompanyLookup() client: the single client path to the
  Lens-backed /lookup, typed outcomes (found / not_found / disabled /
  error / aborted), never throws. Fixes the 403/404 conflation: the
  dispatcher's 404 ("Extension not found") and feature-flag 503
  (EXTENSION_DISABLED) now degrade silently instead of rendering as
  "company not found"; only the TIC handler's own 404 does.
- Step2CompanyDetails consumes the helper; identical UX otherwise.

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

* feat(onboarding): journey state machine reducer with full branch coverage (journey PR B, 1/3)

Pure reducer for the journey onboarding: owns every transition and every
CompanySettings write; the component layer only renders steps, runs the
single TIC lookup, and calls the server action.

Encodes the plan's invariants: entry-snapshot history (Back rolls answers
AND stations), lookupRan gates fact-vs-question per field (BankID prefill
without lookup degrades to questions), vat_registered is never defaulted
without lookup data or an explicit answer, entity change wipes downstream,
org_number_invalid bounces to the Företaget station, station jumps rewind
to a station's first step.

34 unit tests: AB/EF found, not-found manual, ceased, BankID prefill
(found + degraded + disabled), first year, brutet år, moms nej, Back from
every step, station jumps, server-error bounces.

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

* feat(onboarding): journey visual primitives + sandbox gallery (journey PR B, 2/3)

Ports the founder-approved concept (artifact c82c9358) to React:

- JourneyOrb: 320-particle canvas sphere with comet travel, check morph
  and the monogram finale (glyph sampled live from --font-display). Own
  component per plan, NOT thinking-orbs. rAF pauses on document.hidden;
  reduced motion renders static frames.
- JourneyTrack: five stations with inked answers; completed stations are
  keyboard-accessible jump-back buttons; answers mirrored to an aria-live
  region.
- Question primitives: Question (ink title + "?" popover, Esc closes),
  ChipRow (fly-to-orb ghost), YearBand (springy fiscal-year preview),
  JourneyDatePicker (year -> month by name -> day), AddressFields
  (Enter-chained, skippable).
- journey.css: concept stylesheet namespaced under .jny on app tokens,
  incl. the no-scroll composition (100dvh + optical-centering balance
  spacer) and the dawn layer.
- /sandbox/journey: internal primitive gallery (auth-free sandbox path),
  demo data only: this page makes ZERO TIC calls.

i18n note: primitives are copy-agnostic (strings via props); the real
flow's sv/en keys land with their consumer in PR C.

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

* docs: log journey reducer location decision

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

* fix(onboarding): annotate ENTITY_PICKED settings as Partial<CompanySettings>

The wipeDownstream return narrows against the inferred initializer type;
tsc strict rejects the reassignment without the explicit annotation.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:32:45 +02:00
Jakob Wennberg f9ef8913ae refactor(onboarding): extract first-year defaults + shared TIC lookup client (journey PR A) (#1141)
First of four PRs replacing the onboarding wizard with the journey flow
(dev_docs/onboarding_migration_plan.md, local). No UI change.

- Move deriveFirstYearDefaults + parseStartMonthDay out of
  WelcomeOnboarding into lib/company/first-year-defaults.ts and unit-test
  them (11-vs-13-months boundary, UTC month seeding, malformed input).
- Add the missing computeFiscalPeriod unit tests (calendar year, brutet
  ar, first year short/extended, EF calendar-year rule, period names,
  BFL 3 kap. 6-18 month window errors).
- New shared fetchCompanyLookup() client: the single client path to the
  Lens-backed /lookup, typed outcomes (found / not_found / disabled /
  error / aborted), never throws. Fixes the 403/404 conflation: the
  dispatcher's 404 ("Extension not found") and feature-flag 503
  (EXTENSION_DISABLED) now degrade silently instead of rendering as
  "company not found"; only the TIC handler's own 404 does.
- Step2CompanyDetails consumes the helper; identical UX otherwise.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:29:56 +02:00
Jakob Wennberg 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
Mattsson 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 Wennberg be9d630347 feat(home): concept Hem with Att göra + Fortsätt (UI migration PR 11) (#1132)
* feat(home): concept scene 14: greeting + Att gora/Fortsatt panes

Hem becomes the founder-approved two-panel layout: serif time-of-day
greeting with date and company, the Att gora worklist restyled to the
concept pane (eyebrow header, h-rows with count chips, hover chevrons)
and a new Fortsatt pane listing in-progress work derived purely from
draft state (lib/worklist/resume: journal drafts, invoice drafts/unsent,
mid-lifecycle salary runs; deadline boost, cap 3, tested). A completed
flow can never render as a resume row by construction: only draft-state
rows are fetched. KPI tiles, revenue/expense cards and the deadline/tax
widgets leave the page per dev_docs/last_session_resume.md section 8,
which also prunes their fetches (journal-line YTD aggregation, unpaid
totals, deadlines): the page got faster. Banners, checklist,
build-assistant hero and the Skatteverket nudge survive.

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

* fix(home): serif pane titles for Att gora and Fortsatt

Founder feedback: the uppercase eyebrow headers read as a stray font.
Both pane titles are now the Hedvig display serif (text-lg) over the
hairline, matching the page's heading language; the band headers inside
Att gora keep their small uppercase form as grouping devices.

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

* fix(home): Geist pane titles for Att gora and Fortsatt

Founder call: the pane titles use the body sans (14px medium), not the
display serif.

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

* fix(ui): Geist section headers + drop stale-transactions chip

Founder feedback: pane/section headers (Att gora, Fortsatt, the reports
groups Lopande/Bokslut/Skatt & moms etc) render in Geist sentence case
instead of uppercase eyebrows or serif. The global h1-h3 display-font
rule moves into @layer base so utility classes like font-sans can
actually override it (unlayered element rules beat Tailwind's layered
utilities: this was silently eating the override). Also removes the
'N aldre an 14 dagar' chip from the Bokfora transaktioner row and its
stale-count plumbing.

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

* feat(ui): continuous nav crossfade + floating slide-over entrance

The rail/full nav states now stay mounted and crossfade past each other
(the inactive layer absolute, faded, nudged sideways, inert) while the
aside width animates: the switch reads as one continuous motion instead
of a DOM swap. The detail slide-over floats in from the right edge
(slide-in-from-right-full, 300ms decelerating curve) per the concept,
with a quicker ease-in exit.

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

* fix(ui): make transitions and enter/exit animations actually run

Two silent app-wide animation killers found while chasing 'the nav still
is not smooth':

1. The codebase uses the shadcn animate-in/out vocabulary everywhere but
   no animate plugin was ever installed: Tailwind v4 silently dropped
   every such class, so popovers, dialogs, menus and the slide-over all
   appeared instantly. globals.css now defines the exact subset in use
   (accEnter/accExit keyframes + var-driven utilities), plugin-free,
   composing with duration/ease via --tw-duration/--tw-ease and
   collapsing under prefers-reduced-motion. Dialog drops its
   bracket-variant slide classes (zoom+fade carries the entrance).

2. The scrollbar auto-hide block's universal '* { transition:
   scrollbar-color ... }' was unlayered, and unlayered rules beat
   Tailwind's layered transition-* utilities regardless of specificity:
   every width/margin/color transition in the app was dead. The rule now
   lives in @layer base. Verified: the aside animates 248->64 over 300ms
   and the slide-over runs accEnter at 0.3s with the decelerating curve.

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

* fix(ui): finish the rr-mask session-replay masking sweep

Main's #1105 switched one amount cell from the no-op sensitive-field
class to rr-mask (rrweb's built-in text-masking class). The reskinned
tables introduced more sensitive-field cells; all 12 occurrences now use
rr-mask so financial amounts are masked in session replays.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:36:18 +02:00
Jakob Wennberg 4352862845 feat(register): concept scenes 25-31 for the seven register pages (#1131)
Kunder, Leverantorer, Artiklar, Anstallda, Anlaggningstillgangar,
Kontoplan and Kostnadsstallen & projekt all move to the house language:
serif header with quiet secondary actions + primary pill, dry-table lists
(TH/TD from components/ui/dry-table, stagger-enter, clickable rows,
responsive column hiding), muted text for normal states with chips only
for exceptions (Verifierad, Inaktiv, Arkiverad, Avyttrad), and a muted
pgnote count footer. Kontoplan gets the scene 30 treatment: BAS 2026 chip,
Mina konton/BAS-katalogen seg with counts, class band rows as folds, and
the activation toggles preserved. Dimensions swaps its tabs for the house
seg with per-dimension counts and Tagga historik as a quiet header link.
Separate mobile card lists and Card wrappers are gone; all dialogs, sort
params, export menus, gating and load-more behavior preserved.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:24:25 +02:00
Jakob Wennberg 2175eccddd feat(salary): concept Löner landing + nav collapse animation (UI migration PR 9) (#1130)
* feat(salary): concept scene 22 landing + smoother nav collapse

Loner gets the concept header (quiet Anstallda link + Starta lonekorning
primary), the hero de-cards to a flat serif line + description + pill,
and the runs list becomes the scene 22 dry-table (Period, Status,
Anstallda, Bruttolon, Netto) where booked runs read as muted text and
in-flight runs carry a chip plus the payout date. Attention cards and the
hero state machine survive unchanged.

Nav polish: the collapse now animates as one movement (300ms decelerating
curve on both the aside width and the panel margin) and the rail/full
contents slide+fade in on swap; the Register/Bokslut folds match the
RowFoldout timing.

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

* refactor(salary): strip hero and attention cards from the landing

Founder direction: the Loner landing is header + lonekorningar only
(concept scene 22). The open run's table row is the entry point; AGI,
skatt, blockers and semester surfaces live on the run detail and the
employee register. Drops the hero state machine, the four cards, and all
their data plumbing (deadlines query, tax-payment chain, SKV status,
AGI submission hook).

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

* fix(salary): drop the Lonekorningar eyebrow

Founder feedback: the uppercase eyebrow read as a stray font above the
table, and with the landing reduced to the runs table alone the label is
redundant under the Loner page title.

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

* fix(salary): populate the Anstallda column via a count embed

The runs list selected bare salary_runs rows, so the landing's Anstallda
column was always empty (the employees array only ever existed on the
detail response). The list now embeds employee_count via
salary_run_employees(count) and the page reads the PostgREST count shape.

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

* fix(salary): quiet beige chip for in-flight run states

Founder feedback vs the concept: the Granskning chip rendered as the
bordered ochre warning badge and read noisy next to the concept's quiet
Utkast chip. Draft/review/approved now all wear the beige secondary chip;
the payout-date note carries the urgency.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:17:20 +02:00
Jakob Wennberg 658c274d97 feat(supplier-invoices): concept supplier-invoice list (UI migration PR 8) (#1129)
* feat(supplier-invoices): concept scene 21 for the list

Leverantorsfakturor becomes the concept dry-table: Leverantor, Fakturanr,
Fakturadatum, Forfaller, Belopp, Kvar att betala and one status chip per
row (Registrerad outline, Godkand beige, Betald + datum sage). Tabs turn
into the seg [Alla/Registrerade (n)/Godkanda/Att betala (n)/Betalda] with
search and the FyPicker far right. Godkann (attest) is a hover-revealed
quiet action on registered rows; there is deliberately no mark-as-paid
button (payments reconcile via bank matching), which the new help popover
after the H1 explains. Arrival number moves to the detail page.

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

* feat(supplier-invoices): status views behind one chip-picker

Same founder direction as Kundfakturor: the five-tab seg collapses into a
single status ContextPicker chip with Registrerade/Att betala counts on
the trigger and as row annotations.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:09:16 +02:00
Jakob Wennberg 323abc57fe feat(invoices): concept customer-invoice list (UI migration PR 7) (#1128)
* feat(invoices): concept scene 15 for the customer invoice list

Kundfakturor becomes the concept dry-table: Nr, Kund, Forfaller, Belopp
and ONE derivable status chip per row (Utkast/Ej skickad/Skickad/Forfallen
N dgr/Betald datum/Delbetald/Kredit/Makulerad). The eight tabs collapse to
the seg [Alla/Obetalda/Forfallna (n)/Utkast] with the low-frequency views
(Proforma, Foljesedlar, Kreditfakturor, Makulerade) behind a quiet
'Fler ...' menu, plus search and the FyPicker far right. Create paths
collapse into the Ny faktura split button (Ny/Aterkommande/Sjalvfaktura,
last-used mode persisted); sjalvfaktura preselects the editor's tab via
?new=1&self=1. The summary line under the header is dropped per the plan.

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

* feat(invoices): status views behind one chip-picker

Founder direction: the Alla/Obetalda/Forfallna/Utkast seg and the
'Fler ...' menu collapse into a single status ContextPicker chip with the
overdue count on the trigger and as a row annotation. All eight views,
same filtering semantics.

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

* fix(invoices): audit round: Betalda view, viewer states, deep links

Regression-audit fixes: the Betalda view returns as a status-picker row
(it existed on main and was dropped in the seg redesign); the create
options render disabled with the viewer tooltip for read-only members
instead of silently no-opping; and the page answers ?status=/?tab= deep
links again (the Att gora worklist links /invoices?status=unpaid).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:59:46 +02:00
Jakob Wennberg 008b4710d3 feat(granskning): concept review queue (UI migration PR 6) (#1126)
* feat(granskning): concept scene 11 for the review queue

Reskins /pending to the concept: serif header with a Godkann alla primary,
Vantar/Godkanda/Avvisade seg with a count chip on the queue, source filter
as the far-right context picker, and op rows in the concept's row language
(source line, title, risk chip, inline Godkann pill + quiet Avvisa, hover
checkbox). Row click opens the detail as a right slide-over (convention 13)
with the operation preview, period-lock banner, and approve/reject in the
footer. The select-all header becomes the standard bulkbar that pops in on
first selection, carrying the type quick-picks as quiet links. All existing
functionality is preserved: bulk approve/reject with confirm dialogs,
rejection category + feedback, high-risk warnings, period-lock gating,
conversation deep-link filter, realtime refetch, auto-expired markers.

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

* fix(bookkeeping): concept seg styling for the Verifikat/Utkast toggle

The toggle used the old bordered container with a beige active tab: the
inverse of the concept seg used on every other page (muted container,
card-white active with hairline border). Founder feedback.

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

* feat(granskning): match concept scene 11 row language

Founder feedback against the rendered concept: the seg becomes
Vantar/Historik (Historik merges committed + rejected via two parallel
fetches, newest resolution first, distinguished by Godkand/Avvisad chips
with the rejection reason in the sub line); op rows get the concept's
actor circle with a thread line, uppercase source line, and the three
tinted action pills under the text (Godkann sage, Avvisa terracotta,
Visa detaljer neutral) instead of right-aligned hover actions. Risk chip
stays on the right edge; hover checkboxes and the bulkbar are unchanged.

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

* fix(granskning): curved actor thread + boxed, legible detail panel

Founder feedback vs the rendered concept: the thread under the actor icon
now elbows toward the action row (rounded border corner) instead of a
straight line, and the slide-over frames the operation in its own bordered
box with the warning/rejection notes as separate boxed lines. preview_data
arrays shaped like a kontering (account/debit/credit rows, e.g.
preview_lines) now render as a proper Konto/Beskrivning/Debet/Kredit table
instead of the generic '3 rader' dump, so the panel actually says what the
agent is about to post.

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

* fix(granskning): carry #842 failed_partial rendering through the rework

The failed_partial status (irreversible posting followed by a failed
step) landed on main while the review-queue rework was in flight; the
Historik rows and the detail slide-over now render its warning chip,
explanation, and posted_ids like main did.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:51:41 +02:00
Jakob Wennberg 7a6f9dd167 fix(transactions): reconcile #1105's lifted source filter with the account picker (#1134)
The history view now takes the shared, persisted source filter as props
(main's design), with the page narrowing per-account selections to the
bank bucket for the history split. Removes the duplicate local type/state
left by the rebase.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:50:22 +02:00
Jakob Wennberg 1ccf7ab81f feat(transactions): concept shell and row language (UI migration PR 5) (#1124)
* feat(transactions): concept shell for scene 10 (header, toolbar, footer)

- Header: title + Importera SplitButton (import guide / manual entry,
  last-used mode persisted via ui_state.create_mode.transactions),
  replacing the three-button row
- Toolbar in concept order: [Att bokföra/Alla seg with count chip]
  [search] [Välj flera as quiet toggle] ... [source ContextPicker far
  right merging the old in-list source dropdown (bank/Skatteverket)]
- Bank sync status + sync-now + Bankavstämning move to the concept's
  footer status line together with the honest visible-row counter
  ("n att hantera")
- skv-reconnect banner and all dialogs/actions untouched

Row cards -> concept dry-table conversion follows separately.

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

* feat(transactions): concept row language for the inbox cards

Rows read like the scene 10 table: date as the first tabular column,
description on one line, amount right (+income in sage), auto-detected
invoice matches as beige suggestion chips instead of primary pills,
tighter hairline rows at 13px. Every action unchanged: Bokför pill,
match/assistant/overflow menu, batch checkboxes, exit animations,
extraction status.

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

* refactor(transactions): de-bloat rows and chrome to match the concept

- Rows carry only the essentials: date, description, amount, Bokför and
  one overflow menu. The inline match and ask-assistant icon buttons
  fold into the overflow menu (all actions still one click away)
- The "n nya banktransaktioner sen ditt senaste besök" banner is removed;
  the footer sync line covers it
- The Skatteverket reconnect banner becomes the one-ochre-sentence
  AttnLine with an inline action (convention 6)

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

* refactor(ui): extract dry-table primitives into components/ui/dry-table

The concept table styles (TH/TD/VTH/VTD classes, quiet links, RowFoldout)
move out of JournalEntryList so the transaction inbox can render the exact
same table language.

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

* feat(transactions): Synka bank nu in the Importera menu

Extracts the per-connection sync/reconnect logic into a shared useBankSync()
hook (BankSyncNowButton keeps using it) and adds the concept's first menu row
to the Importera split button: sync all active connections, with the
last-synced age as the row description. Hidden without a connection or the
bank_sync capability; the footer button stays the gated conversion surface.

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

* feat(transactions): per-account source picker with balances

The context chip becomes the concept's account chooser: always visible in
inbox mode, one row per enabled cash account (PSD2 balance annotation),
an Övriga bucket for bank rows without a registered account, and a
Skattekonto row annotated with the cached saldo. The trigger reads
'Alla källor · <sum>' where the sum covers SEK ledgers + skattekonto.

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

* feat(transactions): concept dry-table for the inbox

The inbox list becomes the exact Bokforing table: borderless dry-table with
hover-revealed checkboxes and chevrons, one-line rows (date, description,
amount, quiet primary pill + overflow + foldout), and a RowFoldout expansion
carrying the row detail and the full action set as quiet links. Skattekonto
rows render as chip-marked table rows with inline actions. The 'Valj flera'
toggle and the floating batch bar are replaced by the concept bulkbar that
pops in above the table once a row is selected.

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

* fix(i18n): singular form for bulkbar selection count

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

* feat(transactions): suggestion-first Bokfor flow (concept scene 10)

Bokfor on a row with a suggestion now goes straight to the compact
kontering confirm (QuickReview) showing the proposed verifikat; the full
template picker becomes the fallback and the 'Byt mall' path. Rows carry
the concept's 'Forslag: <mall>' chip (with D/K in the tooltip and the
foldout meta). The picker dialog is de-bloated: alternate paths (manuellt,
matcha, ignorera) collapse into quiet links. Modal scroll containers
(DialogContent, SlideOverBody) keep a visible scrollbar thumb: with the
app-wide auto-hide, a long dialog read as cut off at the fold.

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

* refactor(transactions): drop suggestion UI and foldout action links

Founder direction: no booking suggestions in the list for now (logic comes
later), and the foldout duplicated the overflow menu: the three dots are
the clearer surface. The foldout keeps row detail only (FX conversion,
original bank name, 1930/1630 hint, extraction status) and rows without
any detail no longer expand. The Skattekonto picker row drops its saldo
annotation; the Alla kallor sum covers bank ledgers only.

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

* feat(transactions): dry-table for the Alla view + pill chips app-wide

The history list joins the inbox's table language: Datum, Beskrivning,
Belopp, Status columns with Bokford as muted text + Visa verifikat quiet
link (normal state) and Ej bokford as the beige exception chip with an
inline Bokfor pill; the business/private tabs become the house seg and the
KALLA header row becomes the standard chip-picker. Skattekonto rows get
the same treatment. SEK conversion moves to a tooltip (one-line rows).

Badge itself becomes the concept chip: pill radius (99px), 11.5px, quiet
padding, replacing the boxy rounded-md look the founder flagged on the
Underlag saknas badge.

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

* fix(ui): audit round: viewer-disabled split options, touch fallback

Regression-audit fixes: SplitButton options support disabled+disabledTitle
so viewer-gated create paths render inert with the viewer tooltip instead
of silently no-opping (wired on Ny transaktion); the history list gets its
Kopplad till faktura indicator back; dead transactionsWithMatches memo
removed; and coarse-pointer devices now always show hover-revealed
controls (checkboxes, chevrons, quiet row actions) since touch has no
hover.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:41:45 +02:00
Jakob Wennberg 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 Wennberg 5b5ee8e429 feat(ui): shared migration primitives (UI migration PR 3) (#1122)
* feat(ui): shared migration primitives (UI migration PR 3)

The component kit every page migration (PR 4-8) builds on:

- ContextPicker: the one-per-page chip-dropdown context scope (convention
  8), right-aligned popover with checks and muted annotations
- FyPicker: fiscal-year picker on ContextPicker with the same controlled
  API and per-company localStorage key as FiscalYearSelector, which it
  replaces page by page from PR 4
- SplitButton: primary + caret menu, last-used mode persisted per user
  via ui_state.create_mode (lib/ui-state/client, unit-tested); nav
  persistence refactored onto the same helper
- ConfirmDialog: centered min-460px confirm-up-front dialog (convention
  10) with pending state on an awaitable onConfirm
- HelpPopover: 17px "?" after the H1 opening an anchored popover
  (convention 7); PageHeader gets a `help` slot
- AttnLine: the one-ochre-sentence attention pattern (convention 6) with
  optional inline action; new AA-safe --attn token pair
- RowStatus: chips-mark-exceptions helper (convention 5)
- SlideOver: right review panel, 480px, 18px inset, rounded, veil + Esc
  (convention 13), with header kicker / body / footer slots
- Stagger: .stagger-enter applied to the five target pages' list
  containers (bookkeeping, transactions, pending, invoices,
  supplier-invoices); structural loading.tsx added for supplier-invoices,
  customers, kpi, pending, deadlines

No page adopts the new pickers/dialogs yet: that is PR 4-8, one page per
PR against this kit.

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

* fix(ui): FyPicker chip must not double the Rakenskapsar label

Real fiscal periods are often named "Rakenskapsar 2026" already; only
prefix the label when the period name lacks it.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:29:36 +02:00
Jakob Wennberg d59e4708cf feat(nav): concept sidebar with folds, collapse rail, and user menu (UI migration PR 2) (#1133)
* feat(nav): concept sidebar with folds, collapse rail, and user menu (UI migration PR 2)

The concept's navigation, exactly, with all current functionality kept:

- Groups restructured per concept: top (Hem, Assistenten), ARBETA
  (Bokforing, Underlag, Transaktioner, Granskning, Kundfakturor,
  Leverantorsfakturor, Loner), ANALYS, DATA (Register fold +
  Importera/exportera), SKATT & BOKSLUT (Moms, Skattekonto, Viktiga
  datum, Bokslut fold). Entity/capability/dimension gating unchanged.
- Register and Bokslut are animated folds (grid-rows 0fr/1fr), children
  text-indented behind a hairline; closed by default, forced open by an
  active child route; state persists per user.
- Sidebar collapses to a 64px icon rail (toggle top of rail); width is
  one inline --nav-w CSS variable on #dash-shell that aside and <main>
  both read, so the panel follows in lockstep. Server-rendered from
  ui_state so first paint is right.
- Sticky bottom user block (avatar, name, active company) opening an
  upward user menu: identity, company-switcher flyout (search + building
  glyphs + roles + check, real switch mechanism via shared
  lib/company/switch-client), Installningar, Medlemmar och roller,
  Abonnemang, Hjalp, support, terracotta logout. Trial touchpoint kept.
- CompanySwitcher removed from desktop top (lives in the user menu now);
  mobile bottom nav + sheet unchanged.
- New migration 20260723120000: user_preferences.ui_state jsonb bag
  (founder-approved) + POST /api/user/ui-state (requireAuth, strict zod,
  merge semantics) with route tests.
- i18n: fold/collapse/menu keys added sv+en; deadlines -> "Viktiga
  datum", year_end -> "Arsbokslut" per concept.

Discord-community row deferred: no invite URL exists in the repo.
Badges stay the current two (Transaktioner, Granskning); an Underlag
count is a follow-up with lib/worklist.

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

* fix(nav): brand-mark sidebar header + auto-hiding scrollbars

Concept alignment feedback: the sidebar gets a header row (brand mark
left, collapse toggle right) hanging from the same top line as the
panel, instead of a lone right-aligned toggle.

Scrollbars go overlay-style app-wide: transparent at rest, revealed only
while their container scrolls (ScrollbarReveal stamps .is-scrolling via
one capture-phase document listener), fading out after 700ms idle. The
gutter stays reserved so revealing never shifts layout.

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

* fix(nav): company flyout opens downward + Discord community row

The flyout was bottom-anchored to its row and grew upward over the menu;
founder feedback: top-align with the row and grow downward. Adds the
Discord community row to the user menu (external invite link).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 21:28:07 +02:00
Jakob Wennberg 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 288915c152 Fix/fdb fr usrs (#1125)
* fix(invoices): return attachment filename in delivery history summaries

The 20260723003000 hardening dropped attachment_filename from
list_invoice_delivery_summaries, so the delivery history UI always fell
back to the generic "faktura.pdf" label. Recreate the RPC with the
filename included: it is derived from company name, customer name,
invoice number, and date, all already visible to every company member,
so the minimization boundary is unchanged. Addresses stay masked and
message content, BCC, and checksums stay server-side.

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

* fix(reconciliation): surface own-account transfer legs in match-to-voucher by default

The second (incoming) leg of a transfer between two of the company's own
bank accounts was hidden in the 'Matcha mot befintlig verifikation' dialog
because the voucher counted as 'already matched' once its outgoing leg was
linked, even though the incoming account's line had no settling transaction.
Users read the empty default list as 'the app won't let me link this'.

get_account_gl_lines_for_matching now counts links per settlement account:
a transaction provably on another cash account no longer marks the voucher
as matched for the requested account, so the unsettled transfer leg surfaces
by default (and auto-selects on an exact match). Same-account N:1 stays
behind the 'Visa aven matchade verifikationer' opt-in, and transactions
without a resolvable cash account conservatively keep counting everywhere.
get_unlinked_gl_lines is deliberately untouched (feeds auto-reconcile).

Companion guard: mark_entry_as_opening_balance now refuses entries with
linked bank transactions, since half-settled transfer vouchers became
reachable in the reconciliation view's unmatched table where 'Mark som IB'
renders; re-tagging one would strand its transaction against a movement-
excluded entry. getReconciliationStatus counts unmatched GL lines with the
account-scoped RPC so the status card agrees with the table.

Fixes #1026

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

* perf(api): cut prod p95 latency via local JWT auth, single-RT company resolution, and report aggregate RPCs

Baseline 2026-07-23 (487 prod samples): p50 160ms, p95 480ms, 13% of
requests over 300ms. Target: p95 under 300ms.

- requireAuth: verify JWTs locally via getClaims (ES256/JWKS) instead of
  a second network getUser per request; getUser fallback keeps HS256
  self-hosted and existing test mocks working; middleware still
  revocation-checks every /api request
- resolve_active_company RPC (20260723161000): one round trip replaces
  2-3 queries in getActiveCompanyId and middleware; PGRST202/42501 fall
  back to the legacy query path
- arsredovisning build-data: ~33 sequential round trips down to ~7,
  output byte-identical (snapshot-proven)
- currency rate route: stop bypassing the exchange_rates cache (missing
  supabase arg caused an external Riksbanken call on every request)
- document.get: parallelize row fetch, signed URL and audit event
- list_company_accounts RPC (20260723170000): accounts list in one round
  trip instead of paging past PostgREST's 1000-row cap
- vat-declaration route: drop a dead sequential company_settings query
- get_kpi_report_aggregates RPC (20260723180000): KPI report's three
  full-period line scans collapsed into one aggregate call; dimension-
  filtered path unchanged
- lint: fix 9 baseline errors, downgrade 4 react-hooks compiler rules to
  warn, zero the eslint baseline ratchet

All four gates green: lint 0 errors, 9163 tests, check:guards, build.
Migrations applied idempotently to staging only; prod receives them via
Supabase branching on merge.

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

* fix(review): resolve PR review findings across auth, VAT declaration, and IB retag

- requireAuth getClaims fast path: pin iss (project URL) and aud
  ('authenticated'), log every fallback to getUser (ASVS V9.1 finding)
- remove the ignored accountingMethod parameter from calculateVatDeclaration
  and the dead company_settings.accounting_method reads in xlsx/pdf/eskd
  routes; v1 API keeps accepting the query param but documents it as a no-op
- close the mark_entry_as_opening_balance TOCTOU race with a transactions
  trigger (20260723190000, FOR KEY SHARE on journal_entries) + pg tests;
  applied to staging and smoke-verified both directions
- re-add the 42501 tenant guard to branch-local migration 20260723160000
  (function body had silently reverted to the pre-20260619130100 definition)
- document the buildK3Noter tbFullRows full-TB contract (uppskjuten skatt
  opening balance per BFNAR 2012:1 ch.29)
- add KPI VAT-liability test covering reduced-rate output accounts 2621/2631

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

* fix(db): use NULL-safe caller_is_company_member in opening-balance retag guard

The re-added tenant guard carried the pre-20260703180000 raw
NOT IN (SELECT user_company_ids()) pattern, which the
null-safe-tenant-guards ratchet blocks. Staging re-synced.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:16:55 +02:00
Mattsson 43f7ccab9e feat(invoices): allow BAS class 1-3 posting-account overrides and complete the aktiekapital note (#1121)
- invoice/article posting-account overrides accept active class 1-3 accounts;
  class 1-2 (balance-sheet) accounts are rejected on VAT-bearing lines so the
  ruta 05 tax base always books to a 3xxx account
- shared posting-account regex across server schemas, pending-operation
  re-validation, and client forms
- share-capital settings (aktiekapital/antal_aktier) feed the annual-report
  note; kvotvarde derived per ABL 1 kap 6 $; all-or-nothing pair constraint
- signed per-rate VAT breakdown on credit-note PDFs; U+2212 to ASCII hyphen

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:16:00 +02:00
Mattsson b0044bfe98 fix(arsredovisning): make the aktiekapital note completable via compa… (#1118)
* fix(arsredovisning): make the aktiekapital note completable via company settings

The annual report warned every AB that the aktiekapital note was missing
and pointed at Installningar -> Foretag, but the referenced columns
(aktiekapital, antal_aktier, kvotvarde) never existed and no settings UI
was ever built, so the warning was a dead end and no AB could produce a
complete note before Bolagsverket filing.

- migration 20260723103000: company_settings.aktiekapital (numeric) and
  antal_aktier (integer) with positive CHECKs; kvotvarde is intentionally
  not stored since ABL 1 kap 6 defines it as aktiekapital / antal aktier
- build-data.ts (K2 and K3 note paths): select only the two stored
  columns and derive kvotvarde with roundOre
- UpdateSettingsSchema: aktiekapital (positive), antal_aktier (positive
  integer), both nullable to allow clearing
- new ShareCapitalForm section on Installningar -> Foretag, rendered for
  aktiebolag only, with live derived kvotvarde display; wired through the
  existing CompanySettingsContent save path (empty string clears to null)
- sv/en strings; settings route tests (round-trip, clear, 400 on invalid);
  builder tests for derived kvotvarde and the empty-settings warning

Staging (metjnjrhvujscngnpzdv) already has the columns applied and the
note verified end-to-end against a rehearsal company.

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

* fix(arsredovisning): address PR review findings on the share-capital note

- enforce aktiekapital/antal_aktier as an all-or-nothing pair (DB CHECK,
  K2/K3 note guard now requires both, partial pair warns instead)
- numeric(15,2) column, .int() Zod constraint, maxFractionDigits 0 render
- guard numberOrNull against NaN; align kvotvarde preview with schema
- strengthen clearing test, add fractional and partial-pair tests

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 11:41:14 +02:00
Mattsson 466e55a015 Fix/invoice delivery and payment accounts (#1116)
* fix: reconcile annual reports with final closing entries

* test: cover annual report depreciation and VAT balances

* Merge remote-tracking branch 'origin/main' into fix/usr-fdbck-ch

* fix: show exact invoice delivery details

* fix: use currency account in invoice emails

* fix: address invoice delivery review feedback

* fix: harden invoice delivery and payment accounts

* test: assert RLS-denied zero-row updates

* fix: close remaining invoice compliance gaps

* fix: harden invoice archive authorization

* fix: close invoice delivery review findings

* fix: verify delivery finalization results

* fix: cap combined invoice email recipients

* fix: close final invoice compliance findings

* fix: prevent stale payment account saves

* test: prove invoice delivery isolation

* fix: close invoice privacy review findings

* test: normalize delivery retention dates
2026-07-23 09:54:02 +02:00
Mattsson 321e684523 Fix/usr fdbck ch (#1105)
* fix(privacy): mask voucher amounts in session replays

* fix: persist transaction source filter

* fix: clarify invoice filenames and booking previews

* fix: truncate long uploaded filenames

* feat: add invoice delivery history

* fix: harden invoice delivery history

* fix: include invoice deliveries in full archive
2026-07-22 18:49:57 +02:00
Jakob Wennberg 3e1ea29d02 fix(pending-ops): record posted ids and land failed_partial instead of clean rejected after partial commits (#842) (#1110)
Multi-step executors (match_transaction_invoice, credit_invoice) post an
irreversible voucher or persist a credit note and then run later fallible
steps. A failure there previously marked the whole op status=rejected,
hiding the posted entity and its id from operators.

- new migration 20260722134114: add failed_partial to the
  pending_operations status CHECK and treat it as terminal in both
  immutability triggers (immutable, undeletable, never re-claimable)
- PartialCommitError + ExecutorResult.partialPostedIds carry the posted
  ids; the dispatcher writes status=failed_partial with
  result_data.posted_ids and returns code=partial_commit
- instrument only the two named executors; hoist the read-only
  settlement-account resolution above the storno in the match executor
- consumer sweep: status union + query schema widened, failed_partial
  folds into the Avvisade tab with a badge and posted-ids detail line,
  bulk/reject routes and MCP tools message it explicitly, worklist and
  expiry sweep intentionally untouched (not pending work)
- tests: pg-real coverage for the new terminal semantics, dispatcher unit
  tests for both partial paths plus byte-for-byte regression guards

Fixes #842

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:33:49 +02:00
Jakob Wennberg 27ef398623 feat(mcp): briefing returns a recommended tool loadout per workflow (#1098) (#1109)
With deferred tool loading on the client side (Claude Code ToolSearch,
claude.ai connector search), an agent starting a bookkeeping session
burns 4-6 round-trips discovering tools cluster by cluster. The
briefing now returns recommended_tools: five per-workflow loadouts
(categorize_month, close_period, invoice_run, vat_declaration,
payroll_month), each with a stable key, one-line description, the
gnubok_load_skill slug for the full playbook, and the exact registry
tool names ordered by typical call sequence, so a batch-selection
harness loads a whole workflow cluster in ONE ToolSearch select call.

Drift protection: assertRecommendedLoadoutsValid() runs at module init
in server.ts right after the tools array is built, failing module load
(and every test importing the server) if a loadout names a tool absent
from the registry or a skill slug absent from workflowSkills; the
agent-briefing test suite pins the same checks.

The list is static per company: the briefing does not query workflow
state today, so gating inclusion would add reads to the bootstrap hot
path. tools/list payload ceiling bumped 57K to 57.5K with a documented
progression entry (schema prose trimmed to the floor first; headroom
before the change was ~15 tokens).

Fixes #1098

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:31:39 +02:00
Jakob Wennberg 25a7261eda fix(pending-ops): recovery sweep for operations stuck in committing (#843) (#1108)
The commit dispatcher claims an op with an atomic pending -> committing
CAS; if the process dies after side-effects post but before the terminal
committed write (or that write fails, the PR #841 log line), the row sat
in status='committing' forever: the expire cron only sweeps 'pending'.

Add lib/pending-operations/recover-stuck-committing.ts, invoked from the
existing daily expire cron (no new vercel.json entry):

- Only rows whose updated_at (the claim timestamp: the CAS bumps it via
  the update_updated_at_column trigger) is older than 15 minutes, well
  past the 300s Vercel function ceiling, so in-flight executors are
  never raced.
- Positive evidence that side-effects posted finalizes the row to
  committed with result_data.recovered=true. Evidence exists only where
  params identify a target with an unambiguous posted state:
  categorize_transaction (is_transaction_booked RPC, skipped for
  allow_duplicate), link_transaction_journal_entry (exact tx+entry
  link), match_transaction_invoice (invoice_payments pair row).
- No evidence: terminal rejected with an explanatory result_data,
  never back to pending (re-execution could duplicate side-effects
  that posted without a trace). Reason 'stuck_committing' is distinct
  from 'expired' so the UI badge never claims these rows.
- Every terminal write is CAS-guarded on status='committing'; probe
  errors skip the row for the next run.
- One structured 'pending_op_recovery' warn per row (count by outcome);
  runbook comment added next to the #841 finalize-failure log line.

Tests: unit coverage for the decision logic and cron wiring (401, sweep
invoked, failure isolation), plus a pg-real test proving row selection,
the trustworthy updated_at anchor, committing -> terminal transitions
through the real immutability/input-frozen triggers, and the
is_transaction_booked evidence substrate.

Fixes #843

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:30:33 +02:00
Jakob Wennberg 8294899543 test(skatteverket): guard LEGACY_DISCOVERY_HOSTS against config drift (#1093) (#1107)
Add validateLegacyDiscoveryHosts() next to the allowlist in
lib/api/v1/base-url.ts and a unit test that pins the registered
production configuration (app.accounted.se canonical,
app.gnubok.se SKV OAuth pin). The invariant is checked both ways:
the NEXT_PUBLIC_SKV_OAUTH_BASE_URL host must be reflectable by
discovery (canonical or allowlisted), and every allowlist member
must be accounted for by the registered configuration. Drift is
now a red CI test instead of a silent production re-auth failure
near a filing deadline. No runtime behavior changes.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:11:04 +02:00
Jakob Wennberg 826513b4fd fix(tax): representation meal warning inverted the 2017 VAT rule (#313) (#1106)
The restaurant/lunch warning claimed 'Momsen är inte avdragsgill sedan
2017', which is legally backwards. The 2017 reform (Prop. 2016/17:1)
abolished the income-tax deduction for representation meals (IL 16 kap
2 §) but retained the VAT deduction on a base of up to 300 SEK per
person excl. VAT, now ML 2023:200 13 kap 24-25 §§. The old 'ML 8:9'
citation pointed at the repealed ML 1994:200.

Verified against the swedish-vat compliance skill before rewording.
Adds regression tests pinning the corrected message and legalBasis.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:09:18 +02:00
Jakob Wennberg 4a0b524fbb fix(categorization): connect card descriptors to counterparty history (#1095)
* fix(categorization): connect card descriptors to counterparty history

suggest_categories returned no signal for recurring card merchants
(reported: Anthropic booked to 5420 fourteen times, zero suggestions).
Three compounding causes, all fixed:

- normalizeCounterpartyName() now reduces card-network descriptors to
  their merchant segment ("ANTHROPIC* CLAUDE SUB SAN FRANCISCO" ->
  "anthropic"; "PAYPAL *SPOTIFY" -> "spotify"), so monthly per-charge
  tails stop splintering one merchant into unmatchable variants. SQL
  mirror normalize_counterparty_key() updated in lockstep (migration
  20260721140000), keeping the ledger-context template join exact.
- New token_subset match tier bridges templates learned from manual
  bookings ("Claude Dec" -> "claude") to bank descriptors containing
  the token, and card-core descriptors to legacy splintered templates.
  Guarded by a distinctive-token filter so generic/geo words never
  match on their own.
- Merchant history falls back to description when merchant_name is
  null: card purchases never carry merchant_name, so the history path
  was structurally blind to exactly the transactions that need it.
  History keys now share the counterparty-template normalization and
  the 200-row window is ordered by recency.

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

* fix(categorization): guard single-token matches, anchor history on original_description

Review follow-ups (CodeRabbit on #1095):

- token_subset tier: a single shared distinctive token now also requires
  occurrence_count >= 3 on the template, so a template named after a
  common word or first name (one prior booking) cannot vacuum up
  unrelated transfers ("SWISH ANDERS JOHANSSON"). Multi-token agreement
  stays unrestricted; the Claude/Anthropic case (14 bookings) is
  unaffected.
- merchant history keys on original_description ?? description: the raw
  bank descriptor is immutable while description is a user-editable
  working title, so renaming a transaction no longer severs its history
  link for future recurring charges.

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

* chore(migrations): re-timestamp card-descriptor migration after prod moved past it

Prod applied 20260721144311 (#1101) through 20260721201747 (#1104) while
this PR was open; 20260721140000 would sort before them and risk being
skipped by out-of-order auto-apply at merge. Not yet applied to prod, so
renaming is safe; the preview branch re-applies idempotently
(CREATE OR REPLACE).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:58:45 +02:00
Mattsson 3920c893f4 fix(migrations): adjust retention expiry trigger and validation for fiscal periods (#1104) 2026-07-22 00:43:46 +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
Jakob Wennberg 702512437a fix(auth): accept 6-10 digit one-time codes on reset page (#1102)
The Email OTP Length on this project is 8, but the code input capped at
maxLength 6, silently truncating what the user typed so every verify
failed. gotrue allows 6-10 digits; the input now accepts that range and
the placeholder no longer claims a specific length.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:30:09 +02:00
Jakob Wennberg aa6d42a167 fix(import): raise statement_timeout on import_sie_journal_entries to 290s (#1101)
A production Fortnox migration failed with "SIE-verifikationer kunde
inte importeras atomiskt: canceling statement due to statement
timeout": the atomic import RPC (20260712150000) inherits the 8s
authenticator statement_timeout, so any real-world multi-year SIE file
exceeds it and the whole import is cancelled and rolled back. Same
failure class as 20260629160100 (replace_sie_import / undo_sie_import);
same fix, a function-scoped statement_timeout of 290s sitting under the
calling routes' maxDuration = 300 ceiling.

Migration 20260721144311 is already applied to prod (proconfig verified
carrying statement_timeout=290s); the committed file is byte-identical
under the same version. The new pg-real ratchet pins the config on all
three SIE RPCs because CREATE OR REPLACE FUNCTION silently drops
ALTER FUNCTION settings.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:54:14 +02:00
Jakob Wennberg 3cb5ae7716 fix(auth): scanner-proof password reset with button-gated verify and OTP-code fallback (#1100)
Corporate mail scanners (Microsoft Defender SafeLinks) follow links in
auth emails and burn the one-shot recovery token before the user sees
the mail (#1099, first hit: Deepgrid 2026-07-21, verify from an Azure
IP 23s after send).

/reset-password now has three entry modes:
- set-password: recovery session exists (legacy /auth/callback links
  keep working unchanged)
- confirm-link: the email link carries ?token_hash= and verification
  runs ONLY on an explicit button click; scanners render pages but do
  not click buttons, so the token survives scanning
- enter-code: email + 6-digit {{ .Token }} code typed manually, the
  fallback when no link works at all

The Supabase recovery email template switches to
{{ .SiteURL }}/reset-password?token_hash={{ .TokenHash }} + {{ .Token }}
AFTER this deploys (template content in the PR); the link then never
touches gotrue's GET /verify endpoint, leaving nothing to detonate.

Fixes #1099.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 16:50:07 +02:00