Commit Graph

711 Commits

Author SHA1 Message Date
Mattsson ebf69d2933 fix: credit-note overdue countdown + voucher sequence resync after SIE import (#1069)
* fix(invoices): hide overdue countdown for credit notes in invoice list

Credit notes stay in status 'sent' forever (invoices_credit_note_not_paid
blocks paid states), so the relative due-date label rendered an ever-growing
'X dagar forsenad' on every issued credit note. Skip the label for rows with
credited_invoice_id set.

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

* fix(bookkeeping): resync voucher_sequences counters left behind by pre-RPC SIE imports

The batch SIE import path that predated import_sie_journal_entries
(20260712150000) inserted vouchers with explicit numbers but never
updated voucher_sequences, leaving counters behind max (year-end
integrity error, duplicate-key crash on the next voucher) or missing
entirely (next_voucher_number restarts at 1 and collides). Idempotent
data repair: raise lagging counters to the observed max and insert
missing rows attributed to the company owner. Already applied to prod
and staging; replay is a no-op.

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

* fix(bookkeeping): harden voucher-sequence resync per PR review

Address PR #1069 review findings: close the ON CONFLICT race by
upgrading DO NOTHING to DO UPDATE with GREATEST (a row created by
next_voucher_number between snapshot and insert is raised instead of
left at 1), unify the voucher_number > 0 filter across both statements,
and record the manual prod/staging execution timestamps as the change
record (ISO 27001 A.8.32, BFNAR 2013:2 behandlingshistorik).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:35:52 +02:00
Mattsson 9c8e540338 fix(invoices): repair send dialog fiscal-period query + editable issu… (#1066)
* fix(invoices): repair send dialog fiscal-period query + editable issuance lines

The send/mark-sent dialog queried fiscal_periods with start_date/end_date
instead of period_start/period_end; the query always 400ed, and since PR
#1023 made that fatal the dialog closed instantly, blocking mark-as-sent
and email send for everyone.

Also lets accrual companies edit the proposed journal lines before booking
(both send and mark-sent), mirroring the mark-paid editor: untouched
proposals still book via the server generator; edited lines book verbatim
with balance validated at three layers. Credit notes and periodiserade
invoices keep the read-only preview. The dialog now also respects
defer_invoice_booking (#967).

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

* fix(invoices): harden custom issuance-line validation per review findings

Extract the custom-line parse + balance check into a shared validator so
the send and mark-sent routes cannot drift. Reject rows carrying both
debit and credit, and 29xx interim accounts (custom lines skip accrual
schedule creation, so a 29xx balance would never be dissolved). Validate
the payload only after the invoice ownership fetch, and emit structured
log events when user-edited lines are booked or deliberately ignored, so
manual overrides are visible in audit review.

Account existence needs no route-level check: the engine already resolves
every account against the company chart and throws AccountsNotInChartError.

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

* fix(invoices): address CodeRabbit findings on issuance line editing

Reject malformed JSON bodies with 400 instead of silently booking
generated lines; restrict line editing to SEK invoices (custom lines
cannot carry FX metadata); round each line before the client balance
check to match the server; stop claiming a voucher was created in the
mark-sent toast for deferred-booking companies; add programmatic labels
to the editor inputs and remove-row buttons.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:39:56 +02:00
Mattsson 46b8e2bfea Fix/fable design (#1063)
* fix(bokslut): make dispositions storno-safe and derive fond math from opening balances

A reversed year_end voucher kept its storno in the income statement while
the original was excluded (source_type asymmetry), inflating resultat fore
dispositioner by exactly the reversed amount, and the posted-only fond
balance produced a phantom negative 212X that leaked a bogus aterforing
proposal. Support case: a user double-booked periodiseringsfond, reversed
both correctly, and the dispositions page still showed wrong numbers.

- trial-balance excludeYearEndClosing now also excludes entries chained to
  reversed year_end entries via reverses_id/correction_of_id (grammar
  verified against staging PostgREST)
- listExistingPeriodiseringsfonder counts posted+reversed so storno pairs
  cancel, and returns opening balances per fond
- schablonintakt per IL 30 kap 6a: opening balance base, rate = SLR per
  closing year (1.96% FY2025, 2.55% FY2026), replacing the wrong SLR+1pp
  0.0355 constant
- avsattning 25% cap is year-total: already-provisioned current-cohort
  growth consumes headroom in both preview and commit, so re-running the
  flow can no longer double-book the fond
- SLP posts before avsattning (deductible, shrinks the cap base) and is
  posted-aware: no double proposal or double count on resumed runs
- sumPostedYearEndDispositions counts correction replacements of reversed
  year_end entries and exposes the SLP portion

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

* refactor(bokslut): use roundOre for new fond/disposition rounding

Satisfies the naive-ore-round ratchet that tightened on main; identical
arithmetic, pinned by the existing exact-value tests.

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

* fix(bokslut): address PR #1063 review findings

- computeProposal receives the already-validated period row: a transient
  DB failure can no longer silently skip a requested disposition (and two
  redundant per-item period fetches are gone)
- getSchablonintaktRate fails closed for unmapped years instead of
  falling back to the latest known rate: statutory rates are never
  guessed; POST rate override remains the escape hatch
- listExistingPeriodiseringsfonder is opening-balance-entry aware:
  a fond carried via the OB entry booked by year-end closing was counted
  twice (once from history, once from the OB entry); balances now derive
  from OB + current-period activity when an OB entry exists
- periodStart is validated as a real calendar date, not just a shape
- reversed year_end correction targets resolve company-wide in
  sumPostedYearEndDispositions, matching the trial balance exclusion

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:06:41 +02:00
Jakob Wennberg 425674ff35 chore(deadlines): legacy-type cleanup + ICS feed user-deadline fix (#1060)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal

The f_skatt deadline was gated on the F-skatt approval flag (DB default
true), giving nearly every company 12 monthly payment reminders for a tax
Skatteverket may not have debited at all (64% of all system deadline rows,
one lifetime completion). Approval carries no recurring obligation; the
monthly duty is payment of debiterad preliminarskatt and exists only while
the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.).

- Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already
  collected at onboarding, previously unread) and retitle it as a payment.
- Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.).
- Declare the prod-only preliminary_tax_monthly column in a migration so
  installs built purely from migrations stop failing tax-settings saves.
- Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses
  it durably (hard deletes were resurrected by the nightly backfill within
  24h); generator, backfill, and every read surface respect it.
- Prune upcoming f_skatt rows for companies with no debited amount.

Closes part of #1028.

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

* feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation

The arbetsgivardeklaration deadline was gated on pays_salaries, which is
wrong in both directions: a registered employer must file AGI every month
including nil months (SFL 26 kap. 3 par.), and companies actively running
payroll with the flag off got no AGI reminders at all (each missed monthly
filing risks a forseningsavgift).

- New company_settings.employer_registered (nullable, no default) gates
  AGI and the storforetag skatteinbetalning row; pays_salaries remains a
  fallback for rows saved before the flag existed and keeps its UI meaning.
- Migration backfills employer_registered=true from pays_salaries=true and
  from actual payroll activity (salary_runs).
- New employer_seasonal flag: sasongsregistrerade file only for payment
  months plus a December nil declaration, so only the December-period row
  is generated.
- Settings UI: registration + seasonal checkboxes (sv/en strings).
- AGI XML generation no longer auto-completes the deadline as submitted:
  SFL 26 kap. deems the obligation satisfied only when the declaration has
  come in to Skatteverket. The Skatteverket extension's kvittens reconcile
  remains the confirming path; manual filers tick the deadline themselves.

Part of #1028.

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

* feat(deadlines): statutory arsstamma replaces bokslut, moms_yearly auto-complete, EU-sales suggestion

- Replace the non-statutory 'bokslut' deadline (3 months after FY end, no
  legal basis, off-by-one month math for broken FYs) with the statutory
  arsstamma deadline: within 6 months of FY end per ABL 7 kap. 10 par.,
  the corporate act that gates the arsredovisning filing chain. Migration
  deletes pending bokslut rows; the backfill cron generates arsstamma rows.
- Complete moms_yearly on Skatteverket submission/kvittens: the yearly
  branch previously returned null with a stale comment claiming annual VAT
  has no deadline type, leaving yearly filers with an eternally open row.
  The fiscal-year tax_period label is derived from company settings.
- Add /api/settings/eu-trade-signal + a tax-settings callout: companies
  with booked EU sales (3108/3308/3107, last 15 months) but EU-trade/PS
  flags off are prompted to confirm the periodisk sammanstallning
  obligation (SFL 35 kap., 1 250 kr late fee per report). Suggestion only,
  never auto-enables.

Part of #1028.

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

* chore(deadlines): clean up legacy deadline types, fix ICS feed hiding user deadlines

- Migration deletes pending rows of the retired bare 'moms' and
  'inkomstdeklaration' types (completed rows kept as history) and the
  sandbox seed route now inserts the current moms_quarterly /
  inkomstdeklaration_ef types so legacy rows stop reappearing.
- The calendar feed's include_tax_deadlines flag now hides only
  system-generated deadlines: user-created deadlines always appear. The
  old nesting skipped the entire deadlines fetch and dropped the user's
  own rows from the feed when the flag was off.

Part of #1028.

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

* fix(deadlines): include dismissed_at in DeadlineForm payload

The Deadline type gained the required dismissed_at field; the form's
submit payload literal must carry it for the Omit<Deadline, ...> shape.

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

* chore: retrigger Supabase preview check

The initial preview-branch creation failed transiently; the subsequent
migration run applied all four stack migrations (verified via
list_migrations on the preview project), leaving a stale failed check.

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

* fix(deadlines): make system-deadline dismissal atomic

Constrain the dismiss update to source='system' and verify a row was
actually updated: a concurrent regeneration can delete the row between
lookup and update, and the route must not report a phantom success.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:09:31 +02:00
Jakob Wennberg 05b954ac1d feat(deadlines): årsstämma replaces bokslut + moms_yearly auto-complete + EU-sales suggestion (#1059)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal

The f_skatt deadline was gated on the F-skatt approval flag (DB default
true), giving nearly every company 12 monthly payment reminders for a tax
Skatteverket may not have debited at all (64% of all system deadline rows,
one lifetime completion). Approval carries no recurring obligation; the
monthly duty is payment of debiterad preliminarskatt and exists only while
the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.).

- Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already
  collected at onboarding, previously unread) and retitle it as a payment.
- Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.).
- Declare the prod-only preliminary_tax_monthly column in a migration so
  installs built purely from migrations stop failing tax-settings saves.
- Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses
  it durably (hard deletes were resurrected by the nightly backfill within
  24h); generator, backfill, and every read surface respect it.
- Prune upcoming f_skatt rows for companies with no debited amount.

Closes part of #1028.

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

* feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation

The arbetsgivardeklaration deadline was gated on pays_salaries, which is
wrong in both directions: a registered employer must file AGI every month
including nil months (SFL 26 kap. 3 par.), and companies actively running
payroll with the flag off got no AGI reminders at all (each missed monthly
filing risks a forseningsavgift).

- New company_settings.employer_registered (nullable, no default) gates
  AGI and the storforetag skatteinbetalning row; pays_salaries remains a
  fallback for rows saved before the flag existed and keeps its UI meaning.
- Migration backfills employer_registered=true from pays_salaries=true and
  from actual payroll activity (salary_runs).
- New employer_seasonal flag: sasongsregistrerade file only for payment
  months plus a December nil declaration, so only the December-period row
  is generated.
- Settings UI: registration + seasonal checkboxes (sv/en strings).
- AGI XML generation no longer auto-completes the deadline as submitted:
  SFL 26 kap. deems the obligation satisfied only when the declaration has
  come in to Skatteverket. The Skatteverket extension's kvittens reconcile
  remains the confirming path; manual filers tick the deadline themselves.

Part of #1028.

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

* feat(deadlines): statutory arsstamma replaces bokslut, moms_yearly auto-complete, EU-sales suggestion

- Replace the non-statutory 'bokslut' deadline (3 months after FY end, no
  legal basis, off-by-one month math for broken FYs) with the statutory
  arsstamma deadline: within 6 months of FY end per ABL 7 kap. 10 par.,
  the corporate act that gates the arsredovisning filing chain. Migration
  deletes pending bokslut rows; the backfill cron generates arsstamma rows.
- Complete moms_yearly on Skatteverket submission/kvittens: the yearly
  branch previously returned null with a stale comment claiming annual VAT
  has no deadline type, leaving yearly filers with an eternally open row.
  The fiscal-year tax_period label is derived from company settings.
- Add /api/settings/eu-trade-signal + a tax-settings callout: companies
  with booked EU sales (3108/3308/3107, last 15 months) but EU-trade/PS
  flags off are prompted to confirm the periodisk sammanstallning
  obligation (SFL 35 kap., 1 250 kr late fee per report). Suggestion only,
  never auto-enables.

Part of #1028.

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

* fix(deadlines): include dismissed_at in DeadlineForm payload

The Deadline type gained the required dismissed_at field; the form's
submit payload literal must carry it for the Omit<Deadline, ...> shape.

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

* fix(deadlines): make system-deadline dismissal atomic

Constrain the dismiss update to source='system' and verify a row was
actually updated: a concurrent regeneration can delete the row between
lookup and update, and the route must not report a phantom success.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:02:49 +02:00
Jakob Wennberg da4d5a39ae feat(deadlines): gate AGI on employer registration + stop completing AGI at XML generation (#1062)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal

The f_skatt deadline was gated on the F-skatt approval flag (DB default
true), giving nearly every company 12 monthly payment reminders for a tax
Skatteverket may not have debited at all (64% of all system deadline rows,
one lifetime completion). Approval carries no recurring obligation; the
monthly duty is payment of debiterad preliminarskatt and exists only while
the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.).

- Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already
  collected at onboarding, previously unread) and retitle it as a payment.
- Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.).
- Declare the prod-only preliminary_tax_monthly column in a migration so
  installs built purely from migrations stop failing tax-settings saves.
- Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses
  it durably (hard deletes were resurrected by the nightly backfill within
  24h); generator, backfill, and every read surface respect it.
- Prune upcoming f_skatt rows for companies with no debited amount.

Closes part of #1028.

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

* feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation

The arbetsgivardeklaration deadline was gated on pays_salaries, which is
wrong in both directions: a registered employer must file AGI every month
including nil months (SFL 26 kap. 3 par.), and companies actively running
payroll with the flag off got no AGI reminders at all (each missed monthly
filing risks a forseningsavgift).

- New company_settings.employer_registered (nullable, no default) gates
  AGI and the storforetag skatteinbetalning row; pays_salaries remains a
  fallback for rows saved before the flag existed and keeps its UI meaning.
- Migration backfills employer_registered=true from pays_salaries=true and
  from actual payroll activity (salary_runs).
- New employer_seasonal flag: sasongsregistrerade file only for payment
  months plus a December nil declaration, so only the December-period row
  is generated.
- Settings UI: registration + seasonal checkboxes (sv/en strings).
- AGI XML generation no longer auto-completes the deadline as submitted:
  SFL 26 kap. deems the obligation satisfied only when the declaration has
  come in to Skatteverket. The Skatteverket extension's kvittens reconcile
  remains the confirming path; manual filers tick the deadline themselves.

Part of #1028.

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

* fix(deadlines): include dismissed_at in DeadlineForm payload

The Deadline type gained the required dismissed_at field; the form's
submit payload literal must carry it for the Omit<Deadline, ...> shape.

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

* fix(deadlines): make system-deadline dismissal atomic

Constrain the dismiss update to source='system' and verify a row was
actually updated: a concurrent regeneration can delete the row between
lookup and update, and the route must not report a phantom success.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:04:05 +02:00
Jakob Wennberg 3c0bf3f584 feat(deadlines): gate F-skatt reminders on debited preliminary tax + durable dismissal (#1057)
* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal

The f_skatt deadline was gated on the F-skatt approval flag (DB default
true), giving nearly every company 12 monthly payment reminders for a tax
Skatteverket may not have debited at all (64% of all system deadline rows,
one lifetime completion). Approval carries no recurring obligation; the
monthly duty is payment of debiterad preliminarskatt and exists only while
the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.).

- Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already
  collected at onboarding, previously unread) and retitle it as a payment.
- Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.).
- Declare the prod-only preliminary_tax_monthly column in a migration so
  installs built purely from migrations stop failing tax-settings saves.
- Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses
  it durably (hard deletes were resurrected by the nightly backfill within
  24h); generator, backfill, and every read surface respect it.
- Prune upcoming f_skatt rows for companies with no debited amount.

Closes part of #1028.

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

* fix(deadlines): include dismissed_at in DeadlineForm payload

The Deadline type gained the required dismissed_at field; the form's
submit payload literal must carry it for the Omit<Deadline, ...> shape.

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

* fix(deadlines): make system-deadline dismissal atomic

Constrain the dismiss update to source='system' and verify a row was
actually updated: a concurrent regeneration can delete the row between
lookup and update, and the route must not report a phantom success.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:48:25 +02:00
Jakob Wennberg 97907a5a5c fix: article ordering, free-text rows, invoice back-nav, onboarding resilience (#1053) (#1056)
* fix: article number ordering, free-text rows, invoice back-nav, onboarding resilience (#1053)

Four fixes from Discord feedback in issue #1053:

- Articles now order by article number with numeric-aware comparison
  ('2' before '10', unnumbered last, name tiebreak) in the invoice
  editor's article picker and as the register's default sort, via a
  shared lib/articles/sort.ts. Name order put article "1" last.

- Invoice rows with no amounts (quantity 0, unit price 0) render as
  pure text rows on the PDF, the invoice detail page, and the review
  step via shared isTextLikeLine(), instead of printing
  "0 / 0,00 SEK / 0,00 SEK". Display-only; booking untouched.

- The invoice editor navigates with router.replace after saving, so
  the detail page's back arrow returns to the list instead of
  reopening a fresh editor from history.

- A transient query failure no longer reads as "no companies" /
  "onboarding not done": getActiveCompanyId throws
  CompanyContextError('resolution_failed') instead of returning null,
  the Edge middleware fails open on a degraded resolution (no
  onboarding redirect, no cookie clearing, no locale overwrite), and
  the dashboard page only redirects to /onboarding on a positively
  read incomplete/missing settings row. This is the likely cause of
  the completed onboarding wizard reappearing.

Fixes #1053

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

* docs: CLAUDE.md tenancy line matches actual resolution order (prefs-first, cookie not read)

The middleware stopped reading the gnubok-company-id cookie when
user_preferences became authoritative (RLS parity); the stale doc line
still described cookie-first order and misled review tooling.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:45:43 +02:00
Mattsson 0e9cca2750 Add/customer mcp (#1055)
* feat(mcp): kontoplan account tools + verifikat notes exposure

Two gaps reported by an MCP-driven user: no account management in the
API, and verifikat notes invisible to agents (they exist in the product
but MCP could neither read nor write them).

- add staged gnubok_create_account / gnubok_update_account (BAS 2026
  prefill for catalog numbers; rename/VAT-default/SRU/activate via
  update; both LOW risk reference data)
- add staged gnubok_set_voucher_note (notes-only annotation, legal on
  posted entries per the 20260608120000 trigger carve-out) and return
  entry_notes from gnubok_query_journal
- new pending_operations types create_account / update_account /
  set_voucher_note (CHECK migration + validate companion, applied to
  staging)
- tools/list payload ceiling 54K -> 56K (documented; wire contract,
  descriptions trimmed first)

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

* fix(skatteverket): unstick BankID connect flow and stale connection views

- respond to the OAuth callback immediately and run the post-connect
  refresh after the response (next/server after()): users no longer
  stare at Skatteverket's consumed consent page for up to 40s
- open the consent flow in a full tab instead of a 600x750 popup that
  hid the approve button below the fold
- disable connect buttons while the OAuth tab is open (parallel flows
  overwrote oauth_state + the PKCE verifier) and recover via a
  closed-tab watcher plus a delayed status refetch
- persist MISSING_SCOPE token health from the post-connect sync and
  show an actionable "approve all permissions" notice
- refetch connection state on tab visibility (settings connect panel,
  enable-banking panel, /skattekonto) so a connect completed in another
  tab or after a mobile app-switch shows up without a manual reload;
  fix /skattekonto never clearing its not-connected state

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

* feat(article-form): add article number field with validation to ArticleForm

* feat(account): enforce account type consistency with BAS class and add validation

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:13:53 +02:00
Jakob Wennberg bfd5b42eb1 feat(settings): open Assistenten on Kunskap with the konteringskarta first (#1044)
The Assistenten settings hub used to open on Minne, with the
konteringskarta buried two clicks away (Kunskap tab, below a second
nested tab row). Now /settings/assistant opens on Kunskap and the
LedgerGraph hero is the first thing on screen.

- Kunskap is the default view and first tab; Minne moves to ?view=memory
  (old ?view=knowledge links still resolve to the default)
- Drop the nested Kompetens/Minne/Regler & profil tab row inside the
  Kunskap view: Kompetens and Minne duplicated the top-level tabs one
  row above; Regler & profil now renders inline under the graph with a
  section header (KnowledgeTabs.tsx deleted)
- Restore vertical rhythm (space-y-8) between the hero, detail section
  and footer, lost when the view moved into the settings tabs
- Update redirects and memory deep links (/settings/agent-memory,
  AgentChat memory chips, FactsCard manage link) to ?view=memory
- Match the loading skeleton to the new layout

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:43:57 +02:00
Jakob Wennberg fd1c266cb0 chore: salvage unmerged work from the 2026-07-16 worktree audit (#1043)
* chore: salvage unmerged work from the 2026-07-16 worktree audit

Four items survived the 43-file dirty-tree audit as genuinely unmerged:

- CLAUDE.md: Definition of Done rule 9, the last mile is verified
  in-session, not assumed (project-level counterpart of the switch-on
  check; cloud agents only see the repo file).
- DECISIONS.md: eight decision lines from 2026-07-09 to 2026-07-15,
  condensed and scrubbed of production identifiers for the public repo.
- .claude/skills/loop-ignite: skill that audits the agentic loops and
  ignites dead ones; must live on main for cloud routines to load it.
- lib/bokslut/ixbrl testbank manual E2E: encodes the working testbank
  endpoints and the Luhn-valid test pnr (the documented one fails);
  skipped unless BOLAGSVERKET_TESTBANK_E2E=1, so zero CI cost.

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

* chore: address review findings on the salvage batch

- testbank e2e: kontrollera returns HTTP 200 even for invalid documents
  (outcome is in utfall), so assert zero typ='error' entries; also assert
  grunduppgifter returns a company name, not just the echoed orgnr.
- loop-ignite: make ignition explicitly idempotent (enable/repair an
  existing trigger before creating, never duplicate).

Skipped the fourth finding (require an observed firing as switch-on
proof): a just-created cron cannot have fired yet; the audit table
already reports last observed run per loop.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:35:55 +02:00
Jakob Wennberg 5b8e3fa130 fix(vat): enforce decimal vat_rate on supplier invoice items and normalize MCP percent extraction (#1049)
Supplier invoice items store vat_rate as a decimal fraction (0.25) while
customer invoices use integer percent (25). The shared Zod schema accepted
0-100, so a percent-shaped vat_rate silently booked 2500 % VAT via
line_total * vat_rate, and the MCP inbox-conversion path staged the AI
extraction's percent-integer vatRate straight into the decimal column with
per-line vat_amount 0. Part of #310.

- CreateSupplierInvoiceItemSchema.vat_rate is now a literal union of the
  statutory decimal set (0, 0.06, 0.12, 0.25) with a unit-hint error,
  covering the cookie route, the invoice-inbox convert route, and /api/v1
  (whose runtime ALLOWED_SV_VAT_RATES guard stays as defense in depth).
- New shared normalizeVatRateToDecimal() in lib/vat: percent-shaped values
  (25, 12, 6) divide by 100, results snap to the legal Swedish set, and
  anything else (foreign 19/20, non-finite) maps to 0.
- gnubok_create_supplier_invoice_from_inbox normalizes vatRate at the
  extraction boundary and derives per-line vat_amount when the extraction
  carries none, so the staged header vat_amount is honest.
- The pending-operation executor normalizes staged vat_rate on insert, so
  rows staged before this fix cannot book percent-scaled VAT.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:28:36 +02:00
Jakob Wennberg 03fd1b60b7 fix(bokslut): derive preview netResult from the 2099/2010 closing amount (#1045)
The Arets resultat summary card on the bokslut preview step read its
figure from generateIncomeStatement, which excludes entries tagged
source_type='year_end'. Bokslut-flow entries (annual depreciation,
bokslutsdispositioner) carry that tag, so the card showed the
pre-depreciation result while the bokslutsverifikation table below it
(built from the unfiltered trial balance) included depreciation in the
2099 balancing line.

previewYearEndClosing now derives netResult from the closing-lines
totals before the balancing line is appended: it equals, by
construction, the signed amount transferred to 2099 (AB) or 2010 (EF);
positive = credit = vinst, negative = debit = forlust. The posted
verifikat is unchanged: executeYearEndClosing only consumes
preview.closingLines, never netResult.

Fixes #766

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:51:10 +02:00
Jakob Wennberg 88f53350de fix(errors): translate typed engine errors instead of leaking raw messages as journal_entry_error (#1048)
Typed bookkeeping Error instances passed to getErrorMessage() matched the
bare-envelope branch (any object with string code + message) and returned
their raw English message verbatim, so the categorize and match-invoice
routes surfaced strings like DB check-constraint violations directly in the
user's toast (issue #337).

- get-error-message.ts: when the bare-envelope shape is an Error instance,
  normalize it into the structured envelope ({ error: { code, message,
  account_numbers, details } }) so the existing per-code Swedish branches
  own the translation; plain forwarded envelopes keep the passthrough.
- get-error-message.ts: structured-path final fallback now prefers the
  registry's message_sv for known codes whose message is not Swedish, so
  typed codes without a dynamic branch (e.g. CANNOT_REVERSE_STORNO) cannot
  surface English either.
- categorize + match-invoice routes: always map the caught error through
  getErrorMessage (the raw error is already logged); untyped errors fall to
  the Swedish context fallback instead of leaking err.message.
- Tests: new instance-translation suite in lib/errors, typed-error case in
  the categorize route suite, and deliberate updates of the two tests that
  pinned raw 'Period locked' passthrough.

Fixes #337

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:51:07 +02:00
Jakob Wennberg d182cf5d93 refactor(bokslut): convert periodiseringsfond balance query off the journal_entries!inner embed (#977) (#1047)
listExistingPeriodiseringsfonder still selected from journal_entry_lines
with a journal_entries!inner embed and put company_id/status/entry_date on
the embedded side: the shape PostgREST compiles to a correlated lateral
that scans all tenants' lines, and it silently truncated at the 1000-row
cap because it was unpaginated. Convert it to the shared two-step
fetchEntryLines helper (lib/bookkeeping/entry-lines.ts), mirroring
bolagsskatt-calculator.ts, and keep the existing wrapped error contract.

Adds unit coverage for listExistingPeriodiseringsfonder: helper call
shape, entry/line filter callbacks, per-account balance aggregation,
2129 cohort collision rule, 6-year must-return flag, near-zero drop,
sorting, and error wrapping.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:51:03 +02:00
Jakob Wennberg f91536c86c fix(ui): migrate remaining native confirm() calls to DestructiveConfirmDialog (#1046)
Migrates the three remaining bare confirm() sites from issue #1038 to the
imperative useDestructiveConfirm() pattern already mounted on both pages:
resume-autosend and run-now on the recurring-invoices page, and unapprove
on the salary-run page (its dynamically assembled multi-line copy now
renders as paragraphs via whitespace-pre-line on DialogDescription).

Also adds a togglingId in-flight guard to togglePause, mirroring the
deletingId/runningId guards from PR #1036, so the pause/resume button
cannot fire a duplicate PATCH while one is pending.

Fixes #1038

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:51:00 +02:00
Mattsson a5e37d3510 Fix/build (#1041)
* fix(bookkeeping): harden correction account changes

* feat(tax): enhance tax deadline generation with new settings and filing methods

- Added new company settings: tax_turnover_over_40m, vat_has_eu_trade, vat_filing_method, periodisk_sammanstallning_enabled, and periodisk_sammanstallning_filing_method.
- Updated deadline generation logic to accommodate new settings affecting VAT and employer declaration deadlines.
- Implemented tests for new functionality, ensuring that completed obligations are preserved and not replaced by new pending rows.
- Introduced a cron job to backfill missing tax deadlines for companies with settings but no upcoming deadlines.
- Updated API routes for generating tax deadlines and handling cron jobs.
- Modified database schema to include new columns for tax filing profiles and constraints for filing methods.

* fix(invoices): record credit note reconciliation guard

* fix(tax): correct automatic deadline settings

* fix(tax): key AGI deadline to VAT taxable base and add storforetag payment deadline

The 26th filing day for the skattedeklaration (AGI and VAT together) hinges
on one statutory measure, a VAT taxable base above SEK 40 million (SFL 26
kap.), not a separate employer turnover. Drop employer_turnover_over_40m and
derive the AGI schedule from vat_registered plus vat_taxable_base_over_40m,
so a non-VAT-reporting employer is never shown the 26th when its binding
date is the 12th.

Also:
- add a skatteinbetalning deadline row (12th, 17 January) for storforetag,
  whose deducted tax and employer contributions are due before the 26th
  filing date
- normalize legally incoherent over-40m flag combinations to the earlier
  small-company schedule in a follow-up migration
- replace hardcoded 27 December dates with the banking-day adjustment
- extend the 40m help text to cover the SKV-decided early filing election
  and the payment-still-on-the-12th rule
- document the regeneration race repaired by the daily backfill cron

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

* feat(migrations): add AGI and VAT filing logic with employer column removal

* feat(settings): implement VAT registration logic and update related flags; enhance deadline handling

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 00:52:57 +02:00
Jakob Wennberg 1443235cec feat(invoices): registrera utan att bokföra + explicit Bokför-steg (#1040)
* feat(invoices): registrera utan att bokföra + explicit Bokför-steg

Companies where one person registers supplier invoices / sends customer
invoices while ekonomi does the actual bookkeeping had no way to split
the two: under faktureringsmetoden every registration/send booked the
journal entry inline.

- New company setting defer_invoice_booking (default off, accrual only):
  registering a supplier invoice or sending/marking-sent a customer
  invoice creates NO journal entry.
- New explicit booking routes POST /api/supplier-invoices/[id]/book and
  POST /api/invoices/[id]/book: create the registration/revenue entry
  afterwards, CAS-guarded against concurrent booking (a lost race
  cancels the just-posted voucher with a gap explanation), including
  periodisering schedules.
- Detail pages show "Ej bokförd ännu" + a Bokför button for unbooked
  accrual invoices; the settings toggle lives under Bokföringsmetod.
- mark-paid needs no changes: both payment flows already route on the
  journal-entry link, so an invoice still unbooked when paid gets the
  full cash-style entry.
- The mark-sent fail-closed rollback now keys on the same gate so
  deferred sends are not rolled back as booking failures.

Fixes #967

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

* fix(invoices): harden deferred booking after review

CodeRabbit round on #1040:
- CAS link guards also require a still-bookable status (and uncredited,
  customer side) so a concurrent mark-paid/credit cannot end up with a
  double-posting registration/revenue entry.
- Settings reads fail closed instead of defaulting to accrual rules.
- Detail pages surface the ACCRUAL_SCHEDULE_FAILED warning instead of
  showing plain success, and the customer page no longer stringifies
  structured errors into "[object Object]".
- The settings form normalizes defer_invoice_booking to false under
  kontantmetoden so a stale flag cannot re-activate on method switch.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:15:20 +02:00
Jakob Wennberg 14f7478abb feat(reports): reskontra per valfritt datum + PDF-export (#1039)
Kundreskontra and leverantörsreskontra were effectively always "as of
today": the UI never passed a date, the xlsx export ignored the chosen
fiscal year, and no PDF existed.

- Both ledger generators reconstruct the ledger as it stood on a
  backdated as-of date: invoices dated on or before it (including ones
  fully paid since) with outstanding recomputed from the payment-row
  history; paid_at dates row-less full payments; undateable legacy
  amounts degrade to the live values. Today/future dates keep the live
  computation byte-identical.
- New shared reskontra PDF template (aging per counterparty + invoice
  detail for kundreskontra) with PDF routes for both ledgers.
- Both report views get a "Per datum" date control; the export menu
  offers PDF + Excel and passes the chosen date through.

Note: the PDF template deliberately avoids react-pdf's `break` prop:
it deadlocks layout when the section spills across pages (reproduced
at 40+ rows, documented in the template).

Fixes #1020
Fixes #1021

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:14:45 +02:00
Jakob Wennberg f8611f2e89 fix(ui): use styled confirm dialog for salary and recurring-invoice destructive actions (#1036)
* fix(ui): use styled confirm dialog for salary and recurring-invoice destructive actions

Replace native window.confirm() with the existing DestructiveConfirmDialog
/ useDestructiveConfirm() primitive at the six sites from #839: recurring
invoice schedule delete, employee deactivation, salary run draft delete,
remove employee from run, salary calendar bulk delete (all variant
'destructive'), and the nollkorning-to-review guard (variant 'warning').
Confirmation copy is preserved as the dialog description; new title keys
added to both messages/sv.json and messages/en.json.

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

* fix(ui): lock delete and deactivate actions while the request is in flight

The styled confirm dialog resolves before the DELETE settles, so the
trigger button could be clicked again and fire a duplicate request.
Add an in-flight guard (deletingId / deactivating) and disable the
button until the request completes, mirroring the runNow pattern.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:14:34 +02:00
Jakob Wennberg a95872928e fix(supplier-invoices): warn on class 1/6 accounts for reverse charge lines (#1034)
* fix(supplier-invoices): warn on class 1/6 accounts for reverse charge lines (#863)

Item 2 of #863: when omvand skattskyldighet is on, lines booked on an
account starting with 1 (assets) or 6 draw a non-blocking warning banner
in the Kontering card naming the rows; reverse charge purchases normally
sit on 4xxx/5xxx cost accounts. Advisory only, since class 6 has
legitimate reverse charge uses (e.g. 6540 IT-tjanster for EU cloud
services), so submission is never blocked.

Item 1 (block VAT rates outside the legal set 25/12/6/0) already shipped
in PR #902; this change extracts that check plus the new one into a pure
tested helper, lib/vat/supplier-invoice-line-checks.ts, which is now also
the single source for the legal rate list used by the VAT rate preset
dropdown.

Item 3 (confirming the reason for a 0 % rate) is deferred: it is a UX
design question, not a validation gap.

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

* docs(vat): note food-rate transition dates above LEGAL_VAT_RATES

Compliance-bot finding on PR #1034: the allow-list comment now records
that livsmedel moved 12 % to 6 % on 1 April 2026 (Prop. 2025/26:55,
ML 2023:200) and that the reduction is legislated to revert after
31 December 2027, when 6 % stays legal for books/transport but stops
being the food rate. The static list cannot express per-category
temporal validity; revisit at the reversion. Comment-only change.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:14:23 +02:00
Jakob Wennberg 6bd85f94b6 fix(bookkeeping): editable verifikationstext on andringsverifikation (#1035)
The correction header was always built server-side as
"Rattelse: <original description>". When the original entry was labelled
after the wrong account, the correction kept echoing that stale label even
after the user switched to the correct account (follow-up to the
line-description fix in #1029).

- CorrectJournalEntrySchema gains an optional trimmed description
- correctEntry() accepts options.description; blank or absent falls back
  to the canonical "Rattelse: <original>" auto text
- both correct routes (dashboard + v1, which share the schema) thread the
  description through
- CorrectionEntryDialog surfaces an editable verifikationstext field,
  pre-filled with the auto text; an untouched or cleared prefill is NOT
  sent, so the server-side fallback stays the source of truth (same
  only-overwrite-auto-filled principle as #1029)

Forward-only: already-posted corrections are immutable per BFL.

Fixes #1031

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:14:02 +02:00
Jakob Wennberg 90e4f668c9 fix(bookkeeping): tiebreak same-date vouchers in the date-sort direction (#1032)
The verifikat list RPC ordered entry_date in the requested direction but
always tiebroke voucher_series/voucher_number ascending, so under the
default date-descending view every multi-voucher day read the wrong way
(A10, A11, A12 inside a descending list). The RPC now flips the
tiebreaker with p_sort_date, and the route's direct-query fallback gains
the matching voucher_series tiebreak so both paths agree.

Fixes #972

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:13:39 +02:00
Jakob Wennberg f0907da7e9 fix(v1): thread resolved settlement account through FX and cash-method supplier-payment branches (#1033)
* fix(v1): thread resolved settlement account through FX and cash-method supplier-payment branches

The v1 (MCP-facing) match-supplier-invoice route resolved paymentAccount
via resolveSettlementAccount but only passed it to
createSupplierInvoicePaymentEntry for pure-SEK matches (gated on
isPureSek) and never to createSupplierInvoiceCashEntry at all. A
foreign-currency match, or a kontantmetoden match, settling from a
bank/cash account other than the primary 1930 (e.g. a EUR account on
1940) was still misbooked to 1930: the same class of bug PR #985/#986
fixed for the pure-SEK accrual path.

Pass the resolved account through both branches unconditionally (the
generators' internal 1930 default remains the documented no-link
fallback, reached via resolveSettlementAccount's own fallback for
transactions without a cash_account_id), and widen the
findUnresolvableAccounts chart pre-validation from the pure-SEK accrual
path to every non-customLines branch, since all of them now consume the
resolved account.

The dashboard route needed no code change: its FX/cash-method branches
were already threaded inside PR #985 itself. Added branch-level
regression tests on both routes (linked non-1930 account books to that
account; no cash_account_id falls back to 1930; deactivated resolved
account rejects with ACCOUNTS_NOT_IN_CHART before booking).

Fixes #1000

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

* fix(v1): validate settlement account against the chart before the conflicting-JE storno

The widened findUnresolvableAccounts pre-validation ran after the
conflicting-categorization storno, so a request rejected with
ACCOUNTS_NOT_IN_CHART could first reverse the transaction's posted
categorization entry: an irreversible side effect on a failed request.
Move the paymentAccount resolution and the chart validation ahead of
the storno block (same !customLines guard, same error shape) and add a
regression test asserting reverseEntry is never called when the chart
validation fails. The dashboard route has no storno block and no chart
pre-validation on this path, so it is unaffected.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:13:09 +02:00
Alexander Reinthal 1dc85736d8 feat(import): add Wise (TransferWise) CSV import format (#1018)
* feat(import): add Wise (TransferWise) CSV import format

Wise exports a single multi-currency transaction history (one row per balance
movement). Add it as a bank-file format plugin so it flows through the existing
upload -> preview -> confirm -> execute wizard.

- lib/import/bank-file/formats/wise.ts: quote-aware parse (dates contain a
  space), Direction IN/OUT drives the sign, booked on the moved side (target
  for IN, source for OUT). Native currency preserved; SEK conversion is left to
  the downstream FX/booking pipeline (Riksbanken).
- Non-zero Wise fees become their own negative "Wise avgift" row (source and
  target), so the fee books separately and the balance ties out.
- Only COMPLETED rows import. external_id keys on the stable Wise ID
  (TRANSFER-/PLAN_ORDER-, -fee suffix for fee rows) via a new 'wise' branch in
  generateExternalId, so re-imports dedup exactly.
- Register the format (types, parser list), add it to the manual-format picker
  and the v1 /imports/bank format enum.

Tests cover detection, IN/OUT signing + currency, fee splitting, stable
external_id, and COMPLETED-only filtering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

* fix(import): harden Wise parser against malformed rows (CodeRabbit #1018)

- Strict amount parsing: reject "12abc"/"1,234" instead of parseFloat coercing
  them to 12/1 and silently corrupting the imported amount.
- Require Status to be exactly COMPLETED: a blank/missing status no longer
  slips through the completed-only filter.
- Fail hard on an unsupported Direction: a blank or non-IN/OUT value (e.g.
  NEUTRAL for a balance conversion) throws instead of being guessed as income;
  the parse route surfaces it as BANK_FILE_PARSE_FAILED. Proper conversion
  support is tracked in #1019.
- Never invent currencies: a missing movement currency skips the row with a
  warning (no SEK default), and a fee with no currency of its own is dropped
  with a warning rather than inheriting the movement currency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

---------

Signed-off-by: Alexander Reinthal <email@reinthal.me>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-16 16:22:32 +02:00
Alexander Reinthal f8033cb32d fix(transactions): bind manually-fed transactions to a cash account (#1016) (#1017)
* fix(transactions): bind manually-fed transactions to a cash account (#1016)

create_transactions inserted rows with cash_account_id = null, so ledger
accounts fed via MCP/CSV without a PSD2 feed (e.g. 1935 Wise SEK) had no
kassakonto: get_reconciliation_status 404'd with "Okänt kassakonto" and the
"Matcha mot befintlig verifikation" dialog fell back to 1930.

No schema change: cash_accounts.bank_connection_id is already nullable and
source='manual' already exists (every company is seeded a manual 1930). This
is the creation-side leg of the #985-#987 root cause: the resolution chain was
fixed, but manually-fed accounts never got the cash_account_id link.

- Add ensureManualCashAccount (lib/cash-accounts/service.ts): find-or-create a
  manual (source='manual', bank_connection_id=null) cash_accounts row for a
  ledger slot, tolerating the (company_id, ledger_account) UNIQUE race.
- Add an optional ledger_account hint (^19xx) to gnubok_create_transactions;
  commitCreateTransaction resolves/creates the manual account and sets
  cash_account_id on the inserted row.

Reconciliation and voucher matching then resolve the real account unchanged.
Forward-looking; historical cash_account_id=null remediation stays in #1001.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

* fix(cash-accounts): guard ensureManualCashAccount against currency mismatch (CodeRabbit #1017)

The existing-row lookup matched only on (company_id, ledger_account) and
returned the row id ignoring currency, so a SEK transaction hinting at a ledger
already claimed for USD would bind to the wrong-currency cash account. Since
that pair is UNIQUE (one currency per ledger), a mismatch is a real conflict:
throw instead of silently mis-binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

---------

Signed-off-by: Alexander Reinthal <email@reinthal.me>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-16 16:15:06 +02:00
Mattsson aa5edd3aa7 fix(database): unblock credit note constraint validation (#1024)
* docs: record legacy credit note migration repair

* fix(database): assert repaired credit note state

---------

Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-16 16:04:06 +02:00
Alexander Reinthal edef48471c feat: add currency for articles (#834)
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-16 16:00:05 +02:00
Jack Ek 2a1ec5ec2f fix: Make SIE imports atomic (#860)
* fix: Make SIE imports atomic

* fix(import): carry dimensions + harden the atomic SIE RPC

Rebased onto current main. The RPC now:
- carries the per-line dimensions jsonb through the payload + INSERT so
  imported SIE object-list codes are not dropped (dimensions PR5 #866);
- uses the NULL-safe caller_is_company_member guard (drops the banned
  NOT IN (SELECT user_company_ids()) pattern ratcheted since #881);
- verifies the fiscal period belongs to the company;
- enforces per-voucher balance (sum debit = sum credit > 0) since
  SECURITY DEFINER + the direct draft->posted UPDATE bypass the trigger path;
- ships REVOKE ALL FROM PUBLIC, anon / GRANT EXECUTE TO authenticated,
  service_role (house style).
Migration renamed to a current timestamp. Added pg-real coverage for the
dimensions round-trip, unbalanced rejection, and foreign-fiscal-period guard.

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

---------

Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:00:01 +02:00
dependabot[bot] 4e2ca3f2a8 build(deps): bump the npm group with 11 updates (#1013)
* build(deps): bump the npm group with 11 updates

---
updated-dependencies:
- dependency-name: "@supabase/ssr"
  dependency-version: 0.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@supabase/supabase-js"
  dependency-version: 2.110.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: lucide-react
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: next-intl
  dependency-version: 4.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: stripe
  dependency-version: 22.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: eslint
  dependency-version: 10.7.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
- dependency-name: tailwindcss
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): scope npm group bump to non-major updates

Keep @supabase/ssr 0.12.1, lucide-react 1.24.0, next-intl 4.13.2,
stripe 22.3.1. Revert eslint ^10, typescript ^7, @types/node ^26:
all three are majors, CI pins node 20, and the toolchain major bump
is a separate pending decision. Lockfile regenerated with npm 10
against current main (fixes the npm ci desync that failed core-only).

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:55:52 +02:00
Jakob Wennberg 5ac560ce41 fix: generate tax deadlines for the installed base + correct 2893 label carryover (#1029)
* fix(bookkeeping): refresh correction line description on account change

When editing an ändringsverifikation, CorrectionEntryDialog pre-filled each
line's description from the original entry but never re-derived it when the
user changed the account, so a description carried over from the old account
(e.g. 2393 "Lån från närstående personer, långfristig del") stayed stale on
the newly chosen account (e.g. 2893, the kortfristig account). The regular
JournalEntryForm already auto-fills on account change; this mirrors it.

The refresh is guarded: it only overwrites the description when it is empty or
still equals the previously selected account's name, so a memo the user typed
themselves is preserved. Logic is extracted into a pure, unit-tested helper.

Note: the wrong text on an already-posted correction cannot be repaired (line
descriptions of posted verifikat are immutable per BFL / migration 017); this
prevents recurrence on future corrections.

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

* fix(deadlines): generate tax deadlines for the installed base

Automatic tax deadlines only regenerated when a tax-relevant settings field
changed value (didTaxFieldsChange). Companies fill those fields once at
onboarding, so a later save changed nothing and generated nothing; the annual
cron was the only unconditional trigger. As a result only ~5 of ~776 real
companies had any system deadlines, and the /deadlines empty state told users
to "check the tax settings" that were already complete.

- Settings save now also regenerates when the company has zero system
  deadlines yet (safe first-time backfill; cannot reset is_completed/status).
  Decision extracted into shouldRegenerateTaxDeadlines() with tests.
- The empty-state banner gets a "Generera nu" action wired to the existing
  /api/tax-deadlines/generate route (previously it had no caller). New sv/en
  strings.
- generateNewYearDeadlines (annual cron) paginates company_settings via
  fetchAllRows: a plain .select() silently caps at 1000 rows, leaving
  companies beyond the cap without next-year deadlines.
- scripts/backfill-tax-deadlines.ts: one-off that reruns the real generator
  for non-sandbox companies with zero system deadlines.

Known gap (follow-up): moms_period='yearly' has no deadline config, so annual
VAT filers get no momsdeklaration deadline yet.

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

* fix(deadlines): address review feedback + fix settings-route test

- settings/route.ts: fail safe when the system-deadline count query errors.
  A null count on error was treated as 0, which would trigger a
  delete+regenerate and reset is_completed/status on a transient failure;
  now a count error keeps the self-heal off (CodeRabbit, Major).
- Update app/api/settings/__tests__/route.test.ts (added on main via the
  withRouteContext refactor) for the extra deadline-count query and the new
  shouldRegenerateTaxDeadlines export; add self-heal / no-regen cases.
- Soften the "no deadlines created" copy: zero generated rows can also mean
  no applicable obligations (or the moms_yearly gap), not just incomplete
  settings (CodeRabbit, Minor).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:55:41 +02:00
dependabot[bot] 8534ff1006 build(deps): bump the github-actions group with 8 updates (#1014)
Bumps the github-actions group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-node](https://github.com/actions/setup-node) | `4` | `6` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3` | `4` |
| [docker/metadata-action](https://github.com/docker/metadata-action) | `5` | `6` |
| [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) | `3.7.0` | `4.1.2` |
| [The-PR-Agent/pr-agent](https://github.com/the-pr-agent/pr-agent) | `0.38.0` | `0.39.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `7.0.1` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `4.3.0` | `8.0.1` |
| [peter-evans/find-comment](https://github.com/peter-evans/find-comment) | `3.1.0` | `4.0.0` |


Updates `actions/setup-node` from 4 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

Updates `docker/metadata-action` from 5 to 6
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

Updates `sigstore/cosign-installer` from 3.7.0 to 4.1.2
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.7.0...v4.1.2)

Updates `The-PR-Agent/pr-agent` from 0.38.0 to 0.39.0
- [Release notes](https://github.com/the-pr-agent/pr-agent/releases)
- [Changelog](https://github.com/The-PR-Agent/pr-agent/blob/main/CHANGELOG.md)
- [Commits](https://github.com/the-pr-agent/pr-agent/compare/bd09b6cf89c6d6f3d16b159fa7603fa0e7768cf2...8e4d32e5497defd43c023a404f73560c62728961)

Updates `actions/upload-artifact` from 4.6.2 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `actions/download-artifact` from 4.3.0 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

Updates `peter-evans/find-comment` from 3.1.0 to 4.0.0
- [Release notes](https://github.com/peter-evans/find-comment/releases)
- [Commits](https://github.com/peter-evans/find-comment/compare/3eae4d37986fb5a8592848f6a574fdf654e61f9e...b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: The-PR-Agent/pr-agent
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: peter-evans/find-comment
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-16 15:21:18 +02:00
Daniel Schäf 1a0d67bd28 Update repository link in README for self-hosting (#1025) 2026-07-16 15:20:14 +02:00
Jakob Wennberg d704714eef fix(auth): show confirmation-specific copy when a signup link fails (#1027)
A failed email-verification link redirected to /login?error=auth_error with no
flow context, so the login page framed every callback failure as a broken
password-reset link and pushed new users into a reset form for an account that
was never confirmed. The callback now forwards a coarse flow hint (recovery vs
signup); the login page renders confirmation copy without the reset CTA for the
signup case. The new copy names the likely cause (link opened in a different
browser than signup, or a one-time token consumed by a mail scanner) instead of
only "expired or already used".

Silent-team creation is also wrapped in try/catch so a transient insert failure
cannot turn an otherwise-successful first-time confirmation into a 500.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:20:18 +02:00
Mattsson 072aedeaf9 Fix/supp ag fb (#1023)
* fix: prevent credit notes from entering payment flow

* fix: persist and display customer personal numbers

* feat: configure automatic invoice reminder days

* fix: issue credit notes through send flow

* chore: add repository agent guidance

* feat(mcp): route tools across user companies

* fix(articles): delete unused register entries

* feat(invoices): improve issued invoice actions

* feat(supplier-invoices): retain uploaded source documents

* docs: record implementation decisions

* feat: enhance customer personal number handling and validation

- Updated CustomerForm to allow personal numbers in the format of "********-1234" for individual customers.
- Added validation to ensure personal numbers are only accepted for individual customers in CreateCustomerSchema.
- Implemented masking and encryption for personal numbers to enhance data protection.
- Introduced new utility functions for masking and encrypting personal numbers.
- Added database migration to enforce unique constraints on credit note relationships and prevent duplicate entries.
- Enhanced error handling and logging for credit note issuance and invoice processing.
- Updated tests to cover new credit note creation guards and personal number handling.

* test: enhance list companies test with supabase query mocks
2026-07-15 15:53:15 +02:00
Jakob Wennberg a558c75678 fix(bookkeeping): clear the period IB link when stornoing an opening balance (#1022)
* fix(bookkeeping): clear the period IB link when stornoing an opening balance

Reversing a period's opening-balance verifikat left
fiscal_periods.opening_balance_entry_id pointing at the reversed entry, and
nothing reads that pointer's status. The storno was a no-op where it mattered:

- getOpeningBalances() reads the linked entry's lines with no status filter,
  so the Balansrapport kept showing the cancelled IB.
- Year-end blocks while the pointer is non-null and tells the user to
  "reverse it before re-running year-end": advice the storno could never
  satisfy. delete_last_voucher and the opening-balance/correct route both
  refuse an already-reversed entry, so there was no in-app way out.

reverseEntry now drops the link, mirroring the bank-transaction unlink
directly above it. getOpeningBalances falls through to the duplicate-safe
compute_prior_opening_balances RPC, and year-end can re-book the IB. This
also closes the documented residual edge in opening-balance/correct (storno
succeeded, relink failed) and makes runYearEnd's rollback comment true.

Two statements, not one: enforce_opening_balance_immutability rejects a
pointer change while opening_balances_set is still true. Covered by a pg-real
test, since a mocked client happily accepts the single-statement version that
the real trigger rejects.

Found via support: a user could not close 2025 because bogus 2026 opening
balances from a SIE import would not go away.

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

* docs: record the storno/IB-link decision in DECISIONS.md

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:10:51 +02:00
Mattsson b6332e9ff4 Fix/skv connection flow (#1015)
* feat(salary): one-click AGI submission with filing state machine and success feedback

The AGI panel required users to know that "Ladda ner AGI-fil" was the
generate step, then click submit, signing link, and kvittens manually.
A nollkorning filing stalled on "AGI-XML saknas" pointing at a UI path
that does not exist.

- New primary button "Lamna in till Skatteverket" chains the existing
  endpoints client-side: generate XML if missing, POST underlag, poll
  kontrollresultat, create signing link, open Mina Sidor in a tab opened
  synchronously at click (popup-blocker safe). Inline stepper shows each
  step; the four old buttons become collapsed advanced/recovery actions,
  auto-expanded in stale-draft and rejected states. XML download stays
  visible and free for manual filing.
- deriveAgiFilingState() + useAgiSubmission() lift the per-period
  submission record to the run page: the progress rail and salary hero
  now render the real state machine (generated, underlag inskickat,
  vantar pa BankID-signatur, inlamnad med kvittensnummer) instead of
  telling users to "lamna in" an already-submitted declaration.
- Success card with kvittensnummer and signature metadata once signed,
  plus a toast when a poll flips the state while the page is open.
- AGI kvittens cron every 15 min instead of every 2 h so filings signed
  on another device get stamped and emailed promptly.
- Advanced submit also auto-generates, and the stale "Lon -> AGI ->
  Generera" error text now points at the real buttons.

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

* fix(enable-banking): instant OAuth callback feedback and dead-attempt cleanup

The bank redirect landed on a blank page for the several seconds the
callback spent exchanging the PSD2 session and mirroring accounts, and
every failed connect attempt left a status='error' row that rendered
forever as an "Atgard kravs" card next to a successful retry, showing
duplicate connections to the same bank.

- Stream a branded "Slutfor bankanslutningen" progress page from the
  callback: the shell flushes before the session exchange starts and a
  script/meta redirect follows when the work completes, with a 30s
  slow-work escape hatch. Fast outcomes (denial, bad params, unknown
  state) keep their plain redirects.
- Delete never-activated connection rows (no session_id, no
  accounts_data) on denial or exchange failure, and sweep leftovers for
  the same bank on the next connect. Established connections keep their
  "Atgard krävs" card via the accounts_data guard; FKs are ON DELETE
  SET NULL so deletion has no dependents.
- Show "Banken ar ansluten: hamtar dina konton" while the settings
  panel loads after the callback instead of an anonymous spinner.

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

* fix(invoices): reject re-send of issued invoices and gate bookkeeping on the sent flip

A direct POST to /api/invoices/[id]/send against an already-issued
invoice re-emailed the customer and posted a second revenue verifikat
(createInvoiceJournalEntry has no dedup), overwriting journal_entry_id
and orphaning the first entry. Only the UI hid the button; the v1 route
and the MCP commit executor already rejected non-drafts.

- Non-draft invoices now return 409 INVOICE_ALREADY_SENT.
- The draft to sent status flip is an optimistic lock (status guard plus
  row-count check); journal entry, accrual schedules, PDF archival and
  the invoice.sent event only run for the request that won the flip.
- On a flip failure the journal entry is deferred: the row stays draft
  and a retry re-runs the pipeline, ending with exactly one verifikat.

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

* fix(invoices): payment links, failure visibility and sandbox guard for recurring auto-send

- sendInvoiceFromSchedule now auto-creates an online payment link via
  applyPaymentLinkToInvoice before rendering and passes the payment
  link QR to the PDF: parity with the dashboard and v1 send routes,
  which recurring invoices silently lacked.
- The recurring cron persists last_run_warning both when a claimed run
  throws (hourly retries stay visible on the schedule) and when a stale
  schedule is rolled forward, so a deterministic failure can no longer
  skip a month silently.
- Auto-send is blocked for sandbox companies at the email chokepoint
  (freeze-and-retain: the invoice is still generated as a draft),
  covering both the cron and the run-now route with one guard.

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

* feat(salary): close the Fortnox payroll API gaps (phases 1-4)

Payroll now runs end-to-end through the open API, including onboarding a
client from another payroll system, with every write staged for approval.

- v1: per-employee payslips (list/detail/PDF), payslip line writes,
  run roster attach/remove, absence ranges (per-day storage), jamkning
  fields, cutover opening balances (single + atomic bulk PUT), vacation
  balance + vacation-year-close. PUT added to the wrapper's idempotency/
  test-key set (test keys could otherwise write through PUT).
- MCP: 10 new tools (get_employee/get_payslip/list_absence/
  get_vacation_balance reads + staged update_payslip_line,
  register_absence, create_employee, update_employee,
  set_employee_opening_balances, close_vacation_year), executors, risk
  tiers, op-type CHECK expansions. create_employee encrypts personnummer
  at staging: pending_operations never holds plaintext.
- Scope-map audit retrofit: 11 formerly unmapped tools now scoped;
  BREAKING for keys that relied on the 4 default-allow writes.
- Cutover: employee_opening_balances (derived lock trigger, self-unlocks
  on run correction), engine YTD/karens/liability integration,
  Ingaende saldon section in the employee editor.
- Arbetsschema-lite: employees.hours_per_week/workdays_per_week drive the
  hourly/daily divisors; legacy 173/21 preserved exactly at defaults so
  existing pay math is byte-identical.
- Vacation ledger + semesterberedning/arsavslut: recomputed per-year day
  balances (synced on book/correct, non-fatal), year-close with the
  min-20 floor, 5-year sparade-dagar expiry to forced payout, and a
  2920/2940 drift adjustment via the bookkeeping engine; Semester
  dashboard card with preview-then-confirm dialog.
- Fix: Zod 4 defaults leak through .partial(), which made every sparse
  employee PATCH fail validation and reset defaulted columns.

Migrations 20260713100000/101000/110000/121000/122000 (applied to
staging with version rows; prod via merge). vacation_ledger renamed from
20260713120000 to avoid colliding with vat_declaration_totals_rpc.

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

* perf: cut dashboard page-load latency (region, round trips, caching, VAT RPC)

The dominant cost was infrastructure: Vercel functions ran in iad1
(Washington D.C.) while Supabase (DB + auth) lives in eu-north-1
(Stockholm), so every request paid 4-5 transatlantic round trips of
auth + company resolution before doing any real work (measured
530-1900ms for single-query GETs in prod logs). Pin functions to arn1
and cut the redundant work on top:

- vercel.json: functions to arn1, same city as the database
- getActiveCompanyId: preference + first-membership queries run in
  parallel; the fallback result doubles as validation in the common
  single-company case (one round trip instead of two sequential)
- withRouteContext: Server-Timing header and authMs/companyMs/handlerMs
  in the op-completed log, so latency is attributable per phase
- dashboard layout: nav badge counts off the critical path; DashboardNav
  loads them client-side via the new use-worklist-badges SWR hook with
  debounced realtime revalidation
- swr (new dependency, approved): global provider; useCompanySettings
  shares one cache entry across consumers and renders from cache on
  back-navigation instead of re-showing skeletons
- /pending: realtime refetch debounced; bulk operations previously
  fired 4 requests per row-change event
- VAT declaration: new get_vat_declaration_totals RPC returns
  per-account totals, settlement-shape detection (#984) and
  source_type counts in ONE round trip instead of paging every
  entry+line through PostgREST. Account lists stay TS-side parameters
  so ACCOUNT_RUTA remains the single source of truth. Shape-exclusion
  coverage moved to tests/pg/vat-declaration-totals-rpc.pg.test.ts;
  DDL already applied to staging.
- bundle: CommandPalette lazy-mounts on first Ctrl/Cmd+K, AgentChat
  dynamic-imports the markdown parser, @vercel/speed-insights (new
  dependency, approved) added for real-user timings

The /salary fetch-waterfall fix from the same effort already landed
inside 2084a756.

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

* fix(invoices): settle öre-rounded payments from the mark-paid flow

An invoice with öresavrundning shows a rounded "Att betala" on the PDF;
the customer pays that amount (up to 50 öre off the stored öre total) and
the invoice-page mark-paid flow rejected it with
MATCH_AMOUNT_EXCEEDS_REMAINING: a dead end, while the bank-transaction
match flow already absorbed the residual to 3740.

- PaymentBookingDialog now proposes the rounded bank leg plus the 3740
  residual line (credit when rounded up, debit when rounded down),
  resolved via getDisplayTotal from the per-invoice override and
  company_settings.ore_rounding.
- settleInvoicePayment and the v1 mark-paid route absorb the sub-krona
  residual, gated by planInvoicePaymentForLines: absorption applies ONLY
  when the caller lines carry the exact residual on 3740; otherwise the
  strict plan applies (sub-krona partials stay partial, no-3740
  overshoots keep the 400), so the GL can never diverge from the AR
  sub-ledger.
- planInvoicePayment absorb-band boundary tightened to >= 1 kr: an
  exactly-1-kr overshoot used to slip past both the guard and the absorb
  branch and silently over-record paid_amount (pre-existing on the
  bank-match path).

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

* fix(security): resolve all 7 PR compliance findings

- ASVS V3.3: per-request CSP nonce on the enable-banking finalize page
  (mirrors the mcp-oauth consent page); inline scripts are nonce-bound
- ASVS V16: decouple callback finalize work from the response stream
  (eager promise + next/server after()) so a client disconnect cannot
  drop session persistence or the consent_granted audit emit
- ISO 27001 A.8.15: failed audit-event emits log through the structured
  logger with a stable message for log-based alerting
- ASVS V2.3: recurring-invoice cron and run-now routes resolve
  isSandboxCompany themselves and pass an explicit suppressAutoSend flag
  (defence in depth around the email chokepoint, freeze-and-retain kept)
- ISO 27001 A.8.11: stagePendingOperation rejects plaintext
  personnummer-bearing keys in params/preview_data (key-based guard;
  EF org numbers make value-matching unsafe)
- ASVS V4.5: employee PATCH body is truly sparse; cleared number fields
  are omitted instead of resetting DB values to hardcoded fallbacks
- ASVS V8.2.1: route-level tests pin the v1 cross-company deny (404 by
  convention, not 403) on the payslip PDF endpoint

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

* feat: implement vacation-year basis change validation and error handling

- Added tests to block vacation-year basis changes when open balances exist.
- Implemented error handling for open-balances guard query failures in the settings route.
- Enhanced absence route to reject reversed date ranges with a validation error.
- Updated absence handling to use atomic upserts instead of delete+insert for better performance and reliability.
- Refactored salary calculation logic to correctly handle age-based avgifter rates according to Skatteverket's rules.
- Improved error messaging for vacation year closure adjustments.
- Adjusted employee opening balances handling to preserve audit information during upserts.

* feat(settings): add validation to block vacation-year basis change with open balances

feat(absence): reject reversed date ranges in absence queries

fix(absence): update absence handling to use atomic upserts instead of delete+insert

fix(employee): improve validation for jamkning dates in employee updates

fix(opening-balances): ensure created_by field is preserved during upserts

test(absence): enhance tests for absence range and date validations

test(calculation): add tests for age-based avgifter rates and edge cases

test(semesterberedning): validate vacation year closure adjustments and error handling

test(employee-opening-balances): update tests to reflect changes in salary_run_employees schema

* fix(migrations): implement NOT VALID constraints for pending_operations and add validation migration

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:54:33 +02:00
Jakob Wennberg e7e3c35f9e fix(billing): charge Swedish VAT on Stripe subscriptions (#1011)
The subscription price is net (tax_behavior=exclusive in Stripe), so the
Checkout session now enables Stripe Tax and collects what the rate needs:

- automatic_tax: 25% moms for SE customers, reverse charge for EU-B2B with a
  valid VAT number; it carries onto the subscription so renewals and the
  post-trial first charge stay taxed.
- tax_id_collection + billing_address_collection: capture the VAT number and
  address so Stripe issues a compliant momsfaktura.
- customer_update: persist name/address onto the pre-created customer.

BillingActions now shows "199 kr/man exkl. moms" plus the inkl. price.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 10:58:57 +02:00
Mattsson aab7e47c35 Bug/skv auth (#1010)
* fix(settings): open Skatteverket connect in a popup and fix the skahmst scope check

The full-page OAuth round-trip left Skatteverket's pages and the consumed
callback URL in browser history directly beneath /settings/tax, so closing
settings walked Back into a dead OAuth chain and re-prompted BankID auth.
The connect buttons now use the AGIPanel popup + postMessage pattern (the
callback already supports window.opener); the page never navigates and the
panel refetches status on success. Full-page navigation remains only as the
popup-blocked fallback.

Also fixes the reconnect-button condition: it checked for a scope literally
named 'skattekonto', but SKV grants 'skahmst', which kept "Anslut igen"
permanently visible on healthy connections.

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

* refactor(skatteverket): extract per-declaration AGI kvittens reconciliation

Behavior-identical extraction of the kvittens cron's core (fetch kvittens,
promote declaration to submitted, stamp salary_runs, clear cached submission,
complete deadline, notify) into lib/agi-kvittens-reconcile.ts so the upcoming
post-connect refresh can reuse it. Auth-error mapping (needs_reconsent, grant
revocation, APIGW config gaps) and run-level logging stay in the cron, which
is why SkatteverketAuthError propagates out of the helper on purpose.

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

* fix(skatteverket): auto-settle production-format AGI skattekonto draws

The settlement matcher only understood the SKV test environment's single
combined row ("Arbetsgivardeklaration YYYYMM"). Production books the draw as
two rows with Swedish month names: "Avdragen skatt maj 2026" (= total_tax)
and "Arbetsgivaravgift maj 2026" (= total_avgifter), so auto-settlement has
never fired against production data and the salary page kept showing paid
periods as unpaid.

parseAgiPeriod now also reads the month-name form (which additionally lets
match suggestions resolve the period on prod rows, including beslut rows).
Settlement classifies draws with start-anchored regexes so correction rows
like "Beslut 260703 arbetsgivaravgift mars 2026" can never qualify, and
settles a period on either the combined row matching the whole declared
amount or exactly one tax row + exactly one avgift row matching their
respective totals to the ore, dated to the later of the pair. Anything
non-exact (partial draws, duplicates) still falls back to the manual
mark-paid button.

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

* feat(skatteverket): refresh skattekonto + kvittenser right after OAuth consent

Nothing fetched Skatteverket data after a (re)connect: tokens were stored and
the user redirected, with the next fetch left to the nightly cron. SKV's
per-flow tokens (and refresh tokens) live ~65 minutes, so the crons usually
find them dead; right after consent is the one reliable window for a
personal-token fetch, which is why reconnecting never made stale numbers
recover.

The callback now awaits runPostConnectRefresh after storing tokens: a
skattekonto sync (upsert + auto-settlement + balance snapshot) plus a
kvittens re-check for this company's pending_signature AGI declarations.
Awaited on purpose so popup-close means the data is already fresh and UI
refetch listeners never race a background job; every step is best-effort and
a refresh failure can never fail the connect that just succeeded.

The callback's non-popup fallback also switches to window.location.replace
so the consumed callback URL (one-shot code + state) drops out of history
instead of re-running into a guaranteed CSRF error on Back.

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

* feat(salary): reload the salary dashboard after a Skatteverket reconnect

Listens for the BankID popup's skatteverket-oauth-success message and re-runs
load(). Because the OAuth callback awaits the skattekonto sync and AGI
auto-settlement before responding, the refetch already sees settled
tax-payment state: the "Skatt att betala" card flips to paid the moment the
popup closes, without leaving the page.

Also logs the three decisions behind this series in DECISIONS.md (awaited
post-connect refresh over after(), exact-pair settlement over per-period
summing, popup flow over in-place history repair).

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

* fix(skatteverket): verify OAuth popup source identity before trusting postMessage

The three 'skatteverket-oauth-success' listeners accepted any same-origin
message, letting any same-origin script trigger a data reload or a fake
success toast (OWASP ASVS V3.2, SOC 2 CC6.1). The two components that open
the popup now keep its handle in a ref and require event.source to be that
exact window; a window reference cannot be forged by same-origin scripts,
which is strictly stronger than a nonce threaded through the OAuth flow.

The salary dashboard never opens the popup, so its raw message listener is
replaced by a 'skatteverket-connection-updated' CustomEvent dispatched only
after a component has source-verified the popup (and after disconnect, so
connection-state consumers stay in sync).

Also extends the connect consent copy (sv + en) to disclose that connecting
immediately fetches skattekonto data and checks pending AGI receipts
(GDPR Art.5(1)(b) transparency).

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

* fix(skatteverket): CSP nonce and no-store on OAuth callback, deadline on post-connect refresh

Both callback HTML responses now carry a per-response nonce CSP
(default-src 'none'; script-src 'nonce-...'; base-uri 'none';
form-action 'none') so injected markup could never execute, plus
Cache-Control: no-store because the callback URL carries a one-shot
authorization code. The jsLiteral/appUrl injection invariants are
documented at the definition site.

The awaited post-connect refresh is bounded by a 30-second Promise.race
deadline so a hung SKV call cannot hold the OAuth callback open; on
timeout the refresh continues best-effort and the user still gets the
success response. Refresh failures and timeouts now log through the
structured logger with companyId + userId so they are attributable in
log aggregation (ASVS V16, ISO 27001 A.8.15, SOC 2 CC8.1).

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

* fix(skatteverket): tenant guard and redacting logs in kvittens reconcile, slim cron response

The agi_declarations update now also filters on company_id (ASVS V8.2.1),
matching the salary_runs pattern. The reconciler and the cron's error paths
log through the structured logger so third-party error strings pass
personnummer redaction; uuidKvittens is dropped from log context
(GDPR Art.5(1)(f) minimization, declarationId suffices).

response_data gains submittedAtEstimated so the signeradTid fallback can
never be mistaken for the legal filing time, and the submitted_by comment
now states explicitly that it records the technical submitter while
response_data.signeradAv is the authoritative legal signatory
(BFL 5 kap 6 par, BFNAR 2013:2 kap 8).

The cron HTTP response omits companyId per result row and sends
Cache-Control: no-store; the extension_data delete documents why the
period-scoped cache key needs no declaration-id guard. Cron tests observe
the logger via a mock without weakening any assertion.

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

* fix(skatteverket): diagnosable settlement refusals and signeradAv ROPA documentation

Settlement refusals (candidates present but amounts mismatch) now emit a
structured info log with declared-vs-drawn ore amounts per kind, so a
rounding divergence between stored declaration totals and SKV's actual
draw is diagnosable instead of silently falling back to the manual button.
No transaction texts are logged (they can carry personal data).

parseAgiPeriod documents the beslut-row audit: correction rows parse to
their period on purpose for match-suggestion boosting; settlement never
uses parseAgiPeriod (anchored classifiers + parseNumericAgiPeriod only)
and the only callers require an exact amount+side 1630 match first.

.compliance/ropa.yaml documents signeradAv (signer personnummer in the
SKV kvittens stored in agi_declarations.response_data): lawful basis
Art.6(1)(c) via BFL 5 kap 6 par / BFNAR 2013:2 kap 8, 7-year retention
per BFL 7 kap 2 par, access via company-membership RLS. DECISIONS.md
records the accepted-with-documentation calls from the compliance review.

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

* fix(skatteverket): add 'already_claimed' status to reconcile outcomes and enhance logging for pending lookups

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:40:03 +02:00
Jakob Wennberg f04dc4c4e0 fix(transactions): allow re-linking a bank tx stranded on a reversed verifikat (#988) (#1009)
* fix(transactions): allow re-linking a bank tx stranded on a reversed verifikat (#988)

A transaction whose journal_entry_id points at a reversed/cancelled entry reads
as "utan koppling" in the UI (the transactions page enriches only status='posted'
links), yet the re-booking guards treated ANY non-null pointer as "already
linked". So a storno'd/corrected transaction could never be linked to another
verifikat or re-categorized: the exact symptom in issue #988.

Add a shared hasLiveJournalEntryLink() predicate used by every re-booking guard
(linkTransactionToJournalEntry, manualLink, categorize-core, and the MCP link
stage-check): a pointer at a non-posted entry is treated as re-linkable, and the
two optimistic-locked writes now lock on the exact previous pointer (null OR the
stale id) instead of always .is(null), so the overwrite goes through race-safely.
hasLiveJournalEntryLink fails closed on a read error so a transient blip can't
detach a genuinely live link.

The source was fixed in #726 (reverseEntry/correctEntry now detach/re-point the
tx); this makes the guards self-heal for the pre-#726 backlog and any future
best-effort miss.

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

* fix(transactions): detect 0-row CAS before invoice effects; fix categorize commit test

Addresses PR review (CodeRabbit Critical + CI):

- link-journal-entry.ts: the tx UPDATE now .select('id') and treats a 0-row
  result as LINK_TX_TX_ALREADY_LINKED, failing BEFORE any invoice settlement /
  invoice_payments insert. Without this, a concurrent re-link that lost the CAS
  would still mark the invoice paid against a transaction we didn't link (same
  optimistic-lock contract manualLink already enforces).

- pending-operations commit route test: the categorize_transaction "already
  categorized" case now enqueues the hasLiveJournalEntryLink status read (posted
  = live) so it still returns 409. This was the core-only CI failure: the new
  liveness read in categorize-core consumed a queued response.

- Updated the link happy-path / invoice-race test enqueues to return a row for
  the now-selecting tx UPDATE.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:43:02 +02:00
Jakob Wennberg dcd33997b7 feat(agent): move 'Vad din agent vet' into settings (Assistenten -> Kunskap) (#1008)
Relocates the ledger-knowledge surface off the top nav and into the
assistant settings hub as a third tab (Minne / Kompetens / Kunskap), per
the code's own "minne + kunskap under Assistenten" intent and the #935
flag that this was an easy call to change.

Because both settings surfaces (the full-page rail and the intercepting
settings modal) mount each section as a propless component via
SETTINGS_SECTIONS, the knowledge data must be fetched client-side rather
than passed as a server prop:

- New GET /api/agent/knowledge aggregates buildLedgerContext +
  buildDeepEntities + buildAgentCompetence + company name (read-only,
  company-scoped via withRouteContext).
- AgentKnowledgeView + AgentCompetenceSections converted from async
  server components to client components (getTranslations ->
  useTranslations; no other server-only usage).
- New AgentKnowledgePanel client wrapper lazy-fetches the payload when
  the Kunskap tab opens (Radix unmounts inactive tabs), with Skeleton and
  error states matching the memory/skills panels.
- Removed the Brain/agent-knowledge entry (and its now-unused import)
  from the Analys nav group.
- /agent-knowledge kept as a redirect to /settings/assistant?view=knowledge
  so old links/bookmarks resolve.

Tests: new route test (auth 401, no-company 400, happy-path aggregation).
i18n: load_error_* keys added to both locales (parity kept).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:21:08 +02:00
Jakob Wennberg 4b51af3d80 feat(agent): 'Vad din agent vet' page rendering the ledger context (P2) (#935)
* feat(agent): 'Vad din agent vet' page rendering the ledger context (P2)

The human-facing surface for the openwiki ledger-context: a read-only page
that renders the exact payload the AI agent reads (Accounted://ledger/context
+ the briefing digest) as a legible profile of how this company books.

- Route app/(dashboard)/agent-knowledge (server component) calls the shared
  buildLedgerContext(supabase, companyId) directly: one payload, two
  renderers, no new API or data path.
- Sections mirror the payload 1:1: coverage/freshness strip, counterparty
  patterns (monochrome confidence bars + seen/agree evidence), supplier
  patterns, explicit rules shown as authoritative instructions distinct from
  observed patterns, account usage, VAT profile, conventions.
- Nav entry in the Analys group (icon Brain), ungated so it doubles as an
  upsell; flip requiredCapability to paywall.
- Design per .claude/rules/design.md (PageHeader, Card, Table, Badge,
  AccountNumber BAS tooltips); sv + en strings (agentKnowledge namespace).
  VAT/BAS labels stay Swedish in both locales per i18n rules.

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

* feat(agent): deep entity-resolved analysis + radial graph for the knowledge page

Reworks the 'Vad din agent vet' page from tables into a radial-hub graph
driven by a new full-history deep analysis, per founder feedback.

- fix(rpc): median_booking_lag_days now measures real posting promptness via
  committed_at, not entry_date (which the bank flow sets to the transaction
  date, giving a ~0 tautology: 151/152 on prod). migration 20260708120000.
- feat(rpc): get_ledger_deep_context (migration 20260708130000): full-history,
  deterministic, read-side. Merges counterparties by normalize_counterparty_key
  (e.g. Claude = 14 bookings across 12 name variants, weekly, 9 710 kr, always
  5420), mines booked verifikat for SEK spend (coalesce amount_sek), detects
  recurrence cadence, dominant account + share, plus supplier entities. Storno
  excluded, corrections kept; 19xx/26xx excluded from the dominant contra.
- LedgerGraph: radial SVG (company center, accounts inner ring, payees outer
  ring), hover/focus reveals variants + spend + cadence + account. Keyboard
  focusable nodes with per-node accessible names + a screen-reader data table.
- Page fetches the deep context alongside the light context; coverage strip
  gains tracked-payee / recurring / tracked-spend stats. sv + en strings.
- 14 pg tests (light + deep) green; both RPCs applied to prod + version-matched.

Reviewed by an adversarial multi-lens pass (accounting/SQL, frontend/a11y,
prod-fact verification); all four verified findings fixed (SEK currency,
storno-lag guard, keyboard a11y, spacing tokens).

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

* feat(agent): gentle mount fade-in for the radial map (reduced-motion safe)

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

* fix(agent): render the page for a rules-only company (empty-state edge case)

isEmpty ignored explicit_rules, so a company with configured mapping rules
but no posted transactions hit the 'hasn't learned anything' empty state and
lost its rules section. Rules are independent of bookings. (CodeRabbit)

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

* feat(agent): show Kompetens (skills) + Fakta (memory) on the knowledge page

The 'Vad din agent vet' page now shows the full picture of what the agent
knows: alongside the booking map, a compact read-only view of its Kompetens
(the Swedish accounting/tax knowledge atoms it ships with, grouped by
tier as chips with active/dormant state) and the Fakta it remembers (top
learned facts with kind + source), each linking to /settings/assistant for
full management. Server-rendered via a new buildAgentCompetence() that
mirrors GET /api/agent/skills + /api/agent/memory. Also renders in the
no-bookings case so a new company still sees its agent's competence.
sv + en strings.

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

* refactor(agent): restructure knowledge page - hero graph + tabbed detail

Declutters the page per feedback: the booking map is the always-visible hero,
and the supporting detail (Kompetens · Minne · Regler & profil) moves into
tabs so only one view shows at a time instead of a long card stack. Split
AgentCompetenceSections into standalone CompetenceCard + FactsCard for the
tabs; removed the top stat row on request. sv + en.

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

* feat(agent): Reconciliation Aurora rewrite of the ledger knowledge graph

Full rewrite of LedgerGraph: node area = sqrt(spend), colour = cadence,
shape = supplier/counterparty, confidence = depth-of-field; on-mount
descriptor-collapse animation with xN badge; cadence pulse veins;
deterministic seeded layout; framer-motion only (no new deps); keyboard
navigation, reduced-motion and sr-only support. Build-verified; 3-lens
adversarial review findings fixed.

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

* fix(agent): sample-size-honest confidence in the ledger knowledge graph

dominant_account_share was raw cnt/total, so a counterparty with a single
booking rendered as '100% säkerhet': fake certainty by construction (the
data_quality_master Item-C / P3 finding). New migration replaces
get_ledger_deep_context with a Laplace-smoothed share (cnt+1)/(total+2)
(1/1 -> 0.67, 3/3 -> 0.80) and exposes the raw evidence as
dominant_account_count / dominant_account_total. The detail card now shows
'Bokförd hit i k av n fall' under the confidence bar; the existing focus
buckets, stroke widths and percent labels inherit the honest value
unchanged. pg-real test updated to guard the n=1 case.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:25:34 +02:00
Jonas Flodén 64ea0fef02 fix(transactions): resolve customer-invoice payment account from cash_account_id (#987)
* refactor(transactions): add shared settlement-account resolution helper

Cherry-picked from fork/worktree-starry-waddling-wirth (PR #985) commit
34d5d35 — pulling in just the new lib/bookkeeping/settlement-account.ts
helper and its test, without the match-supplier-invoice route changes
from that PR (those depend on 8bfc31d, not yet on main, and are out of
scope here).

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(transactions): resolve customer-invoice payment account from cash_account_id

Customer-invoice payment matching never resolved the bank leg from the
matched transaction's own cash_account_id: it was unconditionally
hardcoded to 1930 in buildInvoicePaymentClearingLines,
createInvoicePaymentJournalEntry, and createInvoiceCashEntry, with no
override parameter at all. Any bank receipt landing in a non-primary
cash/bank account (a secondary SEK account, or a foreign-currency
account like 1940 for EUR) was silently misbooked to 1930 -- the same
class of bug PR #985 fixed on the supplier-invoice side, except
unconditional there (no stale-setting trigger needed).

Adds an optional paymentAccount parameter (default '1930', preserving
behavior for every caller that doesn't pass one) to the three lib
functions, and threads resolveSettlementAccount(cash_account_id) through
every real bank-transaction-matching call site: the dashboard
match-invoice route (POST + preview), its v1/MCP-facing counterpart, and
the agent/MCP match_transaction_invoice commit path. Deliberately left
on default 1930: mark-paid (dashboard + v1, no bank transaction in
scope), fix-cash-mismatch (narrow historical repair tool for a different
bug), and the agent mark_invoice_paid commit path.

Brings in lib/bookkeeping/settlement-account.ts (cherry-picked from
fork/worktree-starry-waddling-wirth commit 34d5d35) so this PR is
mergeable independently of #985's merge order.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* test(invoice-entries): cover ROT/RUT 1513 line stays fixed under a non-default paymentAccount

Compliance-bot finding on PR #987: createInvoiceCashEntry's paymentAccount
override was only tested against a plain standard_25 invoice, never
combined with a ROT/RUT deduction_type item. The 1513 receivable line was
already correctly untouched by paymentAccount (it's never the bank leg),
this just closes the test-coverage gap.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(bookkeeping): abort instead of silently defaulting to 1930 when settlement-account lookup errors

Same shared-helper fix as PR #985/#986: resolveSettlementAccount now
throws BookkeepingDatabaseError on a genuine cash_accounts query error
instead of warning and falling back to 1930. An explicit cash_account_id
almost certainly resolves to a non-1930 account, so a transient failure
masking it risked the same class of misbooking this whole PR series
exists to fix, just via infra flakiness instead of a stale setting.

No route/commit.ts changes needed: match-invoice (POST + preview) run
under withRouteContext's existing catch-all, and commitPendingOperation
already has identical generic bookkeeping-error handling for every other
engine failure. Added regression tests for all three call sites
(dashboard POST, preview, and the agent/MCP commit path) confirming the
abort rather than assuming the shared infrastructure handles it silently.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(v1): guard resolved settlement account against chart of accounts

Closes the two remaining gaps from jakobwennberg's triage on #987
(after rebasing onto main and picking up the already-pushed
resolveSettlementAccount abort-on-error fix):

- Added the v1 match-invoice route-level test coverage that was
  missing (cash-account threading, BOOKKEEPING_DATABASE_ERROR abort,
  ACCOUNTS_NOT_IN_CHART), mirroring the dashboard route's existing
  settlement-account-resolution tests.
- Added the same findUnresolvableAccounts pre-validation guard against
  chart_of_accounts that 32c07c4 added to #986's match-supplier-invoice
  route, gated on !customLines since that is the only branch here that
  consumes the resolved paymentAccount.

Signed-off-by: Jonas Flodén

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* test(bookkeeping): align settlement-account error assertion with #985

Use .rejects.toBeInstanceOf(BookkeepingDatabaseError) instead of
toMatchObject({ constructor: ... }), matching #985's edef79d follow-up
(the assertion was correct either way, but this is the more idiomatic
check and now makes the shared helper's test file byte-identical
across #985/#986/#987, removing the add/add merge conflict between
them noted in the merge-order validation.

Signed-off-by: Jonas Flodén

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* test(invoice-payment-lines): add missing 3740 coverage for non-1930 paymentAccount

CodeRabbit nitpick on #987: the test named "...does not affect the
FX-diff or öresavrundning lines" only exercised the 3960 FX-diff
branch, never the pure-SEK 3740 öresavrundning branch it also claimed
to cover. Split into two tests: the existing one renamed to describe
only its FX-diff coverage, plus a new pure-SEK sub-krona-short case
with a resolved non-1930 paymentAccount asserting the 3740 line books
correctly and the bank leg lands on the resolved account, not 1930.

Signed-off-by: Jonas Flodén

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(ci): quote compliance-pr.yml name to fix invalid YAML

The unquoted colon in `name: compliance: review (advisory)` (introduced
by #890's em-dash removal, which swapped an em dash for a colon
in-place) makes YAML read it as a nested mapping key, so GitHub can't
parse the workflow at all - every run fails with 0 jobs scheduled.

Signed-off-by: Jonas Flodén

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* Revert "fix(ci): quote compliance-pr.yml name to fix invalid YAML"

This reverts commit e7c890245d1834cd8f3c9b13a2bc3247fea7eacb.

Signed-off-by: Jonas Flodén <jonas@floden.nu>

---------

Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-12 21:23:19 +02:00
Jonas Flodén 8a41b5dbf2 fix(v1): resolve supplier-payment/categorize settlement account from cash_account_id (#986)
* refactor(transactions): extract shared settlement-account resolution helper

Dedupe the identical cash_account_id -> ledger_account lookup across
match-supplier-invoice (POST + preview) and categorize into
resolveSettlementAccount, per CodeRabbit's nitpick on PR #985. Pure
extraction, no behavior change.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(v1): resolve supplier-payment/categorize settlement account from cash_account_id

Closes the v1/MCP-facing half of the settlement-account gap left open
by PR #985 (which only fixed the dashboard routes):

- match-supplier-invoice: the pure-SEK accrual path always called
  createSupplierInvoicePaymentEntry with no paymentAccount at all
  (hardcoded internal default 1930), never reading the transaction's
  cash_account_id. Now resolves it via resolveSettlementAccount, same
  as the dashboard route post-#985.
- categorize: never called applySettlementAccount after building the
  mapping result, so every categorization booked the bank leg to 1930
  regardless of which cash account the transaction was linked to.

Left the FX/foreign-currency branch (createSupplierInvoicePaymentEntry)
and the cash-method branch (createSupplierInvoiceCashEntry) on their
pre-existing internal 1930 default, matching #985's own scope decision
on the equivalent dashboard route.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(bookkeeping): abort instead of silently defaulting to 1930 when settlement-account lookup errors

Same shared-helper fix as PR #985: resolveSettlementAccount now throws
BookkeepingDatabaseError on a genuine cash_accounts query error instead
of warning and falling back to 1930. An explicit cash_account_id almost
certainly resolves to a non-1930 account, so a transient failure masking
it risked the same class of misbooking this whole PR series exists to
fix, just via infra flakiness instead of a stale setting.

No route changes needed: both v1 call sites (match-supplier-invoice,
categorize) already run under withApiV1, whose existing catch-all
converts any isBookkeepingError() throw into the correct structured 500.
Added regression tests confirming the abort for both.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(v1): guard resolved settlement account against chart of accounts

CodeRabbit and jakobwennberg's triage on #986 both flagged that
resolveSettlementAccount() returns cash_accounts.ledger_account
unvalidated, so an inactive/removed account surfaced as the generic
MATCH_SI_RECORD_PAYMENT_FAILED instead of an actionable error. Add the
same findUnresolvableAccounts pre-check and AccountsNotInChartError
race-guard the categorize routes already use.

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* test(bookkeeping): align settlement-account error assertion with #985

Use .rejects.toBeInstanceOf(BookkeepingDatabaseError) instead of
toMatchObject({ constructor: ... }), matching #985's edef79d follow-up
(the assertion was correct either way, but this is the more idiomatic
check and now makes the shared helper's test file byte-identical
across #985/#986/#987, removing the add/add merge conflict between
them noted in the merge-order validation.

Signed-off-by: Jonas Flodén

Signed-off-by: Jonas Flodén <jonas@floden.nu>

---------

Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-12 21:14:16 +02:00
Jonas Flodén 528c53ffe7 fix(transactions): stop defaulting supplier-invoice payment account to a stale private-funds setting (#985)
* fix(transactions): stop defaulting supplier-invoice payment account to a stale private-funds setting

match-supplier-invoice (POST + preview) defaulted the credited cash account
from company_settings.last_supplier_payment_account, a sticky setting written
by the manual mark-paid "betald med privata medel" flow. Once that setting
held 2893 (skuld till aktieägare) from an unrelated private payment, every
later match against a real bank transaction reused it instead of the
transaction's actual bank account, silently booking genuine bank payments as
shareholder-loan repayments.

Resolve the credit account from the matched transaction's own
cash_account_id -> cash_accounts.ledger_account instead (falling back to 1930
when unlinked), mirroring the existing settlement-account lookup in
transactions/[id]/categorize/route.ts. last_supplier_payment_account is no
longer read by either route.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* refactor(transactions): extract shared settlement-account resolution helper

Dedupe the identical cash_account_id -> ledger_account lookup across
match-supplier-invoice (POST + preview) and categorize into
resolveSettlementAccount, per CodeRabbit's nitpick on PR #985. Pure
extraction, no behavior change.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* test(transactions): cover settlement-account lookup-error and preview parity gaps

Adds the two test cases CodeRabbit flagged as missing on PR #985:
- POST match-supplier-invoice: cash_accounts lookup errors, falls back to
  1930 and warns (previously unexercised).
- preview match-supplier-invoice: linked cash account other than 1930
  (parity with the equivalent POST-route test).

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(transactions): thread resolved settlement account into FX/cash-method supplier-payment branches

Closes the remaining items from the Swedish-accounting-compliance bot
review on PR #985:
- match-supplier-invoice/route.ts computed paymentAccount via
  resolveSettlementAccount but only passed it into the pure-SEK clearing
  branch; the FX branch (createSupplierInvoicePaymentEntry) and
  cash-method branch (createSupplierInvoiceCashEntry) still defaulted to
  1930 internally even though both already accepted the parameter.
- resolveSettlementAccount now also warns (and falls back to 1930) when
  cash_account_id resolves to a row with no ledger_account, not just on
  a hard query error.
- Documents company_settings.last_supplier_payment_account's scope via
  a column comment: it must never be read to resolve a matched
  transaction's settlement account.

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* fix(bookkeeping): abort instead of silently defaulting to 1930 when settlement-account lookup errors

Compliance-bot finding on PR #987 (applies equally to #985/#986, shared
helper): resolveSettlementAccount treated "no cash_account_id" and "lookup
threw a real DB error" the same way -- warn and fall back to 1930. An
explicit cash_account_id almost certainly resolves to a non-1930 account,
so a transient failure masking it risked the exact class of misbooking
this whole PR series exists to fix, just triggered by infra flakiness
instead of a stale setting.

Now throws BookkeepingDatabaseError on a genuine query error; every
caller already runs under withRouteContext/withApiV1 (or the pending-
operations dispatcher), whose existing catch-all already converts any
isBookkeepingError() throw into the correct structured 500 -- no caller
changes needed. The "row found but ledger_account empty" case stays
warn+fallback (data-integrity gap, not a query failure).

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

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* test(bookkeeping): use rejects.toBeInstanceOf for settlement-account error assertion

Addresses CodeRabbit nitpick from the 2026-07-12 review round: matching
BookkeepingDatabaseError via a `constructor` key in toMatchObject is
non-idiomatic; toBeInstanceOf is the standard vitest assertion for this.

Signed-off-by: Jonas Flodén <jonas@floden.nu>

* docs: scope FX/cash-method paymentAccount gap note to /api/v1 and MCP routes

CodeRabbit flagged the #1000 reference on PR #985 as ambiguous — the main
match-supplier-invoice route's FX/cash-method branches already thread
paymentAccount (per the prior entry), so the still-open gap only applies
to the /api/v1 and MCP-facing route.

Signed-off-by: Jonas Flodén <jonas@floden.nu>

---------

Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
2026-07-12 21:04:39 +02:00
Jonas Flodén 1e19099945 fix(ci): quote compliance-pr.yml name to fix invalid YAML (#1003)
The unquoted colon in `name: compliance: review (advisory)` (introduced
by #890's em-dash removal, which swapped an em dash for a colon
in-place) makes YAML read it as a nested mapping key, so GitHub can't
parse the workflow at all - every run of it fails with 0 jobs scheduled,
on every branch and PR repo-wide.

Signed-off-by: Jonas Flodén

Signed-off-by: Jonas Flodén <jonas@floden.nu>
2026-07-12 20:54:08 +02:00
Jakob Wennberg 715c671b67 fix(app): stop transient login errors flashing the full-screen fallback (#1002)
* fix(app): stop transient login errors flashing the full-screen fallback

The BankID login landing (/auth/callback then the /select-company picker)
fires several Supabase auth/DB queries right as the session cookies are set,
so a transient failure there (most often a refresh-token rotation race, seen
in prod as "Invalid Refresh Token: Already Used/Not Found" on /middleware, or
a stale JS chunk after a deploy) threw during render. Only (dashboard) had an
error.tsx, so these escaped every boundary and hit app/global-error.tsx,
blanking the whole document with a bare "Nagot gick fel" screen for ~1s before
the next request repainted and logged the user in as usual.

Add an app-level error.tsx (AppErrorBoundary) that catches those segments and
their layouts, and harden global-error.tsx. Both recover via a single guarded
hard reload instead of React reset(): a reload re-runs middleware (fresh
rotated auth cookie) and fetches a fresh bundle (ChunkLoadError after a
deploy), matching the browser-navigation self-heal these transients already
relied on, whereas reset() re-renders against the same stale payload/bundle. A
per-path sessionStorage time-guard bounds it to one reload so a persistent
error shows the manual fallback instead of looping.

Reported via support: transient "Nagot gick fel" flash on BankID login.

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

* fix(app): make the error-boundary reload guard loop-proof (review)

CodeRabbit and the PR Agent both flagged that the 12s time-window guard could
still loop if a failing render takes longer than the window (e.g. a slow SSR
that eventually throws). Replace the time window with a per-path, per-tab-session
one-shot flag, so the auto-reload fires at most once per path regardless of
timing and a genuinely persistent error settles on the manual fallback.
sessionStorage is per-tab, so a fresh visit (or a different path) still gets a
fresh auto-recovery.

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

* fix(app): claim the reload flag atomically + add support escape hatch (review)

CodeRabbit round 2:
- Major: the one-shot flag was written in the effect but the reload fired even
  if the write threw (sessionStorage quota full), so it could reload forever
  without ever recording the attempt. Claim the flag inside decideInitialPhase
  instead, so entering the 'reloading' phase guarantees the flag persisted;
  any write failure falls through to the manual 'fallback' (no reload).
- Minor: give global-error.tsx a support escape hatch. It can't use SupportLink
  (no providers when the root layout fails), so use a dependency-free mailto to
  the hardcoded support address.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:05:43 +02:00
Mattsson 98d0c7f2d0 Add/stripe skv (#1004)
* fix(salary): align pain.001 salary file with the Swedish domestic bank dialect

Verified against the Swedish Common Interpretation of ISO 20022
(Bankforeningen, Common Payment Types in Sweden, Appendix 1 Example 4:
Salaries) and Nordea Corporate Access pain.001 examples v2.6 (2026-06-22),
and XSD-validated against the official pain.001.001.03 schema:

- drop SvcLvl SEPA (SEPA credit transfers are EUR-only; omitting SvcLvl
  gets the domestic NURG default)
- drop RmtInf (not allowed for SALA salary payments; the beneficiary
  statement text comes from the Dataclearing LON code)
- address employees domestically: clearing as CdtrAgt ClrSysMmbId SESBA,
  account WITHOUT clearing as CdtrAcct Othr with SchmeNm BBAN
- share the clearing/account split (Swedbank 5-digit shift, Nordea
  personkonto prefix dedup) between the LB and pain.001 generators via
  splitDomesticBankAccount, fixing pain.001 duplicating the personkonto
  clearing
- clamp MsgId/PmtInfId/InstrId/EndToEndId to Max35Text with the per-tx
  counter surviving truncation; carry the org number on Dbtr
- return 400 from the pain001 route on an invalid clearing instead of
  emitting a broken file

Also includes two unrelated decision-log lines from the parallel
revisor-review session (DECISIONS.md is a shared append-only log).

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

* feat(nav): surface the year-end chain in the sidebar

Add Periodiseringar, Arsredovisning (aktiebolag only) and
Inkomstdeklaration (INK2 for AB, NE-bilaga for EF) to the Skatt &
bokslut group, in workflow order. Entity gating via a new entityOnly
flag on NavItem; isActive carve-outs extended so exactly one row
lights up for the new routes. Driven by an external revisor review
that concluded these features did not exist because none of them
were reachable from the nav.

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

* feat(stripe): Stripe Connect integration behind config gate

Connect OAuth per company (only the acct_ id is stored), automatic
single-use Payment Links on invoice send, deterministic payment
settlement against 1686 (BAS moved acquirer receivables 1580 -> 1686),
payout booking with reverse-charge fees (6570 + 4535/4598 + 2645/2614),
and a 15-minute sync cron. Non-deterministic events land as
needs_review, never guessed at.

Fully dark without STRIPE_CONNECT_CLIENT_ID: connect returns 503, the
send hook and cron no-op, and the settings page shows 'Kommer snart'
(hosted) until the Connect platform is verified. Self-hosted keeps the
honest not-configured message.

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

* fix(deadlines): add shared completeTaxDeadline and fix dead AGI deadline auto-complete

generate-declaration.ts has updated non-existent columns (type/period/
status) since inception, so the arbetsgivardeklaration deadline was
never auto-completed. Replace with a shared helper targeting the real
schema (tax_deadline_type/tax_period/is_completed), also used by the
kvittens crons and moms handlers in the follow-up commit.

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

* feat(rot-rut): import Skatteverket beslutsfil and record decisions on payout requests

Parse the beslutsfil JSON from Skatteverkets rot/rut e-tjanst and record
godkant belopp on the matching begaran: matched by stored
skv_referensnummer first, then exact name among active undecided
requests; arenden by fakturanummer then personnummer, exactly-one or the
beslut errors (all-or-nothing). Never auto-settles: recording the beslut
and booking the payout are separate acts. Exposed as an API route and
the gnubok_import_rot_rut_beslut MCP tool.

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

* feat(skatteverket): system auth for background reads, one-click VAT submit, kvittens notifications

Hybrid auth program: system CCG (org certificate) for background reads
while personal BankID stays for interactive submissions, since SKV
per-flow refresh tokens live 65 min and crons structurally cannot run
on them. All system-auth code sits behind SKATTEVERKET_SYSTEM_AUTH_MODE
(default off) with a stub transport until the Expisoft cert and CCG
avtal land; auth resolution is centralized in resolve-auth.ts.

Also in this change:
- One-click VAT submit chaining kontrollera -> utkast -> las
  server-side with a stage discriminator; step-by-step buttons demoted
  to the overflow menu.
- Kvittens crons (AGI + new VAT schedule) with email-only
  notifications, deduped in notification_log under the new
  skv_kvittens type.
- Ombud grant probe + verification UI in the connect panel, and a
  dashboard promo card for unconnected companies.
- skatteverket_company_connections table with pg-real coverage.

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

* feat(salary): auto-settle AGI tax payment from skattekonto and surface SKV reconnect on the tax card

The "Skatt att betala" card only cleared via the manual mark-paid button
on the run detail page; the promised automatic flip from the Skattekonto
sync was never implemented, so paid periods stayed red.

- settleAgiTaxPayments: during every skattekonto sync, a booked
  "Arbetsgivardeklaration YYYYMM" debit row settles the matching
  agi_declarations.tax_paid_at, but only when the amount equals the
  declared total to the ore and the account is not in deficit
  (deterministic; drift or deficit falls back to manual).
- Salary overview card: reconnect hint when the SKV token needs
  re-consent (link to /settings/tax, silent when the extension is off),
  plus an inline "Markera som betald" button reusing the existing
  endpoint and salary_payments strings.

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

* Add cloud backup scheduling and alerting features

- Implement unit tests for scheduling logic in `schedule.test.ts`, covering various scenarios for determining if a backup schedule is due.
- Create a new module `backup-alert.ts` to handle failure alerts for cloud backup auto-sync, including email notifications for reauthentication and repeated failures.
- Introduce `schedule.ts` to manage scheduling logic, including handling local time zones and converting between local and UTC hours.
- Add CSV report generation functions in `archive-csv.ts` for trial balance, income statement, balance sheet, and general ledger, ensuring compatibility with Swedish Excel formats.
- Create a README generator for the archive structure in `archive-readme.ts`, providing clear documentation for users accessing backup files.
- Implement tests for CSV report generation in `archive-csv.test.ts`, ensuring correct formatting and content.
- Establish a full-archive coverage contract test in `full-archive-coverage.pg.test.ts` to ensure all company-scoped tables are properly classified for backup.

* fix(stripe): correct invoice clearing reference and improve type safety in sync logic

* fix(invoices): narrow accountingMethod before resolveInvoicePaymentSourceType

settleInvoicePayment takes accountingMethod as a raw settings string, but
resolveInvoicePaymentSourceType requires the 'accrual' | 'cash' union.
Normalize at the call site (anything but 'cash' books as accrual), matching
the existing useCashEntry semantics.

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

* fix: address CodeRabbit review findings and nitpicks on PR #1004

Review findings:
- backup settings redirect: always force view=export over incoming params
- AGI/VAT kvittens crons: isolate best-effort post-submit calls, check the
  signed-state persist error, guard recovery calls in catch blocks so one
  company cannot abort the rest; surface grant_revoked in the run summary
- kvittens notifications: atomic claim-first dedup with a partial unique
  index; map non-uuid reference keys to deterministic uuids
- grant probe: record the actual 2xx status; mTLS transport: handle
  response-stream errors
- stripe: amount-aware idempotency keys for payment links; emit
  stripe.disconnected on upstream revocations
- ROT/RUT beslut import: mutate in-memory request state after apply, move
  item + header writes into an atomic apply_rot_rut_beslut RPC, add
  rot_rut_payout to JournalEntrySourceTypeSchema
- migrations: use NOT VALID + VALIDATE CONSTRAINT for CHECK constraints on
  journal_entries, notification_log and rot_rut_payout_requests
- cloud backup: hour_utc-only schedule updates clear stale hour_local

Nitpicks:
- stripe sync: enforce the cron time budget inside per-connection event
  processing with idempotent cursor progress; maybeSingle for settings;
  honest partial-customer DTO shared with the settlement boundary
- shared applyPaymentLinkToInvoice helper for both invoice send routes,
  v1 docblock documents step 6b and PAYMENT_LINK_FAILED
- settings panel: drop redundant decodeURIComponent
- cloud backup: document worst-case archive memory headroom

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:14:12 +02:00
Jakob Wennberg ee3c33c7a4 docs(api): correct /docs/api against the v1 implementation (#999)
Audited every endpoint, param, header, request/response field, error code, and
webhook event in the public API docs against the v1 implementation and fixed the
drift; addressed two rounds of CodeRabbit review.

- Error envelope, idempotency, dry-run, and reversal-field corrections.
- Registered the missing articles/dimensions/inbox-items reference resources.
- Cookbook fixes: removed nonexistent endpoints, corrected params/fields, fixed
  the test-key vs live-key quickstart flow and the year-end lock/close sequence.
- Webhooks/changelog: retry window ~87h (incl. route metadata), shipped-vs-
  coming-soon, counts, API-key format, previous_attributes.
- export-docs-to-website.mts absolutises app-served links for the website.

The gnubok-website side is on branch docs/api-correctness (already deployed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-12 12:57:26 +02:00
Mattsson 7d7f604e00 Add/stripe invoice link (#998)
* feat(supplier-invoices): show registered invoices under "Att betala" with inline approve

Registered supplier invoices are already booked as debt (2440) but were
hidden from the "Att betala" tab until approved, which confused users.
The tab now shows registered invoices too, marked "Ej godkand" with a
compact inline approve button. Approval remains the gate for payment,
not visibility; status model and approve API untouched.

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

* feat(reports): add date range filter to huvudbok (kontoanalys)

Mounts the existing ReportDateRange control on /reports/huvudbok so the
ledger can be narrowed to any date range within the fiscal year, matching
Fortnox kontoanalys. Lines before the range roll into each account's
opening balance so running balances stay correct at the range start;
lines after the range are dropped. Applies to the XLSX export too.

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

* feat(invoices): add optional payment link on invoices (paste-link MVP)

The user pastes a payment link created in their PSP dashboard (e.g. a
Stripe Payment Link) onto an invoice. The recipient gets a "Betala
online" button in the invoice email and a QR code + clickable link in
the PDF payment box. No PSP integration server-side: this is the
demand probe; a future Stripe Connect integration would auto-fill the
same column.

- invoices.payment_link_url (migration 20260709090000), https-only +
  2048-char cap enforced in CreateInvoiceSchema; empty string
  normalises to undefined and build-invoice-write always writes a
  concrete value so clearing the field on a draft edit NULLs the column
- editor field (real invoices only) with one-link-per-invoice hint;
  strings in sv+en (messages landed via e0e11066)
- email button (customer.language, hidden for credit notes/proforma/
  delivery notes, URL escaped for the href attribute) + URL in the
  plain-text part
- PDF QR + link row following the Swish QR pattern; wired into send,
  download and preview routes
- derived documents (credit note, proforma convert, recurring) do NOT
  copy the link: it encodes one amount for one specific invoice
- MCP gnubok_create_invoice accepts payment_link_url (validated at
  staging and re-checked in the commit executor); v1 API exposes the
  column; tools/list token ceiling bumped 45K -> 45.5K (ledger entry
  in payload-size.bench.test.ts, headroom was <10 tokens)

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

* fix(invoices): show oresavrundning on editor/form totals, supplier list and invoice email

The rounding logic (getDisplayTotal) was correct but only applied on the
PDF, invoice list/detail and review dialog. The invoice editor summary,
the supplier invoice form totals and the supplier invoice list showed the
raw ore total right next to the toggle, and the invoice email said
"Att betala" with the unrounded invoice.total while the attached PDF
showed the rounded amount (and the email also ignored the ROT/RUT
deduction).

Extract the PDF's Att betala block into getAmountToPay
(lib/invoices/rounding.ts) and point PDF + email at it so they cannot
drift; behavior-identical refactor for the PDF. Booked amounts stay
ore-exact; display-only as designed.

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

* test(reports): adapt huvudbok date-range tests to the two-step entry-lines fetch

The date-range tests (0969168f) mocked the old single-query shape with the
parent entry embedded on each line; main's refactor (fetchEntryLines)
queries journal_entries first and reattaches. Queue entry rows like the
other tests so the merge of the two features is actually exercised.

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

* fix(invoices): fetch full invoice projection in v1 send so ROT/RUT deduction and payment link reach the PDF and email

The v1 send route's hand-rolled column list omitted deduction_total,
deduction_personnummer_last4, payment_link_url and the item-level
ROT/RUT fields, so invoices sent via the public API overstated
'Att betala' and dropped the deduction box. Reuse the shared
INVOICE_FULL_COLUMNS/INVOICE_ITEM_FULL_COLUMNS so the send row can
never drift from the GET shape again.

Also harden the supplier-invoice inline approve: a thrown fetch left
the button stuck spinning; failures now refetch the true server state.

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

---------

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