Commit Graph

57 Commits

Author SHA1 Message Date
Mattsson 60e33c4b51 Fix/cus fee 28 (#820)
* feat(invoices): add Plusgiro input to bank details settings

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

Adds validatePlusgiroNumber/formatPlusgiroNumber helpers + tests.

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

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

Two user-reported bugs:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:34:51 +02:00
Jakob Wennberg 10a0b1d8dd fix(invoices): embed company logo as PNG so it renders on invoice PDFs (#772) (#776)
* fix(invoices): embed company logo as PNG so it renders on invoice PDFs (#772)

@react-pdf/renderer's <Image> only decodes JPG/PNG, but the logo upload route
and the `logos` bucket also accept SVG and WebP. For an SVG/WebP logo @react-pdf
silently swallows the decode error (console.warn inside a try/catch in its
fetchImage step), so the invoice renders with NO logo and nothing surfaces —
"Logotyp kommer inte med på fakturor".

Fix: prepareInvoicePdfRender now fetches the stored logo and re-encodes it to a
PNG data URL via sharp (SVGs rasterized at higher density), handing the template
a company whose logo_url is that data URL. Renders regardless of upload format
and removes the render-time dependency on a remote fetch inside @react-pdf.
Falls back to the original URL unchanged on any failure (network, unreadable
image, sharp unavailable), so behaviour is never worse than before. Result is
cached per logo URL (5-min TTL, bounded to 50) since the logo is re-rendered on
every invoice — twice per send and once per invoice in recurring/batch loops.

prepareInvoicePdfRender becomes async and returns the resolved { branding,
company }; all 8 call sites updated (6 routes, recurring-schedule-service,
pending-operations/commit) to await it and pass the resolved company. Layered
cleanly on top of the Swish-QR feature already on main — both coexist at every
call site.

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

* fix(invoices): bound and dedupe the logo fetch (review hardening)

Review (PR Agent security): resolveLogoDataUrl fetched logo_url with no timeout
or size limit. Add a 5s AbortSignal.timeout and a 5 MB cap (checked on the
declared content-length and the read body) so a slow/oversized logo host can't
hang or balloon an invoice render. SSRF itself isn't reachable today — logo_url
is only ever set to a Supabase logos-bucket URL by the upload route — so an
origin allowlist is intentionally skipped (would break self-hosted storage).

Also coalesce concurrent renders of the same logo (preflight+final on a send,
and recurring/batch loops) onto one in-flight fetch+encode instead of N. New
test covers the size-cap fallback; existing SVG test now asserts the timeout
signal. 9/9 pass.

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:49:33 +02:00
Mattsson 8322830f46 Add/issue in absurdum (#739)
* feat(assets): allow editing fixed asset fields before depreciation

The fixed asset register only offered a "Dispose" action, so correcting a
mis-entered acquisition date/cost/category meant running the disposal flow —
which posts a real divestment voucher plus a Ch. 8a VAT adjustment.
Disproportionate and wrong for a data-entry fix.

Add an Edit action that allows correcting those fields directly, gated for
correctness:

- service: extend updateAsset() with category/acquisition_date/
  acquisition_cost; block the change once the asset is disposed or has posted
  depreciation (AssetCorrectionBlockedError) where it would desync posted
  vouchers from the register; realign the BAS triple on category change.
  Name, useful life, and method stay editable.
- api: extend the PATCH schema; annotate GET /api/assets with
  has_posted_depreciation so the UI can lock basis fields proactively.
- ui: EditAssetDialog + pencil action; disables date/cost/category when
  depreciation has been booked, with an inline explanation.
- errors: register ASSET_CORRECTION_BLOCKED (409).
- tests: unit tests for the guard; pg test for pre-disposal editability.

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

* feat(assets): also block basis edits when depreciation was hand-posted

The correction guard only consulted depreciation_schedules, so an
avskrivning booked as a manual journal entry (no schedule row) slipped
through and a basis correction was wrongly allowed.

Add a ledger scan: any posted credit to the asset's ackumulerade-
avskrivningar account (12x9) counts as depreciation. Entries that
depreciation_schedules attributes to a *different* asset are excluded, so
a sibling's engine avskrivning on a shared 12x9 account doesn't produce a
false block. What remains is depreciation tied to this asset (engine or
manual); a basis correction is blocked there and must go through storno.

Adds two unit tests: blocks on a hand-posted credit, allows when the only
12x9 credit belongs to a sibling's engine entry.

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

* fix(invoices): allow negative unit prices for discount lines

The invoice creation form rejected negative unit prices via a frontend
superRefine check, blocking valid discount lines (e.g. "Rabatt -100").
The unit_price error was never rendered inline, so submission failed
silently. The backend schema already allows negative unit prices (see
CreateInvoiceItemSchema test), so the form was simply out of sync.

Remove the non-negative constraint; empty/NaN prices are still rejected
by the base z.number() type. Drop the now-unused validation_price_positive
translation key from both locale files.

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

* feat(invoices): allow editing draft invoices

Drafts could be saved but not edited — the only way to change a draft's
lines, customer, dates or amounts was to delete and recreate it. Add a
"Redigera" action on draft invoices that opens the invoice editor
pre-filled with the draft and saves changes in place.

A verifikat is only created when an invoice is sent (or paid, under
kontantmetoden), so every status=draft invoice is uncommitted and safe to
edit; sent/paid invoices stay immutable and still require a credit note.

- Extract buildInvoiceWriteData() with the shared validation + computation
  (VAT rules, ROT/RUT, accruals, totals, currency, item rows); POST now
  uses it too, behaviour unchanged.
- Add UpdateInvoiceSchema and PATCH /api/invoices/[id], guarded to drafts
  (status=draft, no journal entry, not self-billed); number and status are
  preserved and no invoice.created is emitted.
- Extract the invoice creator into a shared InvoiceEditor with create /
  edit modes; /invoices/new is now a thin wrapper and /invoices/[id]/edit
  is the new edit page.
- Add a "Redigera" button on draft invoice detail pages + sv/en strings.
- Tests for the builder, UpdateInvoiceSchema and the PATCH route.

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

* feat(reports): make Huvudbok findable via account/saldo search terms

Searching the command palette for natural phrases like 'saldo per konto', 'kontoutdrag', 'kontoanalys' or 'transaktioner per konto' returned nothing, so users couldn't find the general ledger. Enrich the Huvudbok entry's keywords with those synonyms, and let Saldobalans and Balansrapport match 'saldo per konto' too since they are genuinely per-account balance views.

Companion change — the clearer Huvudbok report description ('Saldo och alla transaktioner per konto') — already landed in d5f474cb.

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

* feat(settings): let users edit their personal name

Add an editable Namn field to /settings/account that updates profiles.full_name and best-effort syncs auth user_metadata. Previously the personal name was only ever set from BankID's legal name at signup with no way to correct it, so users whose tilltalsnamn isn't their first given name were greeted by the wrong name (and email/password users had no name at all).

New POST /api/user/profile route (requireAuth, RLS-scoped update) mirrors /api/user/locale. sv/en strings added.

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

* feat(invoices): per-invoice öresavrundning override

Add a display-only öresavrundning flag per invoice that wins over the
company-wide setting. Resolution order in getDisplayTotal: per-invoice
override -> company setting -> default-on. The stored total and the booked
verifikat keep the exact öre; only the rendered total changes.

Supplier invoices gain the same flag but resolve a null to off (they never
had rounding historically), exposed via a toggle on the new-invoice form
and a rounding row on the detail page.

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

* feat(transactions): warn on possible duplicate before booking

Before committing a transaction (via book or categorize), detect an
already-booked sibling with the same date and amount and return a 409
TRANSACTION_BOOK_POSSIBLE_DUPLICATE instead of silently double-booking.

The user can override with force=true, which must be bound to the reviewed
sibling via expected_duplicate_transaction_id; the candidate is re-detected
server-side, so a stale or guessed id is rejected with
TRANSACTION_BOOK_FORCE_CANDIDATE_MISMATCH. Detection is fail-open on the
non-force path and fail-closed under force.

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

* feat(transactions): shadow-mode scope-drift dedup counter in bank ingest

Count rows that an enforcing same-feed scope-drift rule WOULD treat as
re-imports (the IBAN-drift re-imports the external_id check misses) and
surface it as IngestResult.shadow_scope_drift_candidates. Nothing is
blocked yet -- the counter only measures how often the rule would fire so
it can be validated against real data before enforcement.

Also gitignore scripts/delete-duplicate-transactions.ts: a destructive,
hand-run cleanup tool kept out of the repo so it can't run in CI/cron or be
mistaken for a supported feature.

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

* fix(bokslut): base bolagsskatt on post-disposition result

Bokslutsdispositioner are booked as source_type='year_end', which the
income statement excludes, so net_result alone overstates resultat före
skatt and the booked tax ignored the periodiseringsfond avsättning (too-high
tax, ÅR/INK2 mismatch).

calculateBolagsskatt now accepts resultBeforeTaxOverride. The preview builder
mirrors each proposal's P&L effect (+återföring, -avsättning, -SLP) onto the
pre-disposition result; the commit path sums the already-posted dispositions
via the new sumPostedYearEndDispositions (class 88 + 7533) since bolagsskatt
is committed last.

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

* feat(settings): fiscal years manager

Add a FiscalYearsManager to the bookkeeping settings that lists fiscal
periods with their status (closed > locked > open) and creates the next
year via CreatePeriodDialog, seeded to chain forward from the latest
period end.

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

* fix(api): return 400 when locking a period with unbooked transactions

lockPeriod() refuses to lock a period that still has uncategorized business
transactions. Detect that message in the lock route and surface it as a
clear PERIOD_HAS_UNBOOKED_TRANSACTIONS (400) instead of a generic 500.

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

* feat(invoices): implement isEditableInvoiceDraft utility and apply it across invoice edit routes
feat(transactions): log duplicate dismissal events in behandlingshistorik
test(invoices): add tests for isEditableInvoiceDraft function
test(transactions): enhance tests to verify behandlingshistorik logging
refactor(bokslut): update tax calculation test descriptions for clarity

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:42:37 +02:00
Mattsson f9ea9c0082 Add/pdf and templates (#705)
* fix(invoices): apply configured voucher series to payments + preview next voucher

The booking engine resolves the series from
default_voucher_series_per_source_type, but the global "Standardserie"
dropdown wrote a separate field the engine ignored, and cash-method invoice
payments (invoice_cash_payment) weren't exposed in settings — so configured
series were silently dropped to "A".

- Expose cash/private payment source types in the per-source-type form
- Write the global default through to the map on save, keeping overrides
- Resolve voucher-sequences/next by source_type (+date) to match the engine
- Show the upcoming voucher (V2) in the payment dialog title
- Share resolveInvoicePaymentSourceType so preview and booking can't drift

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

* fix(salary): keep AGI panel in sync with Skatteverket signing state

The AGI panel mixed run-scoped generation state (agi_generated_at,
agi_declarations) with period-scoped submission state (extension_data
agi_submission_{period}), so the two could drift and present
contradictory UI. Reconcile them:

- Auto-detect a Mina Sidor BankID signature: while awaiting_signing,
  poll /agi/kvittenser on mount and on tab refocus so the panel flips
  to "signed" (hiding the signing actions) without a manual
  "Hamta kvittens" click.
- Warn instead of offering to sign when the locked granskningsunderlag
  predates the run's latest AGI generation (draftIsStale) — avoids
  filing superseded figures.
- Self-heal a stale "AGI-XML saknas" error once the run's AGI is
  (re)generated out-of-band (MCP/API/other tab).
- Refetch the salary run on tab focus so agi_generated_at reflects
  out-of-band generation without a hard reload.
- /agi/lasUpp now clears the cached agi_submission_{period} record, so
  unlocking drops the panel back to the pre-submission state instead of
  stranding it on a released "redo att signeras" draft.

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

* feat: Implement VAT registration handling and invoice item line types

- Added VAT registration check in commitCreateInvoice to set VAT rate to 0% for non-VAT registered companies.
- Updated invoice creation logic to reflect 'exempt' VAT treatment and adjusted related fields accordingly.
- Introduced support for free-text and blank spacer rows in invoice items by adding a new line_type field.
- Enhanced invoice and credit note handling to accommodate new line types.
- Added new localized messages for text rows in English and Swedish.
- Created tests for salary run approval logic, ensuring bank details are validated correctly.
- Implemented effective net payout calculation for salary runs, considering tax overrides.
- Added SQL migrations to support new invoice item line types and accounting method awareness for linking invoices to vouchers.

* feat(articles): artikelregister with revenue account + VAT rate per article

Article register (non-inventory) with per-article VAT rate and optional
BAS class-3 revenue-account override. Includes API routes, UI pages,
MCP tools, pending-operation staging, and the activate-or-create
account flow (ACCOUNTS_NOT_IN_CHART -> ActivateAccountsDialog,
unknown numbers -> AddAccountDialog) reusing the journal entry UX.

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

* feat(bookkeeping): no-doc-required batch + bulk-missing endpoints

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

* feat(payments): supplier payment lines + cash-method invoice matching

Shared payment-line proposal for supplier invoices, improved
match-invoice/match-supplier-invoice flows (kontantmetoden-aware),
and voucher-link support without requiring a 151x clearing entry.

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

* feat(bookkeeping): new journal entry dialog, SIE import tweaks, misc

New journal entry dialog component, journal list/page updates,
invoice editor updates, SIE import adjustments, transaction ingest
and api-key tweaks, pr-agent workflow update.

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

* feat(invoices): implement tax reduction features and localization updates

* feat(tests): add VAT registration gate to pending operations commit tests

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:52:24 +02:00
Jakob Wennberg c0b006fcc1 feat(invoicing): artikelregister (product/article catalog) with per-article revenue account (#703)
* feat(invoicing): artikelregister (product/article catalog) with per-article revenue account

Add a lean, non-inventory article catalog (artikelregister) so users can define
reusable invoice-line presets (name, unit, price excl VAT, VAT rate) with an
optional per-article BAS class-3 revenue-account override.

- DB: articles table (RLS via user_company_ids(), audit + updated_at triggers,
  unique-per-company article_number), generate_article_number RPC (atomic +
  idempotent), company_settings counter, nullable invoice_items.revenue_account
  + article_id, pending_operations CHECK expansion.
- Engine: generatePerRateLines groups revenue by (vat_rate, account) —
  byte-identical with no override, balance-safe when split (last account absorbs
  the rounding remainder), reverse_charge/export still force 3308/3305.
- API: /api/articles CRUD (soft-deactivate); override validated against
  chart_of_accounts (active class-3) and frozen onto invoice lines at create.
- Propagation: override carried through send/mark-sent/credit/convert/cash and
  the staged commit paths (recurring deferred — documented inline).
- MCP: gnubok_list/create/update_article (staged, scoped, risk-tiered).
- UI: articles register (list/detail/form) + nav + bilingual i18n + invoice-line
  article picker & "Spara som artikel" quick-create.
- Tests: engine regression, route, and pg-real (RPC/RLS/triggers).

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

* fix(mcp): strip ILIKE _ wildcard from gnubok_list_articles search

Underscore is a single-character ILIKE wildcard; stripping it (alongside the
existing %,()\* set) keeps a stray char in the article search from matching
every row. Read-only + RLS-scoped, so no security impact — addresses PR #703
reviewer + compliance-swarm CC6.3 notes.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:05:37 +02:00
Jakob Wennberg b38b3d0230 fix(bookkeeping): settle öre differences to 3740 and improve supplier-invoice matching (#699)
Whole-krona Bankgiro/Swish payments of öre-bearing invoices were stranded
as partially_paid forever (e.g. 11 231 paid on an 11 231,25 invoice left
0,25 kr open). Book the sub-krona residual to BAS 3740 (Öres- och
kronutjämning) and settle the invoice in full, on both the supplier- and
customer-invoice match flows.

New shared pure helpers buildSupplierPaymentClearingLines +
planSupplierPayment mirror the customer-side primitives; routing preview
and commit through the same builder also fixes two pre-existing
preview↔commit drifts (payment account + line descriptions). Öre
absorption is accrual-only — cash entries book the full invoice, so
absorbing there would hide a 1930 discrepancy.

Also improves supplier-invoice ↔ bank matching:
- Pass-3 date window now spans [invoice_date-5, due_date+5] instead of
  due_date ±5, so early payments auto-match; an ambiguity guard demotes
  non-unique amount matches to suggestions.
- New retroactive matcher (on supplier_invoice.registered/.approved)
  surfaces the settling bank payment when the invoice is registered after
  the payment was imported. Matches are written as suggestions for
  one-click confirm-to-book, never silently auto-booked.

Tests: new unit tests for both pure helpers; extended matching, handler,
customer öre, and route suites. Full suite green (407 files / 5364 tests).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:09:39 +02:00
Mattsson 64991eb3c9 Add/transaction deletion (#695)
* feat(salary): add remove-employee button to draft salary runs

The DELETE /api/salary/runs/{id}/employees/{employeeId} endpoint already
existed (draft-only, cascades to the employee's line items) but had no UI
trigger, so a mistakenly added employee could only be cleared by deleting
the whole draft. Add a trash-icon action column to the "Anställda" table,
gated on draft status + write permission to match the endpoint's guard,
with a confirm prompt and success/error toast.

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

* fix(settings): prevent horizontal overflow on mobile

The company settings invite form was a non-wrapping fixed-width flex row that overflowed narrow viewports, forcing the full-screen settings modal to scroll on the x-axis. Stack the form vertically on mobile (sm:flex-row at and above the sm breakpoint) and add the missing min-w-0 guard to the modal content pane.

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

* feat(bookkeeping): move journal entry filters into a filter dialog

The ledger toolbar showed every filter inline (fiscal year, sort, series,
date range, missing-documents toggle), which felt cluttered. Keep only the
search field visible and move the rest into a "Filtrera" dialog with an
active-filter count badge.

- JournalEntryList now owns the fiscal-year scope, restored from the same
  localStorage key FiscalYearSelector writes, so the page no longer renders
  the selector separately.
- Filters apply live and the dialog stays open; "Rensa alla filter" clears them.
- Export STORAGE_KEY_PREFIX / ALL_YEARS_VALUE from FiscalYearSelector so the
  list reuses the persisted selection without duplicating the key.

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

* feat(transactions): implement imported transaction guard for deletion

- Added a guard to prevent deletion of transactions that are imported via bank sync or file uploads.
- Introduced `isImportedTransaction` utility to determine if a transaction is user-created or imported.
- Updated DELETE endpoint to return a 409 status for attempts to delete imported transactions.
- Enhanced transaction history and inbox components to reflect the new deletion rules.
- Added tests for transaction origin determination and deletion behavior.
- Updated UI components to include a confirmation dialog for clearing journal entry forms.
- Localized new strings for clearing form functionality in English and Swedish.

* feat(transactions): enhance transaction deletion guard and improve fiscal year visibility

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:25:27 +02:00
Jakob Wennberg f538401988 Invoice correctness bundle: voucher-link race, agent send guards, payment-reversal restore (audit C2/C17, F-2026080) (#666)
* fix(invoices): atomic link_invoice_to_voucher RPC — close the customer voucher-link race (audit C2)

linkInvoiceToVoucher() did UPDATE-then-INSERT with a manual rollback restoring a STALE pre-link snapshot: under concurrent linking on the same invoice, A's failed insert could overwrite B's successful link while B's payment row remained — corrupting paid_amount/AR. Mirrors the supplier-side link_supplier_invoice_to_voucher fix (PR #602).

- New SECURITY DEFINER RPC locks the invoice FOR UPDATE, re-validates (status, posted voucher, 151x AR credit, currency, overshoot, already-linked) and applies UPDATE + INSERT in one PG transaction. Inherits the supplier RPC's remaining-amount fix (trust stored remaining_amount even at 0 — the TS '> 0' guard let rounding drift slip past FULLY_PAID). Hardened per audit A5: REVOKE from PUBLIC/anon, GRANT to authenticated + service_role.
- linkInvoiceToVoucher() now delegates to the RPC — same signature, same LINK_VOUCHER_* codes, so all callers (route, pending-op executor, MCP) are unchanged. Keeps the invoice.paid event (now emitted with the post-link row, mirroring the supplier wrapper) and the best-effort bank auto-reconcile.
- pg-real tests: full/partial link, overshoot leaves the invoice untouched, ALREADY_LINKED, and the race regression (two concurrent full links -> exactly one wins, paid_amount never exceeds total, exactly one payment row). Verified locally against supabase/postgres:15.8.1.060 with all 334 migrations replayed: 10/10 pass. Two unrelated pg tests fail locally with AND without this change (pre-existing env sensitivity; green in CI).
- Unit tests re-mocked to the RPC-wrapper contract.

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

* fix(invoices): agent send path — block cancelled invoices + preflight PDF render (audit C17)

commitSendInvoice (the agent/MCP path) was missing two guards the send route has:

- No cancelled guard: a cancelled invoice passed the already-sent check, got re-rendered and EMAILED (a 'MAKULERAD' PDF delivered as if live), and the unguarded status flip silently re-activated it to 'sent'. Now rejected with the registry's INVOICE_SEND_CANCELLED message (400), mirroring the route.
- No preflight render: the executor assigned the F-series number BEFORE rendering, so a render failure left a numbered-but-never-issued invoice (an F-series gap if the draft is abandoned). Now mirrors the route: on fresh allocation, render with an 'F-PREVIEW' placeholder first and reject with INVOICE_SEND_PDF_RENDER_FAILED before any number is consumed; retries with an existing number skip the preflight.

Items/credit-note lookup moved above the preflight (it needs them); the real render and everything downstream are unchanged.

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

* fix(bookkeeping): payment reversal restores invoice state and releases bank line (F-2026080)

Reversing a payment voucher left the customer invoice deadlocked: status
stayed 'paid' while remaining_amount stayed stale (= total), and the bank
transaction kept pointing at the reversed JE so the line could neither be
re-matched nor deleted.

- Customer branch now recomputes remaining_amount from total (the supplier
  branch already did) and clamps paid_amount at 0.
- Both branches delete the payment row(s) tied to the reversed voucher so a
  re-match doesn't double-count or trip the unique indexes.
- New releaseLinkedTransactions() detaches bank transactions from the
  reversed JE (by journal_entry_id and by captured payment transaction ids),
  clearing the link/categorization columns so the line returns to the inbox.

Covers every standalone storno path (reverse route, MCP reverse tool,
delete-last-voucher); the match-invoice route already handled its own case.

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

* fix(transactions): match-invoice preview double-subtracted VAT on per-item path (F-2026080)

InvoiceItem.line_total is the NET line amount (it sums to invoice.subtotal,
each line's vat_amount = line_total * rate), but the preview's per-item rate
aggregation computed sub = line_total - vat_amount, double-subtracting VAT
and producing an unbalanced previewed verifikat (revenue credit too low
against the 1930 debit). The commit path (generatePerRateLines) was already
correct; only the preview disagreed.

Regression test mirrors the F-2026080 invoice: multi-item 25% SEK cash entry
must balance, with 3001 = subtotal and 2611 = vat_amount.

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

* fix(bookkeeping): address PR #666 review — supplier cash reversal, RPC tenant guard, CI fixes

Review feedback fixes:

- Supplier cash-payment reversal (Greptile): the supplier branch required a
  payment row before restoring status/amounts, so reversing a
  supplier_invoice_cash_payment (which books no payment row) left the invoice
  deadlocked at paid/remaining=0 — the same bug the customer branch fixed.
  Mirror the customer fallback (revert full paid_amount when no row exists).

- Payment-row lookups now filter by invoice id + company_id: a batch voucher
  (match_batch_allocate) carries one payment row per invoice under the same
  journal_entry_id, so the unfiltered .single() errored out and silently
  yielded null.

- Tenant guard on the voucher-link write RPCs (compliance V8.2.1, audit A5):
  link_invoice_to_voucher and link_supplier_invoice_to_voucher are SECURITY
  DEFINER + authenticated-executable, so any signed-in user could mutate
  another tenant's invoices via PostgREST. New migration applies the PR #625
  claims-based membership guard to both, caps p_notes at the Zod layer's
  2000 chars, and gives the supplier RPC the explicit REVOKE/GRANT it never
  had (was default PUBLIC execute). Covered by a new pg-real test.

- releaseLinkedTransactions now logs Supabase errors (compliance V16.1) —
  a failed release leaves a bank line stuck on a reversed JE and must be
  observable.

CI fixes:

- naive-ore-round ratchet (core-only): payment-sync.ts converted to
  roundOre() from @/lib/money (-4 occurrences vs baseline).
- match-batch-allocate.pg.test.ts flake (pg-real): Date.now()+random arrival
  numbers collided in CI; now time-component + monotonic counter.

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

* fix(bookkeeping): address PR #666 review round 2 — payment attribution, batch-scoped deletes, send guard

- RPC payment attribution (GDPR Art.32): user-session callers can no longer
  attribute invoice_payments / supplier_invoice_payments rows to an arbitrary
  user via p_user_id — the JWT sub is authoritative when role is
  anon/authenticated. service_role / direct callers keep p_user_id verbatim
  (their scoping happens in TS). pg-real test asserts the spoofed id is
  ignored.

- Payment-row deletes scoped to the source invoice (SOC 2 CC6.3): a batch
  voucher carries sibling payment rows for other invoices whose status this
  sync doesn't restore; deleting them desynced paid_amount from the rows.

- releaseLinkedTransactions success audit log: transactions has no
  write_audit_log trigger, so clearing the link/categorization columns now
  logs the affected transaction ids for incident reconstruction.

- commitSendInvoice guard extended with partially_paid/credited (ASVS V2.3):
  both imply the invoice was already issued; the status flip would have
  regressed them to 'sent'.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:32:39 +02:00
Mattsson 3e42fc6f32 Feat/voucher docs (#664)
* feat: implement inbox document picker and linking functionality

* feat: implement self-billing invoice functionality

- Added support for registering self-billed invoices received from customers.
- Updated the invoice schema to include fields for self-billing metadata such as `is_self_billed`, `external_invoice_number`, `self_billing_agreement_ref`, and `received_date`.
- Created API route for handling self-billed invoice submissions, including validation and error handling.
- Implemented database migrations to add necessary columns and constraints for self-billing invoices.
- Developed tests to ensure correct behavior of self-billing invoice creation and validation rules.
- Updated Swedish localization files to include new terms related to self-billing.

* feat: enforce SIE import requirement for non-Fortnox providers in migration process

* feat: streamline invoice processing and enhance error logging across APIs
2026-06-04 13:14:57 +02:00
Jakob Wennberg 5777f51940 Reject overpayment on all invoice-match paths (audit C3) (#647)
* fix(invoices): reject overpayment on all invoice-match paths (audit C3)

The paid/remaining math was copy-pasted across three sites; the dashboard match-invoice route guarded against overpayment but the v1 public API route and the agent/MCP commitMatchTransactionInvoice had drifted WITHOUT it — silently accepting payment > remaining (recording paid_amount > total, over-crediting AR; cleanup needs storno, not edit).

- New lib/invoices/apply-invoice-payment.ts planInvoicePayment(): single source of the paid/remaining/status math + overpayment guard, via canonical roundOre (@/lib/money, guard rail #9). FX-agnostic — caller passes the invoice-currency amount.
- All three sites delegate; the guard runs BEFORE journal-entry creation so a rejected match never burns a voucher number. Dashboard behaviour unchanged (faithful extraction — its existing overpayment test still passes, the equivalence anchor). v1 returns MATCH_AMOUNT_EXCEEDS_REMAINING; commit returns the same registry message at 400.
- Removes 7 hand-rolled Math.round(x*100)/100 sites; antipattern guard ratchets 668 -> 661.
- Unit tests for the helper (overpayment rejection, half-öre tolerance, remaining_amount fallback).

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

* review: run overpayment guard before the storno (PR #647)

greptile: in commit.ts and the v1 route the conflicting-JE storno ran BEFORE the new guard, so a rejected overpayment would still reverse the transaction's prior JE and null its journal_entry_id — a side effect on a rejected match. Move planInvoicePayment above the storno so a rejection leaves the transaction fully untouched. (The dashboard route's pre-existing storno-before-guard ordering is FX-entangled and unchanged here; noted as a follow-up.)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 17:00:22 +02:00
Jakob Wennberg c74b19df1b Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances

Two related fixes to bank reconciliation correctness:

1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
   an existing voucher previously advanced only the invoice — the bank
   transaction that paid it kept sitting in the Transactions inbox with a null
   journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
   call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
   links the bank transaction to the same verifikat when exactly one unbooked
   line matches it. Best-effort and post-commit: a failure here never fails the
   link. The result surfaces reconciledTransactionId; the inbox row leaves the
   list and the UI shows link_success_tx_reconciled.

2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
   matching RPCs identify a cash account's ingående balans solely by
   journal_entries.source_type='opening_balance'. Companies migrated from other
   systems often booked the bank IB as an ordinary voucher (source_type
   'import' or 'manual'), so it was never excluded and surfaced as a phantom
   reconciliation difference equal to the opening balance. Adds:
   - migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
     immutability trigger plus a SECURITY DEFINER RPC that validates the entry
     (balance-sheet lines only, dated on a fiscal-period boundary), flips the
     source_type, and writes an audit row — no blanket data sweep.
   - POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
   - BankReconciliationView action to trigger it from the IB diff.

The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.

Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.

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

* chore: rebrand gnubok → Accounted and prune swarm agent skills

Product rebrand and skills housekeeping. No runtime behaviour change.

Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).

Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:52:01 +02:00
Mattsson f6ee0c2a82 Bug/customer invoice bug (#628)
* fix(supplier-invoices): self-assess reverse-charge VAT + link payments to vouchers

Reverse-charge supplier invoices now carry a per-item reverse_charge_rate (0.06/0.12/0.25). Under omvänd skattskyldighet the supplier charges 0% VAT, so the line vat_rate stays 0 and the buyer self-assesses fiktiv moms at the statutory rate. Centralizes rate resolution (resolveReverseChargeRate) and the ruta 20-24 basis-account guard (isReverseChargeBasisAccount) in vat-entries so the booking engine and review-dialog preview can no longer drift.

Adds the link_supplier_invoice_voucher pending operation: mark a leverantorsfaktura paid by linking an existing posted verifikat that debits 2440, with no new journal entry. Exposes find-candidates/link MCP tools and the bulk-reconcile helper, scoped under suppliers:read/write.

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

* fix(vat): report yearly VAT over the rakenskapsar, not the calendar year

Annual VAT (helarsmoms) is filed per beskattningsar/rakenskapsar (SFL 26 kap), which can be extended or shortened up to 18 months. The previous Jan-Dec calendar span silently dropped part of an extended first year. calculateVatDeclaration now accepts a fiscalPeriodId and resolves the period's actual bounds for yearly; monthly/quarterly stay calendar. The reports UI passes the selected fiscal period, defaults the periodicity from the company's moms_period setting, and carries the period into the ruta drill-down. full-archive export threads the period id through too.

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

* fix(migration): resolve supplier invoice status from payment amounts

The provider's lifecycle status and its payment status are computed independently upstream and can contradict each other (e.g. a Fortnox invoice marked booked but fully paid). Both the arcim entity-mapper and the Fortnox mapper now let payment state win: fully paid -> paid, partial -> partially_paid, otherwise the mapped lifecycle status, with credit notes forced terminal. Balance is compared numerically (never strict === 0) so float drift or a residual ore resolves cleanly, and an absent Balance is treated as unpaid.

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

* fix(enable-banking): only ingest booked transactions to stop re-import drift

Pending entries are skipped during sync: a pending row is unstable across syncs (a later 'synka nu' returns it still pending or finally booked, often with a different effective date). Because both the dedup external_id and the content-dedup key are date-derived, that drift minted a new id and re-imported a transaction that already existed - observed in production as the same amount+description landing twice with different dates. Gating the import set on a stable booking_date removes the drift at the source and leaves booked rows' ids byte-identical.

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

* chore(gitignore): ignore local SIE test fixtures

tests/fixtures/sie/ may contain real or scrubbed company data and must never be committed.

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

* fix(invoice): handle errors during registration journal entry creation and ensure invoice rollback
feat(tests): add test for reverse charge rate handling on supplier invoice line items
feat(fortnox): ensure paid status reflects zero balance for fully paid invoices
chore(migrations): add reverse_charge_rate to supplier_invoice_items and backfill link_supplier_invoice_voucher

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:25:48 +02:00
Jakob Wennberg 7bcd46d503 feat(transactions): match overshoot guards + supplier voucher linking (#602)
* feat(transactions): match overshoot guards + supplier voucher linking

Three changes that together close the "I can't link a bank transaction
to an already-booked verifikat on the supplier side" gap and fix a
latent data-corruption bug on the per-tx match endpoints.

1. fix: clamp paid_amount on match endpoints when tx > remaining

   /api/transactions/[id]/match-{invoice,supplier-invoice} previously
   used transaction.amount wholesale as the paid amount, pushing
   invoice.paid_amount past invoice.total whenever the bank tx was
   larger than what was owed. Both endpoints now reject with
   MATCH_AMOUNT_EXCEEDS_REMAINING / MATCH_SI_AMOUNT_EXCEEDS_REMAINING
   and a structured { transaction_amount, remaining_amount, excess }
   payload that points the user at the future split-payment flow.
   FX branch already clamps to invoice.remaining_amount and is
   unchanged.

2. feat: supplier-side "link existing verifikat" (mirror of #591)

   lib/invoices/supplier-voucher-matching.ts mirrors the customer
   voucher-matching module: finds posted JEs that debit 2440
   (Leverantörsskulder), validates currency + remaining-amount, and
   atomically links them as supplier_invoice_payments rows. New
   /api/supplier-invoices/[id]/{voucher-candidates,link-to-voucher}
   routes wrap it. LinkVoucherPicker gains a mode='supplier_invoice'
   prop so the same component renders both flows. The supplier-invoice
   mark-paid dialog now uses Tabs ("Ny betalning" / "Befintlig
   verifikation") to match the customer-side UX.

3. infra: transaction_voucher_links junction + denorm guard

   Foundation migration for upcoming multi-tx ↔ multi-voucher flows.
   Adds the junction table (with RLS, updated_at, indexes), a
   block_contradictory_invoice_denorm trigger on transactions that
   refuses to set invoice_id/supplier_invoice_id to a value that
   contradicts an existing payment row, and is_transaction_booked(uuid)
   as a single source of truth for "is this tx anchored?" once
   multi-allocation leaves denorm columns NULL. No application code
   uses these yet — they unlock the batch allocation and bulk-book
   flows in follow-up PRs.

Tests: 98 unit tests pass across the touched paths (match-invoice,
match-supplier-invoice, supplier-voucher-matching, link-to-voucher).

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

* fix(supplier-invoices): PR review — atomic link RPC, computeRemaining edge case, pg-real tests

Addresses the three real issues raised by Greptile on PR #602.

1. (P1) Atomic supplier voucher linking — new
   link_supplier_invoice_to_voucher PL/pgSQL RPC. The TS-side
   linkSupplierInvoiceToVoucher() previously did UPDATE-then-INSERT with
   a manual unconditional rollback. Under concurrent linking against the
   same invoice, request A's rollback could overwrite a sibling B's
   successful write while leaving B's payment row in place. Moving both
   writes into a single PG transaction (one RPC call) lets PG's own
   rollback handle the failure path correctly. TS wrapper now just
   translates the structured RPC return into the lib's Result type.

2. (P1) pg-real tests — tests/pg/transaction_voucher_links.pg.test.ts.
   CLAUDE.md mandates *.pg.test.ts for any PR adding a trigger, RPC, or
   RLS. The Phase 1A foundation migration added all three but had no
   pg-real coverage. Tests now cover:
     - trg_block_contradictory_invoice_denorm refusing contradictory
       UPDATEs on invoice_id and supplier_invoice_id
     - the same trigger PERMITTING a matching UPDATE (no false positives)
     - is_transaction_booked() returning true via journal_entry_id, via
       invoice_payments, and via transaction_voucher_links rows.

3. (P2) computeRemaining edge case — trust remaining_amount whenever
   the column is non-null (including the legitimate 0 for fully-paid
   invoices). The old "> 0" guard fell through to total - paid_amount,
   which under rounding drift could compute a tiny positive residue and
   slip a fully-paid invoice past LINK_SI_VOUCHER_INVOICE_FULLY_PAID.

The fourth Greptile comment (overdue invoices silently get no
candidates) was a misread: 'overdue' IS in the open-state list at
route.ts:35. No code change needed there.

Tests: 100 unit tests pass (16 in the directly-touched paths).

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

* fix(supplier-invoices): PR review round 2 — broaden AP range, log event failures

Addresses the actionable findings from the compliance-swarm and
Swedish-accounting-compliance bot reviews on PR #602.

1. (swedish-accounting-compliance, high) AP account hardcoded to 2440
   rejected legitimate samlingsverifikationer that debit 2441
   (Leverantörsskulder i utländsk valuta), 2443 (Skuldfakturor), etc.
   BAS 2026 reserves the full 2440–2449 range for Leverantörsskulder.
   The TS-side AP_ACCOUNT constant becomes AP_ACCOUNT_PREFIX ('244')
   used with .like() and .startsWith(). The PL/pgSQL RPC's
   account_number filter becomes LIKE '244%'. The
   LINK_SI_VOUCHER_NO_AP_DEBIT error message updates to reference the
   244x range with examples.

2. (ISO 27001:2022 A.8.15 / OWASP V16) Empty catch on the
   supplier_invoice.paid event emission now logs with log.warn so a
   failure in the downstream reminder/audit subscriber leaves an
   auditable trail without blocking the response.

3. (GDPR Art.5(1)(c)) Documented design rationale for retaining
   select('*') on the post-link invoice re-fetch: the
   supplier_invoice.paid event payload is typed as
   `supplierInvoice: SupplierInvoice` in lib/events/types.ts, narrowing
   would break the subscriber contract. The event stays in-process
   and consumers legitimately need the full context.

Skipped findings:
  - V8.2.1 ownership concerns: route + RPC already filter by
    company_id from withRouteContext; the RPC's WHERE clause covers it.
  - DELETE policy scoping: matches the gnubok pattern across all
    company-scoped tables — any member with write access manages records.
  - transaction_id = NULL on the voucher-link path: by design — the
    flow has no bank tx (the voucher's 1930 line represents it).
  - Reverse-charge VAT (2614/2647) validation on linked vouchers:
    real concern but invasive change; tracked for follow-up.
  - Storno-chain integrity (linking the original of a storno pair):
    edge case; tracked for follow-up.

Tests: 26 unit tests pass in the directly-touched paths. RPC patch
applied to remote via Supabase MCP.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 12:31:19 +02:00
Jakob Wennberg ccdfed5fea feat: voucher linking, recovery ops, and salary overrides (#591)
* feat: voucher linking, recovery ops, and salary overrides

Adds reversible/correction-style write paths that customers and agents have
been asking for, plus per-run salary employee overrides.

Invoice → voucher linking
- POST /api/invoices/[id]/link-to-voucher and
  GET /api/invoices/[id]/voucher-candidates
- lib/invoices/voucher-matching.ts with full + pg test coverage
- LinkVoucherPicker UI in PaymentBookingDialog
- pending_operations.operation_type expanded with link_invoice_voucher
  (medium risk) and a (journal_entry_id, invoice_id) unique guard
- MCP: gnubok_find_voucher_candidates_for_invoice and
  gnubok_link_invoice_to_voucher tools

SIE undo
- POST /api/import/sie/[id]/undo + undo_sie_import RPC
- sie_imports.status gains 'undone'
- ImportResultStep surfaces the action; structured error SIE_UNDO_FAILED

Edit-recreate journal entries
- POST /api/bookkeeping/journal-entries/[id]/edit-recreate
- Bookkeeping detail page wires it into the existing edit flow

Delete-last-voucher clears IB link
- Trigger + pg test ensure deleting the last voucher of a period nulls the
  opening_balance_journal_entry_id link so a re-import lands cleanly

Salary employee overrides
- salary_run_employees gains per-run override fields + migration
- lib/salary/effective-values.ts centralises resolved values; all payslip,
  payment, AGI, KU, and booking routes read through it
- SalaryOverridePanel on the employee detail page

Account classifier
- lib/bookkeeping/account-classifier.ts + tests; AddAccountDialog uses it
- backfill-import-accounts script updated

Misc
- toast: minor styling tweak
- AGI generate-declaration: respect effective values
- structured-errors: new LINK_INVOICE_VOUCHER namespace

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

* feat: add link_invoice_voucher operation type to pending_operations

* feat: refactor salary run calculations and update error handling for SIE imports

* fix: PR review feedback on voucher linking and SIE recovery

pg-real (blocking):
- tests/pg/delete-last-voucher-ib: drop posted_at = now() from the seed
  UPDATE — journal_entries has no posted_at column.
- lib/invoices/__tests__/voucher-matching.pg: seed the posted voucher
  before closing the fiscal period so enforce_period_lock doesn't block
  the INSERT during setup.

voucher-matching error codes and rollback:
- Add LINK_VOUCHER_DB_ERROR (HTTP 500) and return it on real invoice
  UPDATE / payment INSERT failures. Previously these returned
  LINK_VOUCHER_VOUCHER_NOT_FOUND (404) which the pending-op dispatcher
  auto-rejects on transient DB errors.
- Log rollback failures explicitly so an invoice left in a half-linked
  state (advanced status, no payment row) surfaces for manual
  reconciliation instead of disappearing silently.

resyncNextPeriodOpeningBalance ordering:
- Create the new IB first, relink the period FK, then storno the old IB.
  Previously the storno ran first; if createJournalEntry failed the next
  period was left with a reversed IB and nothing to replace it, and
  executeSIEImport swallows the error as a non-fatal warning.

replace_period_opening_balance_link:
- Tighten role check to owner/admin (was owner/admin/member). Matches
  delete_last_voucher and undo_sie_import.

Data minimisation:
- /api/invoices/[id]/voucher-candidates and the matching MCP tools now
  project only the invoice and customer fields the matcher reads, instead
  of returning the full customer row.

Schema bounds:
- SalaryEmployeeOverrideSchema caps each numeric override at 10 MSEK to
  catch typos before they reach the ledger or AGI.

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

* fix(tests): supply user_id when seeding voucher_sequences

voucher_sequences.user_id is NOT NULL (per the multi-tenant refactor in
20260330130000). The previous test seed only set company_id /
fiscal_period_id / voucher_series, which made the seed fail with a
constraint violation on the latest pg-real run. Pass the same userId
used elsewhere in the seed helper.

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

* fix(tests): scope delete-last-voucher RPC assertions inside the tx

withUserContext always ROLLBACKs, so any DELETE the RPC performs is
discarded when the callback returns. The previous test then queried
journal_entries via a fresh getPool() connection that only saw the
pre-RPC committed seed state — hence "expected '1' to be '0'".

Move every post-RPC assertion (entry count, period FK clear,
opening_balances_set flip, audit log entry, sie_imports clear) inside
the same withUserContext callback so they observe the uncommitted state
before ROLLBACK fires.

Also fix the sie_imports INSERT: the column is `filename`, not
`file_name`, and `sie_type` is NOT NULL.

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

* fix(tests): assert against the IB-marker audit row directly

DELETE on journal_entries fires two audit_log writes: the generic
write_audit_log() trigger row ("Deleted journal_entries record") and the
delete_last_voucher RPC's explicit "(was period IB)" entry. Both land
at the same statement_timestamp(), so ORDER BY created_at DESC LIMIT 1
returned the trigger row non-deterministically in CI.

Switch to a presence check with a LIKE filter on the IB marker so the
test verifies what it actually cares about — that the RPC's IB-aware
audit row exists.

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

* fix(db): set company_id on delete_last_voucher audit_log rows

20260528120000_delete_last_voucher_clears_ib_link.sql inserts directly
into audit_log without setting company_id. audit_log's SELECT policy
filters company_id IN user_company_ids(), so those rows landed with
company_id=NULL and were invisible to every reader — only the generic
write_audit_log() trigger row remained visible. That broke BFL audit-
trail intent: the "(was period IB)" provenance row was never readable.

Republish delete_last_voucher with p_company_id populated on both
audit_log INSERTs (draft path and posted path). Behavior is otherwise
unchanged; the pg-real test for the IB-clear flow now sees the
RPC-written marker row as expected.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Emil <emilmattsson14@gmail.com>
2026-05-28 21:09:43 +02:00
Mattsson a9b43ebeb7 Bug/vat selection warning (#583)
* refactor: update VAT handling logic for non-registered sellers and improve related comments

* chore: gate automated email flows behind 503 responses

Disables user-facing access to invoice payment reminders and salary
payslip email sending. Underlying lib code (reminder-processor,
PDF templates, notification_settings) is preserved for easy re-enable.

- Invoice reminders cron route returns 503; settings UI section removed.
- Payslip send route returns 503; original implementation kept as
  _sendPayslipsImpl for future re-enable.
- Push notifications were already extension-disabled, no change needed.

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

* chore: remove Recapt feedback widget

Strips the third-party Recapt SDK and its floating feedback bubble from
the app. The in-app contact form keeps working via the existing email
channel (/api/support/contact). Drops the Recapt entries from the CSP
and the subprocessor list in the privacy policy.

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

* feat: reject meaningless rättelser in correctEntry

Guard against zero-economic-effect corrections in the storno engine:
- Reject when proposed lines net to zero on every account (e.g. 1930
  debit 100 / 1930 credit 100), which would erase the original posting
  without representing any affärshändelse (BFL 5 kap. 5 §).
- Reject when proposed lines are an exact multiset match of the original
  entry — a rättelse must actually change something.

New MeaninglessCorrectionError wired through bookkeepingErrorResponse
(HTTP 400) and the Swedish error translator.

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

* feat: add date-range picker to resultat- and balansrapport

Adds optional from/to date filtering to the four operational financial
reports (resultatrapport, balansrapport, income-statement, balance-sheet)
so users can view a month, quarter, or custom range inside a fiscal year
without leaving the report. Defaults to YTD; "Hela året" preserves the
prior full-period behaviour (URL-identical, cache-stable).

- trial-balance engine accepts optional fromDate/toDate, rolling prior
  in-period activity into IB and clamping period activity to the window
- 12 API routes accept and validate from_date/to_date query params
- ReportDateRange chip picker persists preset per company, only renders
  on the four relevant tabs
- FiscalYearSelector now emits the period object so the range picker
  has bounds without an extra fetch
- PDF/XLSX filenames reflect the chosen range
- Resultatrapport drops the prior-year column when narrowed (full-year
  vs partial-year would mislead)
- 11 new tests (engine + parser); all existing report tests pass

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

* feat: add support for marking journal entries as "no document required"

- Introduced a new sidecar table `journal_entry_no_doc_required` to track entries that do not require separate documentation (e.g., bank fees, interest).
- Implemented API routes for creating and deleting exemptions, including validation and authorization checks.
- Added a toggle component in the UI to allow users to mark entries as exempt, with an optional reason.
- Updated relevant tests to cover the new functionality, including RLS checks and cascading deletes.
- Enhanced existing schemas and types to accommodate the new `vat_amount` field for supplier invoice items.

* fix: address PR review findings on no-doc-required + VAT changes

- pg-real cascade test wraps DELETE in gnubok.allow_delete='true' txn so the
  immutability trigger bypass fires (mirrors delete_last_voucher RPC).
- Clamp supplier-invoice item vat_amount to <= line_total * vat_rate via Zod
  refinement (with 1-öre rounding tolerance) so the manual override can't
  inflate the 2641 debit beyond the statutory ceiling.
- groupVatByRate falls back to line_total * rate when stored vat_amount is 0
  with a positive rate, so legacy/import paths leaving the column at its
  NOT NULL DEFAULT 0 don't silently understate ruta 48.
- ReportDateRange todayIso() and preset endpoints use local date components
  instead of toISOString() (UTC) — fixes the midnight-to-02:00 off-by-one
  that truncated a day from YTD / this-month / this-quarter for Swedish
  users.
- NoDocRequiredToggle restores the previous reason on failed POST/DELETE so
  the rolled-back toggle state stays consistent with the rendered reason.
- Document the company-scoped (not user-scoped) DELETE authorization policy
  on the no-document-required route.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:56:09 +02:00
Mattsson 627109b5bd Mcp/entry bug (#580)
* feat: restore document version supersession on posted entries

* refactor: update VAT handling logic to remove dependency on seller registration status

* feat: update VAT handling to respect seller registration status across invoice processing

* feat: add no-op migration placeholder for document supersession
2026-05-27 17:07:27 +02:00
Mattsson a2a556d837 Bug/UI wrong display (#573)
* fix(dashboard): exclude credit notes from unpaid invoices widget

Credit notes (status='sent', negative total) were summed into the
"Att få betalt" widget, producing confusing negative totals like
"2 st, -38 625 kr". Filter them out via credited_invoice_id IS NULL,
matching the existing pattern in reminder-processor and the AR ledger.

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

* fix(documents): harden PDF preview and upload validation

- JournalEntryAttachments: switch inline PDF preview from <iframe> to
  <object type="application/pdf">. Mirrors the AttachmentPreviewSheet
  fix from #572 — Chrome's frame pipeline intermittently surfaced
  "Det här innehållet har blockerats" on iframes even with permissive
  CSP. <object> invokes the PDF plugin directly. crbug.com/271452.

- /api/documents/:id/inline: resolve Content-Type via file extension
  when mime_type is null or application/octet-stream. Legacy uploads
  landed with empty File.type from some drag sources; combined with
  the new X-Content-Type-Options: nosniff header on this route,
  Chrome refused to render valid PDFs. Extension fallback covers
  every legacy row without a DB backfill.

- /api/documents POST: surface DB-trigger period-lock errors as a
  400 DOC_UPLOAD_PERIOD_LOCKED with a Swedish reason. Previously
  every catch was bucketed into DOC_UPLOAD_STORAGE_FAILED (500 /
  "Filen kunde inte sparas") which hid the real cause from users
  attaching to verifikationer in closed/locked fiscal periods.

- document-service: add validateDocumentMagicBytes() that inspects
  the first bytes for valid PDF/PNG/JPEG/WebP headers (PDF tolerates
  a leading UTF-8 BOM). Wired into uploadDocument() and
  createNewVersion() so every upload path is protected — UI, MCP,
  and future email/webhook ingestion. Defends against agents that
  send a base64-encoded text placeholder instead of real binary
  bytes via the gnubok_upload_document MCP tool, which produced
  tiny (15-561 byte) "PDFs" that failed to render in Chrome and
  in external viewers.

Tests use a minimal valid PDF buffer (%PDF-1.4 … %%EOF).

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

* feat(arsredovisning): emit ÅRL-required notes and FTE-weighted medelantal

Five compliance gaps fixed in the K2 and K3 noter builders:

- Anläggningstillgångar roll-forward per ÅRL 5:8 § — per-category IB
  anskaffningsvärde, tillkommande, avgående, UB and accumulated
  avskrivningar movement (was only emitting avskrivningstider).
- Långfristiga skulder förfallande efter mer än fem år per ÅRL 5:13 §.
- Ställda säkerheter and Eventualförpliktelser as separate notes per
  ÅRL 5:14-15 § (K2 previously combined them).
- Koncernförhållanden per BFNAR 2016:10 kap. 19 / BFNAR 2012:1 kap. 8.

Replaces medelantal anställda — the old query filtered employees by an
is_active column that doesn't exist, so the note never emitted. Now
uses an FTE-weighted day-based average per ÅRL 5:20 §.

Six disclosure fields persist on arsredovisning_narratives as per-period
overrides; the UI extends the existing förvaltningsberättelse editor
with a "Lagstadgade upplysningar" subsection sharing the same Spara
button — no new pages, no settings changes.

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

* fix(invoices): respect vat_registered=false and hide personnummer for B2C

- PDF address block no longer prints org_number for individual customers
  (GDPR data minimization; ML 17 kap 24§ requires name + address only).
- Wire company_settings.vat_registered through the rule helpers, invoice
  creation API, preview-pdf API, and the new-invoice form so a non-VAT-
  registered seller cannot charge VAT (ML 1 kap. 1§). The PDF suppresses
  the empty "Moms 0%" row and shows a dedicated "Företaget är inte
  momsregistrerat" notice instead of the ML 3 kap. exempt notice.
- Engine unchanged: 'exempt' treatment already routes to 3004/3100 and
  skips VAT lines.

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

* fix(settings): remove approval rules from sidebar and routes

* fix(invoices): ensure vat_registered defaults to true for invoice previews and API

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:01:53 +02:00
Mattsson 32d9978f1b Fix/chrome pdf preview csp (#572)
* feat: add option to exclude year-end closing entries in SIE export and related reports

* delete docs

* fix: allow Chrome's PDF viewer in verifikat document preview

The /api/documents/:id/inline route shipped with
`object-src 'none'` in its CSP, which blocked Chrome's built-in PDF
viewer (it renders inline PDFs via an internal <embed>). Users on
Chrome saw "Det här innehållet har blockerats" when expanding a PDF
attachment in the bookkeeping view; Firefox (PDF.js) and Edge (own
viewer) were unaffected, and JPGs worked because <img> isn't subject
to object-src.

Drops the CSP for this route to the minimum needed for embeddability:
`frame-ancestors 'self'`. X-Content-Type-Options: nosniff plus the
fixed Content-Type from the handler already block MIME confusion;
X-Frame-Options: SAMEORIGIN + frame-ancestors still block clickjacking.

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

* feat(auth): add webmail deep link to email confirmation screens

Mirrors Stripe's signup UX: after asking the user to verify their email,
detect their webmail provider from the domain and show a button that
opens the inbox in a new tab. Gmail gets a from:<sender> search
pre-populated; Outlook/Yahoo/iCloud/Proton open the inbox directly.
Unknown / custom domains fall back to the existing copy.

Sender address is configurable via NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM
(default noreply@gnubok.se) so white-label installs can match their
Supabase Auth SMTP config.

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

* fix(auth): unblock first-time password set for BankID users with MFA

Supabase rejects updateUser({password}) and mfa.unenroll with "AAL2 session
is required" whenever a TOTP factor is enrolled. BankID magic-link logins
produce AAL1, and middleware skips MFA enforcement for bankid_linked users,
so they had no path to AAL2 — leaving them unable to set a backup password
or disable MFA without going through the email-recovery escape hatch.

- /api/account/password: branch on app_metadata.has_password. First-time set
  writes via service.auth.admin.updateUserById (no existing credential to
  protect, AAL2 guard does not apply). Change-password keeps the user-session
  updateUser so AAL2 still fires for credential rotation.
- /mfa/verify: accept a safeReturnTo query param and route there after
  successful verify, so step-up flows can land back where they came from.
- SecuritySettings: detect the AAL2 error from both change-password and
  mfa.unenroll and redirect through /mfa/verify?returnTo=/settings/account
  instead of toasting a dead-end error.

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

* Add tests and rounding utility for öre precision in bokslut calculations

- Implemented `roundOre` function for rounding SEK amounts to two decimal places, ensuring consistent monetary calculations.
- Introduced `ORE_TOLERANCE` constant for comparing rounded amounts, facilitating invariant checks in financial entries.
- Created comprehensive tests for `roundOre`, covering typical cases, edge cases, and idempotency.
- Added year-end invariants tests to verify database-level guarantees for closing entries, ensuring they balance to the öre and reject discrepancies.
- Developed end-to-end tests for the dispositions chain, validating the correctness of calculations across various scenarios.

* fix: update PDF rendering to remove Swish QR code generation and set default to disable Swish visibility

* fix: enhance security by rejecting data URIs in safeReturnTo function tests

* fix: improve rounding logic in roundOre function and add customer_type migration

* fix: add customer_type column to customers and enforce CHECK constraint

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:29:41 +02:00
Mattsson 0087b7be3f feat: add option to exclude year-end closing entries in SIE export and related reports (#567) 2026-05-25 17:51:54 +02:00
Mattsson 78c91e00e4 feat: add language preference for customers to support invoice locali… (#561)
* feat: add language preference for customers to support invoice localization

- Introduced language support for invoices, allowing customers to choose between Swedish and English.
- Updated invoice PDF generation to reflect the selected language for titles, labels, and messages.
- Enhanced email templates to generate content in the customer's preferred language.
- Added migration to include a language column in the customers table with a default value of Swedish.
- Updated tests to verify correct language usage in invoice emails and PDFs.

* fix: debounce API requests in InvoicePreviewCard and update F-skatt terminology in email templates
2026-05-22 15:21:05 +02:00
Jakob Wennberg cc351158f8 Invoicing & account-security polish bundle (#550)
* feat: invoicing & account-security polish bundle

Five independent improvements bundled to ship together:

- BankID/password lockout fix: BankID-only users could enroll MFA and
  brick themselves (Supabase requires AAL2 to change password or unenroll
  MFA, and AAL2 needs a password sign-in). New app_metadata.has_password
  flag tracks this; middleware gates /mfa/enroll behind it, /account/set-
  password is the unlock path, SecuritySettings shows a banner, and
  /api/account/password is the single write path that flips the flag.
  Backfill script for existing users.

- Swish invoice payment method: company_settings.swish + invoice_show_swish
  columns, validation in lib/api/schemas.ts (accepts 123XXXXXXX företag or
  07XXXXXXXX mobile, strips whitespace/hyphens), rendered on invoice PDFs.

- Send-reminders kill switch: per-company company_settings.send_invoice_
  reminders toggle in PdfPrintSettings/Automatisering. Reminder processor
  also tightened: positive status allowlist (sent + overdue) so terminal
  statuses can never match; skip when customer already responded via
  reminder link; race-window re-check before send.

- First-invoice logo prompt: one-shot dialog when creating the first
  invoice without a logo (issue #520). Self-limits via head-only count.

- SIE export opening-balance fallback: route IB through getOpeningBalances
  so the compute_prior_opening_balances RPC supplies #IB after multi-year
  imports where opening_balance_entry_id is intentionally NULL. Previously
  #IB silently went to zero and #UB collapsed to current-period movements.

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

* fix(account-polish): address PR review feedback

- BankID-link path (extensions/general/tic/index.ts): read-merge-write
  app_metadata instead of passing { bankid_linked: true } alone.
  updateUserById REPLACES app_metadata wholesale, so the previous code
  would have wiped has_password for any user who later linked BankID,
  causing the set-password banner to (incorrectly) reappear and blocking
  the standard MFA enrollment button. The comment is now corrected.

- Middleware (lib/supabase/middleware.ts): thread inner returnTo through
  the /mfa/enroll → /account/set-password redirect so the user lands on
  their original destination after the full chain completes, not on /.

- safeReturnTo helper (lib/auth/safe-return-to.ts): replace the
  starts-with-/-but-not-// guard on mfa/enroll and set-password pages.
  The previous guard let /\evil.com and /@evil.com through. The new
  helper parses against a synthetic base origin and verifies it matches.

- set-password page (app/(auth)/account/set-password/page.tsx): remove
  CLAUDE.md design system violations — bg-gradient-to-b on page bg,
  inline shadow-md style on the card, space-y-5, font-medium on the h1,
  rounded-xl on the card. Flat surface, hairline border, font-display
  h1 per the design tokens.

- Swish dedup (lib/payments/swish.ts): extract normaliseSwish() and
  isValidSwish() helpers and use them in lib/api/schemas.ts,
  components/settings/BankDetailsForm.tsx, and the invoicing settings
  page. Single source of truth for the regex.

- Password route (app/api/account/password/route.ts): emit a structured
  success log so the audit pipeline can detect password-set events, not
  just failures.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:44:09 +02:00
Mattsson e211ab31be UI/settings api mcp (#524)
* feat(voucher): add create voucher and correct entry previews; update commit methods

* feat: add support for pending operations in API key scopes and OAuth client management

- Introduced new API key scopes for reading and approving pending operations.
- Updated the scope groups to include pending operations.
- Added new tools for listing and managing pending operations.
- Implemented OAuth client registration and revocation endpoints.
- Created a UI panel for managing OAuth clients, including registration and revocation.
- Added tests for pending operations tools and OAuth allowlist functionality.
- Implemented a database migration for OAuth client registrations with appropriate policies and constraints.

* feat: Implement OAuth client registration rate limiting and enhance security measures

- Added IP-based rate limiting to the OAuth client registration endpoint to prevent enumeration attacks.
- Introduced a service-role client for allowlist lookups, ensuring trust boundaries are maintained.
- Updated error responses to be uniform across different types of redirect URI validation failures.
- Enhanced tests to reflect changes in OAuth scope handling, ensuring fallback to read-only scopes when no scopes are provided.
- Improved handling of high-risk pending operations, requiring explicit confirmation for approvals.
- Added audit logging for OAuth client revocations and pending operation approvals/rejections to maintain a security audit trail.
- Refactored API key scope management to include default read-only scopes for OAuth-issued keys and added segregation-of-duties checks.

* feat: add recurring invoice scheduling functionality

- Implemented recurring invoice schedules with a new database schema.
- Created API routes for managing recurring invoices (GET and POST).
- Added cron job to automatically generate invoices based on schedules.
- Developed service functions for computing next run dates and executing schedules.
- Added tests for the new functionality, including validation and success cases.
- Introduced error handling for various scenarios in the invoice creation process.

* feat: refine VAT rate validation and enhance recurring invoice handling
2026-05-19 13:48:32 +02:00
Mattsson d27c3dd3dc Bug/customer cron job (#516)
* fix(reminder-processor): filter out credited invoices in overdue reminders

* feat: implement linking of transactions to journal entries

- Added POST endpoint for linking a bank transaction to an existing journal entry without creating new bookkeeping.
- Implemented validation for required fields and error handling for various scenarios (e.g., missing journal_entry_id, transaction already linked, journal entry not found).
- Created tests for the new endpoint to cover various cases including successful linking, error responses, and invoice handling.
- Introduced duplicate payment detection logic to prevent double-booking of bank receipts.
- Added a new component for correction affordance in the UI to facilitate user corrections on journal entries.

* feat(invoice-matching): enhance force matching with expected journal entry validation
2026-05-18 13:17:15 +02:00
Jakob Wennberg b46b572ee9 fix(invoices): duplicate-payment guard on customer mark-paid + categorize (#502)
* fix(invoices): duplicate-payment guard on customer mark-paid + categorize

Two-pronged fix preventing duplicate verifikationer when a customer
invoice is marked paid OR a 19xx→1510 categorization is applied to an
inbound bank tx that already belongs to an open invoice.

Prong A (mark-paid): before booking, scan unlinked positive business
bank txs from the same customer within ±2% / ±60 days. If candidates
exist, return 409 INVOICE_PAID_LIKELY_DUPLICATE with per-candidate
match_reason (ocr_exact > name_amount_fuzzy > amount_only). Override
via `{ force: true }`. Applied to both legacy /api/invoices/[id]/
mark-paid and v1 /api/v1/.../invoices/[id]/mark-paid; v1 guard runs
before dry-run so previews can't mask the warning.

Prong B (categorize): when the user assigns 1930→1510 directly on a
positive business tx with a matching open customer invoice (by name
OR by OCR-normalized reference), return 409
TX_CATEGORIZE_SUGGEST_CI_MATCH routing them to /match-invoice.

Mirrors the supplier-side guard from #461. Shared helpers
(DUPLICATE_AMOUNT_TOLERANCE_PCT, escapeLikePattern) reused as-is.
New helper normalizeOcrReference() strips non-digits for Swedish OCR
equality. New shared candidate-finder
lib/invoices/duplicate-payment-candidates.ts keeps the legacy and v1
routes calling the same code.

Frontend:
- PaymentBookingDialog intercepts the 409, renders candidate list
  with match_reason badges (Exakt OCR-träff / Sannolik träff /
  Möjlig träff), offers "Länka transaktion" or "Bokför ändå"
  (force-retry generates a fresh Idempotency-Key for v1 callers)
- transactions/page.tsx mirrors siMatchSuggestion handling as
  ciMatchSuggestion with a parallel "Matcha mot kundfaktura?" dialog

v1 caveat documented in the route's pitfalls block:
INVOICE_PAID_LIKELY_DUPLICATE force-retry requires a fresh
Idempotency-Key because the original is body-hash bound; reusing it
returns 400 IDEMPOTENCY_KEY_REUSE.

Tests: 5 new mark-paid tests (legacy + v1) covering 409, force
bypass, partial-payment skip, ocr_exact match_reason, multi-candidate
ranking. 1 v1-only test verifying dry-run also surfaces the 409. 2
categorize Prong B tests (409 + confirm_no_match bypass).

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

* fix(invoices): address compliance-swarm review on duplicate-payment guard

Three review-driven fixes:

1. **PostgREST .or() injection (OWASP V1.2.5).** `escapeLikePattern` neutralises
   LIKE wildcards but NOT PostgREST filter-DSL chars (`,`, `.`, `(`, `)`). A
   customer name like `Acme,fake.eq.true` could otherwise inject a synthetic
   filter clause into the `.or('merchant_name.ilike.%X%,description.ilike.%X%')`
   string. Replaced with two parameterised `.ilike()` queries dispatched in
   parallel and merged by id in JS. Slight perf cost (two index hits per call),
   eliminates the DSL-injection surface entirely.

2. **Date window anchored on invoice_date instead of due_date
   (swedish-accounting-compliance bot).** The Prong B categorize intercept
   filtered open customer invoices by `invoice_date ± 60d` relative to the
   bank-tx date. For invoices with 60–90 day payment terms, the actual
   payment lands well after `invoice_date`, so the legitimate match falls
   outside the window and the guard silently misses it. Switched to
   `due_date ± 60d` — the better proxy for "around when payment is expected."
   No corresponding change for Prong A (mark-paid), which is correctly
   anchored on `paymentDate` (the user-supplied or default-today date) and
   scans bank-tx dates around that anchor.

3. **Force-bypass log enrichment (ISO A.8.15, OWASP V16).** Both
   `duplicate-payment guard bypassed` warn entries now include `userId` and
   `paymentAmount`. Attribution was previously incomplete — the bypass log
   carried only `invoiceId`, which forced a join in log aggregation to
   identify the acting principal.

Tests updated for the two-query pattern (legacy mark-paid suite enqueues
two transactions-table responses per guard invocation; v1 tests already
worked with the single-entry-per-table mock semantics).

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

* chore(docs): redirect /docs/api and /llms-full.txt to docs.gnubok.se

Canonical docs host is now docs.gnubok.se. Every `docs_url` field on the
v1 error envelope still points at /docs/api/* on this app; the 308
permanent redirect forwards humans and agent crawlers to the docs
subdomain without us needing to mass-update structured-errors.ts.

/llms-full.txt also routes through the docs host where it's served from
the docs site's own build.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:46:02 +02:00
Jakob Wennberg 07a7964e8d fix(supplier-invoices): guard against duplicate payment when bank tx already booked (#461)
* fix(supplier-invoices): guard against duplicate payment when bank tx already booked

Two-pronged fix for a UX trap where a supplier invoice could be marked paid
even though the bank payment was already booked on 2440, creating a duplicate
verifikation.

Prong A — mark-paid duplicate guard: before booking, scan for an unlinked
outgoing bank transaction matching this supplier (merchant_name ILIKE) within
±2% / ±60 days. If found, return 409 SI_PAID_LIKELY_DUPLICATE with candidates
so the UI can offer "link existing" instead. Override via { force: true }.

Prong B — categorize match suggestion: when the user assigns 2440 directly on
a negative business transaction and an open supplier invoice from the same
supplier covers the same amount, return 409 TX_CATEGORIZE_SUGGEST_SI_MATCH
with candidates and route the user to match-supplier-invoice. Override via
{ confirm_no_match: true }.

Frontend dialogs added on the supplier-invoice detail page and the
transactions inbox. Partial payments skip the mark-paid guard (deliberate
action). Tests cover the 409 path, the override path, and the no-candidates
happy path on both routes.

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

* fix(supplier-invoices): apply PR review fixes to duplicate-payment guards

- Add `is_business = true` filter to mark-paid candidate query so private
  bank withdrawals don't surface as false-positive duplicates
- Escape LIKE wildcards (`%`, `_`, `\`) in both ILIKE patterns to avoid
  silent over-matching when a supplier/merchant name contains those chars
- Round paymentAmount and remaining_amount to 2 decimals before the
  partial-payment guard comparison to avoid float-equality fragility
- Require credit account to be in the 1xxx (bank/cash) series for the
  Prong B 2440 intercept so 2440 against clearing/equity accounts isn't
  misinterpreted as a supplier payment
- Extract DUPLICATE_AMOUNT_TOLERANCE_PCT (0.02) and
  DUPLICATE_DATE_WINDOW_DAYS (60) into a shared helper module with the
  LIKE-escape utility

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

* fix(supplier-invoices): broaden 244x match, audit log overrides, drop JE id from response

Second-round PR review fixes:

- Widen Prong B regex from /^2440$/ to /^244\d$/ so payments mapped to BAS
  sub-accounts (e.g. 2441 leverantörsskulder i utländsk valuta) also trigger
  the suggestion (swedish-invoice-compliance bot)
- Log a structured warning when force=true or confirm_no_match=true is honored,
  with the relevant context (amount, date, accounts) so the override is
  traceable per BFNAR 2013:2 kap 8 (behandlingshistorik)
- Drop journal_entry_id from the SI_PAID_LIKELY_DUPLICATE candidate response
  payload (data minimization, GDPR Art.5(1)(c)); the UI never rendered it

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

* fix(supplier-invoices): third-round PR review — date window on Prong B, length cap, VAT message

- Add the missing date window to the Prong B categorize candidate query
  (swedish-compliance bot): without it, an open invoice from years back can
  surface as a "match" for an unrelated bank transaction. Uses the shared
  DUPLICATE_DATE_WINDOW_DAYS against invoice_date.
- Cap supplier/merchant names to 200 chars before they enter escapeLikePattern
  (OWASP V1.2.5 / ISO A.8.28). Bounds DB work on pathological inputs.
- Log a structured warning when the mark-paid guard is skipped because the
  invoice has no resolved supplier name (BFL 5 kap 7 § — motpart should be
  identifiable; the absence is itself worth surfacing).
- Update the SI-match suggestion error and the matching UI copy to call out
  the actual compliance risk: a duplicate 244x posting double-deducts ingående
  moms (ML 8 kap 3 §), not just bookkeeping symmetry.
- Reword "Bokför på 2440 ändå" to "Bokför på leverantörsskulder ändå" now that
  the regex covers BAS sub-accounts 244x.

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

* fix(supplier-invoices): correct Prong B framing — duplicate verifikation, not VAT double-deduction

Latest swedish-compliance review correctly walked back the earlier
finding that asked for ML 8 kap 3 § VAT framing. Plain 244x
categorization via account_override does not include VAT lines (account
class 2), so the risk is a duplicate verifikation (BFL 5 kap 5 §), not
a double VAT deduction. Update both the structured error message and
the dialog body to reflect the actual mechanism.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:38:19 +02:00
Mattsson 64e6aa67a7 Fix/minor UI fixes (#459)
* feat(settings): add option for company name position in invoice PDF

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* fix(ui): improve accessibility for company name position toggle in PDF settings
2026-05-13 10:45:08 +02:00
Mattsson 7738f286af feat(settings): add toggle for displaying company name on invoice PDF… (#457)
* feat(settings): add toggle for displaying company name on invoice PDF header

* fix(migrations): rename duplicate-timestamped migration to unique version

Two migrations shared timestamp 20260513120000, causing schema_migrations
PK collision (SQLSTATE 23505) on apply. Bump the VAT seed migration to
20260513120100 so both insert cleanly.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:33:06 +02:00
Mattsson 3fa871c742 Bug/accounting suggestion (#456)
* feat: add bike benefit handling and optional vacation accrual

- Introduced bike benefit (cykelförmån) with calculations for annual market value and monthly taxable value.
- Updated schemas to include new benefit types and validation rules.
- Implemented API routes for creating, updating, and deleting employee benefits.
- Enhanced salary calculation logic to accommodate new vacation rule options, including a 'none' option for no accrual.
- Added UI components for managing employee benefits, including input for bike benefit specifics.
- Created database migrations for employee benefits and updated salary line items to support new benefit types.

* chore: remove Langfuse env var checks

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

* feat: enhance OAuth callback URL handling and update default scopes for Visma integration

* feat: remove trade_name field and simplify company naming in invoices

* refactor: destructure canWrite from useCanWrite for consistency across components

* feat: enhance PATCH endpoint to validate existing benefits and handle bike benefit updates

* feat: add missing label for bike benefit in salary line item types

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:26:04 +02:00
Mattsson ed9bdc4c00 Bug/transaction import (#435)
* fix(invoices): drop UTKAST banner on numbered invoices and preserve logo aspect ratio

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

* revert(invoices): restore UTKAST banner for drafts; keep logo aspect ratio fix

Numbered drafts intentionally surface UTKAST until manually marked sent.

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

* revert(invoices): drop logo objectFit change

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

* feat(enable-banking): implement transaction fetch strategy and update related logic

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:27:44 +02:00
Mattsson 75043632de Fix/compliance and invoices (#433)
* feat(invoices): implement inline membership checks for invoice-number RPCs and enhance error handling

* fix(invoices): enhance unpaid amount calculation to support currency-specific rounding

* fix(api-client): handle response.text() error for 403 status in skvRequest
2026-05-10 19:09:57 +02:00
Mattsson 1876b3b402 fix(agi): update AGI submission logic to reflect SKV's auto-persistence and adjust error handling (#432)
fix(pdf): align company logo and document title in invoice PDF layout
2026-05-10 15:37:19 +02:00
Mattsson 0ee5219b6c feat(invoices): implement öresavrundning logic and next invoice numbe… (#429)
* feat(invoices): implement öresavrundning logic and next invoice number preview

- Added `getDisplayTotal` utility to handle rounding for SEK invoices based on company settings.
- Updated `InvoicesPage` to utilize the new rounding logic when displaying totals.
- Introduced `peek_next_invoice_number` function to allow previewing the next invoice number without consuming the sequence.
- Modified invoice number generation to remove the year prefix and prevent truncation of numbers exceeding three digits.
- Enhanced tests for invoice number generation and rounding functionality to ensure correctness.
- Updated PDF template to reflect new rounding logic for totals and display appropriate values.
- Adjusted company switcher to hide options in sandbox mode.
- Improved error handling and logging in sandbox seeding process.

* fix(skatteverket): remove unused scope labels from SCOPE_LABELS and DEFAULT_SCOPES
2026-05-10 14:21:26 +02:00
Jakob Wennberg 97db09a3ff feat(invoices): allocate-on-save, makulera flow, manual invoice picker (#405)
* feat(invoices): allocate-on-save, makulera flow, manual invoice picker

Three coordinated invoice changes:

1. Allocate F-series number when the draft is created (Fortnox-style),
   not at send time. Users can download a numbered draft and send it
   manually. If number allocation fails, the invoice + items are rolled
   back so no orphaned rows remain. Adds INVOICE_CREATE_NUMBER_ASSIGN_FAILED.

2. DELETE /api/invoices/[id] now soft-cancels (status='cancelled') instead
   of hard-deleting. The F-series number is retained, keeping the sequence
   gap-free per ML 17 kap 24§ and BFNAR 2013:2 — no voucher_gap_explanations
   needed. Sent/paid invoices stay immutable (credit note required). Adds
   "Makulerade" tab to the invoice list; cancelled invoices are hidden from
   "Alla" by default. PDF draft banner stays visible on numbered drafts and
   only clears when the invoice is marked sent.

3. New InvoicePicker component lets users manually match an income
   transaction to an open invoice from the booking dialog ("Matcha med
   faktura..."), complementing the existing auto-match flow.

Also: new-invoice review dialog reads accounting_method from settings and
shows a cash-vs-accrual warning so users know when the verification posts.
seed-demo-account adds year-end closing + opening balance helpers so
multi-year demo data is balanced.

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

* fix(invoices): address review feedback on PR #405

Greptile P1 + Swedish compliance reviewer findings:

- app/api/invoices/route.ts — replace hard-delete rollback on number-
  allocation failure with a soft-cancel (status='cancelled'). If
  generate_invoice_number bumped the sequence before failing to write
  the number back, hard-deleting would leave a permanent gap in the
  F-series in violation of ML 17 kap 24§. Re-fetch invoice_number
  first so any partially-written value is logged for operator follow-up.
  Log loudly if the cancel itself fails so an orphan row doesn't go
  unnoticed.

- app/api/invoices/[id]/route.ts — close TOCTOU race on the cancel
  update. The .eq('status','draft') guard prevented data corruption
  but Supabase returned error: null with 0 affected rows on a
  concurrent flip, and the handler reported success. Add .select('id')
  and return new INVOICE_CANCEL_RACE (409) when no row updated.

- components/transactions/InvoicePicker.tsx — memoize createClient()
  so the supabase reference is stable across renders. Without this,
  including supabase in the useEffect dep array fires the open-invoices
  fetch on every render.

- app/(dashboard)/transactions/page.tsx + match-invoice/route.ts —
  read category from the match-invoice response instead of hardcoding
  'income_services' client-side. Server now echoes the category it
  actually booked; client falls back to 'income_services' if absent.

- lib/invoices/pdf-template.tsx — add MAKULERAD banner for cancelled
  invoices (red, distinct from the yellow draft banner). A cancelled
  invoice PDF previously rendered with no warning if it had a number,
  or with the draft banner if it didn't — both could be mistaken for a
  valid faktura. Cancelled takes precedence over draft so the legacy
  un-numbered-cancelled case is also covered.

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

* fix(invoices): guard cancelled status on send + rollback symmetry

Two follow-up fixes from the second-round Swedish compliance review on
PR #405:

- app/api/invoices/[id]/send/route.ts — reject sending a cancelled
  invoice. The existing flow had no status guard before
  .update({ status: 'sent' }), so a cancelled invoice could be silently
  re-activated to sent and a "MAKULERAD"-watermarked PDF could be
  delivered to the customer as if it were a live faktura. New
  INVOICE_SEND_CANCELLED (400) returned at the top of the handler.

- app/api/invoices/route.ts — add .eq('status', 'draft') to the
  rollback-cancel update so the rollback is symmetric with the DELETE
  handler's only-drafts-may-be-cancelled rule. At the create flow's
  current shape the row can't realistically be anything other than
  draft, but the symmetry prevents a future caller adding a status flip
  between insert and number-allocation from accidentally cancelling a
  posted invoice.

mark-sent (rejects non-draft), mark-paid (only sent/overdue), and
convert (explicitly rejects cancelled proformas) already guard
correctly — no changes needed there.

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

* fix(invoices): InvoicePicker filters settled invoices; drop dead error code

Two cleanups from the third-round Swedish compliance review on PR #405:

- components/transactions/InvoicePicker.tsx — add .gt('remaining_amount', 0)
  defensively. The picker filtered by status IN (sent, overdue,
  partially_paid), but a stale 'sent' or 'overdue' row with
  remaining_amount=0 (data inconsistency) would otherwise be selectable
  here and could be matched a second time, double-booking the income —
  a direct BFL 5 kap accuracy violation.

- lib/errors/structured-errors.ts — remove INVOICE_DELETE_NUMBERED.
  The numbered-draft refusal was replaced by the soft-cancel path
  earlier in this PR; the entry has no remaining callers.

Verified-safe and not changed:
- Cancel-without-storno concern: createInvoiceJournalEntry only fires
  inside mark-sent (after the draft→sent guard) or send (after the
  cancelled-status reject). Drafts never have posted verifications, so
  cancelling a draft cannot leave an orphaned bokföringspost.
- Hardcoded category: 'income_services' in match-invoice is a
  pre-existing classification concern that warrants a larger refactor
  (derive from invoice's revenue accounts) rather than a one-line patch.

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

* fix(invoices): InvoicePicker excludes proforma invoices

Add .eq('document_type', 'invoice') to the open-invoice query. A
proforma is not a faktura per ML 17 kap 24§ — no VAT obligation, no
binding commercial document — and must never be matched against a
bank receipt. Without this guard a sent proforma could be selected
in the picker, triggering a payment booking and VAT-rate journal
entry that violates BFL 5 kap accuracy rules.

Other findings from the third-round Swedish compliance review were
verified-safe and not changed:

- Cancelled-invoice PDF download path: the MAKULERAD watermark added
  earlier in this PR is the safeguard. Blocking the download endpoint
  outright would prevent legitimate audit access; the visible banner
  prevents the doc being mistaken for a valid faktura.
- Cancel-without-storno: createInvoiceJournalEntry only fires inside
  mark-sent / send / pending-operations, all behind status guards.
  Drafts never carry a posted verifikation, so cancel can't orphan one.
- Allocate-on-save for proforma uses F-series: not true. The
  generate_invoice_number RPC (migration 20260427150100) routes
  document_type='proforma' to a separate 'PF-' prefix sequence; the
  F-series is untouched.
- closeYearForSeed 2099 → 2091 transfer: real demo-data correctness
  issue but a seed-script polish item — separate PR.

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

* fix(match-invoice): server-side document_type='invoice' guard

The InvoicePicker filter excluding proformas (added in the previous
commit) is client-only. A direct API call to /api/transactions/[id]/
match-invoice with a proforma id would otherwise still book a payment
journal entry against a document that has no VAT obligation per
ML 17 kap 24§. Add a defense-in-depth check after the invoice fetch.

New error code MATCH_INVOICE_NOT_INVOICE_TYPE (400). Test added.

Other findings from the latest compliance review were verified-safe and
not changed:

- Cancelled-invoice PDF download path: /api/invoices/[id]/pdf always
  re-renders through InvoicePDF, so the MAKULERAD banner is always
  present. The bot's "cached pre-cancellation PDF" scenario does not
  apply to this codebase.
- Proforma F-series allocation: the generate_invoice_number RPC routes
  document_type='proforma' to a separate 'PF-' prefix; the F-series is
  not polluted.
- Soft-cancel rollback gap when number not written: the RPC is a
  single-transaction PL/pgSQL function — sequence bump (UPDATE
  company_settings) and row write (UPDATE invoices) commit or roll
  back together. The "sequence advanced but row null" scenario the
  bot describes is impossible by construction; a thrown exception in
  the row-write step rolls back the bump.
- closeYearForSeed obeskattade reserver: seed-script demo accuracy,
  separate PR.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:49:56 +02:00
Mattsson fd1db89603 Fix/invoice numbers (#365)
* feat: make invoice_number nullable and assign on send

- Updated the invoices table to allow invoice_number to be nullable.
- Modified the logic to assign invoice numbers only when the invoice status transitions to 'sent'.
- Refactored related code to handle nullable invoice numbers, including UI components and API routes.
- Added tests to ensure correct behavior when handling invoices with null invoice numbers.
- Introduced a utility function to display invoice numbers, defaulting to '(Utkast)' for drafts.

* fix: update fiscal period handling to return names of open periods in error messages

* fix: enhance period creation logic to account for company-wide bookkeeping lock-through

* fix: remove unnecessary customer_type field from customer insertion query

* fix: scope invoice number count query to specific companies to avoid test interference

* feat: Implement atomic invoice number generation and ensure compliance with invoice numbering rules

- Introduced `ensureInvoiceNumber` function to assign invoice numbers atomically, handling concurrency and ensuring compliance with document types.
- Updated invoice-related components to utilize the new `invoiceNumberDisplay` utility for consistent invoice number formatting.
- Added checks to ensure that invoices in non-draft statuses have valid invoice numbers, preventing violations of legal requirements.
- Created tests for the new invoice number generation logic, ensuring correct behavior under various scenarios, including concurrent requests.
- Added a draft banner to PDF templates for invoices without assigned numbers, clarifying their status to users.
- Updated database migrations to support the new atomic invoice number generation logic and enforce constraints on invoice statuses.
2026-04-27 16:29:58 +02:00
Jakob Wennberg b387a77bfd chore: remove Sentry, consolidate migrations, add test coverage (#244)
* chore: remove Sentry, consolidate migrations, add test coverage

Remove @sentry/nextjs and all Sentry integration code — error tracking
now handled by Recapt. Consolidate 22 incremental migrations into a
single schema sync migration. Add 6 new test suites (auth, invoice
matching, VAT rules, opening balances) and extend report tests with
edge cases. Update Docker image name to gnubok, sync crontabs and
extension presets, fix CSP missing space, simplify journal entry
missing-document dialog.

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

* fix: remove viewer bank import migration never applied to production

20260413150000_viewer_bank_import_permissions.sql (PR #234) was merged
to main but never applied to the production database. It references
current_active_company_id() which does not exist in production either.
This breaks fresh installs and Supabase preview branches because the
migration runs before the consolidated schema sync.

Remove it so the migration chain matches production. The viewer bank
import RLS policies should be re-added in a future migration alongside
the helper functions they depend on.

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

* fix: correct delete policies for tables without company_id column

Seven tables in the generic delete-policy loop don't have a direct
company_id column, causing fresh installs to fail with "column
company_id does not exist". Fix by moving them out of the loop:

- invoice_items, journal_entry_lines, receipt_line_items,
  supplier_invoice_items → join through parent table
- extension_toggles, notification_settings, push_subscriptions →
  user-scoped (auth.uid() = user_id)

All policies match their existing production definitions.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:52:00 +02:00
Mattsson bb336eba88 Fix/user feedback (#210)
* Add delete policies for provider consent tokens and provider OTC

* Add trade name support for companies in settings and documents

* Resolved currency selection issue

* Enhance invoice line display with foreign currency support and update delivery date schema to allow empty values

* Add currency display for journal entries and include currency metadata in transaction creation

* Add trade_name column to company_settings for external display
2026-04-09 16:22:51 +02:00
Jakob Wennberg 7a18d89c70 feat: INK2 declaration improvements, invoice delivery date & Swedish compliance skills (#204)
* feat: add INK2 declaration improvements, invoice delivery date, and Swedish compliance skills

Expand INK2 engine with full INK2S/INK2R support and improved SRU generation.
Add delivery_date field to invoices and corresponding PDF/migration support.
Add Claude skills for Swedish asset accounting, invoice compliance, SIE import/export, SRU filing, and tax planning.

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

* fix: address PR review — map BAS 4500–4899, strip CRLF in SRU, document P3

- Map BAS accounts 4500–4599 (legoarbeten), 4700–4899 (diverse
  varuinköpskostnader) to SRU 7512 so they are not silently dropped
  from INK2R declarations
- Strip \r\n in sanitizeString to prevent CRLF injection in SRU fields
- Document P3 period suffix limitation for brutet räkenskapsår

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

* fix: correct BAS 4500-4599, 4700-4899 mapping from 7512 to 7511

Per the official BAS-to-SRU mapping, these account ranges are cost of
goods (legoarbeten, inkurans, svinn) and belong under 7511 (Råvaror
och förnödenheter), not 7512 (Handelsvaror). 7512 remains 4600-4699.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:16:10 +02:00
Jakob Wennberg d0b3f21bde feat: remove AI extensions, restructure settings, and add atomic voucher commits (#157)
Remove AI-dependent extensions (ai-chat, ai-categorization, receipt-ocr,
invoice-inbox) and their infrastructure (lib/ai/*, ai-consent, LangChain/
Anthropic/OpenAI deps) to simplify core and reduce bundle size.

Restructure monolithic settings page into dedicated sub-pages (company,
bookkeeping, invoicing, tax, banking, api, account, team, templates) with
shared layout and sidebar navigation.

Add atomic commit_journal_entry RPC so voucher number increment and status
update happen in a single transaction — prevents burned numbers on constraint
failures. Add continuity check report and voucher gap explanation tracking.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:08:00 +02:00
Jakob Wennberg e89f2c402d feat: complete multi-tenant refactor + settings validation + fiscal period API (#156)
* feat: complete multi-tenant refactor for reconciliation, arcim, settings validation

- Migrate bank-reconciliation to company_id (all functions + tests)
- Migrate arcim-migration entity mappers and orchestrator to company_id
- Fix enable-banking reconciliation calls to use companyId
- Add Swedish law validation to settings schema:
  - VAT number required when VAT-registered (ML 11 kap. 8§)
  - Moms period required when VAT-registered (SFL 26 kap.)
  - Aktiebolag must use accrual accounting (BFNAR 2006:1)
- Fix fiscal year period creation: always 12 months after first year (BFL 3 kap.)
- Add plusgiro, website, pays_salaries fields to CompanySettings
- Add plusgiro to invoice PDF template
- Add fiscal period CRUD and opening balances API routes
- Add frame-src CSP directive for future iframe embedding
- Fix unlinked_1930_lines RPC to use company_id parameter
- Update CLAUDE.md documentation

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

* fix: address PR review findings (P1 + P2)

- Fix reconciliation events emitting companyId as userId — thread
  actual userId through runReconciliation and manualLink
- Move VAT cross-field validation (vat_number, moms_period) from
  schema refinements to route handler where effective stored state
  is available, preventing false rejection on partial updates
- Add plusgiro format validation regex (N-N pattern)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:46:41 +02:00
Mattsson fad4899cb4 fix: update RPC calls to use company_id instead of user_id for invoice and arrival number generation (#154) 2026-03-31 17:34:51 +02:00
Mattsson 0dd1f5ebc1 feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19)

Introduce companies table, company_members, and user_preferences to
support multiple companies per user. All data scoping changes from
user_id to company_id across the entire codebase.

Key changes:
- Database migration: new tables, company_id on 40+ tables, backfill,
  RLS rewrite from user_id to company-member-based, updated RPCs
- Types: Company, CompanyMember, CompanyRole, UserPreferences types;
  company_id added to all entity interfaces; companyId on all events
- Engine: all 7 core functions take companyId; storno, period, year-end
  services updated; 16 report generators updated
- Middleware: company context resolution (cookie → prefs → first company)
- API routes: ~120 routes updated with requireCompanyId()
- Frontend: CompanyProvider context, layout/dashboard/onboarding updated
- Extensions: context factory, 9 extensions, all lib files updated
- Tests: 1880 tests passing, all helpers updated with company_id defaults

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

* feat: add database migrations for multi-tenant company and team system (GNU-19)

Adds company_invitations, company creation RPC, team_members, account
deletion RPC, and teams table refactor migrations. Updates base
multi-tenant migration with cascading FKs and onboarding_step column.

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

* feat: add team types and update core infrastructure for multi-tenancy (GNU-19)

Adds TeamRole, MemberSource, and Team types. Refactors Supabase service
client to be stateless, updates middleware for team-aware routing, extends
CompanyContext with team/role fields, and updates extension service types
to accept companyId.

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

* refactor: thread company_id through business logic functions (GNU-19)

Replaces user_id scoping with company_id across all lib modules:
bookkeeping, documents, transactions, invoices, reconciliation, tax,
deadlines, and import. Updates corresponding tests.

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

* refactor: thread company_id through API routes and extensions (GNU-19)

Updates all existing API routes to extract and pass companyId. Updates
enable-banking and arcim-migration extensions for company-scoped
transaction ingestion and sync.

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

* feat: add company and team management API routes (GNU-19)

Adds CRUD endpoints for company members, company invitations, team
members, and team invitations. Includes invite token utilities, email
templates, and company switch server action.

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

* feat: add team/company UI components, pages, and dashboard updates (GNU-19)

Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company
members and team management panels. Updates dashboard layout for
team-aware routing, onboarding for multi-step role choice, and auth
callback for team invite acceptance. Ignores supabase/.branches/.

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

* fix: add null guards for company in import page (GNU-19)

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

* fix: move appUrl declaration to outer scope in invite route (GNU-19)

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

* fix: add optional chaining for company.name in members section (GNU-19)

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

* fix: add optional chaining for second company.name in members section (GNU-19)

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

* fix: add null guards for company in extension components (GNU-19)

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

* fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19)

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

* fix: update tests to use companyId instead of userId and improve type handling

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:41:52 +02:00
Mattsson 6d84828fcc Reference UI fix (#132)
* feat: display invoice references as tags instead of plain text

Replace the single-line text input for "Er referens" with a tag-style
input that lets users add/remove individual references as chips. Updates
the detail page, review modal, and PDF template to render comma-separated
references as wrapped badge/tag elements instead of a single string.

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

* Fixed our reference field

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:10:44 +01:00
Jakob Wennberg 7c3518a080 feat: payment matching audit trail and partial payments (#89)
* feat: payment matching audit trail, partial payments, and match improvements

- Add payment_match_log table (append-only audit trail per BFL 7:1) with
  immutability triggers and proper RLS
- Add invoice_payments table for partial payment tracking, mirroring
  supplier_invoice_payments pattern
- Add remaining_amount column + partially_paid status to invoices
- Add match-log.ts service for recording match/unmatch state transitions
- Improve match-invoice route: support partial payments, record audit log,
  emit payment.matched events, handle storno conflicts
- Improve match-supplier-invoice route: audit logging, partial payment support
- Update invoice-entries.ts for partial payment journal entries
- Update transaction ingest to detect and auto-suggest invoice matches
- Update bank-reconciliation to handle partial payment state
- Add invoice_payments, payment_match_log types and helpers
- Extend tests for match-invoice route and transaction ingest

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

* fix: address review feedback — payment matching correctness bugs

- P1: Return 500 on non-23505 payment insert failures instead of
  silently continuing with corrupted invoice state (both routes)
- P1: Fix foreign-currency partial payment matching — use proportional
  remaining SEK amount instead of full total_sek
- P2: Check error when clearing journal_entry_id after storno
- P2: Add updated_at column + trigger to invoice_payments table
- P2: Make supplier_invoice_payments.user_id NOT NULL after backfill

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:00:37 +01:00
Mattsson 565d956371 feat: add Bankgiro number validation and formatting, update settings and invoice templates (#60) 2026-03-20 09:32:29 +01:00
Jakob Wennberg 03b569d708 refactor: consolidate extension system to general-only with manifest-driven architecture
- Remove all sector-specific extensions (construction, ecommerce, export,
  hotel, restaurant, tech) — only general-purpose extensions remain
- Move NE-bilaga and SRU export from extensions to core reports (lib/reports/)
- Move moms-box-mapping from extensions/export/shared to lib/vat/
- Replace per-extension API routes with catch-all dispatcher
  (app/api/extensions/ext/[...path]/route.ts)
- Add manifest.json for each extension with metadata, env vars, and deps
- Add api-routes.ts pattern for extension-defined API endpoints
- Add code generation scripts (generate-extension-registry, create-extension)
- Add extensions.config.json for opt-in extension loading
- Add extensions.schema.json for config validation
- Add email service interface with noop default (lib/email/service.ts)
- Add CI workflow (core-build.yml) to verify core builds with zero extensions
- Add migration 045: expand account_type CHECK for untaxed_reserves
- Update CLAUDE.md with comprehensive extension system documentation
- Update all report engines and bookkeeping services for new imports
- Clean up extensions.schema.json to only list existing extensions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:32:56 +01:00
Jakob Wennberg 39e407644d feat: unified document inbox, full BAS 2026, and document-transaction matching
- Expand BAS reference from ~180 to ~1,276 accounts (full BAS Kontoplan 2026)
  with K2 exclusion flags, per-class data files, and computed SRU codes
- Evolve invoice inbox into unified document inbox handling invoices, receipts,
  and government letters with AI-powered classification (Claude Haiku Vision)
- Add multi-pass document-to-transaction matching engine with greedy assignment
  for both supplier invoices (reference/amount/date/name) and receipts
  (weighted amount/merchant/date scoring)
- Add supplier invoice matching in transaction ingest pipeline
- Inject booking template suggestions into AI extraction prompts
- Surface matched documents in swipe categorization UI with one-tap booking
- Auto-activate missing BAS accounts during SIE import against full reference
- Add K2 filter toggle in Chart of Accounts manager
- Add receipt confirmation route with BFNAR representation fields
- Add database migrations for K2 support and document matching columns
- Remove obsolete extension migration scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:59:02 +01:00
Emil d9230950db Merge remote-tracking branch 'origin/main' into logger 2026-02-24 10:06:42 +01:00
Jakob Wennberg 4d2140bcf8 fix: correct engine column names, dashboard revenue calc, email config, and UX issues
- Fix buildLineInserts() using wrong DB column names (cost_center_id → cost_center,
  project_id → project) and add missing tax_code field
- Replace transaction-based dashboard income/expense with journal-entry-based
  calculation using account classes (3xxx revenue, 4-7xxx expenses)
- Add email/phone fields to CompanySettings type, validate RESEND_FROM_EMAIL
  in isResendConfigured(), remove unsafe type casts in invoice send/reminders
- Always auto-fill line description from account name in JournalEntryForm
- Reorder expense lines in TransactionBookingDialog so 1930 is always first row

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:02:54 +01:00
Emil 941fe3a416 Merge remote-tracking branch 'origin/main' into logger 2026-02-24 10:02:36 +01:00