Community PR #2416 by @olofpinzke, adopted and finished by maintainers (rebased so every commit is signed).
Why the problem occurred: no Zettle integration; POS sales only reached the books as bank descriptors while Woo/Shopify already had order underlag via webshop_orders. The contributor's version also failed at the database (platform CHECKs listed only woocommerce/shopify), which the mocked unit tests never saw.
What was simplified: reused the Orders/book/invoice path instead of a new inbox; Finance API payouts/fees deferred. Sales the one-account, revenue-per-rate model cannot book (split tender, gift cards, tips) import unbookable with a "bokför manuellt" title instead of guessing accounts. Reset parity uses the rename-and-wrap pattern instead of re-issuing the reset body.
Why this solution: per-purchase rows give the radunderlag BFL verifikat need and the bulk-book path exists; daily kassarapport aggregation and Finance API fees/payouts are the follow-up (DECISIONS.md). Skeptic-refuted paths fixed before merge: concurrent refresh-token rotation (sync claim), cron offset paging (candidate snapshot), platform CHECKs, writer-role gate, migration-reset parity, white-label return origin re-validated at callback, VAT net from product rows.
Not live until ZETTLE_CLIENT_ID / ZETTLE_CLIENT_SECRET / ZETTLE_CREDENTIALS_ENCRYPTION_KEY are set on Vercel and a Zettle developer app is registered with the callback redirect URI.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtYqzKPoTSRHskYYdf7MwB
An out-of-pocket purchase differs from any other receipt only in the
credit account, so the Underlag pane now asks one question for an
unmatched underlag (Företaget / Jag, privat / En anställd / Ingen ännu)
and books a privately paid receipt in place through POST
/api/expense-claims, replacing the "Andra sätt att bokföra" dropdown and
the deep link into the two-step wizard. The verifikat editor stays
reachable below as the escape hatch (BFL 5 kap 6-7 §).
The person owed surfaces in Att göra under a new Betala band, one row per
person (lib/worklist expense_payout, counted in the total and exposed to
agents through the attention resource). The Utlägg nav row is gated on
existing claims, the same hybrid gate as Körjournal, since the entry
point for a new utlägg is now the Underlag pane.
Claude-Session: https://claude.ai/code/session_01P8YsvPqjfGxGZUkGeBVUWQ
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The dashboard layout runs on every hard load, hard refresh, company switch
and the 16 router.refresh() sites, and loading.tsx cannot paint until it
resolves. It cost ~20 network calls in 4 sequential waves: a third
getUser() round trip to Supabase Auth (after the proxy's and the route
guard's), the company resolution, then 16 reads including four limit-1
probes whose only job is to decide whether to render the Webshop and
Körjournal nav rows, and an entitlements read that itself ran two waves.
- lib/auth/claims.ts: claimsPinned/userFromClaims extracted from
require-auth.ts (unchanged) so the dashboard request context shares the
exact pinning + mapping. getDashboardAuthContext verifies the JWT locally
and falls back to getUser() only when claims are missing, unpinned or
unverifiable: the proxy already performed the per-request revocation
check before the layout runs (same semantics approved for routes on
2026-07-23).
- Wave 1 (user-keyed, parallel with the company resolution): team
membership, profile, user preferences and the memberships join, which
now also supplies the active company's row and role, so the separate
companies and company_members reads are gone.
- Wave 2 (company-keyed): settings, agent profile, the switcher's settings
names, entitlements in ONE wave (getCompanyEntitlements takes the
team_id the join already carries and runs the grants read alongside
config + subscription), and get_dashboard_nav_flags().
- supabase/migrations/20260826120000_get_dashboard_nav_flags.sql:
SECURITY INVOKER, STABLE, EXECUTE for authenticated only; RLS applies
inside. lib/dashboard/nav-flags.ts wraps it with the pre-RPC four-probe
fallback on PGRST202/42883/42501 (self-hosted not yet migrated, deploy
ordering) and degrades to hidden rows on any other error.
- tests/pg/dashboard-nav-flags-rpc.pg.test.ts (6): fresh company, active vs
pending WooCommerce, active Shopify, mileage trips, RLS for a member of
another company, EXECUTE grants. Unit tests for the wrapper (RPC row,
single-object payload, each fallback code, other errors) and for the
entitlements teamId option.
~20 calls / 4 waves -> ~12 calls / 2 waves, 0 auth network calls.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>