fce6faff2c05d9a3889b07ea35c2000eb9d486b2
533 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fce6faff2c |
fix(api): stabilize report pagination + declare real { data, meta } envelope on v1 single/write endpoints (#811)
* fix(reports): stabilize fetchAllRows paging to stop doubled/dropped balances (#790, #791) PostgREST `.range()` paging is only correct when the underlying query has a stable TOTAL order. Several aggregating report queries (general ledger, trial balance, grundbok, supplier/AR ledgers, etc.) paginated without `.order()`, so on datasets larger than one 1000-row page Postgres could return rows in a different order between requests — silently DUPLICATING or SKIPPING rows on a page boundary and doubling or dropping financial totals. - fetch-all.ts: document the ordering invariant and add an optional `dedupeBy` defense-in-depth that drops cross-page duplicates and warns when it fires (surfaces a missing `.order()` in logs instead of corrupting money). - Add a stable `.order()` (line PK or account_number) to every paginated query in lib/reports/ and the account-balances route; pass `dedupeBy` on the money-aggregating line queries. - Add fetch-all unit tests and update report test fixtures to carry row ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): declare the real { data, meta } envelope on v1 single/write/204 endpoints (#794) The OpenAPI generator derives each endpoint's documented body purely from its registered `response.success` Zod schema, and that schema is never validated at runtime — so a route could advertise a shape its handler never sends. #802 fixed this for list endpoints; the same drift was latent on single-resource and write endpoints, which declared the bare resource schema instead of the `{ data, meta }` envelope the handlers actually return. - registry.ts: extend `ResponseMetaSchema` with the optional `audit` block and `partial_expansions` list that writes/expansions emit; add the `NoBodyResponse` sentinel so 204 DELETE handlers document a bare 204 instead of a phantom 200. - Wrap every single/write endpoint's `response.success` in `dataEnvelope(...)` (or `NoBodyResponse` for 204s) across the v1 routes. - Add a response-envelope contract test that fails CI if any JSON endpoint forgets to wrap its schema, with binary downloads and 204s as the only exemptions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): extend paging dedupeBy to rc-basis-gaps and opening-balances Address PR review: these two money-aggregating line queries already had the stable `.order('id')` (so paging was correct) but didn't carry `id` in the select, so they couldn't use the `dedupeBy` defense-in-depth that general-ledger and trial-balance got. Select `id` and pass `dedupeBy: r => r.id` so the whole report layer applies the ordering invariant consistently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ae17b304d7 |
fix(import): add stable .order() to account-sync chart paging (#790, #791 follow-up) (#812)
`syncMappedAccounts` pages the company's full chart via `fetchAllRows` to avoid the silent 1000-row PostgREST cap, but the query had no `.order()`. Like the report queries fixed in #811, PostgREST `.range()` paging is only correct with a stable total order — without it, a chart larger than one page could duplicate or skip accounts across page boundaries, corrupting the existing-account Map and causing spurious create/update churn on import. Order on the unique `account_number` (stable total order; the result is read into a Map so the order is invisible to callers). Extend the test mock's query chain to include `.order()`. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fc2b4d1e23 |
fix(api): declare the real { data, meta } envelope for v1 list endpoints (#802)
The OpenAPI success schemas for v1 list endpoints declared a bare
{ <name>: [...] } object that no handler returns, so the published spec
advertised a shape the API never emits (#781, item 2). response.success is
doc-only (feeds zodToJsonSchema for /openapi.json; not validated at runtime),
so this is a documentation fix with no behaviour change.
Add listEnvelope() ({ data: [...], meta }) and dataEnvelope() ({ data, meta })
plus a shared ResponseMetaSchema. Ten endpoints that return paginated() now use
listEnvelope; the three that deliberately wrap their array under a named key via
ok() (accounts, fiscal-periods, webhooks — a shape their route tests lock in)
use dataEnvelope. Also corrects the accounts/fiscal-periods examples, which
showed an unwrapped data: [...] that contradicted their handlers.
Refs #781.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
55ba66908b |
feat(salary): let an enskild firma employ staff while blocking owner/board payroll (#797)
An enskild firma that hires staff should get the payroll module, but its owner or board can never be on payroll (owner compensation is egna uttag / BAS 2013, not lön). - Migration 20260628120000 adds the enforce_ef_no_owner_employee trigger (BEFORE INSERT OR UPDATE OF employment_type) as the all-paths backstop. - lib/salary/employment-rules.ts is the app-layer mirror (forbidden set kept byte-identical to the trigger); getCompanyEntityType() resolves the same company_settings -> companies precedence. - The two UI salary routes and the v1 POST guard before insert/update for a clean 400 with guidance. - Payroll nav + Lön settings now show for any employer (aktiebolag OR company_settings.pays_salaries), wired through the dashboard layout. Fixes #782. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5bacda4839 |
fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14 (#796)
* fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14
Onboarding derived the VAT number as SE${orgNumber}01. For an enskild firma the
org number is a 12-digit personnummer, producing SE + 14 digits, which fails the
^SE\d{12}$ validation — the pre-filled value is re-submitted on save and the tax
settings page becomes unsavable.
New shared helper lib/vat/vat-number.ts (normalize/validate/derive, reusing
normalizeOrgNumber to drop the century + Luhn-validate). UpdateSettingsSchema,
the onboarding wizard, the onboarding upsert in lib/company/actions.ts, and the
arcim-migration provider import all route through it. Backfill migration repairs
existing SE+14 rows to SE+12 (idempotent, scoped to ^SE\d{14}$ only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(arcim): warn when a provider VAT number is dropped as malformed
The provider VAT guard silently discarded a value that doesn't normalise to a
valid SE+12 momsregistreringsnummer. Emit a structured warn (provider +
company, no raw value — it can embed a personnummer) so consistently-bad
provider data is observable rather than invisible. Addresses the OWASP V16
logging finding on the arcim VAT-normalisation change in this PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9278221616 |
fix(api): guard params await so static v1 routes don't 500 (#795)
Next.js 16 invokes a static route handler (no [segment]) with
{ params: undefined }. /api/v1/companies is the only authenticated static
route on the v1 surface, so awaiting params.params null-derefs and the catch
turns it into a 500 for every valid API key. Guard the await:
((await params?.params) ?? {}). Dynamic routes are unaffected. Fixes #781.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
bc09cea07e |
chore(scripts): add prod repair scripts for Arcim and Capelix incidents (#773)
* chore(scripts): add prod repair scripts for Arcim and Capelix incidents Two idempotent, dry-run-by-default repair scripts, committed for the audit trail (matching the existing scripts/repair-*.ts convention). Neither runs automatically — applying requires an explicit --execute/--commit flag. - repair-arcim-supplier-payments.ts: Arcim Technology AB (2026-06-11). Two supplier invoices left in inconsistent half-states (swallowed AccountsNotInChartError on 3740; bank-sync auto-link without a booked payment) plus expense booked on 5010 instead of 5420/6580. Runs through the real engine (createJournalEntry/correctEntry) so voucher numbering and balance triggers behave as in-app; every step checks its precondition. - repair-capelix-invoice-payment.ts: Capelix AB invoice-001 double-booking (2026-05-29), root-caused to the invoiceAlreadyBooked dead-column read fixed in PR #713. Storno-only per BFL/BFNAR 2013:2: reverse the wrong cash entry, post the correct 1930/1510 clearing entry, relink the bank tx + payment row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(scripts): scope Capelix invoice_payments relink to company_id Address review (PR Agent + compliance swarm): the Step 3b invoice_payments update filtered on journal_entry_id only; add .eq('company_id', COMPANY_ID) to match the sibling transactions update directly above it (tenant isolation / defense-in-depth). invoice_payments carries company_id (multi-tenant refactor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
739f18fd1c |
fix(reconciliation): drive bank avstämning period from the report header (#771) (#774)
The bank reconciliation view hosted its OWN FiscalYearSelector inside the action bar — but that bar renders below the loading-skeleton early-return, so the selector never mounted, its onReady/onChange never fired, the periodReady gate never flipped, and the page hung on a permanent skeleton (#771). Make the report period-scoped like the ledgers: lib/reports/catalog.ts marks bank-reconciliation `params: 'fiscal'`, so the report page's räkenskapsår selector owns the period. FocusedReport passes periodId + periodBounds down, and BankReconciliationView takes them as props instead of self-selecting. The window seeds from periodBounds and a periodId-keyed effect re-seeds (and writes dateFromRef/dateToRef synchronously) on a year switch, preserving the #751 period-scoped IB-floor behaviour without the deadlock. Manual date edits still apply on demand via "Filtrera". Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
43007fa869 |
feat(mcp): self-describing agent surface — staging _meta, company identity, clean skill summaries (#775)
* feat(mcp): make the agent surface self-describing (staging _meta, company identity, clean summaries)
A pass over the MCP server's agent-facing surface so an agent can act
correctly without parsing description prose:
- Machine-readable staging contract: deriveToolMeta() attaches _meta to
tools/list (and search detail=full) — { requires_approval, approve_tool,
preflight? } — keyed off the STAGED_OPERATION_SCHEMA output schema. Literal
_meta (e.g. UI widget hints) wins on collision. TOOL_PREFLIGHT_MAP names the
read-only pre-flight for the few writes that have one (year-end readiness,
VAT validate, depreciation proposal). Guarded by staging-meta.test.ts.
- Company identity in gnubok_get_agent_briefing: returns a `company` block
(id, name, org_number, entity_type, accounting_method) so the agent can
confirm WHICH entity it operates on and pick the right settlement account
(accrual = credit 1510; cash = debit 19xx) before any write. Best-effort —
a missing row never blocks the briefing. Covered by agent-briefing.test.ts.
- toSummary(): trims the long, keyword-stuffed SKILL.md frontmatter into clean
one-liners for gnubok_list_skills / gnubok_get_agent_briefing so the client
never truncates one mid-sentence; full bodies stay in gnubok_load_skill.
Covered by to-summary.test.ts.
- bank-reconciliation skill: a match/link decision tree (what you have x
whether a verifikat exists) and kontant- vs faktureringsmetoden settlement
accounts.
- Prose/description clarifications: "Stages"/"Stages for approval" on the
link tools; propose_dispositioner/accruals note there is no dedicated MCP
poster; server-info documents _meta and the legacy gnubok_ tool prefix.
All 34 touched MCP tests pass. Merged cleanly on top of #759/#760 (server.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mcp): make accounting_method description state the full settlement posting
Review (swedish-accounting-compliance): the agent-briefing schema described
accrual as "credit 1510 on payment", which reads as a one-sided entry. Spell
out both sides (payment debits 19xx AND credits 1510) so an agent can't infer a
single-leg posting that violates BFL 5 kap double-entry. Mirrors the precision
already in the bank-reconciliation skill body. Payload-size guard still passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d5abc8dd1c |
fix(inbox): set_inbox_extracted_data should accept and persist accountSuggestion (#760)
The tool was using InvoiceExtractionSchema which forces every
lineItems.accountSuggestion to null via .transform() — the same guard
that prevents the AI extractor from hallucinating BAS accounts.
Agents supplying their own extraction should be able to pin a cost
account per line.
Adds AgentExtractionSchema (exported alongside ExtractionSchema)
where accountSuggestion accepts a validated BAS expense account
(class 4–7, /^[4-7]\d{3}$/) or null. set_inbox_extracted_data now
parses through this schema so the field survives the round-trip to
the DB and is available when gnubok_create_supplier_invoice_from_inbox
builds line items.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
6fe4164adc |
fix(inbox): fall back to supplier.default_expense_account in create_supplier_invoice_from_inbox (#759)
The line-item account lookup was using snake_case `li.account_number` (never populated) instead of camelCase `li.accountSuggestion` from the extraction schema. When accountSuggestion is null, the fallback now checks supplier.default_expense_account before hard-coding account 4000. Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
a3491b6d89 |
fix(transactions): resolve bank account from cash_account_id in bulk-book and direct-book dialogs (#770)
Follow-up to #769: applies the same fetch-on-open + resolveAccount pattern to BulkBookDialog (manual tab bank-leg pre-fill) and BookDirectlyDialog (settlement line in buildPrefillLines). Both dialogs now fetch /api/cash-accounts when they open, gate the pre-fill on the fetch resolving, and resolve the correct BAS ledger account per transaction instead of always emitting '1930'. Adds lib/cash-accounts/resolve-account.ts (cherry of the utility from #769) since that PR is not yet merged into main. Signed-off-by: Jonas Flodén <jonas@floden.nu> |
||
|
|
a4da2d62df |
feat: add ignore menu item to transaction inbox card (#758)
Imported bank transactions can now be hidden from the inbox via a new
"Ignorera transaktion" item in the ⋯ overflow menu. The backend
(POST/DELETE /api/transactions/[id]/ignore) and the is_ignored DB column
already existed; this wires up the missing UI affordance.
- TransactionInboxCard: add onIgnore prop + EyeOff menu item (imported
rows only — manually created rows use delete instead)
- page.tsx: pass onIgnore={handleIgnoreTransaction} to the card; add
handleBatchIgnore for the batch action bar's new "Ignorera" button
- sv.json / en.json: add ignore_btn translation key to tx_inbox_card
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
5279dfb494 |
fix(bookkeeping): gnubok_categorize_transaction now handles incoming expense refunds correctly (#761)
Incoming expense refunds (positive amount on an expense category) previously booked with inverted debit/credit — crediting the bank and debiting the expense account — which both imbalanced the book and reported negative bank flow. getCategoryAccountMapping now detects amount > 0 on expense categories and returns the reversed mapping: debit 1930, credit expense account, with 2641 as vatCreditAccount so ingående moms is correctly reversed on the VAT line. The VAT line description is "Återföring ingående moms X%" rather than the income-side "Utgående moms" label. Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
bbc8063f53 |
fix(bookkeeping): reverseEntry defaults to original entry_date, not today (#762)
* fix(bookkeeping): reverseEntry defaults to original entry_date, not today When reverseEntry() is called without an explicit reversalDate, it was defaulting to getSwedishLocalDate() (today). This caused the storno to land in the current period rather than the original entry's period — any reversal of a past verifikation through uncategorize, re-categorize, the dashboard reverse button, salary correction, or fix-cash-mismatch would produce a makulering dated today instead of the original booking date. The entry is already fetched before the date is resolved, so defaulting to original.entry_date is safe. Callers that intentionally want a different date (credit notes, mark-paid with payment date, user-provided reverse date) still pass an explicit reversalDate and are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * feat(api): v1 endpoints to stamp invoice inbox items as consumed Adds inbox_item_id support to POST /api/v1/companies/{companyId}/documents/{id}/link (best-effort stamp on the originating invoice_inbox_items row) and a new dedicated POST /api/v1/companies/{companyId}/inbox-items/{id}/stamp endpoint for stamping independently of the document link — both use documents:write scope and require Idempotency-Key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Jonas Flodén <jonas@floden.nu> * Revert "feat(api): v1 endpoints to stamp invoice inbox items as consumed" This reverts commit f1bf3a86385ec556a49830c9e0966236a10d3164. Signed-off-by: Jonas Flodén <jonas@floden.nu> --------- Signed-off-by: Jonas Flodén <jonas@floden.nu> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
429ab7b230 |
build(deps): bump The-PR-Agent/pr-agent from 0.36.0 to 0.37.0 (#765)
Bumps [The-PR-Agent/pr-agent](https://github.com/the-pr-agent/pr-agent) from 0.36.0 to 0.37.0. - [Release notes](https://github.com/the-pr-agent/pr-agent/releases) - [Changelog](https://github.com/The-PR-Agent/pr-agent/blob/main/CHANGELOG.md) - [Commits](https://github.com/the-pr-agent/pr-agent/compare/ffe1f89a4dafc7d8e88b9cf010a3233e30b49f43...85178bef87b7a03081cd30592a5aad100284f9a7) --- updated-dependencies: - dependency-name: The-PR-Agent/pr-agent dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
be9bec6b0d |
build(deps): bump actions/checkout from 6 to 7 (#764)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
931f4fe36d |
build(deps): bump alpine from 3.22 to 3.24 in /docker (#732)
Bumps alpine from 3.22 to 3.24. --- updated-dependencies: - dependency-name: alpine dependency-version: '3.24' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
26c5798b6e |
build(deps): bump docker/login-action from 3 to 4 (#696)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
027734ffc7 |
fix(reconciliation): scope bank avstämning to a fiscal period so the IB stops counting (#751) (#754)
* fix(reconciliation): scope bank reconciliation to a fiscal period so the IB stops counting (#751) The bank reconciliation widget defaulted its date window to "full history" (empty dateFrom). With no lower bound the GL side spans the fiscal-year boundary: a prior period's movements on the account net to exactly the opening balance, and the new period's IB entry adds another copy. The IB *summary* was excluded but the prior-period *detail* stayed in the period movement while the bank feed only covered the current period — a phantom difference equal to the IB (the "räknar med IB fast den säger borträknad" report in #751). - Server: getReconciliationStatus now floors the window at the most recent opening-balance date on the account (effectiveFrom = max(dateFrom, ibDate)) and clamps both the GL movement set and the bank-feed set identically. Derived from the already-fetched lines — no extra query. A no-op when the caller already passes period_start; a safety net otherwise. - UI: BankReconciliationView scopes to a fiscal period via FiscalYearSelector (defaults to the newest period), seeding dateFrom/dateTo and gating the initial fetch so the full-history numbers never flash. - Tests: two regression cases reproducing the cross-period scenario. Proven against prod: full-history -> difference -10 172,94 (matched the screenshot); period-bounded -> 0,00. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): re-fetch on fiscal-period switch; document IB-floor choice Review follow-up on #754: - BankReconciliationView: add selectedPeriodId to the gated fetch effect deps so switching räkenskapsår re-fetches with the new window, and a late period selection (selector signalling ready before the company context hydrates) still triggers the real period-scoped fetch instead of leaving the empty-window result. Manual date edits still stay on the explicit "Filtrera" action. - getReconciliationStatus: comment why ibFloor takes the LATEST opening-balance date (one IB per period invariant; across a multi-year window the most recent IB is the intended floor; same-date duplicates cancel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reconciliation): cover mid-period window (dateFrom after the IB date) Review follow-up on #754: documents that a per-month reconciliation window starting after the fiscal-year IB correctly excludes the IB and reconciles on the in-window movements alone (gl_1930_opening_balance = 0 by design). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
2d6ddeafc5 |
feat(import/export): article import + register export (xlsx/csv) (#750)
* feat(import/export): article import + register export (xlsx/csv)
Add CSV/Excel import for the article register (artiklar), mirroring the
existing customer/supplier import pipeline, plus Excel + CSV export for
articles, customers and suppliers.
Import (lib/import/articles + app/api/import/articles):
- Column auto-detection tuned to Fortnox/Visma/Bokio export headers,
Swedish-decimal price parsing, VAT snapped to {0,6,12,25}, type/unit
normalization.
- Dedup by article number then name; 23505 soft-skip; auto-number
backfill; revenue-account override kept only when active, otherwise
dropped with a warning (never mutates the chart of accounts).
- New "Artiklar" flow in the /import hub.
Export (app/api/export/* + lib/export/register-export):
- Read-only xlsx (default) / csv (?format=csv, UTF-8 BOM) downloads.
- Headers chosen so files round-trip back through the importer.
- "Exportera" menu added to the articles, customers and suppliers pages.
Refs #746. Direct Fortnox/Visma API article fetch tracked in #749.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(import/export): address PR review — lint ratchet + export hardening
- xlsx-export: keep `SheetSpec<any>` on the eslint-disabled line (fixes the
core-only lint ratchet regression: no-explicit-any 16 -> 15) and define
UTF8_BOM as an explicit `` escape instead of a raw BOM character.
- export routes (articles/customers/suppliers): move the data queries inside
the try/catch, add `Cache-Control: no-store`, and emit a `register exported`
audit log line (entity, format, rowCount).
- articles parse route: validate `column_overrides` against a Zod schema before
trusting it to drive the parser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(import): drop öre-round pattern on article column-detector confidence
The confidence score is a 0-1 heuristic, not money, and is only compared
against the 0.8 skip-mapping threshold. Removing the Math.round(x*100)/100
form clears the core-only antipattern ratchet (naive-ore-round 660 -> 659).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(import): flag adjusted VAT rows in the article import edit step
Surface VAT snapping/defaulting per row, not just as a file-level warning:
the parser sets `vat_rate_adjusted`, the edit step highlights those rows'
VAT selector and shows a count banner, and confirming a rate clears the flag.
Addresses the Swedish-compliance review note that silent snapping could
otherwise store a wrong VAT rate at scale.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7aa37fd3b8 |
fix(mcp): return 405 (not 401) on GET /mcp to stop client re-auth storm (#747)
The Streamable HTTP GET handler returned 401 unconditionally. This server is stateless and offers no server-initiated SSE stream, for which the MCP Streamable HTTP spec requires 405 Method Not Allowed. Returning 401 made spec-compliant clients (Claude connector, Claude Desktop, Cursor) treat the SSE GET as an auth failure and enter a refresh-token → re-open-GET → 401 retry loop. Across the active connector base this storms /api/extensions/ext/mcp-server/mcp (observed ~steady GET→401 traffic on app.gnubok.se) and churns OAuth API-key rotation — and tripped a Vercel usage anomaly (edge requests + function invocations spiking ~16x). OAuth discovery remains bootstrapped on the POST 401 (WWW-Authenticate + .well-known/oauth-protected-resource); the POST JSON-RPC channel and the POST-only npm bridge are unaffected. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ca3ae65b12 |
fix(deps): bump ws to 8.21.0 to clear fixable HIGH CVE failing docker-publish (#745)
ws@8.19.0 (transitive via @supabase/supabase-js -> @supabase/realtime-js) carries GHSA-96hv-2xvq-fx4p (memory-exhaustion DoS, CVSS 7.5), fixed in 8.21.0. The docker-publish "Scan image with Trivy" step runs severity=CRITICAL,HIGH with ignore-unfixed=true, so this fixable HIGH has been failing the image scan on every merge to main. Force ws>=8.21.0 via an npm override. The only remaining HIGH (xlsx) has no upstream fix and is skipped by ignore-unfixed, so the Trivy gate should pass. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2a8bf9b42e |
Bug/year end numbers (#744)
* fix(bookkeeping): allow creating a fiscal year that fills an interior gap Fiscal-period creation only allowed chaining a new räkenskapsår before the earliest or after the latest existing period, so a company with a gap between years (e.g. 2024 + 2026 from an SIE import, missing 2025) could not create the missing year — it failed with "New period must chain before the earliest or after the latest existing period". Generalise forward chaining onto the new period's immediate predecessor, which covers both appending a new latest year and filling an interior gap. The "prior year must be locked" guard now applies only to true appends, not gap fills (a backfill, like backward chaining). previous_period_id is set to the predecessor and the successor is relinked so the BFNAR 2013:2 continuity chain stays intact. The create dialog suggests the missing year (capped so it never overlaps the next period), the settings page seeds the dialog at the earliest gap, and the default suggested name is now "Räkenskapsår <year>". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): omföra föregående års resultat (2099 → 2098) at year-end Year-end closing posts the result to 2099 "Årets resultat" and the opening balance carried it forward on 2099 every year, so 2099 accumulated across years and the prior result never moved off "Årets resultat". executeYearEndClosing now posts a separate "Omföring av föregående års resultat" verifikat (Dr 2099 / Cr 2098 for a profit, reversed for a loss) into the new period after the continuity check passes, so 2099 starts each year at zero. Kept as a standalone entry rather than folded into the opening balance so the IB stays a faithful mirror of the prior UB and IB/UB continuity still holds. Aktiebolag only; idempotent; no-op when 2099 is flat. The 2098 → 2091/2898 disposition (bolagsstämma decision) is intentionally left to a separate step. - new source_type 'result_appropriation' (migration + type + Zod enum) - generateResultAppropriation helper (planner + poster) wired as step 11 - ResultStep surfaces the omföring voucher - unit tests + pg-real invariant - scripts/repair-result-appropriation.ts: retroactive catch-up (dry-run default) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): shadow-detect date-drift duplicate bank transactions The content-dedup bridge buckets on exact (date, ore), so the same transaction re-imported with a booking date that drifted a day lands in a different bucket and slips past every dedup layer. Add a measure-only ("shadow") detector that flags would-be +/-1-day duplicates and counts them, without changing what is inserted - so the gap can be validated on real data before any enforcement, mirroring the scope-drift shadow. - shiftIsoDate(): pure, deterministic adjacent-date helper - ingest: DEDUP_DATE_DRIFT_MODE flag (default on), pre-loop bucket snapshot, per-row gate with desc-bridge + cross-channel-symmetry signals; logs shadow_date_drift_candidates, never alters inserts - fail-safe date guard so the measurement can never abort an import - regression tests for both signals, account/window/distinct guards, no-double-count, and the malformed-date fail-safe Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(bookkeeping): anonymize a customer reference in fiscal-period tests Remove a real customer name ("AXMD AB") from regression-test comments; no logic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(workflows): enhance Docker image scanning and caching mechanisms * fix(bookkeeping): enhance year-end result appropriation handling and error reporting --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5c40fa9aeb |
fix(docker): run runtime as nextjs so self-host boots under cap_drop: ALL (#737)
Self-hosted Docker image now runs fully unprivileged (USER nextjs): the entrypoint populates the .next/public tmpfs mounts and substitutes NEXT_PUBLIC_* placeholders as nextjs, so the container boots under the hardened compose (cap_drop: ALL + read_only: true) with no capabilities. Also: substitute *.json (fixes CSP connect-src in routes-manifest.json), escape sed metacharacters in brand name, healthcheck via 127.0.0.1, and a fully-self-hosted Supabase docs section. |
||
|
|
45b31ad50f |
feat(bookkeeping): page-size selector + First/Last pagination on verifikationslista (#738) (#743)
* feat(bookkeeping): page-size selector + First/Last pagination on verifikationslista Closes #738. The voucher list (verifikationslista) had a hardcoded page size of 20 and only Previous/Next buttons. Adds: - Page-size selector: 20 / 50 / 100 / Alla. Persisted per company in localStorage (same convention as the sort order and FiscalYearSelector) and hydrated in an effect so the first fetch already uses the saved size. "Alla" loads everything in the current scope and hides the pager. - Pagination footer: First / Previous / Next / Last icon buttons, a page indicator, and a "Visar 1–20 av N" result-range label. - Server: clamp limit to [1, 100000] and offset to >=0 so "Alla" sends a bounded large limit (defense in depth, ASVS V1.2.5). Sorting asc/desc on date and voucher already existed in the filter dialog; amount-column sorting is intentionally out of scope (journal_entries has no stored total — the voucher amount is summed client-side from debit lines — so ordering by it needs a schema change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(bookkeeping): keep page-size selector reachable + clarify offset clamp Addresses PR review feedback: - Pagination footer now shows whenever a non-default page size (50/100/Alla) is active, not only when count > 20. A user who picks a larger size and then filters the list below 20 rows can still switch the size back. Default-20 users are unchanged — no selector under 21 rows. Empty results stay hidden. - offset clamp reads `rawOffset >= 0` instead of `> 0` (behaviour identical; clearer intent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
36e3f6ceb0 |
Design critique: normalize daily-operator flows + UX improvements (#741)
* style(design): normalize daily-operator flows to locked design system Sweep the dashboard, transactions, reconciliation, invoicing and supplier flows for design-system violations (.claude/rules/design.md): - font-medium removed from Hedvig display headings/numerals - font-mono -> tabular-nums on monetary values (voucher ids stay mono) - raw Tailwind status colors -> Badge variants / muted-alert pattern - semantic colors removed from chrome backgrounds (deadline widgets, icon halos) - hand-rolled skeletons/empty-states -> Skeleton / EmptyState primitives - opacity-suffixed borders, the invisible warning-foreground count color, and shadow-sm/rounded-xl on non-overlay surfaces normalized The four files that also received UX changes carry their token fixes in the following commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ux): clearer dashboard CTA, match confidence, AI provenance, invoice actions Four high-impact UX fixes from the design critique (these files also carry their design-system token normalization): - Dashboard: render the next-best-action hero for every agent-built company, not only 'slim' nav density, so there is always one obvious next step instead of four equal-weight metric tiles. - Reconciliation: surface the match engine's 0-1 confidence as a graded strength badge (Stark / Trolig / Svag traff) in the shared verifikat picker rows and selected chip; drop the uninformative binary "Foreslagen traff" badge from the match dialog. - Supplier inbox: show AI-filled provenance per extracted field (a success dot that clears once the user verifies/edits the value) so misparsed amounts/dates get proofread before they post to an immutable verifikat. - Invoice detail: keep each status's primary action only in the header row; the sidebar "Status actions" card now holds secondary/reversible actions only (makulera, ta bort, skapa kreditnota, manual-send alternative), removing the duplicated CTAs and closing a viewer-permission gap on the old sidebar buttons. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ux): Tier-B medium design-critique fixes across daily-operator flows - Reconciliation: standardise the match-confirm verb on "Matcha" (was "Koppla" in MatchVoucherDialog) and replace the hand-rolled date <input>s in the bank reconciliation view with the Input primitive. - Supplier inbox: fold the two alternative bookings (Skapa leverantorsfaktura / Bokfor som verifikat) behind a single "Andra satt att bokfora" dropdown so the default path (Matcha mot transaktion) stays the lone primary action. - Duplicate-payment guard: demote the "Skapa ny verifikation anda" escape hatch to a ghost button so the safe "Koppla till befintlig" path dominates. - Onboarding: raise the "start fresh" escape hatch from a muted text link to a visible secondary button; normalise the checklist's off-scale spacing. - Supplier flows: finish the font-mono -> tabular-nums sweep on monetary values in the supplier-invoice detail / create / review surfaces (ids stay mono). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deadlines): keep overdue rows visually distinct (destructive chrome is allowed) The Tier-A normalization stripped all semantic-color row tints from the deadline widgets, but design.md exempts --destructive ("only --destructive survives in chrome"). Restore a subtle bg-destructive/5 on OVERDUE rows so missed tax/AGI deadlines (-> skattetillagg) stay noticeable in a list scan; action-needed (warning) rows stay clean since warning is data-only. Surfaced by the Swedish compliance review on #741. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): hide match-strength badge on already-matched verifikat Per the Swedish compliance review on #741: a green "Stark traff" confidence badge rendered alongside "Redan matchad" could visually nudge an accidental double-match of a posted verifikat (a BFL 5 kap audit-trail concern). Suppress the strength badge when linked_transaction_count > 0 so "Redan matchad" is the lone signal there; N:1 matching stays an explicit opt-in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
274fe0743f | fix(migrations): restore Swedish diacritics in chart of accounts names (#740) | ||
|
|
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> |
||
|
|
d95a0b6105 |
fix(bookkeeping): comprehensive chart_of_accounts charset repair (#736)
* fix(bookkeeping): comprehensive chart_of_accounts charset repair The 20260625120000 backfill (PR #734) only covered the 26 short-name seed accounts. Investigation found the corruption was far broader — ~4,500 rows across 858 companies — in four signatures, and verified the root cause is already closed (prod's seed_chart_of_accounts() carries correct diacritics; the corruption was prod-migration-drift, the seed fix reached prod ~2026-06-12, no companies corrupted since). Adds a tested, reusable repair core + a guarded script: - lib/bookkeeping/charset-repair.ts — pure, unit-tested resolvers: * stripped diacritics ("Utgaende moms forsaljning...") → restore from a de-accent-equal clean sibling. DIRECTIONAL guard (only acts on a fully de-accented input) so a correct name is never stripped down; unique-match only, so user-renamed accounts are never clobbered. * double-encoded UTF-8-as-CP1252 ("Företagskonto") → lossless CP1252-aware byte reversal (recovers custom names too). * CP437-as-CP1252 ("F”rmedlad", "™vriga", "V„rdef”r„ndring") → lossless CP437 letter reversal. * lost-byte U+FFFD ("p� bilar") → fill via single-char-wildcard match to a unique clean sibling (the byte is gone, so only a confident sibling wins). isClean() rejects mojibake AND mid-word CP1252 artifacts, but treats a space-padded en-dash ("Kundfordringar – delad faktura") as legitimate. - scripts/repair-chart-of-accounts-charset.ts — dry-run by default, --execute to apply; idempotent; refuses any non-prod project. Sources canonical names from the table's own clean sibling rows + BAS_REFERENCE. Applied to production (UPDATE-only, account_name is display-only): 4,499 rows across 858 companies repaired, 0 double-encoded remaining, 0 errors. 247 rows left untouched and reported — custom account names with lost bytes and no canonical (unrecoverable from the data; need the source SIE file or manual fix). 21 unit tests cover every transform with real prod fixtures, plus the two dry-run bugs caught before any write (correct→stripped direction; matching a CP437-mojibake sibling). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scripts): avoid supabase-js generic mismatch in charset repair fetch next build's tsc rejected fetchAll(supabase: ReturnType<typeof createClient>) — the default-generic SupabaseClient type doesn't unify with the inferred createClient() return. Make fetchAll a closure over the inferred client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scripts): add TOCTOU guard to charset repair updates Per PR review: only write when the row still holds the exact corrupted value read (.eq account_name), so a concurrent rename is skipped, not clobbered, and the script is strictly idempotent. Track skipped count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(charset-repair): build combining-marks regex from ASCII string Per PR review: the deaccent regex literal embedded raw U+0300–U+036F combining marks (invisible, encoding-fragile). Build it via RegExp('[\\u0300-\\u036f]') so the source is plain ASCII. Behavior-identical; 21 tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e5f1d7a916 |
fix(reports): count reversed entries in bank/supplier/AR reconciliation (#734)
A reversed (storno-corrected) journal entry must be summed together with
its storno + correction, exactly as the trial balance and balance sheet
already do (.in('status', ['posted','reversed'])). The reconciliation
paths used posted-only and manufactured phantom differences.
- bank-reconciliation getReconciliationStatus: gl_1930_balance now counts
posted+reversed (so it equals the balansräkning for the account). Dropped
the correction_adjustment subtraction and the reversed-linked-tx drop — a
corrected/amount-corrected bank receipt now reconciles. The prior model
broke once correctEntry began re-pointing the bank transaction to the live
correction (the two changes were mutually inconsistent and produced a
difference equal to the corrected amount).
- supplier-reconciliation (2440) and ar-reconciliation (1510/1513): same
posted -> posted+reversed fix. Removes the false "Ej avstämd" gap a fully
paid, fully corrected company shows — the books net to 0 over posted+reversed
while a posted-only query double-counted the payment legs.
Also in this change:
- MCP gnubok_get_reconciliation_status: add account_number param (was hardcoded
to 1930; the lib already supported per-account reconciliation).
- counterparty-templates normalizeCounterpartyName: strip trailing month /
personal-initials tokens so "ngrok JW" / "Ngrok Mars" learn as one merchant.
- pending-operation reject 409 (route + MCP tool): clarify a resolved op was
approved explicitly (no auto-commit path exists) instead of a bare
"already committed".
- create_voucher: normalize description with String().trim() for consistency
with line_description.
- migration 20260625120000: backfill stripped diacritics on ~830 companies'
seeded chart-of-accounts names (Foretagskonto -> Företagskonto, etc.); the
seed function was fixed for new companies in 20260516130000 but never
backfilled. UPDATE-only and idempotent. Already applied to production.
Tests: 899 passing across the touched suites; lint + typecheck clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
fa4daf6f98 |
fix(docker): upgrade apk packages to address Alpine CVEs (#728)
fix(deps): update Next.js to version 16.2.9 in package.json and package-lock.json |
||
|
|
88f49c0ccc |
fix(bookkeeping): harden correction flow and align VAT/cashflow reports (#726)
Bundles a set of bookkeeping-correctness fixes developed together. Correction / storno flow - correctEntry resolves (and seeds standard BAS) accounts for the corrected lines BEFORE writing the storno. The old order created and posted the storno first, then hit AccountsNotInChartError on the corrected lines and had to cancel it again — leaving a voided 0 kr storno in the chain and permanently burning a voucher number (an unexplained BFNAR 2013:2 gap). It now fails fast with nothing written. - correctEntry re-points the bank transaction and underlag from the reversed original to the live corrected entry, so the transaction keeps reading as booked (and stays correctable) and the underlag travels with it. recordateEntry delegates both relinks to correctEntry. - reverseEntry (engine) clears transactions.journal_entry_id for rows booked by the reversed entry, so a plain storno returns the bank row to "Att bokföra" with a re-booking affordance. The agent paths did this manually; the dashboard reverse route did not. - findUnresolvableAccounts replaces findMissingActiveAccounts in the categorize routes: a standard BAS account merely absent from the chart is seeded on demand by the engine, so pre-validation must not 400 on it — only unknown numbers or deactivated accounts block. - CorrectionChain dims cancelled (0 kr) entries and labels them so they no longer render like a live storno. Report accuracy - calculateVatLiability() (lib/reports/kpi.ts) is shared by the KPI route, the KPI xlsx export and the MCP period-summary tool, and uses the same 26xx accounts as the momsdeklaration (ruta 49). Reverse-charge and import pairs (e.g. 2614 credit + 2645 debit) net to zero instead of inflating the receivable (#715). VAT_OUTPUT_ACCOUNTS / VAT_INPUT_ACCOUNTS are derived from ACCOUNT_RUTA so the widget can never drift from the declaration. - Kassaflödesanalys records erhållna aktieägartillskott (2093) as a financing inflow and counts överkursfond (2086/2097) toward nyemission. 2093 was previously unmapped, so any contribution broke the 19xx reconciliation by exactly the contributed amount (#716). Wired through the report type, both PDF templates, the K3 PDF, the dashboard client and the årsredovisning summary type. Agent guidance - shared-rules: describe the real Accounted correction flow (Rätta rader / Rätta datum / Radera verifikat, on-demand BAS backfill) so the assistant stops inventing flows that don't exist. - verifikation-draft: clearer locked-period guidance. Tests cover all of the above (storno fail-fast + seeding + relink, reverseEntry unlink, findUnresolvableAccounts, VAT netting and the cashflow reconciliation cases). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7af4b96d10 |
Bug/docker build (#725)
* fix: update Trivy action version to v0.30.0 for improved security scanning * fix: update Trivy action version to v0.36.0 for enhanced security scanning |
||
|
|
43925bc2d3 |
fix(import): SIE bulk-delete on service client + provider/reporting/b… (#724)
* fix(import): SIE bulk-delete on service client + provider/reporting/banking fixes Rebuilt branch onto main as a single commit. - import: run SIE bulk-delete RPCs on the service client to escape the 8s statement_timeout; undo_sie_import now takes an explicit actor (p_user_id) so its owner/admin gate works when auth.uid() is NULL on the service client (migration 20260624120000) + pg-real regression test - providers: distinguish missing Fortnox license from expired connection; provider_consent_tokens PK regression test - reports: include unmapped BAS expense groups in the income statement - enable-banking: reconnect closed/expired bank sessions in place - bookkeeping: surface linked invoices as underlag on the verifikat view - scripts: track BL cleanup/diagnostic tooling; data files (*.csv) are git-ignored and consentId is now a required arg with no silent default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): add Cache-Control header to journal entry references response --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
8e8b63a200 |
fix(bookkeeping): honor underlag VAT via vat_amount override in categorize flow (#717)
* fix(bookkeeping): honor underlag VAT via vat_amount override in categorize flow
The categorize flow always derived VAT as rate × gross/(1+rate) from the
transaction amount, with no way to use the underlag's actual moms. On e.g.
a restaurant receipt with dricks (no VAT on the tip), the agent could see
the document's correct VAT but the staged booking recomputed the wrong
rate-based amount on every attempt.
- buildMappingResultFromCategory: optional vatAmountOverride replaces the
rate-derived VAT line ("Ingående/Utgående moms (enligt underlag)"; 0 =
no VAT line). Rejects negatives, amounts above the 25%-extraction bound,
and combination with reverse_charge / VAT-less treatments / private.
- gnubok_categorize_transaction: new vat_amount input, threaded into the
staged preview and persisted in the operation params.
- commitCategorizeTransaction: reads params.vat_amount so the approved
posting matches the staged preview exactly.
- PATCH /api/pending-operations/[id]: accepts vat_amount (null clears);
preserves a staged override across category edits while the treatment
still carries rate-based VAT, drops it when it no longer does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: guard order + agent guidance on vat_amount (PR #717 bots)
- Check treatment compatibility before the 25%-extraction bound so an
oversized override on reverse_charge reports the actual mistake (the
treatment), not the amount. Document why the typeof re-check stays:
commit-time params come from jsonb, so TS types don't hold at runtime.
- vat_amount property description now warns that foreign VAT is never
deductible as ingående moms and that a 0-moms document should use
vat_treatment="exempt" rather than vat_amount=0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): tools/list payload budget + reject vat_amount 0
core-only failed: the verbose vat_amount descriptions pushed the projected
tools/list payload to 36,051 tokens (ceiling 36,000; main is at 35,862).
Per the guard's own guidance, trim descriptions instead of bumping:
now 35,943.
Folds in the Swedish review's round-2 point while trimming: vat_amount 0
is now rejected with a pointer to vat_treatment "exempt". A 0-moms
document is an exempt supply — "exempt" produces the identical expense
booking and the correct income account (3004), so 0 had no use case and
only created a silent momsdeklaration misclassification path. Schema
declares exclusiveMinimum: 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(bookkeeping): use roundOre for vat_amount math (antipattern ratchet)
Second core-only failure: the naive-ore-round ratchet caught the new
Math.round(x*100)/100 lines (662 > baseline 661). Switch the override
path to roundOre from lib/money — including the pre-existing computed-VAT
line this PR touched — and ratchet the baseline down (659, raw-route-auth
168 locked in from main-side fixes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f548b04aed |
fix: update Claude.ai MCP connector instructions to current UI (#709)
Updates the en/sv claude_ai_instructions strings from 'Settings → Integrations → Add Integration' to 'Settings → Connectors → Add custom connector' to match Claude's current UI. i18n-only; no code/migration changes. |
||
|
|
5078b4e02d |
fix(mcp): grace window + idempotent refresh-token replay for OAuth (#710) (#714)
* fix(mcp): grace window + idempotent refresh-token replay for OAuth (#710) OAuth refresh rotated BOTH the refresh token and the access key in one zero-grace CAS. Claude Code's MCP OAuth client fails to persist the rotated refresh token (or fires concurrent refreshes), re-presents the stale one, the CAS matches 0 rows, and the grant dies with invalid_grant — forcing a full re-authorization roughly every 60s in a loop. Regression from #392. Keep rotation (RFC 9700 §4.14.2 requires it for public clients) but add a bounded grace window with idempotent replay, atomic in one SECURITY DEFINER RPC: - Migration adds previous_key_hash / previous_refresh_token_hash (+ *_expires_at) shadow columns. validate_and_increment_api_key accepts the current OR an unexpired previous key_hash, with the rate-limit increment keyed off the resolved row id. - New rotate_mcp_refresh_token RPC: rotated | replayed | reuse_revoked | revoked | invalid. In-grace replay re-issues a fresh pair and slides the window so an actively-refreshing client that cannot persist the rotated token keeps working; reuse after the window revokes the grant family (RFC 9700 4.14.2 reuse detection preserved). - The refresh grant now calls the one RPC, closing the old SELECT-then-CAS TOCTOU gap. All previous_* columns default NULL, so existing keys are unaffected and the RPC return shape is unchanged (callers untouched). Tests: rewired the token-route unit tests to the RPC and replaced the test that codified the bug with a #710 regression (in-grace replay returns 200, not 400); added tests/pg/mcp-oauth-rotation-grace.pg.test.ts (grace accept/expire, revoke-never-graced, rotate->demote, idempotent replay, reuse-after-grace->revoke). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: retrigger checks for #714 No code change — re-running CI. The Supabase Preview check fails on a pre-existing main-branch migration-history drift ("Remote migration versions not found in local migrations directory"), not this PR; pg-real (full migration replay) passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7f60b23f5 |
fix(invoices): v1 mark-paid booking-state routing + journal_entry_id backfill (#713)
invoices.journal_entry_id means "the registration verifikat that booked this invoice at issuance" — payment flows route on it (set → clear 1510, NULL → kontantmetoden cash entry). Two bugs in v1 mark-paid broke that: - The pre-flight select omitted journal_entry_id, so invoiceAlreadyBooked always read false — a kontantmetoden company paying an already-registered invoice would re-recognise revenue + VAT (double-booking) and orphan the 1510 receivable. Fixed by fetching the column for routing only; the response contract and invoice.paid event payload are unchanged. - The update wrote the just-created PAYMENT/cash entry id into the column (wrong semantic) — once routing reads the column, a cash partial payment #1 would make payment #2 clear a 1510 that was never debited. Removed; the payment entry id still returns in the response body. New backfill migration links the earliest posted invoice_created entry to historical invoices (353 registered-but-unlinked rows in hosted prod), repairs any payment-type links, and links credit_note reversal entries to credit-note rows. Idempotent; rows with no registration entry stay NULL (correct for kontantmetoden/unsent invoices). Tests: 3 new unit tests lock the select projection, the already-booked→ clearing routing, and the no-write-back semantics (the supabase mock now records call args). New pg-real suite (11 tests) runs the actual migration SQL: earliest-wins, reversed/draft exclusion, no-overwrite, cash stays NULL, payment-link repair, credit notes, cross-company isolation, idempotency. insertDraftJournalEntry fixture gains optional sourceType/ sourceId/createdAt (defaults unchanged). Hosted prod requires manual migration apply after merge (Supabase MCP). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0521c385d2 |
feat(transactions): underlag status badges + attach dialog; auto-expire stale pending ops (#712)
* feat(transactions): per-row underlag status + attach-document dialog
- New "Matcha mot underlag" dialog on /transactions (inbox pick or fresh
upload), the tx→doc mirror of the Documents view's matcher
- Per-row Underlag/Underlag saknas badges on booked history rows, driven
by computeJeUnderlagStatus — same posted-only, exemption-aware scope as
the worklist count so badge and count never disagree
- attach-document route + commit dispatcher now propagate the doc onto
the verifikation when the tx is already booked (BFL 5 kap 6 §), with a
409 guard for docs consumed by a different verifikation, idempotent
re-attach (no same-value rewrite under period lock), and an honest 409
when the period-lock trigger blocks the propagation
- Booking-dialog doc links also pin the doc to the transaction row
(first linked doc wins) via the link route's new transaction_id param
messages/{sv,en}.json also carries the strings for the pending-ops
expiry UI that lands in the next commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pending-operations): auto-expire stale staged operations after 30 days
- New daily cron (02:30 UTC, vercel.json + both docker crontabs) flips
>30-day-old pending ops to rejected with the dispatcher's
{ auto_rejected: true, reason: 'expired' } result_data shape — rows are
never deleted, the table is the audit trail
- /pending renders an "Utgick automatiskt" badge + detail line for these,
orders terminal tabs by resolved_at so a fresh expiry sweep isn't
buried, and adds a first-time-reviewer explainer
- Origin labels spell out where a proposal came from (AI chat, MCP key,
API, cron) instead of the raw actor_label
- agent_chat actor type added to PendingOperationActorType/AuditLogEntry
(DB CHECK already widened in 20260519090000) and to the agent filter
- ApprovalCard notes that ignoring a proposal is safe
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(mcp): surface the client telemetry marker in connect instructions
Tag the connector URLs shown in ApiKeysPanel, the connect-claude doc and
the gnubok-mcp README with ?client=<surface> (claude-connector /
claude-code) and GNUBOK_CLIENT=claude-desktop for the npm bridge.
Telemetry-only — the server already reads the param/header; this just
lets us measure which Claude surface connected.
The claude mcp add copy blocks quote the URL: an unquoted ? in the query
string trips zsh globbing ("no matches found").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: fix stale-closure badge flip + zod-validate link route body (PR #712)
- handleDocumentAttached read journal_entry_id off the render-time
transactions snapshot; if the list changed while the attach dialog was
open the optimistic badge flip was silently skipped. Read it off the
dialog's own subject (attachDocTx) instead.
- POST /api/documents/[id]/link now validates the body against the new
LinkDocumentSchema (uuid-strict, all four fields) instead of a bare
presence check on journal_entry_id — same canonical VALIDATION_ERROR
envelope. Test fixtures switched to real UUIDs accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e978136210 |
fix(supplier-invoices): payment-match integrity — no more paid-without-voucher half-states (#711)
* fix(transactions): abort supplier-invoice match when payment voucher fails The match route caught a payment-JE creation failure and proceeded anyway: invoice marked paid with payment_journal_entry_id NULL, a payments row with no voucher, and the bank line linked but unbooked. That half-state is unrecoverable from the UI — mark-paid rejects 'paid' invoices and the match route rejects already-linked transactions (the "user can re-book" comment was wrong). The v1 route was already strict; this aligns the cookie route. A failed voucher now fails the whole match before any state mutation, with bookkeeping errors mapped to their structured codes and a new MATCH_SI_JE_FAILED fallback. Incident: Arcim 2026-06-11 — invoice 20250928 marked paid with no payment voucher because account 3740 was missing from the chart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): bank-sync supplier-invoice match is a suggestion, not a hard link A high-confidence (>=0.85, unambiguous) supplier-invoice hit at sync time set transactions.supplier_invoice_id directly — without booking a payment or touching the invoice. The half-link then BLOCKED the match route (MATCH_SI_TX_ALREADY_LINKED), stranding the bank line with no path to a payment voucher and the invoice stuck on 'registered'. Sync now always writes potential_supplier_invoice_id; the hard link is reserved for completed matches where the payment voucher is booked. High-confidence hits still drain the matching pool and skip the mapping engine. Incident: Arcim 2026-06-11 — RosholmDell 18299 (29 890 kr) auto-linked at sync, unmatchable afterwards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): seed standard BAS accounts on demand in the engine A minimal company chart routinely lacks accounts that legitimate engine flows reach — 3740 (öres- och kronutjämning) the first time a Bankgiro payment lands a sub-krona off the invoice, 6580 on a first legal invoice. createDraftEntry threw AccountsNotInChartError and turned a standard account into a dead end. The engine now backfills missing accounts from BAS_REFERENCE (full metadata incl. SRU code) before failing. Conservative by design: unknown numbers still throw, and deactivated accounts are never resurrected — deactivation is a deliberate user choice. Concurrent seeding (23505) counts as success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): require explicit expense account, drop the 5010 seed Every new line item (and every AI-prefilled line) was silently seeded with account 5010 Lokalhyra. AI extraction deliberately never suggests accounts, so any invoice saved without touching the field was misbooked as premises rent — legally wrong verifikat that need rättelse to fix. Lines now start with an empty account: the supplier's default_expense_account fills empty rows when set, and submit blocks with a clear toast until every row has an account. Incident: Arcim 2026-06-11 — a legal-services invoice (should be 6580) and a SaaS subscription (should be 5420) both posted to 5010. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(bookkeeping): clarify voucher description suffix to (ankomstnr N) "(ankomst 2)" read as "arrived twice" / a duplicate marker; it is the company-internal sequential arrival counter for supplier invoices. "(ankomstnr 2)" says what the number is. Existing posted vouchers keep their old description (immutable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): cancel orphaned payment voucher when match loses the CAS race When the payment JE posts but the invoice CAS update matches 0 rows (a concurrent request settled it first), both match routes returned MATCH_SI_NOT_OPEN and left the voucher orphaned in the ledger. mark-paid has always compensated for exactly this case; the compensation is now a shared helper (cancelOrphanedPaymentEntry: cancel + voucher-gap explanation per BFNAR 2013:2) used by all three routes. Flagged by the compliance swarm and the Swedish compliance review on PR #711 — the one finding both converged on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): next_voucher_number user_id fallback for service-role contexts Mirrors 20260421170500 (commit_journal_entry got this fix; its twin did not). Under a service-role client auth.uid() is NULL and the voucher_sequences upsert fails its user_id NOT NULL check before ON CONFLICT can arbitrate — even when the sequence row exists. Every non-interactive caller of the storno/correction path (getNextVoucherNumber → correctEntry) was broken. Fallback: companies.created_by (same source seed_chart_of_accounts uses). Interactive flows still record auth.uid(); DO UPDATE never touches user_id on existing rows. Also restores SET search_path = public, lost when 20260330 recreated the function after the 20260304 hardening. pg-real: new test exercises the RPC on the superuser connection (auth.uid() IS NULL) and asserts sequential numbers + owner attribution. Found live: the Arcim repair script booked payment vouchers fine (commit_journal_entry) but failed on corrections (next_voucher_number). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): harden cancelOrphanedPaymentEntry — never throw, breadcrumb before mutating Two hardenings from the PR #711 review round: - Whole body wrapped in try/catch: the caller is returning the correct CAS-conflict response, so an unexpected client rejection must not replace it with a 500 (best-effort is now a hard guarantee). - The gap-recovery data (series, number, period, explanation) is logged BEFORE the cancel: the cancel and gap insert are separate statements, and a crash between them would otherwise leave a cancelled voucher with no BFNAR 2013:2 gap explanation and no way to reconstruct it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4253afc343 |
fix(company): validate user_preferences write when switching company (#708)
Fixes #701. setActiveCompany upserted active_company_id without checking the result, then set the gnubok-company-id cookie unconditionally. A failed write — including an RLS-filtered UPDATE, which affects zero rows without raising an error — looked like a successful switch: switchCompany returned {}, the UI hard-reloaded, and middleware (which reads user_preferences, not the cookie) resolved the old company. - setActiveCompany now verifies the upsert with .select().single() and throws a typed CompanyContextError ('not_member' | 'persist_failed'); the cookie is only set after the write is confirmed, so it can no longer diverge from the database. - switchCompany logs the failure and returns distinct error codes instead of reporting every failure as a permissions problem. - CompanySwitcher now shows a destructive toast on failure (it previously failed with no feedback); BankIdCompanyPicker translates the codes. Messages added to sv/en under company_switcher and select_company. - The remaining fire-and-forget user_preferences writers (middleware fallback write-back, team invite accept, auth callback invite accept) now check and log errors; non-fatal by design since each has a working fallback path. - New tests cover every failure mode, including cookie-not-set on a failed write and the silent zero-row write caught by the read-back. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
205610d200 |
feat(mcp): distribution-channel client marker in MCP telemetry (#706)
* feat(mcp): distribution-channel client marker in MCP telemetry
Record an optional client marker on mcp.tool_called, mcp.tools_list_called
and mcp.resource_read events so per-channel adoption (e.g. the OpenClaw
skill) is measurable in event_log (180-day TTL).
- Server reads X-Gnubok-Client header, falling back to a ?client= query
param on the endpoint URL. Sanitized ([A-Za-z0-9._-]{1,64}, lowercased),
telemetry-only — same trust level as Mcp-Session-Id, never auth.
- The query param works with the already-published gnubok-mcp 1.0.1 via
GNUBOK_URL, so no npm release is required to start measuring.
- Bridge 1.1.0 additionally forwards GNUBOK_CLIENT as X-Gnubok-Client.
OAuth-path attribution via DCR client_name is a possible follow-up — DCR
is stateless today, so client_name isn't recoverable at token time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): address PR #706 compliance findings
- ropa.yaml: declare the distribution-channel marker in the mcp.telemetry
processing activity (GDPR Art. 30 — RoPA was drifting from actual flow)
- bridge: mirror the server's allow-list on GNUBOK_CLIENT so an invalid
value degrades to no header instead of fetch() rejecting every request
- lib/events/types.ts: annotate client as client-supplied/telemetry-only
- test: pin that the allow-list runs on the percent-decoded ?client= value
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
21cfcbe180 |
feat(bookkeeping): edit & customize booking templates in GUI (#704)
* feat(bookkeeping): edit & customize booking templates in GUI Make bokföringsmallar editable from /settings/templates and surface the ratio (Andel) field, addressing two user requests. - Refactor CreateTemplateForm into a shared TemplateForm (create / edit / duplicate) reusing the existing POST and PUT routes. - Add an Edit (pencil) action on company/team templates, and an Anpassa (customize) action on read-only "Standard" templates that forks a company-scoped copy — letting a company override the standard 1930 settlement account with e.g. 1920 without mutating the shared template. - Show the ratio field progressively (only when a template splits across more than one cost line), with an InfoTooltip, a non-blocking "shares must sum to 1.0" warning, and a live "1 000 kr" split preview. - Add settings_booking_templates i18n keys in both sv and en. Frontend-only: no API, schema, type, or migration changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): show ratio input on cost lines only Addresses PR review: the Andel input also appeared on settlement lines when a template had multiple cost lines, but settlement ratios don't feed the "shares sum to 1.0" check or balance validation. Restrict the editable ratio to cost/revenue lines; the settlement leg (full counter-amount) is shown in the live preview instead. No behavior change to applyTemplate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |