Commit Graph
71 Commits
Author SHA1 Message Date
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

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

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

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

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00
Jakob WennbergandClaude Fable 5 755e0f7e47 feat(dimensions): PR7 producers — auto-tagged documents (invoices, supplier invoices, bulk-book, templates, MCP) (#868)
* feat(dimensions): PR7 producers — invoices/supplier invoices carry dims, generators propagate, BulkBook + templates + MCP bags

Source documents now carry dimension tags and every entry generator
propagates them onto journal lines (dev_docs/dimensions_implementation_plan.md PR7):

- invoices/supplier_invoices.default_dimensions + per-item dimensions
  (migration 20260702200000; jsonb DEFAULT '{}' + object CHECK)
- invoice-entries: issuance/payment/cash/credit propagate — item bags merge
  over the invoice default per revenue line (account+bag aggregation
  identity), payment vouchers re-propagate the linked invoice's bag onto
  every leg incl. FX result lines; ROT/RUT 1513 carries the item bag
- supplier-invoice-entries: registration/payment/cash/privately-paid/credit
  propagate with the same merge rules (expense buckets keyed account+bag)
- bulk_book_transactions RPC persists per-line bags + derives
  cost_center/project mirrors in SQL (migration 20260702201000; malformed
  bags rejected with BULK_BOOK_INVALID_DIMENSIONS); route merges the header
  default into template/manual lines
- counterparty templates: LinePatternEntry.dimensions learned from SIE
  voucher history (kept only when every occurrence agrees), applied to
  business lines on booking; QuickReviewDialog shows a dims badge
- categorize: staged dimensions bag tags business lines only (bank/VAT
  untagged); credit/convert/inbox copy paths carry bags forward
- propose-payment/send-lines stamp the invoice default so the editable
  payment grid books what the preview shows; mark-paid override lines
  accept dimensions
- UI: InvoiceEditor + NewSupplierInvoiceForm header KS/Projekt pair with
  per-row override; BulkBookDialog header default pair (both tabs)
- MCP: default_dimensions/items[].dimensions on create_invoice +
  create_supplier_invoice_from_inbox, dimensions on categorize_transaction,
  per-line bags on bulk_book_transactions — resolve-don't-select via the
  shared registry helpers, resolutions echoed

32 new propagation unit tests + 4 pg-real tests for the RPC migration.

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

* test: use roundOre in new dims rounding assertions (ratchet)

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

* fix: copy dimension bag per payment line, document dimensionsBagKey normalization contract (review)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:21:01 +02:00
Jakob WennbergandClaude Opus 4.8 5df6199bd1 fix(invoices): remaining_amount + invoice.paid on agent mark-paid path (#825) (#845)
* fix(invoices): book remaining_amount + emit invoice.paid on agent mark-paid path (#825)

The agent/MCP commit path commitMarkInvoicePaid flipped status to 'paid' and set
paid_amount = total but never wrote remaining_amount (left at the original total)
and never emitted invoice.paid — so partial state and webhooks diverged from the
dashboard and v1 mark-paid routes.

Route the agent path through the shared planInvoicePayment helper (the source of
truth introduced in #841): compute paid/remaining/status with the overpayment
guard BEFORE booking the JE (so a rejected payment never burns a voucher number),
persist remaining_amount + the partially_paid transition, and best-effort emit
invoice.paid for webhook parity.

Adds lib/pending-operations/__tests__/mark-invoice-paid.test.ts covering the
state + event behaviour of this path.

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

* fix(invoices): address review feedback on agent mark-paid path (#825)

- CAS-guard 409 message now reflects the expanded payable states: the UPDATE
  filter accepts partially_paid (reachable via a concurrent settle race), not
  just sent/overdue.
- Derive the settle amount from total − paid_amount when remaining_amount is
  null (legacy rows) instead of falling back to the full total, so a prior
  partial payment is not double-counted into a false overpayment rejection.

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

* fix(invoices): derive remaining from total − paid_amount on all mark-paid surfaces (#825)

The dashboard and v1 mark-paid routes defaulted the settle amount to invoice.total
when remaining_amount was null, which over-settles a legacy invoice that has a
prior partial payment recorded in paid_amount (false overpayment / AR over-credit).
Align both with the agent path (commit.ts): remaining_amount ?? total − paid_amount.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:01:07 +02:00
Jakob WennbergandClaude Opus 4.8 f8504f3bd0 fix: audit batch — pagination truncation, MFA/dead-code cleanup, mark-paid fail-closed (#841)
* fix(reports): paginate 8 more report/ledger queries (1000-row truncation)

Raw .select() without fetchAllRows() silently caps at PostgREST's 1000-row
limit, producing wrong statutory output for high-volume companies. Following
#806 (trial-balance/VAT), wrap the remaining offenders in
fetchAllRows + a stable .order('id') + dedupeBy:

- ink2-engine / ne-engine: INK2 & NE-bilaga tax declarations under-counted
- ar-reconciliation (1510/1513), supplier-reconciliation (2440): phantom
  "Ej avstämd" gaps
- full-archive-export: 7-year DR archive (added a unique total order so rows
  are not silently skipped/duplicated across pages)
- avgifter-basis, currency-revaluation, vat-declaration

Adds a regression guard test asserting >1000 ledger lines are summed, not
truncated at 1000.

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

* fix(api): close extension-dispatcher MFA gap, scope /api/events to API key, sweep dead code

Security/correctness:
- ext/[...path] dispatcher now uses requireAuth() instead of inline
  supabase.auth.getUser(), enforcing MFA (AAL2) on hosted across the whole
  enabled-extension surface (banking sync, document upload/booking, supplier
  invoices, migration). Ratchets antipatterns-baseline raw-route-auth 168->165.
- /api/events now filters by the API key's bound company_id instead of the
  user's active company (was a cross-company read with a scoped key).
- enable-banking OAuth callback calls ensureInitialized() at module load so
  the PSD2 consent audit event (ASVS V16 / GDPR Art.30) isn't dropped on a
  cold-start instance.

Dead-code sweep (all confirmed zero importers):
- delete lib/tax/calculator.ts, lib/salary/engangsskatt.ts (+test),
  lib/email/resend.ts, lib/salary/salary-transaction-matcher.ts,
  lib/webhooks/diff.ts, lib/salary/effective-values.ts,
  lib/bookkeeping/template-prompt.ts
- trim unused lib/vat/eu-countries.ts helpers (keep EU_COUNTRIES)
- remove dead getAutomaticStatus() and the abandoned Activepieces CSP entry

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

* fix(invoices): fail closed when a payment journal entry doesn't post

Three mark-paid paths (legacy route, v1 API, agent commit) diverged on the
"mark paid but the JE failed" case — two would flip the invoice to paid (or
leave an orphaned posted voucher) with no booking, silently diverging the GL
from the AR/AP sub-ledger. Unify on fail-closed:

- legacy + v1 + agent commitMarkInvoicePaid: never mark paid without a posted
  voucher; on a null/failed JE return INVOICE_PAID_BOOK_FAILED before any
  state mutation (v1 mirrors the match-invoice strict mode).
- agent path: add the .in('status',[...]).select('id') CAS guard and cancel
  the orphaned voucher (cancelOrphanedPaymentEntry) on a lost race or update
  error, matching the web route.
- legacy route: cancel the orphan on a non-race update error too (was only
  handled on the race branch).
- supplier mark-paid: stop swallowing a failed supplier_invoice_payments
  insert — that row drives the reversal amount in payment-sync; roll back the
  status flip and cancel the voucher instead.
- pending-ops orchestrator: error-check the terminal 'committed' write so an
  op stranded in 'committing' (the expire sweep only targets 'pending') is at
  least logged loudly.

Adds a guard test for the legacy fail-closed path. Full unit suite green.

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

* fix(ci): unblock core build + address compliance-review findings

- avgifter-basis.ts: fix the core-build TypeScript error — PostgREST's
  type-level select parser models the salary_run embed as an array, which
  wasn't assignable to the object-typed generic. Type it `unknown` (rows are
  read via an explicit cast), making it robust across postgrest-js versions.
- /api/events: add a non-null companyId guard before the event_log query
  (defense-in-depth for the API-key-bound scope) — addresses ASVS V8.2.1 /
  ISO A.5.15.
- supplier mark-paid: add a CAS guard (.eq('status', newStatus)) to the
  payment-insert-failure rollback so a concurrent settlement can't be
  clobbered — addresses ASVS V2.3.
- dispatcher: add an AAL2 regression test asserting a non-MFA session is
  rejected (403) and the extension handler never runs — addresses the
  GDPR Art.32 review ask for the single extension chokepoint.

Verified deletions are safe: effective-values.ts was a dead duplicate — the
live AGI/payslip path inlines the same `?? override` coalescing
(generate-declaration.ts), so AGI correctness is unaffected.

next build: exit 0. Full unit suite: 6147 passing. ESLint clean.

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

---------

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

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

Adds validatePlusgiroNumber/formatPlusgiroNumber helpers + tests.

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

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

Two user-reported bugs:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:34:51 +02:00
Jakob WennbergandClaude Opus 4.8 4f0a7b1db0 feat(entitlements): per-company capability paywall — gate, trial seeding, UI upsells, Stripe checkout (#815)
* feat(entitlements): capability-grant gate substrate (paywall + modularity)

Two-axis capability primitive behind the SaaS paywall and the per-tenant
modularity/marketplace vision:
- migration: capability_grants (entitlement axis, polymorphic company/firm
  scope), company_capability_config (enablement axis), metered_events
  (append-only), company_has_capability() RPC reusing the 20260619130100
  tenant guard; SELECT-only RLS (writes service-role only, no self-grant).
- lib/entitlements: hasCapability/requireCapability gate (mirrors guardSandbox,
  fail-closed, NEXT_PUBLIC_SELF_HOSTED bypass), capability key namespace,
  metering helper.
- unit (11) + pg-real tests (RPC/RLS/tenant-guard incl. no-self-grant).

Gate not yet wired into call sites (follow-up commit). Paid keys:
ai, bank_sync, skatteverket, email_send.

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

* feat(entitlements): enforce capability gate at paid external-service chokepoints

Wire the gate into the paid surfaces (keys: ai, email_send, bank_sync, skatteverket):
- AI routes (agent invoke/composer/onboarding stream): requireCapability(ai)
- Invoice send (web + v1): requireCapability(email_send)
- document-extraction event handler: skip Bedrock extract if ai not entitled
- enable-banking + skatteverket crons: per-company hasCapability skip in loop
- colocated send-route test mocks updated (requireCapability -> null)

Free per founder decision: TIC org lookup, VIES VAT validation, FX auto-fetch,
cloud backup, BankID login, all internal bookkeeping.

DEPLOY ORDER: fail-closed by design — do NOT deploy before trial/comp grant
seeding lands, or companies without grants lose these features. Seeding +
Stripe checkout/webhook are the next steps.

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

* feat(entitlements): seed trial + comp capability grants

Makes the fail-closed gate safely deployable — nobody is locked out at cutover:
- AFTER INSERT trigger on companies grants every NEW company a 30-day trial on
  the PAID keys (ai, bank_sync, skatteverket, email_send), on ALL creation paths
  (RPC/MCP/direct) — so a new signup can use onboarding AI immediately.
- one-time backfill for EXISTING companies: created <=2026-06-07 -> trial ends
  2026-07-07; created later -> created_at + 30 days.
- permanent comp grants for Arcim/Mattsson (matched by name, no hardcoded UUIDs).
- pg tests: clearGrants() for controlled resolver tests + trigger coverage.

Trigger fn is SECURITY DEFINER so it writes grants regardless of caller RLS
(table has no INSERT policy for authenticated — no self-grant).

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

* feat(entitlements): client capability visibility + billing page

Non-payers get a clean upsell instead of broken/empty features:
- CompanyContext gains capabilities[] + useCapability(key); resolved once
  server-side in the dashboard layout via getCompanyCapabilities (batched, 2
  queries), all three provider branches wired.
- /settings/billing upgrade page — the destination upsells point to (Stripe
  Payment Link via NEXT_PUBLIC_STRIPE_PAYMENT_LINK; degrades to 'coming soon'
  until automated checkout lands).
- ChatEmptyState: non-payer sees an Uppgradera CTA (mirrors the sandbox state).
- SendInvoiceDialog: email send disabled + upsell note when email_send missing
  (extends the existing sandbox-disable pattern).

Fast-follow: chat input/FAB + document-inbox empty state + bank/skatteverket/
AI-suggest buttons + a shared capability_blocked->toast backstop.

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

* feat(entitlements): gate remaining paid UI surfaces with upsell (fast-follow)

disable-with-upsell across the rest of the paid surfaces (keys: bank_sync, skatteverket, ai):
- BankSyncNowButton: sync/reconnect disabled + note when !bank_sync (CSV/SIE stays free)
- AGIPanel: AGI submit-to-Skatteverket disabled + note when !skatteverket
- SkatteverketConnectPanel: BankID connect/reconnect disabled + upsell
- ApprovalCard: AI re-propose (correction) gated; manual approve/reject stay free
- InvoiceInboxWorkspace: upsell when extraction empty AND !ai (deterministic parse + manual entry unaffected)
- AgentTrigger FAB: routes to /settings/billing when !ai (no dead chat)
- settings nav: 'Abonnemang'/'Subscription' link to /settings/billing (sv/en)

TaxPaymentPanel + TransactionInboxCard intentionally untouched — only local/
deterministic actions there, nothing paid+external to gate.

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

* feat(entitlements): automated Stripe subscription checkout + webhook

Self-serve revenue wired to the same capability-grant primitive:
- migration: company_subscriptions (company<->Stripe link/status) + stripe_webhook_events (idempotency)
- lib/stripe: getStripe singleton, plan->price mapping, subscription-sync (statusGrantsAccess / subscriptionToState / applySubscriptionState / handleStripeEvent). Active sub -> upsert source='stripe' grants for PAID keys (expiry = period_end + 3d grace); canceled/unpaid -> remove ONLY stripe grants (freeze-and-retain).
- routes: POST /api/billing/checkout (hosted subscription Checkout, company_id metadata), POST /api/billing/portal (Customer Portal), POST /api/stripe/webhook (raw-body signature verify, event-id dedup; handles checkout.session.completed + customer.subscription.*)
- billing page: real plan-toggle Checkout CTA / manage-subscription portal, gated on isStripeConfigured()
- adds stripe@22; unit tests for sync logic

Provisioning is webhook-driven (never trusts the success redirect). Needs env: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_MONTHLY, STRIPE_PRICE_YEARLY.

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

* fix(entitlements): validate UUIDs in capability filter + log webhook errors

Addresses PR review (Superagent Security / PR Agent):
- has-capability.ts: validate companyId/teamId as UUIDs before interpolating into the PostgREST .or() filter (fail-closed) — removes the latent injection vector flagged in the entitlement gate. Unit tests updated to use UUIDs.
- stripe/webhook: log processing failures with event id + type before the generic 500, so a failing webhook is visible to operators.

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

* fix(salary): always-free AGI XML download for manual filing; only direct API submit is paid

Per founder decision on the swedish-compliance-review finding: AGI is a mandatory statutory filing, so producing/downloading the AGI XML must never be paywalled. Adds a free 'Ladda ner AGI-fil' button (generates + downloads the XML for manual upload to Skatteverket's e-service) on all tiers; the gated 'Skicka in underlag' stays the paid convenience (direct API submission — which also requires the paid BankID connection). Upsell reworded to point to the manual path.

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

* fix(entitlements): harden comp-grant match after prod verification

Verified Arcim/Mattsson in prod (pwxtzglxptnnvjrpixpg): the name match was case-sensitive (missed the active 'Arcim technology AB' lowercase variant) and would have granted 3 archived dupes. Now match by org_number (5595386219 / 5595719864) OR case-insensitive name, active companies only — hits exactly the 3 active comp companies, excludes archived dupes and the unrelated 'Amnäs Mattsson, Emil' enskild firma.

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-29 15:56:19 +02:00
MattssonandClaude Opus 4.8 9ed0b9515a Fix/invoice booking vat fixes (#778)
* 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>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:54:35 +02:00
Jakob WennbergandClaude Opus 4.8 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
MattssonandClaude Opus 4.8 241959513b Fix/mcp and req (#753)
* feat(api): test-mode API keys force dry-run on the v1 REST API

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:49:33 +02:00
MattssonandClaude Opus 4.8 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
MattssonandClaude Fable 5 db8983ba9e Add/bokslut (#718)
* feat(arcim-migration): Briox provider with SIE-over-API import

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

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

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

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

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

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

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

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

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

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

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

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

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

* Add tests for bookkeeping accruals dissolution and supplier invoices

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

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:35:30 +02:00
MattssonandClaude Fable 5 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 WennbergandClaude Opus 4.8 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
MattssonandClaude Opus 4.8 0ca9c25aba Add/user feedback (#679)
* feat(bookkeeping): make blocked fiscal-year creation actionable

When creating a new räkenskapsår is blocked because a prior period is
still open, the "Skapa räkenskapsår" dialog no longer dead-ends on an
English toast. The API now returns the canonical bilingual error envelope
with the blocking periods (id/name/dates) under details, and the dialog
renders a Swedish panel that locks them inline (reversible locked_at) via
the existing /lock endpoint and retries creation.

The guard rule is unchanged and remains BFL-compliant: BFL 6 kap allows
löpande bokföring of the new year in parallel with the prior year's
bokslut, so a lock (not a full close) is sufficient and reversible.

- Add PERIOD_CREATE_BLOCKED_BY_OPEN_PERIODS structured error code
- Return envelope + details.blockingPeriods from the 409 (was English string)
- CreatePeriodDialog: inline "lås och skapa" panel + lock-and-retry
- Update route tests for the new envelope shape

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

* fix(ui): prevent mouse wheel from mutating number inputs

A focused <input type="number"> would change its value on scroll,
silently turning e.g. a 20000 salary into 19998. Blur number inputs
on wheel so the page scrolls instead of editing the value. Applied
at the Input primitive so all number fields are protected.

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

* feat(salary): auto-derive skattetabell and kolumn for employees

Replace the opaque manual "Skattetabell (29-42)" and "Kolumn (1-6)" inputs
on the employee form with a self-deriving flow: the user picks their
folkbokföringskommun from a searchable dropdown and the tax table fills
itself in, while the column derives from the personnummer we already collect.

- Add a searchable municipality picker (MunicipalityCombobox) backed by a
  new cached GET /api/salary/tax-tables/kommuner endpoint.
- Wrap the whole "Skatt" card in a self-contained EmployeeTaxCard used by
  both the create and edit pages, with InfoTooltips and named column options.
- deriveTaxColumn(): auto-select column 1 for under-66 employees; leave the
  ambiguous 66+ case (pension vs working senior) to a clearly-named manual
  choice.
- Fix fetchKommunTaxRates() to page through all ~1300 församling rows instead
  of a single 500-row page (which silently dropped ~200 kommuner, incl.
  Göteborg) and normalize the uppercase names to title case.

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

* fix(import): correct CSV amount-column guess and surface skipped rows

Manual CSV column-mapping auto-guess walked each data row right-to-left
and picked the first numeric cell as the amount, so on the common
...;Belopp;Saldo layout it grabbed the trailing running-balance column.
Extract the guess into a pure, tested suggestColumnMapping(): match
header labels first (belopp/amount -> amount, saldo/balance -> balance),
auto-fill the balance field, and fall back to value heuristics that skip
the balance column and prefer a column carrying negative values.

Also surface stats.skipped_rows + parse warnings in BankFileConfirmStep -
the manual-mapping path skips the preview step that was the only place
they showed, so skipped rows were silently dropped from view.

Add a unit test reproducing the Saldo-as-amount regression.

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

* feat: add "Save as draft" functionality for invoices

- Implemented a new feature to allow users to save invoices as unnumbered drafts without generating an invoice number until finalized.
- Added a `save_as_draft` flag to the CreateInvoiceInput schema to handle draft saving logic.
- Updated the invoice creation API to skip number allocation when saving as a draft.
- Introduced a new endpoint for finalizing drafts, which allocates an invoice number and emits an `invoice.created` event.
- Enhanced the UI to include a "Save as draft" button, with loading states and tooltips.
- Updated tests to cover the new draft saving and finalization logic, including race conditions for concurrent modifications.
- Added relevant error handling for draft finalization and deletion scenarios.

* feat(employee): add employment start and end date fields to employee forms

* feat: enhance invoice and salary run handling with improved validation and event logging

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:26:40 +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 WennbergandClaude Opus 4.8 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
MattssonandClaude Opus 4.7 ea1bf01f1e Fix/m sprint fixes (#613)
* fix(dashboard): exclude ignored and already-triaged transactions from stale count

The "Gamla transaktioner" widget counted transactions that had been ignored
or already marked as is_business=true but not yet booked, so users saw a
nag for a row they had already dealt with — and the /transactions inbox
correctly hid it. Align the count with the inbox criterion (is_business
IS NULL, is_ignored = false) so the widget clears when the row leaves
the inbox.

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

* fix(transactions): read entity_type from settings response wrapper

The transactions page read entityRes.entity_type directly, but
/api/settings returns { data: { entity_type, ... } }. The expression
was always undefined, so setEntityType never fired and entityType
stayed at its initial 'enskild_firma'. The template picker's
entity_type filter then dropped every aktiebolag-tagged user template
for AB customers — only entity_type='all' templates made it through.

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

* stale templates
bank sync
journal entry from transaction

* fixed pr comments

* fixed pr comment

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 01:28:41 +02:00
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
Jakob WennbergandClaude Opus 4.7 20989379bb feat(sandbox,branding): prod-parity demo with AI gating + accounted rebrand (#585)
* feat(sandbox,branding): prod-parity demo with AI gating + accounted rebrand

Sandbox now ships with seeded suppliers, supplier invoices, an asset,
a verified agent_profile, and pending operations so the demo company
exercises every prod surface. Server-side `guardSandbox()` short-
circuits any AI or paid-external API call (Bedrock chat/composer,
Resend invoice send, Riksbanken FX, VIES, etc.) and the AgentSheet
swaps in a SandboxAgentPreview that explains what's gated and offers
a register CTA. DashboardContent no longer mounts the
NewUserChecklist when the agent is already built, fixing the path
that let sandbox users still trigger /onboarding/agent.

Visible branding flips from Gnubok to Accounted: new BrandWordmark
component (Hedvig Letters Serif 700), new app/icon.png + PWA icons
generated from the accounted icon, default appName updated. URLs,
header names, API key prefixes, hostnames, and event/cookie/
localStorage keys keep `gnubok` — the rebrand is visual only.

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

* fix(sandbox): hardcode supplier-invoice arrival numbers in seed

get_next_arrival_number is MAX(arrival_number) + 1 against the same
table we're about to insert into. Calling it twice before either row
lands made both calls return 1, which then violated the
(company_id, arrival_number) unique index — POST /api/sandbox/seed
500'd on first sandbox start.

The seeded company is brand new in this branch so 1 and 2 are
guaranteed unused; hardcoding side-steps the race entirely.

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

* fix(sandbox): set paid_amount=0 on unpaid supplier invoice

PostgREST normalizes the column set across rows in a bulk insert, so
the second supplier invoice (Espresso House, status=registered) was
being sent with paid_amount=null because the first row (Telia, paid)
set it. supplier_invoices.paid_amount is NOT NULL DEFAULT 0; the
default only kicks in when the column is *absent* from the payload,
not when it's explicitly null. Set it inline to side-step the
normalization.

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

* fix(sandbox): set actor_type=agent_chat on seeded pending_operations

pending_operations only allows user-scoped INSERTs via the
`pending_operations_chat_insert` policy, which requires
actor_type='agent_chat' alongside auth.uid()=user_id +
company membership. The seed was inserting with the default
actor_type='user', tripping the RLS check.

Also lift risk_level from preview_data (where it was unused) onto
the row itself, matching the column added in
20260430120000_pending_operations_actor_and_risk.

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

* fix(pr-review): address PR #585 review feedback

Fixes called out by the core-only CI check, Greptile, and the
compliance + Swedish-accounting bots:

- AGI Programnamn pinned back to 'gnubok' (CI blocker). The XML
  Skatteverket receives must keep the stable software identifier
  regardless of the visual rebrand — same rule as the v1 health
  endpoint's `service: 'gnubok'` literal.
- handleCreateAccount in SandboxAgentPreview + ChatEmptyState now
  wraps signOut() in try/catch so a transient Supabase failure
  doesn't strand the user on a dead button (greptile P2 × 2).
- /api/currency/rate hard-fails on missing companyId instead of
  conditionally skipping the sandbox guard (greptile P2 / compliance
  V8.2.1).
- topUpSandboxAdditions now delegates to ensureSandboxAgentProfile;
  the assistant persona lives in exactly one place across the seed,
  layout backfills, and top-up path (greptile P2 outside-diff /
  compliance SOC2 CC6.1).
- ensureSandboxAgentProfile drops the userId param and sets
  verified_by_user_id to NULL — synthetic seed data should not
  attribute verification to a real user (compliance V8.2.1 /
  GDPR Art. 25(2)). Errors now logged via the structured logger
  instead of being silently swallowed (V16).
- Sandbox seed swaps real-world company names (Telia, Espresso
  House) for clearly-synthetic Demo-prefixed brands using the
  5559... documentation org-number range (compliance A.8.33).
  Asset cost bumped 24 000 → 35 000 SEK so the demo clears the
  förbrukningsinventarier threshold and illustrates capitalization
  unambiguously (swedish-asset-accounting).
- Representation pending-operation preview corrected: VAT label
  fixed from 6% → 12%, and input VAT split between the avdragsgill
  (2641) and ej-avdragsgill (5811) portions to match
  swedish-vat / ML 8 kap rules (swedish-vat).

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

* fix(pr-review): seed preview consistency + AGI Programnamn constant

Two last review-bot items before merge:

- Sandbox seed: the representation pending-operation preview was
  splitting the 240 SEK café meal 60/180 between 5810 and 5811,
  which is wrong for a single attendee under the 300 SEK / person
  avdragsgill cap (ML 8 kap) — the entire amount is fully
  avdragsgill in that case. Collapse the preview to a single 5810
  + 2641 + 2440 entry so it matches the supplier_invoice_items row
  1:1 and stops teaching demo users an incorrect bookkeeping
  pattern.
- Hoist the AGI Programnamn 'gnubok' literal into a named constant
  with a comment pointing to potential future Skatteverket vendor
  registration (per the swedish-compliance bot's nit).

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

* fix(sandbox): avoid BFL duplicate-verification on pending op + fix VAT cap comment

Swedish-compliance bot caught two final nits:

- The pending operation for the Demokafé representation was using the
  same supplier_invoice_number as the already-seeded supplier_invoices
  row (88245). If the sandbox user approved the staged operation, the
  insert would have created (or attempted) a duplicate verification —
  BFL 5 kap. requires each affärshändelse be recorded exactly once.
  Swap the staged operation's invoice number to a distinct value
  (INKOMMANDE-2026-001) so approval cleanly creates a new row.
- The preview comment described the 300 SEK threshold as an
  "avdragsgill cap". The actual rule (ML 8 kap. 9 §) caps the
  deductible VAT at 25 % × 300 SEK × antal_personer = 75 SEK per
  person — the 300 SEK is the tax base, not the total. Math here is
  correct either way, but the comment now states the correct formula
  so future seed edits don't propagate the wrong understanding.

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 13:19:41 +02:00
MattssonandClaude Opus 4.7 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
MattssonandClaude Opus 4.7 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
MattssonandClaude Opus 4.7 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 50e5520b6d Add/preview invoice image (#565)
* feat: enhance invoice preview functionality with mock customer support

* feat: update invoice preview logic to handle mock customers and improve error handling
2026-05-24 16:59:36 +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
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
Jakob WennbergandClaude Opus 4.7 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 WennbergandClaude Opus 4.7 1163fd3bee fix(invoices): drop UTKAST banner from sent/archived PDFs (#495)
The "UTKAST – inte en giltig faktura" banner in pdf-template.tsx fires
whenever `invoice.status === 'draft'`. All four send-equivalent code
paths render the PDF while the in-memory invoice still reads 'draft',
so the customer's email attachment and the archived underlag are both
stamped as not-a-valid-faktura.

- /api/invoices/[id]/send: status flip happens after email delivery
  (kept that way so a provider failure leaves the row in draft); render
  with `{ ...invoice, status: 'sent' }` instead of mutating order.
- /api/invoices/[id]/mark-sent: DB flip happens before render but the
  in-memory copy is never re-fetched.
- /api/v1/companies/.../invoices/[id]/send: same shape as the internal
  send route; extend the existing `renderableInvoice` override.
- lib/pending-operations/commit.ts (MCP send path): same shape.

The banner condition itself is unchanged — the genuine draft preview
(`/api/invoices/[id]/pdf`) still surfaces the watermark correctly.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:43:14 +02:00
MattssonandClaude Opus 4.7 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 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 WennbergandClaude Opus 4.7 30f5877e57 fix(invoices): init remaining_amount on create + attach invoice PDF to payment JE (#406)
* fix(invoices): initialize remaining_amount on create

The invoices table column remaining_amount has DB default 0. The create
path never set it, so brand-new fakturor were stored with
remaining_amount=0 even though no payment had been received. The
InvoicePicker (and any future open-invoice query that filters on
remaining_amount > 0) treated these as fully settled and hid them from
match candidates — surfaced when a real user reported "Inga öppna
fakturor" despite having 5 sent invoices.

Set remaining_amount = total on insert for document_type='invoice'.
Proformas and delivery notes have no payment obligation, so they keep
the 0 default.

Backfill of the 46 existing rows across 20 companies (1.32M SEK in
orphaned receivables) ran separately as a one-shot UPDATE — restricted
to rows with paid_amount IS NULL OR 0 so any legitimately-paid invoice
with stale status was untouched (verified: 0 such rows).

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

* feat(match-invoice): attach invoice PDF as underlag for payment JE

The payment verifikation created on transaction match (debit 1930 /
credit 1510) had no document attachment. The invoice PDF was archived
on send and pinned to the AR-booking JE, but document_attachments
.journal_entry_id is one-to-one — the payment JE was left without
underlag, a BFL 7 kap audit-trail gap.

Cheapest fix: after the payment JE is created, look up the invoice's
existing document_attachment row and insert a parallel row that points
at the same storage_path with the new journal_entry_id. The original
WORM file is untouched (single storage object, two DB pointers); no
schema change. Wrapped in non-blocking try/catch so a document lookup
failure doesn't abort the payment match.

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

* fix(match-invoice): destructure document_attachments insert error

Supabase JS client returns { data, error } on Postgres-level failures
(unique constraint, RLS reject) instead of throwing. The surrounding
try/catch only caught thrown JS exceptions, so DB errors on the payment
JE document attachment were silently swallowed — the txLog.warn path
was unreachable for the most likely failure mode.

Destructure { error: attachErr } and log on error with both JE ids so
attachment failures are visible and reparable. Greptile P1 on PR #406.

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 23:41:03 +02:00
Jakob WennbergandClaude Opus 4.7 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 ce3af4d17e Fix/multiple domain issue (#401)
* feat: enhance invoice management and immutability checks

- Update InvoiceDetailPage to prevent deletion of drafts with assigned invoice numbers, providing user feedback.
- Modify the invoice conversion API to ensure invoice number allocation occurs only after successful item insertion and proforma cancellation.
- Implement structured error responses for invoice deletion, ensuring only drafts without assigned numbers can be deleted.
- Add comprehensive tests for invoice deletion and conversion scenarios, including edge cases for draft invoices.
- Introduce immutability checks in the document management system to prevent unauthorized changes to linked documents.
- Create SQL migration to enforce document metadata immutability, ensuring compliance with accounting regulations.

* fix(invoice): prevent invoice number consumption on PDF render failure

* feat: add document journal entry immutability enforcement for delete_last_voucher RPC

* fix(invoice): implement rollback for orphan invoices on proforma cancel failure

* fix(document): extend immutability trigger to protect journal entry links
2026-05-06 14:08:38 +02:00
MattssonandClaude Opus 4.7 5725c25bf1 Logs/improved logging (#398)
* feat(mcp): add create_transactions tool with /pending approval gate

New MCP tool gnubok_create_transactions stages 1–10 transactions per call
as pending_operations of type create_transaction (risk: medium). Each item
becomes its own card on /pending; on confirm, the executor inserts the row
into transactions with import_source='mcp' so MCP-staged ingestion is
distinguishable from PSD2 sync. Designed for skill workflows that pull
external data (e.g., Airtable) and want the user to gate the writes.

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

* fix(bas): strip concatenated group headers from corrupted account names

A chart-data import bug had glued the next group's header onto the last
account in each preceding group across all eight bas-data class files
(e.g. account 2670 read "Utgående moms på försäljning inom EU, OSS 27
PERSONALENS SKATTER, AVGIFTER OCH LÖNEAVDRAG"). The corrupted names
surface in transaction dropdowns, ledgers, SIE exports and årsredovisning,
and risk VAT miscategorization on the OSS (2670) and blandad-verksamhet
(6999) accounts specifically.

- Cleans 69 account_name and 64 description fields across class-1..8 files
- Adds a regression test asserting no name contains a concatenated header
- Ships an idempotent safety-net migration that updates already-seeded
  chart_of_accounts rows, gated on the corrupted string so user
  customizations are preserved

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

* feat(errors): add structured error codes and handling for various operations

- Introduced a new structured error registry in `structured-errors.ts` to standardize error handling across the application.
- Added Swedish and English messages for various error scenarios, including validation, authorization, and bookkeeping errors.
- Implemented a client-side error toast in `use-error-toast.ts` to display user-friendly error messages with remediation hints.
- Created a wrapper for recording operation outcomes in `record-operation.ts`, enhancing audit capabilities for operations.
- Developed a provider call wrapper in `with-provider-call.ts` to handle external HTTP calls with structured logging and error mapping.
- Added a new SQL migration to extend the processing history with new event types and aggregate types for better operational telemetry.

* Refactor supplier API routes to use context-based logging and error handling

- Replaced direct Supabase client usage in GET and POST routes with context-based approach using `withRouteContext`.
- Enhanced error handling to provide structured error responses for supplier creation and listing.
- Updated logging to include request IDs for better traceability.
- Introduced new error codes for supplier-related operations.
- Refactored tax deadlines cron job to utilize context and improved error handling.
- Updated ESLint configuration to enforce logging practices across API and lib directories.
- Enhanced arcim migration extension with structured error handling and logging.
- Added classification for provider errors to improve user-facing error messages.
- Introduced request ID in extension context for better log correlation.

* fix(route-context): update DynamicParams type for improved type safety in route handlers

* feat(transactions): add 'create_transaction' operation to PendingOperationType

* fix(route): ensure companyId is non-nullable in loadAndDeriveAbsence function

* fix(route-context): ensure companyId is always non-null by short-circuiting with COMPANY_CONTEXT_MISSING

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:12:02 +02:00
Mattsson d1267c37af feat(invoice): add PDF archival for sent invoices and implement tests (#373)
* feat(invoice): add PDF archival for sent invoices and implement tests

* fix(invoice): add company_id filter when fetching original invoice
2026-04-28 13:03:53 +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
Mattsson 0222e084bb Refactor bookkeeping error handling and introduce new error classes (#356)
- Introduced new error classes for better error categorization:
  - JournalEntryNotBalancedError
  - FiscalPeriodNotFoundError
  - EntryDateOutsideFiscalPeriodError
  - JournalEntryNotFoundError
  - CannotReverseNonPostedError
  - CannotCorrectNonPostedError
  - EntryAlreadyReversedError
  - CurrencyRevaluationAlreadyExistsError
  - InvalidMappingResultError
  - BookkeepingDatabaseError

- Updated existing functions in engine.ts and transaction-entries.ts to throw specific errors instead of generic ones.
- Enhanced error response handling in get-error-message.ts to provide localized messages for new error types.
- Added unit tests for new error classes and error handling functions to ensure correctness and coverage.
2026-04-23 14:49:45 +02:00
Jakob WennbergandClaude Opus 4.7 adf58a51c0 Prompt to activate missing BAS accounts at commit (#308)
* feat: prompt to activate missing BAS accounts at commit

Booking to an account not in the active chart previously threw a
generic 400 "Account(s) not found: 5010" and the user had to leave
the form to enable the account via /bookkeeping > BAS-katalog.

- New AccountsNotInChartError thrown from resolveAccountIds in the
  engine (and the parallel resolver in core/storno-service). The
  query also now filters on is_active=true, so deactivated accounts
  are treated the same as never-added ones.
- API routes that call the engine (journal-entries, reverse, correct,
  transactions/book + match-invoice + match-supplier-invoice +
  uncategorize, invoices/mark-paid, supplier-invoices + mark-paid +
  credit, salary/runs/correct, import/opening-balance/execute,
  pending-operations/commit) catch the typed error and return a
  structured 400: { error: { code: ACCOUNTS_NOT_IN_CHART,
  account_numbers, message } }.
- /api/bookkeeping/accounts/activate now also reactivates rows that
  already exist but are is_active=false, not only INSERTs. Returns
  { activated, reactivated, skipped, unknown }.
- New GET /api/bookkeeping/accounts/bas-lookup?numbers=... resolves
  BAS names client-side so the dialog can show "5010 · Lokalhyra"
  without bundling the full 1,276-account catalog.
- ActivateAccountsDialog lists the missing accounts (BAS names + any
  unknown non-BAS numbers) and confirms with a single action.
- useSubmitWithAccountActivation wraps an async submit: on
  ACCOUNTS_NOT_IN_CHART it opens the dialog, activates on confirm,
  then retries the original submit so the user never re-enters data.
- AccountCombobox accepts any 4-digit numeric value, not just items
  from the active chart — the activation dialog handles the rest.
- JournalEntryForm wired to the hook + dialog. Other submit surfaces
  now surface a clear Swedish message ("Följande konton behöver
  aktiveras: …") via getErrorMessage; wiring the dialog into those
  is an additive follow-up.

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

* docs: sync CLAUDE.md with current codebase state

Catch-up on growth since the last CLAUDE.md revision:
- Integrations list now includes AWS Bedrock, Upstash Redis,
  Google Drive, Recharts, PDF.js, @react-pdf/renderer, xlsx,
  fuse.js, ics.
- Extension table reflects cloud-backup enabled; adds
  inbox-smart-match and example-logger; reorders to match current
  extensions.config.json.
- Updated counts: 36 event types (was 30+), 35 MCP tools (was 26),
  ~60 tables (was ~47), 118 migrations (was 93), 19 report
  endpoints (was 16), 20 report generators (was 17).
- lib/ directory table now covers salary, providers,
  company-lookup, processing-history, support.ts; removes the
  deleted settings/ subdir.
- App routes table adds /salary/*, /help, /settings/salary,
  /settings/backup.
- API endpoints table adds /api/salary/*, /api/support/contact,
  /api/account/delete, /api/audit-trail/*, /api/log,
  /api/currency/rate, top-level extension routes.
- Tables section adds Salary, Third-party providers, Inbox &
  Migration groups; removes salary_payments (replaced by
  salary_runs + salary_line_items).
- Skills list updated to enumerate the Swedish domain skills by
  name instead of the old single /swedish-bookkeeping.

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

* fix: address PR review feedback on account activation

Seven fixes based on Greptile + Swedish compliance review on #308.

- ActivateAccountsDialog: disable the confirm button when any
  entered number isn't a valid BAS account. Previously activation
  would succeed for the knowns and the retry would immediately
  fail again on the unknowns, giving a confusing double-toast UX.
- pending-operations/commit: revert commitSendInvoice and
  commitMarkInvoiceSent to swallow AccountsNotInChartError
  silently. The prior PR upgrade made these blocking, which
  regressed invoice delivery for users whose AR accounts are
  inactive — and since the activation dialog isn't wired into
  those flows yet, there's no one-click recovery. The silent
  catches now append an InvoiceJournalEntrySkipped event to
  processing_history so the missing verifikation is actionable
  in audit trails rather than silently understating the
  momsdeklaration (revenue / utgående moms unposted).
- engine.reverseEntry: resolve account IDs with includeInactive=true
  so storno of an already-committed entry goes through even when
  the user has since deactivated one of its accounts. Blocking
  the reversal would leave the original entry uncorrected in
  violation of BFL 5 kap 5§ (rättelse must be documented). The
  default (includeInactive=false) still applies to createDraftEntry
  so new bookings to inactive accounts continue to trigger the
  activation dialog.
- supplier-invoices POST + credit: roll back the just-inserted
  supplier_invoices row (items cascade-delete) on any JE failure,
  not only AccountsNotInChartError. An orphan supplier_invoices
  row without a registration / credit JE leaves leverantörsskuld
  (2440) and ingående moms (2641) unposted — a silent
  understatement / overstatement in the momsdeklaration (ML
  2023:200 / BFL 5 kap). The catch now returns a clear Swedish
  error message for non-activation failures (typically period
  lock or DB error) instead of silently logging.

Test mocks for chart_of_accounts updated for the new query chain
(eq.in.eq instead of eq.eq.in after the is_active conditional).

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-04-22 09:58:54 +02:00
Jakob WennbergandClaude Opus 4.6 e46654ab25 feat: concurrency guards, account validation, and reversal side-effects (#247)
* feat: add concurrency guards, account validation, and reversal side-effects to bookkeeping engine

Prevent double-booking via CAS guards on mark-paid and categorize routes (409 on conflict),
make payment GL entries blocking (AP/AR must match GL), validate account resolution in engine,
and auto-sync invoice status on payment reversal. Adds journal_entry.reversed event type.

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

* fix: address Greptile review — company_id filter, voucher gaps, status restore

- Add missing company_id filter on supplier-invoice CAS update (defense in depth)
- Add voucher_gap_explanations insert on CAS-cancelled entries in both mark-paid
  routes (BFNAR 2013:2 compliance, matching categorize route pattern)
- Fix reversal status restore: check due_date to determine overdue vs sent/approved
  instead of always reverting to sent/approved
- Rename shadowed reversedLines variable to originalLines (P2 clarity)

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

* fix: derive reversal payment amount from payments table, not GL lines

The reversal GL entry is already a line-by-line mirror per BFL 5 kap 5§.
For the business-level invoice sync, use the payment record amount from
supplier_invoice_payments / invoice_payments instead of inspecting GL
account numbers — works identically for kontantmetod and faktureringsmetod
without needing to know which accounts were used.

Also adds company_id filter on all reversal sync queries (defense in depth).

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

* fix: allow reversal of partially_paid customer invoices

Widen the status filter from .eq('status', 'paid') to
.in('status', ['paid', 'partially_paid']) so that reversing a partial
payment GL entry correctly updates the invoice state.

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 15:23:08 +02:00
Mattsson a1a816b4a5 Delete features (#218)
* Implement company and account deletion features

- Add event types for company and account deletion to CoreEvent.
- Enhance Supabase middleware to handle company context resolution and cookie management for archived companies.
- Create API routes for deleting accounts and companies, including necessary validations and event emissions.
- Implement tests for account and company deletion endpoints to ensure proper functionality and error handling.
- Add retention notice component to inform users about bookkeeping data retention during destructive actions.
- Create database migrations to support soft deletion of companies and anonymization of user accounts, ensuring compliance with retention laws.

* feat: enhance account deletion process and update user notifications

* Add service client for onboarding completion check and update escape hatch visibility

* Enhance invite flow and email handling for company members

* Refactor company context and RLS policies for active company isolation

- Update `switchCompany` to remove unnecessary revalidation as client handles navigation.
- Revise `getActiveCompanyId` to prioritize `user_preferences` and validate against non-archived memberships.
- Modify `setActiveCompany` to ensure `user_preferences` is the authoritative source while maintaining cookie compatibility.
- Enhance middleware to resolve active company using `user_preferences` and fallback to first non-archived membership.
- Introduce new API route `/api/company/current` to fetch the active company ID for cross-tab synchronization.
- Implement `CompanyTabSync` component for real-time active company enforcement across tabs.
- Create migration for RLS policies to enforce single-active-company isolation using `current_active_company_id()`.

* feat: implement viewer role enforcement for write permissions

- Added `useCanWrite` hook to determine if the current user has write permissions based on their role in the active company.
- Updated various components (JournalEntryForm, CustomerForm, DeadlineForm, etc.) to disable write actions and show a lock icon with a tooltip for users without write permissions.
- Introduced `requireWritePermission` function to enforce write permissions at the API level, returning a 403 response for viewers.
- Created tests to verify the behavior of the viewer role and write permissions.
- Added database migration to enforce read-only access for viewers at the database level.
2026-04-11 17:06:32 +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 WennbergandClaude Opus 4.6 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
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
MattssonandClaude Opus 4.6 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
MattssonandClaude Opus 4.6 550cadcb06 fix: harden auth, cron secrets, and provider flows (GNU-17) (#148)
- Replace === with crypto.timingSafeEqual in all 7 cron routes via shared lib/auth/cron.ts
- Add in-memory rate limiting (60 req/min) and expires_at support to calendar feed
- Add exponential backoff on MFA verify after 3 failed attempts
- Add 60s cooldown on password reset requests
- Validate bank callback auth code format before API call
- Redact session IDs from bank sync and callback logs
- Validate OAuth redirect_uris against allowlist (claude.ai, claude.com, localhost)
- Remove excessive PII/debug console logging from login page

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:49:09 +02:00
Mattsson db528c3652 Delete invoice feature (#64)
* I mplement delete API for draft invoices with ownership verification

* Color change dark mode
2026-03-20 14:42:43 +01:00
Jakob WennbergandClaude Opus 4.6 86348c80c7 feat: BFL-compliant journal descriptions and cancelled entry status (#55)
* fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2)

Reversed entries (storno) must appear alongside their original posted entries
in reports for a complete audit trail. Previously, filtering by status='posted'
excluded them, causing discrepancies when corrections had been made.

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

* feat: semi-manual invoice payment booking with editable journal lines

When marking an invoice as paid, users now see a dialog where they can:
- Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.)
- Review and edit the proposed journal entry lines before committing
- The happy path remains fast — lines are pre-filled correctly

Implementation:
- Pure proposePaymentLines() function for line computation (accrual + cash)
- PaymentBookingDialog with AccountCombobox, balance validation, date picker
- API accepts optional custom lines, falls back to auto-generation without them
- 18 tests (8 unit + 10 API) all passing

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

* fix: address Greptile review — validation fallback, balance check, error handling

- P1: Return 400 on invalid body instead of silently falling back to
  auto-generated lines (split JSON parse from schema validation)
- P1: Add server-side balance check for custom lines before committing
  (debit must equal credit, totalDebit > 0)
- P2: Wrap PaymentBookingDialog init() in try/catch with toast on
  failure and auto-close instead of silent empty state
- Add 2 new tests: unbalanced lines → 400, invalid schema → 400

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

* fix: OAuth callback redirect for local dev and timeout resilience

- Pass redirectUri dynamically from NEXT_PUBLIC_APP_URL so OAuth
  callbacks work on localhost (not just production)
- Encode consentId/provider in OAuth state (base64url JSON) so the
  callback doesn't depend on session storage
- Add skipAuth flag to extension API routes for OAuth callbacks
  (external provider redirects have no user session cookie)
- Wrap AbortError in descriptive timeout messages in arcim-client
- Make preview endpoint resilient to partial failures (company info
  and SIE fetch are individually non-blocking)
- Simplify login page (remove unused magic link auth mode)

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

* fix: create journal entry before marking invoice as paid

Move journal entry creation before the invoice status update so that
if accounting fails, the invoice is not permanently marked paid without
a corresponding entry. Previously the error was silently swallowed.

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

* fix: update mark-paid tests for journal-first ordering

Reorder mock queue to match new flow (settings before update), update
failure test to expect 500 instead of silent success, add try-catch
with proper error response in route handler.

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

* feat: add reverse charge VAT (ruta 20-32) and improve mobile UX across dashboard

Add full reverse charge (omvänd skattskyldighet) support to the VAT declaration:
- Map accounts 2614/2624/2634 to ruta 30/31/32 for self-assessed output VAT
- Calculate purchase bases (ruta 20-24) from supplier invoices by supplier type
- Include ruta 30-32 in ruta 49 formula and totalOutputVat summary
- Display reverse charge section in reports UI and composition chart
- Add comprehensive test coverage for all reverse charge scenarios

Improve mobile UX across the app:
- Convert nav drawer to bottom sheet with drag handle and safe area padding
- Add mobile card layout for PaymentBookingDialog journal lines
- Replace settings tab pills with dropdown selector on mobile
- Make wizard step indicators responsive (collapsed on mobile)
- Ensure all dialog footers stack buttons full-width on mobile
- Add 44px minimum touch targets throughout
- Make onboarding buttons full-width on mobile

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

* fix: address Greptile review — indentation, query efficiency, tab dedup

- Fix misleading try-block indentation in mark-paid route
- Filter reversed entries at DB level (.eq('status', 'posted')) instead
  of fetching then discarding in memory
- Extract shared settingsTabs array so mobile Select and desktop
  TabsList stay in sync automatically

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

* feat: add resilience fallbacks, Arcim retry logic, and client tests

Add FallbackPrompt component and integrate it across banking and migration
error states so users always have a manual import escape hatch. Add retry
with exponential backoff to Arcim API client for transient failures (429,
502, 503, 504) and timeouts. Expand import page deep-linking with ?mode=
parameter. Add persistent error banner on settings page for bank connection
failures. Include 18 new tests for the Arcim client covering retry, backoff,
pagination, timeout, env validation, and singleton resource unwrapping.

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

* fix: address Greptile review — setActiveTab, test cleanup, redundant clearTimeout

- Add missing setActiveTab('banking') when handling bank_error query
  param so the error banner is actually visible (P1)
- Guard env-var cleanup with try/finally in arcim-client tests to
  prevent state leakage on assertion failure (P2)
- Only mock retry-range setTimeout delays in backoff test, letting
  AbortController timers pass through real setTimeout (P2)
- Remove redundant clearTimeout in catch block — finally handles it (P2)

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

* feat: add BFL-compliant counterparty names to journal descriptions and cancelled entry status

Journal descriptions now include customer/supplier names for traceability
(e.g. "Kundfaktura 1001, Foretag AB"). Failed draft entries are marked as
'cancelled' instead of deleted, respecting immutability constraints.
Includes DB migration for the new journal_entries status value.

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

* fix: address Greptile review — Swedish typos, missing source type, trigger and reversal cleanup

- Fix Swedish spelling: leverantor → leverantör in all supplier description prefixes
- Add supplier_credit_note to supplierSourceTypes in VAT declaration so credit
  notes correctly reduce reverse-charge bases (ruta 20–24)
- Mark orphaned concurrent reversals as cancelled instead of attempting deletion
  that the immutability trigger blocks
- Allow posted → cancelled transition in trigger for orphaned reversal cleanup
- Restrict cancelled entry line trigger to DELETE-only (block INSERT/UPDATE)

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

* fix: use main's Step3TaxRegistration (onboarding restructured in PR #54)

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

* chore: retrigger Greptile review

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-18 17:53:54 +01:00
Jakob WennbergandClaude Opus 4.6 ceec8c02a8 feat: reverse charge VAT (ruta 20-32) + mobile UX improvements (#50)
* fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2)

Reversed entries (storno) must appear alongside their original posted entries
in reports for a complete audit trail. Previously, filtering by status='posted'
excluded them, causing discrepancies when corrections had been made.

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

* feat: semi-manual invoice payment booking with editable journal lines

When marking an invoice as paid, users now see a dialog where they can:
- Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.)
- Review and edit the proposed journal entry lines before committing
- The happy path remains fast — lines are pre-filled correctly

Implementation:
- Pure proposePaymentLines() function for line computation (accrual + cash)
- PaymentBookingDialog with AccountCombobox, balance validation, date picker
- API accepts optional custom lines, falls back to auto-generation without them
- 18 tests (8 unit + 10 API) all passing

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

* fix: address Greptile review — validation fallback, balance check, error handling

- P1: Return 400 on invalid body instead of silently falling back to
  auto-generated lines (split JSON parse from schema validation)
- P1: Add server-side balance check for custom lines before committing
  (debit must equal credit, totalDebit > 0)
- P2: Wrap PaymentBookingDialog init() in try/catch with toast on
  failure and auto-close instead of silent empty state
- Add 2 new tests: unbalanced lines → 400, invalid schema → 400

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

* fix: OAuth callback redirect for local dev and timeout resilience

- Pass redirectUri dynamically from NEXT_PUBLIC_APP_URL so OAuth
  callbacks work on localhost (not just production)
- Encode consentId/provider in OAuth state (base64url JSON) so the
  callback doesn't depend on session storage
- Add skipAuth flag to extension API routes for OAuth callbacks
  (external provider redirects have no user session cookie)
- Wrap AbortError in descriptive timeout messages in arcim-client
- Make preview endpoint resilient to partial failures (company info
  and SIE fetch are individually non-blocking)
- Simplify login page (remove unused magic link auth mode)

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

* fix: create journal entry before marking invoice as paid

Move journal entry creation before the invoice status update so that
if accounting fails, the invoice is not permanently marked paid without
a corresponding entry. Previously the error was silently swallowed.

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

* fix: update mark-paid tests for journal-first ordering

Reorder mock queue to match new flow (settings before update), update
failure test to expect 500 instead of silent success, add try-catch
with proper error response in route handler.

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

* feat: add reverse charge VAT (ruta 20-32) and improve mobile UX across dashboard

Add full reverse charge (omvänd skattskyldighet) support to the VAT declaration:
- Map accounts 2614/2624/2634 to ruta 30/31/32 for self-assessed output VAT
- Calculate purchase bases (ruta 20-24) from supplier invoices by supplier type
- Include ruta 30-32 in ruta 49 formula and totalOutputVat summary
- Display reverse charge section in reports UI and composition chart
- Add comprehensive test coverage for all reverse charge scenarios

Improve mobile UX across the app:
- Convert nav drawer to bottom sheet with drag handle and safe area padding
- Add mobile card layout for PaymentBookingDialog journal lines
- Replace settings tab pills with dropdown selector on mobile
- Make wizard step indicators responsive (collapsed on mobile)
- Ensure all dialog footers stack buttons full-width on mobile
- Add 44px minimum touch targets throughout
- Make onboarding buttons full-width on mobile

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

* fix: address Greptile review — indentation, query efficiency, tab dedup

- Fix misleading try-block indentation in mark-paid route
- Filter reversed entries at DB level (.eq('status', 'posted')) instead
  of fetching then discarding in memory
- Extract shared settingsTabs array so mobile Select and desktop
  TabsList stay in sync automatically

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-18 10:42:57 +01:00
Jakob WennbergandClaude Opus 4.6 bac49b6ee6 fix: OAuth callback redirect and timeout resilience (#43)
* fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2)

Reversed entries (storno) must appear alongside their original posted entries
in reports for a complete audit trail. Previously, filtering by status='posted'
excluded them, causing discrepancies when corrections had been made.

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

* feat: semi-manual invoice payment booking with editable journal lines

When marking an invoice as paid, users now see a dialog where they can:
- Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.)
- Review and edit the proposed journal entry lines before committing
- The happy path remains fast — lines are pre-filled correctly

Implementation:
- Pure proposePaymentLines() function for line computation (accrual + cash)
- PaymentBookingDialog with AccountCombobox, balance validation, date picker
- API accepts optional custom lines, falls back to auto-generation without them
- 18 tests (8 unit + 10 API) all passing

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

* fix: address Greptile review — validation fallback, balance check, error handling

- P1: Return 400 on invalid body instead of silently falling back to
  auto-generated lines (split JSON parse from schema validation)
- P1: Add server-side balance check for custom lines before committing
  (debit must equal credit, totalDebit > 0)
- P2: Wrap PaymentBookingDialog init() in try/catch with toast on
  failure and auto-close instead of silent empty state
- Add 2 new tests: unbalanced lines → 400, invalid schema → 400

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

* fix: OAuth callback redirect for local dev and timeout resilience

- Pass redirectUri dynamically from NEXT_PUBLIC_APP_URL so OAuth
  callbacks work on localhost (not just production)
- Encode consentId/provider in OAuth state (base64url JSON) so the
  callback doesn't depend on session storage
- Add skipAuth flag to extension API routes for OAuth callbacks
  (external provider redirects have no user session cookie)
- Wrap AbortError in descriptive timeout messages in arcim-client
- Make preview endpoint resilient to partial failures (company info
  and SIE fetch are individually non-blocking)
- Simplify login page (remove unused magic link auth mode)

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

* fix: create journal entry before marking invoice as paid

Move journal entry creation before the invoice status update so that
if accounting fails, the invoice is not permanently marked paid without
a corresponding entry. Previously the error was silently swallowed.

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

* fix: update mark-paid tests for journal-first ordering

Reorder mock queue to match new flow (settings before update), update
failure test to expect 500 instead of silent success, add try-catch
with proper error response in route handler.

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-17 17:31:04 +01:00
Jakob WennbergandClaude Opus 4.6 3e82295cce feat: semi-manual invoice payment booking dialog (#37)
* fix: include reversed entries in all reports (general ledger, trial balance, VAT, SIE, NE, INK2)

Reversed entries (storno) must appear alongside their original posted entries
in reports for a complete audit trail. Previously, filtering by status='posted'
excluded them, causing discrepancies when corrections had been made.

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

* feat: semi-manual invoice payment booking with editable journal lines

When marking an invoice as paid, users now see a dialog where they can:
- Choose which bank/cash account the payment goes to (1910, 1920, 1930, etc.)
- Review and edit the proposed journal entry lines before committing
- The happy path remains fast — lines are pre-filled correctly

Implementation:
- Pure proposePaymentLines() function for line computation (accrual + cash)
- PaymentBookingDialog with AccountCombobox, balance validation, date picker
- API accepts optional custom lines, falls back to auto-generation without them
- 18 tests (8 unit + 10 API) all passing

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

* fix: address Greptile review — validation fallback, balance check, error handling

- P1: Return 400 on invalid body instead of silently falling back to
  auto-generated lines (split JSON parse from schema validation)
- P1: Add server-side balance check for custom lines before committing
  (debit must equal credit, totalDebit > 0)
- P2: Wrap PaymentBookingDialog init() in try/catch with toast on
  failure and auto-close instead of silent empty state
- Add 2 new tests: unbalanced lines → 400, invalid schema → 400

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-17 11:20:29 +01:00
Jakob WennbergandClaude Opus 4.6 091d043c85 feat: UI polish, lint fixes, onboarding redesign, help page expansion, and test improvements
Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:05:49 +01:00