64ea0fef0200e38fdbd142ec2a646d2db5cec6f4
125 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
98d0c7f2d0 |
Add/stripe skv (#1004)
* fix(salary): align pain.001 salary file with the Swedish domestic bank dialect Verified against the Swedish Common Interpretation of ISO 20022 (Bankforeningen, Common Payment Types in Sweden, Appendix 1 Example 4: Salaries) and Nordea Corporate Access pain.001 examples v2.6 (2026-06-22), and XSD-validated against the official pain.001.001.03 schema: - drop SvcLvl SEPA (SEPA credit transfers are EUR-only; omitting SvcLvl gets the domestic NURG default) - drop RmtInf (not allowed for SALA salary payments; the beneficiary statement text comes from the Dataclearing LON code) - address employees domestically: clearing as CdtrAgt ClrSysMmbId SESBA, account WITHOUT clearing as CdtrAcct Othr with SchmeNm BBAN - share the clearing/account split (Swedbank 5-digit shift, Nordea personkonto prefix dedup) between the LB and pain.001 generators via splitDomesticBankAccount, fixing pain.001 duplicating the personkonto clearing - clamp MsgId/PmtInfId/InstrId/EndToEndId to Max35Text with the per-tx counter surviving truncation; carry the org number on Dbtr - return 400 from the pain001 route on an invalid clearing instead of emitting a broken file Also includes two unrelated decision-log lines from the parallel revisor-review session (DECISIONS.md is a shared append-only log). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(nav): surface the year-end chain in the sidebar Add Periodiseringar, Arsredovisning (aktiebolag only) and Inkomstdeklaration (INK2 for AB, NE-bilaga for EF) to the Skatt & bokslut group, in workflow order. Entity gating via a new entityOnly flag on NavItem; isActive carve-outs extended so exactly one row lights up for the new routes. Driven by an external revisor review that concluded these features did not exist because none of them were reachable from the nav. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(stripe): Stripe Connect integration behind config gate Connect OAuth per company (only the acct_ id is stored), automatic single-use Payment Links on invoice send, deterministic payment settlement against 1686 (BAS moved acquirer receivables 1580 -> 1686), payout booking with reverse-charge fees (6570 + 4535/4598 + 2645/2614), and a 15-minute sync cron. Non-deterministic events land as needs_review, never guessed at. Fully dark without STRIPE_CONNECT_CLIENT_ID: connect returns 503, the send hook and cron no-op, and the settings page shows 'Kommer snart' (hosted) until the Connect platform is verified. Self-hosted keeps the honest not-configured message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deadlines): add shared completeTaxDeadline and fix dead AGI deadline auto-complete generate-declaration.ts has updated non-existent columns (type/period/ status) since inception, so the arbetsgivardeklaration deadline was never auto-completed. Replace with a shared helper targeting the real schema (tax_deadline_type/tax_period/is_completed), also used by the kvittens crons and moms handlers in the follow-up commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(rot-rut): import Skatteverket beslutsfil and record decisions on payout requests Parse the beslutsfil JSON from Skatteverkets rot/rut e-tjanst and record godkant belopp on the matching begaran: matched by stored skv_referensnummer first, then exact name among active undecided requests; arenden by fakturanummer then personnummer, exactly-one or the beslut errors (all-or-nothing). Never auto-settles: recording the beslut and booking the payout are separate acts. Exposed as an API route and the gnubok_import_rot_rut_beslut MCP tool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skatteverket): system auth for background reads, one-click VAT submit, kvittens notifications Hybrid auth program: system CCG (org certificate) for background reads while personal BankID stays for interactive submissions, since SKV per-flow refresh tokens live 65 min and crons structurally cannot run on them. All system-auth code sits behind SKATTEVERKET_SYSTEM_AUTH_MODE (default off) with a stub transport until the Expisoft cert and CCG avtal land; auth resolution is centralized in resolve-auth.ts. Also in this change: - One-click VAT submit chaining kontrollera -> utkast -> las server-side with a stage discriminator; step-by-step buttons demoted to the overflow menu. - Kvittens crons (AGI + new VAT schedule) with email-only notifications, deduped in notification_log under the new skv_kvittens type. - Ombud grant probe + verification UI in the connect panel, and a dashboard promo card for unconnected companies. - skatteverket_company_connections table with pg-real coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(salary): auto-settle AGI tax payment from skattekonto and surface SKV reconnect on the tax card The "Skatt att betala" card only cleared via the manual mark-paid button on the run detail page; the promised automatic flip from the Skattekonto sync was never implemented, so paid periods stayed red. - settleAgiTaxPayments: during every skattekonto sync, a booked "Arbetsgivardeklaration YYYYMM" debit row settles the matching agi_declarations.tax_paid_at, but only when the amount equals the declared total to the ore and the account is not in deficit (deterministic; drift or deficit falls back to manual). - Salary overview card: reconnect hint when the SKV token needs re-consent (link to /settings/tax, silent when the extension is off), plus an inline "Markera som betald" button reusing the existing endpoint and salary_payments strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add cloud backup scheduling and alerting features - Implement unit tests for scheduling logic in `schedule.test.ts`, covering various scenarios for determining if a backup schedule is due. - Create a new module `backup-alert.ts` to handle failure alerts for cloud backup auto-sync, including email notifications for reauthentication and repeated failures. - Introduce `schedule.ts` to manage scheduling logic, including handling local time zones and converting between local and UTC hours. - Add CSV report generation functions in `archive-csv.ts` for trial balance, income statement, balance sheet, and general ledger, ensuring compatibility with Swedish Excel formats. - Create a README generator for the archive structure in `archive-readme.ts`, providing clear documentation for users accessing backup files. - Implement tests for CSV report generation in `archive-csv.test.ts`, ensuring correct formatting and content. - Establish a full-archive coverage contract test in `full-archive-coverage.pg.test.ts` to ensure all company-scoped tables are properly classified for backup. * fix(stripe): correct invoice clearing reference and improve type safety in sync logic * fix(invoices): narrow accountingMethod before resolveInvoicePaymentSourceType settleInvoicePayment takes accountingMethod as a raw settings string, but resolveInvoicePaymentSourceType requires the 'accrual' | 'cash' union. Normalize at the call site (anything but 'cash' books as accrual), matching the existing useCashEntry semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address CodeRabbit review findings and nitpicks on PR #1004 Review findings: - backup settings redirect: always force view=export over incoming params - AGI/VAT kvittens crons: isolate best-effort post-submit calls, check the signed-state persist error, guard recovery calls in catch blocks so one company cannot abort the rest; surface grant_revoked in the run summary - kvittens notifications: atomic claim-first dedup with a partial unique index; map non-uuid reference keys to deterministic uuids - grant probe: record the actual 2xx status; mTLS transport: handle response-stream errors - stripe: amount-aware idempotency keys for payment links; emit stripe.disconnected on upstream revocations - ROT/RUT beslut import: mutate in-memory request state after apply, move item + header writes into an atomic apply_rot_rut_beslut RPC, add rot_rut_payout to JournalEntrySourceTypeSchema - migrations: use NOT VALID + VALIDATE CONSTRAINT for CHECK constraints on journal_entries, notification_log and rot_rut_payout_requests - cloud backup: hour_utc-only schedule updates clear stale hour_local Nitpicks: - stripe sync: enforce the cron time budget inside per-connection event processing with idempotent cursor progress; maybeSingle for settings; honest partial-customer DTO shared with the settlement boundary - shared applyPaymentLinkToInvoice helper for both invoice send routes, v1 docblock documents step 6b and PAYMENT_LINK_FAILED - settings panel: drop redundant decodeURIComponent - cloud backup: document worst-case archive memory headroom Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7d7f604e00 |
Add/stripe invoice link (#998)
* feat(supplier-invoices): show registered invoices under "Att betala" with inline approve Registered supplier invoices are already booked as debt (2440) but were hidden from the "Att betala" tab until approved, which confused users. The tab now shows registered invoices too, marked "Ej godkand" with a compact inline approve button. Approval remains the gate for payment, not visibility; status model and approve API untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reports): add date range filter to huvudbok (kontoanalys) Mounts the existing ReportDateRange control on /reports/huvudbok so the ledger can be narrowed to any date range within the fiscal year, matching Fortnox kontoanalys. Lines before the range roll into each account's opening balance so running balances stay correct at the range start; lines after the range are dropped. Applies to the XLSX export too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): add optional payment link on invoices (paste-link MVP) The user pastes a payment link created in their PSP dashboard (e.g. a Stripe Payment Link) onto an invoice. The recipient gets a "Betala online" button in the invoice email and a QR code + clickable link in the PDF payment box. No PSP integration server-side: this is the demand probe; a future Stripe Connect integration would auto-fill the same column. - invoices.payment_link_url (migration 20260709090000), https-only + 2048-char cap enforced in CreateInvoiceSchema; empty string normalises to undefined and build-invoice-write always writes a concrete value so clearing the field on a draft edit NULLs the column - editor field (real invoices only) with one-link-per-invoice hint; strings in sv+en (messages landed via e0e11066) - email button (customer.language, hidden for credit notes/proforma/ delivery notes, URL escaped for the href attribute) + URL in the plain-text part - PDF QR + link row following the Swish QR pattern; wired into send, download and preview routes - derived documents (credit note, proforma convert, recurring) do NOT copy the link: it encodes one amount for one specific invoice - MCP gnubok_create_invoice accepts payment_link_url (validated at staging and re-checked in the commit executor); v1 API exposes the column; tools/list token ceiling bumped 45K -> 45.5K (ledger entry in payload-size.bench.test.ts, headroom was <10 tokens) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): show oresavrundning on editor/form totals, supplier list and invoice email The rounding logic (getDisplayTotal) was correct but only applied on the PDF, invoice list/detail and review dialog. The invoice editor summary, the supplier invoice form totals and the supplier invoice list showed the raw ore total right next to the toggle, and the invoice email said "Att betala" with the unrounded invoice.total while the attached PDF showed the rounded amount (and the email also ignored the ROT/RUT deduction). Extract the PDF's Att betala block into getAmountToPay (lib/invoices/rounding.ts) and point PDF + email at it so they cannot drift; behavior-identical refactor for the PDF. Booked amounts stay ore-exact; display-only as designed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): adapt huvudbok date-range tests to the two-step entry-lines fetch The date-range tests (0969168f) mocked the old single-query shape with the parent entry embedded on each line; main's refactor (fetchEntryLines) queries journal_entries first and reattaches. Queue entry rows like the other tests so the merge of the two features is actually exercised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): fetch full invoice projection in v1 send so ROT/RUT deduction and payment link reach the PDF and email The v1 send route's hand-rolled column list omitted deduction_total, deduction_personnummer_last4, payment_link_url and the item-level ROT/RUT fields, so invoices sent via the public API overstated 'Att betala' and dropped the deduction box. Reuse the shared INVOICE_FULL_COLUMNS/INVOICE_ITEM_FULL_COLUMNS so the send row can never drift from the GET shape again. Also harden the supplier-invoice inline approve: a thrown fetch left the button stuck spinning; failures now refetch the true server state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b4a21b1029 |
fix(documents): RFC 5987 Content-Disposition so NFD filenames stop crashing inline view (#964)
* fix(documents): RFC 5987 Content-Disposition so NFD filenames stop crashing inline view macOS/iOS uploads carry NFD-decomposed filenames (base letter + combining diaeresis U+0308, char code 776). undici Headers require ByteString values (every code unit <= 0xFF), so splicing the raw filename into the Content-Disposition header threw while building the response and the inline document route 500ed. 122 prod documents across 35 companies hit this; last crash 2026-07-09T16:17. Add lib/api/content-disposition.ts emitting the RFC 6266 dual form: an ASCII quoted fallback (NFC-normalize, then replace anything outside printable ASCII plus quote and backslash with _) and filename*=UTF-8''<percent-encoded> per RFC 5987 (encodeURIComponent on the NFC name, additionally escaping ! ' ( ) * which it leaves bare). Use it in the inline document route and in the two latent same-shape sites that embed raw employee names in payslip PDF headers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): sanitize lone surrogates before percent-encoding Content-Disposition (CodeRabbit) Unpaired UTF-16 surrogates survive normalize('NFC') and make encodeURIComponent throw a URIError, so replace them with U+FFFD via String.prototype.toWellFormed() before encoding so the helper always returns a valid header value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2e7931b36b |
feat(customers): let users set a customer number shown on the invoice (#957)
Implements #914 (kundnummer on customers, printed on the invoice PDF). - Migration: nullable text column customers.customer_number, no unique constraint in v1 so existing rows and imports keep working. - API: CreateCustomerSchema/UpdateCustomerSchema accept an optional customer_number (trimmed, max 32 chars, nullable-then-optional so the OpenAPI registry sees it as not required); create/update routes persist it and normalize empty string to null so it can be cleared. - v1 public API: customers create/detail/update round-trip the field (insert and update field lists, response projections, response schemas), and the invoices :send route fetches customer_number in its explicit customer join so the emailed PDF matches the downloaded one (the pdf route already selects customers(*)). - UI: optional Kundnummer field in CustomerForm (next-intl keys in both sv and en), wired into the edit dialog's initialData; read-only Kundnummer row on the customer detail page's business-details card. - Invoice PDF: renders "Kundnr:" / "Customer no.:" in the customer box when set; the PDF reads the live customers join, so no snapshot column is needed. - Tests: route tests cover 400 validation, trimming, clearing with null/empty, and omit-leaves-untouched on POST and PATCH; v1 tests cover the create/update round-trip (insert/update payload + response projection) and the :send customer-join projection. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c1ea0d9bf2 |
fix(salary): make pain.001 betalfil generatable (company IBAN + BIC) (#950)
The ISO 20022 pain.001 salary payment file could never be generated: the route required company_settings.iban/bic, but no settings screen wrote those columns, so every request returned 400. The specific reason was also swallowed by getErrorMessage (isSwedishUserMessage did not know "krävs"/"saknar"), surfacing only the generic "Förfrågan innehåller ogiltiga uppgifter" (issue #945). - Add IBAN + BIC inputs to Settings > Fakturering > Bankuppgifter. BIC auto-derives from the clearing number / bank already entered, so in practice only the IBAN is typed. Validated client- and server-side. - Route requires the company IBAN (canonical debtor form every Swedish bank accepts) and derives the BIC, with clear actionable errors. - Employees are unchanged: domestic clearing + account (BBAN), which is what Swedish payroll collects. Only the company (debtor) uses IBAN. - getErrorMessage recognizes "krävs"/"saknar" so payment-file reasons surface instead of the generic 400. Fixes #945 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bacc5914af |
Fix/dependabot cus feedback (#946)
* feat(bookkeeping): per-account default VAT, oresavrundning momsfri Add a per-account "Standard moms" setting to the chart of accounts and use it to auto-fill the moms on a leverantorsfaktura-rad when that konto is picked. Oresavrundning (3740) ships as "Ingen moms", so a rounding line no longer inherits the 25 % rad-default and skews the moms. - chart_of_accounts.default_vat_rate (0/0.06/0.12/0.25, CHECK-constrained) - BEFORE INSERT trigger ships 3740 momsfri on every insert path; backfills existing 3740 rows - kontoplan editor: dead free-text momskod replaced with a Standard moms select - supplier-invoice rad auto-fills the rate from the konto default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(supplier-invoices): configurable start number for the ankomstnummer series Add a company_settings.next_arrival_number start floor so a company can continue its leverantorsfaktura numbering from a previous system (e.g. Fortnox) instead of restarting the ankomstnummer at 1. get_next_arrival_number now floors the series via GREATEST(MAX(arrival_number)+1, next_arrival_number), so the floor can never move the series backwards or collide with the (company_id, arrival_number) unique index. The RPC is hardened while rewritten: SET search_path to empty, schema-qualified refs, and an auth.uid() membership check matching generate_invoice_number. Includes the settings UI field, sv/en strings, migration, and pg-real coverage. The CompanySettings type and Zod schema field for this feature landed earlier in 1bf3b641 (swept into the per-account VAT commit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dependabot): reduce open pull requests limit and group updates for better management --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
abe9ac9d8c |
Fix/attributes config (#926)
* fix(git): pin LF on generated extension registry and vitest snapshots setup:extensions and vitest write these files with LF; with core.autocrlf=true git expects CRLF and flags them as phantom modifications on every dev/build run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): enforce MFA on mcp-oauth consent and gate viewer storno route mcp-oauth/authorize renders an HTML consent page and issues 303 redirects that withRouteContext cannot express, so it kept raw getUser() and thereby skipped the AAL2 gate: a password-only (AAL1) session could approve consent that mints a long-lived, MFA-bypassing API key. Add a route-local requireAal2() step-up on GET and POST; AAL1 sessions redirect to /mfa/verify, BankID users are exempt. Separately, POST /api/reports/vat-declaration/rc-basis-gaps/fix calls correctEntry() (storno of a posted entry) but lacked requireWrite, so viewer-role members could trigger it. Add { requireWrite: true }. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route transactions endpoints through withRouteContext Migrate the transactions routes off hand-rolled supabase.auth.getUser() onto the MFA-enforcing withRouteContext wrapper; add requireWrite on mutating handlers (book, uncategorize, attach-document, ignore, batch-match, create-from-document). Behavior and response shapes preserved; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route SIE import and bank reconciliation through withRouteContext Migrate import/sie and reconciliation/bank routes onto the MFA-enforcing wrapper; requireWrite on mutations (import execute, create-accounts, mappings write verbs, link/unlink/run/mark-opening-balance). Reads (status, unmatched-entries) stay ungated. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route salary endpoints through withRouteContext Migrate salary employees and runs routes (plus ku, payroll-config, tax-tables) onto the MFA-enforcing wrapper; requireWrite on mutations. Personnummer masking/encryption untouched; file downloads (AGI XML, payslip PDF, payment files) keep their headers. Two payment-file GETs retain requireWrite because they stamp *_file_generated_at and previously gated viewers. Tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route report endpoints through withRouteContext Migrate the read-only report routes (trial balance, balansrapport, resultatrapport, income statement, ledgers, KPI, VAT declaration, salary journal, monthly breakdown, journal register, continuity check, full archive, etc.) onto the MFA-enforcing wrapper. All read-only, no requireWrite. JSON/XLSX/PDF/ZIP response bodies and headers preserved byte-for-byte; tests updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route invoices, skatteverket, agent and extension endpoints through withRouteContext Migrate invoices, supplier-invoices, skatteverket tax-payments, and dynamic extension routes onto the MFA-enforcing wrapper with requireWrite on mutations. The two NDJSON streaming agent routes (invoke, onboarding/stream) use requireAuth() directly (the wrapper can't wrap a streaming response) so MFA is still enforced. skatteverket payment-file GET keeps requireWrite (stamps a generated-at field). Response shapes and file headers preserved; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route documents, events, team and account endpoints through withRouteContext Migrate documents, events, kpi/preferences, vat/validate, support/contact onto the MFA-enforcing wrapper with requireWrite on mutations. account/password, team/accept and team/members use requireAuth() directly (user-level or pre-membership flows with no active company context) so MFA is still enforced. events keeps its dual API-key-or-session auth. Document retention guard untouched; tests added/updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): route settings and pending-operations endpoints through withRouteContext Migrate settings (api-keys, oauth-clients, booking-templates, counterparty-templates, logo, company settings) and pending-operations (commit, bulk-commit, reject, edit-before-approve) onto the MFA-enforcing wrapper with requireWrite on mutations. Credential-guarding routes keep their per-user ownership filters. Response shapes preserved; tests added/updated to the wrapper mock pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(guards): ratchet raw-route-auth baseline 119->1 after A1 migration Lock in the withRouteContext migration so the count cannot regress. The single remaining entry, mcp-oauth/authorize, is a documented exception (HTML consent + redirects, MFA enforced via route-local step-up). Record the campaign and requireWrite decisions in DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(vat): add eSKD momsdeklaration file export for "Deklarera via fil" Generate the Skatteverket eSKDUpload v6.0 XML file so users can file VAT by upload instead of typing every ruta into the form. Extract buildFiledAmounts() as the shared whole-krona source of truth (öre truncated per SFL 22 kap 1 §) so the XML file and the manual-filing PDF can never disagree. Adds the /eskd API route, an XML option in the report export menu, and the upload button on the manual-filing card. Strings in sv + en. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(vat): add 'vat_settlement' source type and update related components * fix(booking): adjust search input layout and enable autofocus * fix(vat): support 12-digit org numbers and adjust emission order for eSKD file * fix(migration): add 'vat_settlement' to journal_entries.source_type CHECK --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cbffcd7292 |
fix(invoices): accept empty self-billing fields on invoice create (#920)
#911 added self-billing fields to the shared CreateInvoiceSchema with external_invoice_number: z.string().min(1), but the invoice form has always sent that field (plus self_billing_agreement_ref and received_date) as '' on every normal invoice. The empty string failed min(1), so every invoice create returned 400. Normalise the empty optional self-billing strings to undefined in the schema (matching the existing optionalIsoDate / deduction_brf_org_number patterns), and strip the unused empty carriers client-side before the form POSTs. Required-when-self-billed is still enforced post-parse in the v1 route, so the self-billed path is unaffected. Adds schema regression tests. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3a88b53fd9 |
Add/api and invoice (#911)
* feat(salary): validate employee clearing/kontonummer at entry Bank details on the "Anställda" form had no structural validation, so a typo in clearing/kontonummer was saved silently and only surfaced at Bankgirot LB generation (or never, on the SEPA path). Adds a shared validator (lib/salary/payment/bank-account.ts) wired into the create dialog, edit page, CreateEmployeeSchema, and the PATCH route: 4-digit clearing or 5-digit Swedbank (8xxxx), 5-11 digit account, both-or-neither. Mirrors encodeReceiverAccount so entry-time validation matches what the payout layer can encode. Update validates only when a bank field actually changes, so legacy free-text data stays editable. Includes a conservative clearing to bank-name hint (null for unknown ranges). Per-bank mod10/mod11 checksum deferred to a soft-warning follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(chart-of-accounts): styled delete warnings and bulk select-all Replace the native window.confirm() on single-account delete with the styled DestructiveConfirmDialog, and add to the prune dialog a master 'select all unused accounts' checkbox plus an explicit confirmation step before bulk deletion. New sv/en strings for the confirm titles and actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): encrypt personnummer on v1 employee create; tolerate legacy plaintext on read The v1 REST create route stored personnummer unencrypted, which then threw ERR_CRYPTO_INVALID_AUTH_TAG on every decrypt-on-read path and 500'd the employees roster. Encrypt on write in v1 create, decrypt on read in the v1 list/detail/patch responses, and make decryptPersonnummer pass a raw 12-digit value through with a warn so a legacy plaintext row can't take the roster down. Encrypt seeded personnummer. Add a gated, idempotent backfill for existing rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): save a manual entry as a reusable template Add a "Spara som mall" action to the manual journal-entry form next to the existing "Anvand mall" picker, so users can capture a booking pattern the moment they work it out. Opens the shared TemplateForm (create mode) pre-seeded from the current lines via deriveTemplateLinesFromBooking, and saves through the existing POST /api/settings/booking-templates. Rendered in both the mobile and desktop layouts and in create + edit modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(pending): label all staged operation types The Granskning list rendered the raw snake_case operation_type (e.g. create_supplier_invoice_from_inbox) for any type missing from the label map, which hogs the meta row and wraps awkwardly on mobile. Add short sv/en labels for all operation types in OPERATION_RISK_TIERS, plus a humanized fallback for future ones, and simplify the label map to a plain operation_type -> i18n-key record (the icon/variant fields were dead). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reports): let users file moms without a Skatteverket connection The momsdeklaration was never gated on the Skatteverket connection (it renders from the bookkeeping), but the not-connected "Anslut med BankID" card read as a wall. Make manual filing a first-class path: - Add a "Lämna in din momsdeklaration" card under the report with a PDF download (SKV 4700 layout, hela kronor) and a skatteverket.se link. - Add a momsdeklaration PDF route + template; buildManualFilingRows() rounds each ruta to whole kronor and recomputes ruta 49 per the SKV 4700 formula so it ties out. The PDF is a read/record copy, not a submission file (moms has no upload channel). - Offer PDF alongside Excel in the report's export menu. - Reframe the not-connected SkatteverketPanel to "Skicka direkt till Skatteverket (valfritt)". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(salary): compact new-employee dialog and warn on bad account check digit Redesign NewEmployeeDialog into a compact layout: borderless sections split by hairline dividers (no per-section cards), a fixed header + scrolling body + solid footer (fixes content showing through the old sticky bar), and denser grids. EmployeeTaxCard gains a `flat` variant so the dialog can host it without card chrome; the edit page keeps the boxed version. Add non-blocking Swedish account check-digit validation (lib/bankgiro/account-number.ts): mod10 (reuses luhn) + mod11, with a clearing->method table from the Bankgirot "Bankernas kontonummeruppbyggnad" spec, cross-checked against jop-io/kontonummer.js and verified against a real account (Forex 9420/4172385). Surfaced as a soft warning in both employee forms; unrecognised clearings return 'unknown' so we never warn on a valid but unmapped account. Never blocks saving. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(invoices): configurable send time + editing for recurring invoices Re-register the accidentally-removed recurring cron (now hourly) and add a per-schedule send hour (Europe/Stockholm, DST-aware). The cron never sends for a past date, and the enabling migration pauses every existing schedule on deploy so nothing auto-sends behind a user's back; users reactivate consciously (with a confirm) or click "Skapa faktura nu" to send this month on demand. Automatic sending now requires a customer email. Adds a full edit flow (row click opens the prefilled form, PATCH), fixing the row-click 404. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(invoices): configure självfaktura via the invoice API Add an optional is_self_billed flag (plus external_invoice_number, self_billing_agreement_ref, received_date) to the public invoice-create endpoint so callers can register a received self-billing invoice (mottagen självfaktura, ML 17 kap 15§) via the API. It was previously only reachable from the internal dashboard route, so it was missing from the API docs. Extract the booking into a shared service (lib/invoices/self-billed-sale.ts) and refactor the internal /api/invoices/self-billed route to a thin wrapper over it, so the dashboard and the API cannot drift. Books as a sale (Debit 1510 / Credit 30xx+26xx) with the counterparty's number; no own number is consumed. Fields are plain optionals (no schema refine) so UpdateInvoiceSchema.omit() keeps working; required-when-self-billed is enforced in the route. Documented in the endpoint registry. No migration (columns already exist). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(settings): allow a partial voucher-series-per-source-type map In Zod 4 an enum-keyed z.record is exhaustive (every source_type required), so saving a default_voucher_series_per_source_type map that omits a source type (e.g. the newly added result_appropriation) failed with "expected string, received undefined". Use partialRecord so the map can be sparse; the engine falls back to series 'A' for any unmapped key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(salary): resolve employer name via getCompanyDisplayName Payslip PDFs, the payslip email, AGI, KU10, and the BG/LB + SEPA payment files now resolve the employer name through getCompanyDisplayName (company_settings.company_name, falling back to companies.name), matching how invoices already display it. Read-side coalesce, so no migration or backfill: companies.name is write-once at onboarding and not authoritative for these surfaces. The sidebar company switcher uses the same coalesce for the non-active companies in the list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(kontoplan): index-only account usage counts + lighter reference load Add a covering index on journal_entry_lines (journal_entry_id, account_number) so get_account_usage_counts becomes an index-only scan (prod worst case ~440ms). Slim /api/bookkeeping/accounts/reference to return only the company's activation rows and merge against the client-bundled BAS_REFERENCE instead of re-sending the full ~1,300-account catalog every load, and defer the BAS catalog + usage counts off the first-paint critical path in ChartOfAccountsManager. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(salary): add bank-account checksum warning string sv/en strings for the employee bank-account (clearing/kontonummer) soft checksum warning shown by the create/edit forms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: update decision log Append the 2026-07-06/07 decision entries (salary employer-name coalesce, sidebar switcher, employees API personnummer fix, kontoplan load optimization, momsdeklaration manual filing, recurring invoices resend + reactivation + editing, "spara som mall", voucher-series partial map, and självfaktura via the invoice API). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: address compliance-review findings on recurring invoices + moms filing - recurring cron: close the double-send window with an atomic compare-and-set claim on last_run_at (release-on-failure) so two overlapping hourly runs can't both spawn from the same stale batch row - recurring edit dialog: force auto_send=false whenever the effective customer has no email, so a disabled-but-checked box can't PATCH auto_send=true after the async customer load - momsdeklaration manual-filing: truncate rutor to whole kronor (öretal faller bort per SFL 22 kap 1 §) instead of round-to-nearest, matching the SRU path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cdac1808c9 |
feat(api): ROT/RUT, articles, and project lifecycle on the v1 API (#904)
* feat(api): ROT/RUT + articles + dimensions on the v1 invoice surface (#895) - v1 invoice POST now routes through buildInvoiceWriteData, the same builder as the dashboard: ROT/RUT deduction lines (server-side compute, personnummer encryption), article_id + revenue_account linkage, accruals, and line_type no longer get silently dropped on the wire. - v1 invoice PATCH accepts default_dimensions so integrations can tag a draft with a project/cost centre after creation. - New PATCH/DELETE /dimensions/:id/values/:valueId: rename, archive, set end_date on project codes; delete unreferenced values (409 with an archive hint when the BFL retention trigger blocks). - New GET /articles: read-only artikelregister list (incl. housework_type) so callers can resolve article_id before composing invoice lines. - Invoice GET/POST projections now expose deduction fields and full item columns; dry-run previews never echo the encrypted personnummer. Closes #895 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): address review on #904 - Extract shared v1 invoice projections to lib/api/v1/invoice-columns.ts so create/detail/patch responses can't drift; PATCH now returns deduction_total + deduction_personnummer_last4 like GET/POST. - Narrow the v1 create customer fetch back to the three fields the builder reads instead of select('*'). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
2c2743eb79 |
Check/salary bankid api (#892)
* fix(bankid): harden login/signup flow — polling, signup rollback, metadata merge, enrichment lookup - middleware: read BankID enrichment from the bankid_enrichment table (the extension_data path has been dead since the multi-tenant refactor), so company-less BankID users land on /select-company instead of the manual wizard - BankIdAuth: hard 6-min poll deadline; every failed poll counts toward the give-up limit; guard overlapping ticks so completion runs exactly once (a double /complete regenerated the magic link and invalidated the first, failing logins intermittently); retry clicks wait out the start cooldown instead of silently no-oping; Swedish messages for 429/unknown start errors - bankid/complete: all-or-nothing signup — delete the created user when the identity insert, app_metadata update, or magic-link generation fails, so a retry starts clean instead of hitting account_exists with an unusable account - bankid/unlink: read-merge-write app_metadata so has_password survives unlink (BankID-only users could otherwise strand themselves with no login method) - login: BankID "create account" CTA now links to /register instead of dismissing the notice; sv.json: fix missing å/ä/ö in settings_bankid strings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: move secondary guides into docs/, delete dead root files Move DOCKER.md, SELF-HOSTING.md, WHITELABEL.md and extensions.md (renamed EXTENSIONS.md) into a new docs/ folder and update all path references (README, setup.sh, .dockerignore image rules, docker-publish workflow comment, _example-branding, lib/branding/service.ts). Delete two dead root files: customer.json (stray API-test payload) and findings.md (point-in-time swarm audit export, criticals already filed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(api): security & correctness hardening + withRouteContext MFA migration across API routes Audit of ~100 app/api routes. Highlights: Security - agent/conversations: list leaked colleagues' titles + message previews (company-scoped RLS, no user filter) -> user-scoped - calendar/feed PUT: raw body into .update() allowed feed_token fixation on a public unauthenticated URL -> strict schema, content toggles only - bokslutsdispositioner: unbounded schablonintaktRate could inflate the IL 30 kap 25% periodiseringsfond cap base -> bounded - agent profile/composer/onboarding: viewers could rewrite the agent profile while sibling /verify blocked them -> role-gated Correctness - account-totals / listAssets: unbounded queries silently truncated at 1000 rows (under-counted money; skipped assets at year-end depreciation) -> fetchAllRows with stable order (+3 more pagination fixes) - voucher-gaps: swallowed detect_voucher_gaps RPC errors (BFNAR gap view could show "no gaps" when the check never ran) -> surfaced - 5 phantom-success writes (OK on zero matched rows) fixed - assets K3 component-sum validated against stale acquisition_cost -> fixed - invite silent email-send failure -> response carries email_sent; deadlines/calendar cast-then-check JSON crashes -> Zod Convention - ~44 legacy routes converted to withRouteContext (MFA); added Zod validation, corrected status codes, console.* -> lib/logger Response shapes preserved for existing callers. ~110 new tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): save a booking as a reusable template from Bokför direkt Add a "Spara som mall" action to the manual booking dialog so users can capture a kontering they just worked out as a booking template — right where they figured out how something should be booked. - derive amount-parameterised template lines from the concrete booking (settlement = the non-VAT leg nearest the total, 26xx = a VAT line with its rate snapped to the nearest standard rate, the rest = business ratios; line labels come from the loaded BAS chart) - extract the shared TemplateForm out of BookingTemplatesPanel so the booking dialog reuses the same editor, live preview and convertibility hints instead of duplicating them - save via the existing POST /api/settings/booking-templates endpoint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bokslut): render arsredovisning RR/BR at ÅRL post level — no kontonummer Bolagsverket rejected a user's filed årsredovisning with "Balansräkning och resultaträkning ska inte innehålla kontonummer": the PDF built every statement row as per-account "1930 Företagskonto" lines while the iXBRL filing path already aggregated to statutory posts, so the two artifacts diverged. The PDF statements now derive from the same K2 risbs mapping the iXBRL document uses (mapTrialBalancesToK2), via a new statement-rows.ts that emits post-level rows in uppställningsform order for both the K2 and K3 templates. Also fixed along the way: - Jämförelseår column (ÅRL 3:5 §) — previous-year trial balances now load and render; the old PDF had no comparatives at all. - mapping.warnings (unmapped accounts, RR ≠ 2099, obalans, reclass nudges) flow into ArsredovisningData.warnings so the wizard flags a non-fileable document before download. - Flerårsöversikt current/previous year overridden with the mapper's strict-3000–3799 Nettoomsattning, mirroring build-input's duplicate-fact rule, so the FB table ties to the RR. - FB eget kapital-table is post-level and drops obeskattade reserver (never eget kapital); K3 equity-changes statement uses real prior-year opening balances with derived utdelning/nyemission residuals that tie the roll-forward exactly to booked UB. - build-input dedupes warnings now that the PDF path runs the same mapping. Regression test asserts no RR/BR label ever contains a four-digit account number again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports): diagnose untransferred prior-year results behind balance-sheet differens Prod incident (97 kr): a multi-year SIE migration lacked one year's omforing av arets resultat; the residual corrupted every later derived opening balance and Balansrakningen showed a bare "Differens: 97 kr" with no explanation. Continuity checking cannot catch this failure mode (prior-year UB and derived IB match per-account by construction) - the invariant that actually breaks is per-year P&L = 0 for all non-latest years. - lib/reports/imbalance-diagnosis.ts: shared detector (findUntransferredResults + buildImbalanceDiagnosis) - Balansrakning/Balansrapport attach imbalance_diagnosis when unbalanced, naming the exact culprit years; rendered in web views + PDF; MCP gnubok_get_balance_sheet inherits the field via spread - SIE import: parse-time warning when a completed year's vouchers leave a P&L residual, plus a post-import DB walk surfacing culprits as warnings and structured details.untransferredResults; the Arcim migration workspace previously dropped result.warnings entirely and now renders them - opening-balance/correct: pre-flight the company lock date and return 409 OB_COMPANY_LOCK_DATE (retryable: false, lock date interpolated in the client message) instead of the retryable 500 that invited blind retries; catch-path maps a raced trigger rejection to the same code Diagnosis runs only on unbalanced paths (zero cost when healthy) and never fails the report or the import. No migration, nothing persisted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: production error remediation — FX rates, deadlines, log levels, correction relink Batch of fixes for recurring Vercel runtime errors: - Riksbanken FX rates: persistent read-through cache (exchange_rates table), one retry honoring Retry-After on 429/5xx, bounded ingest concurrency, and an honest fallback — most recent cached observation or null, never a hardcoded rate silently booked into amount_sek. Unrated transactions stay repairable via refresh-exchange-rate. - Tax deadline regeneration inserts replacement rows before deleting the superseded set, so a failed insert no longer wipes a company's deadlines (the 23502 user_id regression did exactly that). Migration makes deadlines.user_id nullable for system-generated rows. - Route wrappers + errorResponse log 4xx outcomes at warn so only genuine 5xx reach Vercel's runtime-error clustering; client-supplied /api/log telemetry demoted to warn as well. - application/json documents (raw PSD2 responses archived per BFL) validate as parseable JSON with object/array root instead of always failing the magic-byte check. - correctEntry surfaces document-relink failures to callers, and the BFL document-immutability trigger now allows relinking underlag from a reversed entry to its correction (migration + pg test). - Middleware clears stale session cookies on /api requests too, using scope 'local' so cleanup doesn't re-trigger the failed token refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skatteverket): persist token health and stop retrying dead consents Terminal auth errors (SESSION_EXPIRED, REFRESH_EXHAUSTED, MISSING_SCOPE, TOKEN_CORRUPTED) mark the token row needs_reconsent with the error code and timestamp — SKV per-flow refresh tokens live 65 minutes, so once expired nothing recovers without a fresh BankID consent. The AGI kvittens and skattekonto sync crons skip flagged connections instead of failing every night, and the settings panel prompts for re-consent proactively. A successful reconnect resets the row to active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(banking): allocate distinct BAS ledger slots for PSD2 mirror accounts A bank returning N same-currency accounts used to map them all onto the currency default (1930/1932/1933/1934), tripping the UNIQUE (company_id, ledger_account) constraint per-account — swallowed errors left accounts silently unmirrored. allocatePsd2LedgerAccount now hands out the currency default first, then free 1931–1959 sub-account slots, skipping slots held by any existing row. - Callback persists allocations to accounts_data so the picker pre-fills reality; reconnect reuses previously mirrored ledgers instead of re-deriving (a user remap to 1935 survives). - Selection save resolves effective ledgers up front and rejects duplicates or cross-connection conflicts with a 400 instead of silently skipping the mirror. - Bank error codes + psu_type are forwarded to the settings page for every OAuth error, keying the Handelsbanken corporate fullmakt guidance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent): stage exact journal lines on categorization previews Categorization previews only carried debit/credit accounts, the GROSS amount, and separate VAT rows — read together that looks like an unbalanced 'gross on cost account + VAT debit' entry, and it misled both users and agents into rejecting correct proposals. The MCP preview and the pending-operation PATCH now materialize the exact lines the commit executor will post (net cost line, VAT line, gross bank line, SEK) via buildTransactionEntryLines, and PATCH re-derives them from the new mapping instead of spreading stale staged lines. ApprovalCard and /pending render the verifikat lines, falling back to the legacy summary only for operations staged before this fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): prune unused imported accounts from the chart SIE imports routinely bring in hundreds of accounts that were never used and clutter the kontoplan. New account_usage_counts RPC (one grouped query instead of a count per account) backs GET /api/bookkeeping/accounts/usage, and POST /api/bookkeeping/accounts/prune deletes zero-usage accounts — dry-run first, then an explicit account list capped at 2000. Accounts with journal lines are skipped, never deleted. The chart manager shows a usage column and a prune dialog grouping custom accounts vs unused BAS-seeded ones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): carry dimensions through v1 invoice and supplier-invoice surfaces Credit-note creation now copies default_dimensions and per-line dimensions from the original, so the reversing journal entry nets against the same dimension cells instead of dropping them. List/detail responses expose the dimension fields, and the OpenAPI spec snapshot follows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: batch serial Supabase round-trips on hot dashboard paths Every dashboard render pays the layout's query chain, so serialized awaits are direct wall-clock: the layout, chat conversation, invoice detail, supplier detail, select-company, and agent-onboarding pages now run their independent lookups in parallel batches, and getCompanyCapabilities folds its disabled-config read into the same round-trip. JournalEntryList hydrates the saved fiscal-year scope optimistically instead of serializing the first entries fetch behind the fiscal-periods request. The supplier detail page filters invoices server-side via a new supplier_id query param instead of fetching the whole company ledger, and the invoice editor (with its framer-motion dependency) lazy-loads so it stops shipping with the invoice list bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(salary): one-click runs, payslip delivery, payments settings, run cockpit Salary P1 batch, driving the 20-click flow toward 3 clicks: - One-click 'Starta lönekörning': POST /api/salary/runs accepts an empty body and resolves defaults server-side — period follows the latest non-corrected run, payment date from the new salary_pay_day setting, series from the per-source-type map. The separate /salary/runs/new page is gone. - Run detail page rebuilt as a step-railed cockpit (progress rail, KPI cards, employee ledger, journal preview) on a deliberately wider canvas; components extracted to components/salary/run/. - Payslip delivery: tokenized public payslip pages (/payslip/[token], backed by salary_payslip_links) plus per-employee email send with PDF — employees need no account, and the middleware exempts the route from auth redirects. - Payments settings: salary pay day, default bank, and pain.001 vs Bankgirot Lön format with per-bank upload instructions and an LB sunset warning (banks retire LB during 2026). - AGI panel: full submission status flows (stale drafts, signing links, kvittens polling, error reports); tax payment panel with skattekonto shortcut and mark-as-paid. - Salary calendar bulk editing, employee benefits/tax-card polish, municipality tax-table lookup improvements. messages/sv+en also carry the strings for the account-prune, skatteverket-reconsent, and banking surfaces committed just before this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: adopt Next 16 proxy.ts convention + repo housekeeping - Rename middleware.ts to proxy.ts with the proxy() export (Next 16 renamed the middleware convention; behavior unchanged). - Exclude dev_docs/ from tsconfig so stray snippets in planning docs don't break the build type-check. - Ratchet antipatterns-baseline down (raw-route-auth 165 → 119) to lock in the withRouteContext migration from 5cfd2b76. - template-library uses roundOre() instead of inline rounding. - database.md: drop account_balances from the key-tables list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bookkeeping): robust service-role detection in correction document relink relink_documents_to_correction() keyed its service-role branch on auth.role(), which reads the singular request.jwt.claim.role GUC that PostgREST v10+ and the pg-real harness no longer populate. Genuine service-role callers (pending-ops executor / MCP approve) landed in the auth gate and could not relink underlag. Read the role from the request.jwt.claims JSON directly, mirroring the canonical link_voucher_rpcs_tenant_guard convention. Validated on staging. Also: harden the salary run page's error paths (res.json().catch) against non-JSON error bodies, and roll back the pg-real service-role case in finally so an aborted transaction cannot poison a pooled connection for the next test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(documents): restore journal_entry_line_id link durability (BFL 7 kap) Migration 20260704103000 rewrote enforce_document_journal_entry_immutability to guard journal_entry_id but left journal_entry_line_id to the metadata trigger, which exempts draft-linked docs -- and the entry-level trigger only fired on UPDATE OF journal_entry_id, so a line-id-only UPDATE never invoked it at all. That let a set journal_entry_line_id be cleared to NULL, breaking the "link durable from first set" invariant (document-immutability.pg regression). Widen the trigger to fire on journal_entry_line_id too and guard it with the same uuid-durability rule as journal_entry_id (setting NULL -> uuid stays allowed; clearing/re-pointing a set value is blocked, status-independent). The correction-relink GUC path, which legitimately clears line_id when moving underlag to the posted correction, stays exempt. Validated on staging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ec27228a8e |
style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
764348e99c |
feat(dimensions): PR10 advanced — custom dimensions, hierarchy, account rules, commit enforcement (#886)
* feat(dimensions): PR10 advanced — custom dimensions, hierarchy, account rules, commit enforcement The final rung of the dimensions ladder (dev_docs/dimensions_implementation_plan.md §7 row 10): - custom dimensions: POST /api/dimensions creates registry dims (next free SIE number >= 20 when omitted; explicit numbers allowed — SIE import already mints reserved ones); register gets a 'Ny dimension' dialog with a quiet Avancerat disclosure for the #UNDERDIM parent; GET now carries parent_sie_dim_no (the column + SIE round-trip existed since PR1/PR5 — this exposes it) - account_dimension_rules (migration 20260703120000): one rule per (account, dimension) — required / default / fixed, per-rule is_active, company-scoped RLS, composite FK to the registry, value-presence CHECK - enforcement, opt-in BY CONSTRUCTION (zero rules = engine byte-identical; deliberately NO settings toggle — a rule that exists but is ignored is worse than either extreme): default/fixed apply onto line bags at draft creation (fixed overwrites, default fills); required asserts at commitEntry with a Swedish MANDATORY_DIMENSION_MISSING naming every account + dimension; the bulk-book route runs the same policy before its RPC; storno/correction paths never pass through commitEntry so history always reverses regardless of policy; rule fetches fail open incl. thrown exceptions - chart of accounts: per-account Dimensionsregler section in EditAccountDialog (Krävs/Förval/Låst, value picker, pause switch), gated on the existing dimensions toggle, quiet when empty - pickers: LineDimensionFields is registry-driven (one combobox per active dimension, cached fetch, hardcoded 1/6 fallback) — every existing mount lights up custom dims with zero changes - agent briefing: per-dimension required_on_accounts/default_on_accounts so agents self-correct instead of bouncing off the policy error - rules CRUD API with existence/active/company validation and qualified DTO ids; firm_id FK deferred until the firms table lands (per plan) 39 new tests (pure-fn rules, engine enforcement, both new API surfaces, pg-real RLS/CHECK/cascade suite); full suite 6,791 green; migration replayed on a fresh container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: renumber migration to 20260703200000 — version collision with prod The concurrent session shipped pending_operations_add_link_document_to_voucher as 20260703120000 today; the Supabase preview branch (cloned from prod) rejected the duplicate version key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: review round — auto-pick retry on collision, fail-open warnings, query schema - POST /api/dimensions retries once past a concurrent number claim when the number was auto-picked (explicit choices still 409) - every fail-open skip of the dimension-rules policy now logs a structured warning (engine draft/commit paths + bulk-book) — deliberate fail-open, but observable - GET /api/dimensions/rules validates its query through ListDimensionRulesQuerySchema instead of an inline regex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
237b77a366 |
feat: custom inbound mail domains, rot/rut payout file, invoice email texts, security hardening (#878)
* fix(security): guard MCP test keys, RLS role gate + voucher RPC guards, /api MFA gate, deps - MCP: force dry-run / block writes for test-mode API keys in tools/call (extensions/general/mcp-server) - DB: current_user_can_write role gate on write policies (40 tables) + tenant guards, SET search_path, REVOKE anon on commit_journal_entry / next_voucher_number / detect_voucher_gaps (migration 20260702093000) - Middleware: MFA (AAL2) gate on cookie-authenticated /api routes via apiPathSkipsMfaGate - Deps: npm audit fix clears mailparser/linkify-it/nodemailer/svix/uuid highs; xlsx -> SheetJS 0.20.3 Adds unit + pg-real tests. Does not touch in-progress ROT/RUT or invoice-email-texts work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): rot/rut begäran om utbetalning — HUS XML (V6), payout tracking + settlement, MCP tool Generates Skatteverkets begäran-om-utbetalning file (schema V6) from paid ROT/RUT invoices — no submission API exists, the file is uploaded manually at skatteverket.se. Headless by design for now: API routes + MCP tool (gnubok_generate_rot_rut_file), no UI surfaces. - lib/invoices/rot-rut-file.ts: pure XML generator with deterministic per-invoice blockers (hours, work type, personnummer, property info, mixed rot+rut, XSD limits) + 31 January deadline warnings - rot_rut_payout_requests(+items) tables: one active begäran per invoice (DB triggers incl. reactivation guard), RLS, audit, pg-real tests - Settlement: POST /settle books debit 1930 / credit 1513 via the engine (source_type rot_rut_payout); partial payouts → partially_paid - Work-type lists corrected against Begaran.xsd: IT-tjänster is rut-only, snöskottning/tillsyn/tvätt added (schablontjänster utfört-only) - Fix: invoice-level fastighetsbeteckning was validated but never persisted — now stamped onto rot lines in build-invoice-write; API accepts bostadsrätt pair (lägenhetsnr + BRF orgnr, editor UI deferred) - invoice_items.brf_org_number migration + MCP scope invoices:write Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): per-company editable invoice email texts Add an "E-posttexter" section under Settings -> Fakturering where the subject, greeting, body and sign-off of the standard invoice email can be customized per company in Swedish and English. Fields pre-fill with the standard texts and only diffs from the standard are stored (company_settings.invoice_email_texts JSONB), so future improvements to the stock wording still reach companies that have not customized. Each field has a reset-to-standard button; cleared fields snap back. Texts support a fixed placeholder set (invoice number, customer name, first name, company, due date, amount) substituted at send time in a single pass; unknown placeholders stay literal. Custom texts are HTML-escaped after substitution, newlines become <br> in the HTML variant, and subject lines are flattened to a single header line. Overrides apply to standard invoices only - credit notes, proforma and delivery notes keep the stock texts. All send paths (UI, v1 API, MCP approval, recurring) pick the texts up via the existing settings row. The Zod schema half of this change (InvoiceEmailTextsSchema in lib/api/schemas.ts) was inadvertently included in 8291f745. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(documents): accept PDFs with preamble before %PDF- header, surface content rejections as 400 detectFileMagic required the %PDF- signature at byte 0 (BOM aside), rejecting genuine PDFs that carry a leading newline or junk bytes — files every ISO 32000 reader opens fine. Now scan the first 1024 bytes for the signature, matching real-reader behavior. Image types stay strict at offset 0 to keep the anti-placeholder defense tight. Magic-byte rejections were also mislabeled as DOC_UPLOAD_STORAGE_FAILED (500 'Filen kunde inte sparas'), blaming storage for a client-side file problem. Both upload routes now map them to a new DOC_UPLOAD_INVALID_CONTENT (400) with an accurate message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): full keyboard flow for manual journal entry Enter now drives the whole verifikat flow: verifikationstext drops into the first row missing an account, konto commits advance to debet, Enter on an empty debet hops to kredit, and an entered amount jumps to the next row. Once the voucher balances, Enter opens the review (unchanged gate) and the auto-focused confirm posts it — including through the no-underlag warning dialog. Escape in the inline review goes back to the form. Also fixes an Enter footgun in AccountCombobox: a bare Enter on a freshly focused field no longer selects the first account in the list — selection now requires typing or arrow navigation; otherwise Enter re-commits the current value or bubbles to the form-level handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add custom inbound domains management for companies - Implemented functionality to allow companies to claim and manage their own inbound email domains via Resend's API. - Created a new table `company_inbound_domains` to store domain information, including status and DNS records. - Added necessary RLS policies to restrict access based on user roles (owner/admin). - Developed functions for domain normalization, validation, claiming, verification, and removal. - Implemented webhook handling for domain status updates from Resend. - Added comprehensive tests for RLS, constraints, and triggers related to the new domain management feature. * fix: address PR #878 review findings and CI failures - migrations: drop the ai_usage_tracking policy block from the role-gate migration — the table was removed by 20260504120000_remove_ai_subsystem and only lingers on staging as drift; a from-scratch chain (pg-real, Supabase preview) failed on it - invoice-inbox: never flip a custom domain to verified off a domain.updated webhook alone — confirm the receiving capability with Resend first (fail-closed); normalize both sides of the orphan-adoption domain match - rot/rut: block files where begärt belopp exceeds what the buyer paid (DEDUCTION_EXCEEDS_PAYMENT); tighten brf_org_number validation to real orgnr shapes; parameterize the settlement bank account (19xx, default 1930) - rot/rut routes: log acting user on financial mutations, stop swallowing item mirror errors, narrow response projections (no customer ids through the invoice join); document the deliberate inline-XML decision - documents: stop echoing raw storage-layer error messages to clients Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: round-2 CI + compliance findings on PR #878 - migrations: the role-gate migration targeted automation_webhooks, which 20260515170000_webhooks_v2 renamed to webhooks on the canonical chain (staging kept the old name — drift); gate public.webhooks instead, dropping legacy schema-sync policy names defensively. Restore the 20260623130000 owner fallback in next_voucher_number that the stale copied-verbatim body silently reverted (caught by engine.pg locally). Full migration chain verified from scratch against supabase/postgres:15. - mcp: bump the tools/list payload ceiling 44K -> 45K — main's #877 qualified-identifier schemas plus this branch's rot/rut tool crossed the ceiling only in combination; documented in the test's history log. - rot/rut: refuse partial settlement before Skatteverkets beslut is recorded (would bypass the PATCH lifecycle and strand the request); block zero-kronor ärenden (ZERO_DEDUCTION); require sekelsiffra 16 on 12-digit brf orgnr in both schema validation and normalizeBrfOrgNr Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: rename branch migrations off main's colliding versions After the merge with main, two versions were shared by two files each (20260702100000: rot_rut_payout_requests vs company_settings_dimensions_ enabled; 20260702130000: invoice_email_texts vs pending_operations_add_ create_dimension_value). psql-based CI applies by filename and doesn't care, but Supabase branching records migrations by version (PK) — the second file with the same version breaks the preview with a schema_migrations_pkey duplicate. Neither branch migration is version- recorded on staging or prod, so renaming to fresh 20260703 versions is safe; nothing between the old and new positions depends on these objects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): scope the /api MFA-gate bypass to real Bearer-auth surfaces Any Authorization header — attacker-controlled — used to skip the AAL2 gate for every /api route, so a stolen-password AAL1 cookie session could reach cookie-authenticated routes (which ignore the header) by attaching `Authorization: x`. The skip is now scoped to the surfaces whose auth contract IS the header (/api/v1 API keys, the MCP endpoint's OAuth tokens); pure Bearer callers elsewhere (cron secret, signed webhooks) carry no cookie session and were never touched by the gate, which only fires for cookie users. Superagent P2 on PR #878. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: normalize path separators in dimension statutory guard scan The route scan compared walked file paths against a POSIX-path allowlist, so the suite failed on Windows (backslash separators) while passing on Linux CI. Normalize the scanned paths to forward slashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ea236cbcdf |
fix(reconciliation): Bankavstämning phase 0 — correctness + feedback batch (+ nav IA regrouping) (#879)
* feat(nav): interaction-mode sidebar grouping — Arbeta/Analys/Data/Skatt & bokslut Nav IA redesign phase 0 (dev_docs/nav_ia_redesign.md): same routes, regrouped by what the user is doing. CLAUDE.md restructured around Hard Rules (doc references updated); pending-page explainer removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): correctness + feedback batch for Bankavstämning (phase 0) Engine: fetchAllRows pagination on status/run/RPC fetches (silent 1000-row cap corrupted totals), optimistic-lock guards on manualLink + apply, unlink audit rows attributed to the acting user (was: company UUID), selected_matches partial apply intersected with a fresh match run. View: silent in-place refresh instead of a full-page skeleton per action, checkbox-gated apply with confidence badges (fuzzy unticked) in chunks of 500, honest result toasts, dry-run errors surfaced, ranked per-row picker candidates pinned to the applied date window, currency-correct amounts (bank side in account currency, GL side SEK), voucher links, translated source types, colored differens, dirty-date-filter guard. Discovery: year-end preflight 404 href fixed (/reconciliation/bank never existed), ⌘K palette entry, real links from the transactions page. v1: status registry schema now matches the actual ReconciliationStatus payload, errors documented as a count, false ~0.85-threshold pitfall replaced, route test mocks the real shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
28827c2613 |
feat(dimensions): voucher-level retro-tagging workbench — reversal pairs hidden by default (#874)
* feat(dimensions): voucher-level retro-tagging workbench, reversal pairs hidden by default UX rework of the BulkTagWorkbench (follow-up to #867): the verifikat is now the unit of work, matching how users think ('that invoice belongs to project X') and how every Swedish bookkeeping tool presents entries. - /api/dimensions/tagging/lines returns voucher-grouped results via a two-step query: filters select QUALIFYING vouchers (line-level predicates become 'voucher has such a line' through the inner join), then the complete line set for each — tagging a voucher always covers the whole verifikat, never the filtered subset of one - reversal pairs are EXCLUDED by default: an annulled entry and its storno net to zero in every dimension bucket as long as both sides carry the same tag, so retro-tagging them is a no-op with an asymmetry foot-gun attached; 'Visa annullerade' opts them back in, and the blocking motverifikat confirmation survives only in that view (correction rebooks remain taggable — only the original+storno pair is hidden) - voucher rows: label, description, date, line count, distinct tag chips + 'Delvis taggad' state, single total amount (no debit/credit columns); chevron expands to per-line rows (signed amount, per-line checkboxes) for the mixed case (a voucher split across projects) - selection stays line-id based under the hood (the retag RPC is per line); shift-click ranges operate on vouchers; apply groups by resulting map and now chunks to the apply route's 500-line cap; failed vouchers auto-expand with their Swedish RPC errors inline - 'endast otaggade' now means 'vouchers with at least one untagged line'; the cap counts vouchers (default 150, max 300) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pull counter-vouchers into the annulled view even outside filters (review) Without this, a pair leg whose counter fell outside the date range would show no motverifikat warning at all — one-sided tagging would slip through silently, exactly the Srf U 14 skew the guard exists to prevent. Also scope the line fetch explicitly through the parent company filter (defense in depth). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
163fbd8222 |
feat(dimensions): PR8 salary — employees.default_dimensions, per-employee cost lines, aggregation re-key (#869)
Employees carry a default dimensions bag and the salary booking puts each
employee's cost on their kostnadsställe/projekt
(dev_docs/dimensions_implementation_plan.md PR8):
- employees.default_dimensions (migration 20260702220000; jsonb DEFAULT
'{}' + object CHECK)
- salary-entries: the one-line-per-account aggregation is re-keyed to
account+bag — P&L cost lines (löner incl. line items + base remainder,
arbetsgivaravgifter, semesteravsättning + dess avgifter, pension, SLP)
split per employee bag while every balance-sheet/settlement leg (2710,
1930, 2731, 29xx, 2740, 2514) stays aggregated; liability credits equal
the sum of the rounded debit buckets so entries balance by construction;
dimension-less runs book byte-identically to before. Replaces the dead
SalaryRunEmployee.cost_center/project pair (never wired)
- both book routes (dashboard + v1) read the bag via the employees join —
read-at-book, so the run review shows exactly what will book
- employee form (new + edit) gets a gated Kostnadsställe/Projekt card;
run review shows per-employee dims chips; run GET + v1 employee
routes/schemas + MCP list_employees carry the field
- pre-merge audit: all salary reports (salary-journal, AGI,
avgifter-basis, vacation-liability) read salary_run_employees — not
journal lines — and every ledger consumer sums per account, so the
line split breaks nothing; SIE export + dimension P&L pick the split
up as intended
8 new engine propagation tests + book-route dims flow test.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
755e0f7e47 |
feat(dimensions): PR7 producers — auto-tagged documents (invoices, supplier invoices, bulk-book, templates, MCP) (#868)
* feat(dimensions): PR7 producers — invoices/supplier invoices carry dims, generators propagate, BulkBook + templates + MCP bags
Source documents now carry dimension tags and every entry generator
propagates them onto journal lines (dev_docs/dimensions_implementation_plan.md PR7):
- invoices/supplier_invoices.default_dimensions + per-item dimensions
(migration 20260702200000; jsonb DEFAULT '{}' + object CHECK)
- invoice-entries: issuance/payment/cash/credit propagate — item bags merge
over the invoice default per revenue line (account+bag aggregation
identity), payment vouchers re-propagate the linked invoice's bag onto
every leg incl. FX result lines; ROT/RUT 1513 carries the item bag
- supplier-invoice-entries: registration/payment/cash/privately-paid/credit
propagate with the same merge rules (expense buckets keyed account+bag)
- bulk_book_transactions RPC persists per-line bags + derives
cost_center/project mirrors in SQL (migration 20260702201000; malformed
bags rejected with BULK_BOOK_INVALID_DIMENSIONS); route merges the header
default into template/manual lines
- counterparty templates: LinePatternEntry.dimensions learned from SIE
voucher history (kept only when every occurrence agrees), applied to
business lines on booking; QuickReviewDialog shows a dims badge
- categorize: staged dimensions bag tags business lines only (bank/VAT
untagged); credit/convert/inbox copy paths carry bags forward
- propose-payment/send-lines stamp the invoice default so the editable
payment grid books what the preview shows; mark-paid override lines
accept dimensions
- UI: InvoiceEditor + NewSupplierInvoiceForm header KS/Projekt pair with
per-row override; BulkBookDialog header default pair (both tabs)
- MCP: default_dimensions/items[].dimensions on create_invoice +
create_supplier_invoice_from_inbox, dimensions on categorize_transaction,
per-line bags on bulk_book_transactions — resolve-don't-select via the
shared registry helpers, resolutions echoed
32 new propagation unit tests + 4 pg-real tests for the RPC migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: use roundOre in new dims rounding assertions (ratchet)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: copy dimension bag per payment line, document dimensionsBagKey normalization contract (review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
816b1769c8 |
feat(dimensions): PR6 retro-tagging — audited retag carve-out, BulkTagWorkbench, staged MCP tool (#867)
* feat(dimensions): PR6 retro-tagging — audited retag carve-out, workbench, staged MCP tool
Tier-2 retro-tagging (founder decision №1, approved 2026-07-02): posted
entries in OPEN periods can have their dimension tags changed through ONE
audited path — everything about the verifikat itself stays immutable.
Carve-out (migration 20260702170000): the line-immutability trigger gains a
single narrow branch — while the transaction-local GUC set by the RPC is
active, an UPDATE of a posted line is admitted iff every non-dimension
column is unchanged, enforced by a whole-row to_jsonb diff (any future
column is protected by construction; mirrors cost_center/project are in the
changeable set because they are derived views of dimensions['1']/['6']).
Precedent: mark_entry_as_opening_balance (20260613120000).
retag_line_dimensions RPC: tenant guard (20260619130100 pattern), writer
gate (viewers rejected), posted-only, open period + company lock date
enforced, every code validated against the ACTIVE registry, immutable
dimension_retag_log row (before/after/actor/reason, INSERT-only via its own
trigger, no FKs so the trail survives hard-deletes) written BEFORE the
carve-out UPDATE. Idempotent no-op without a log row. Untag ({}) supported.
Legal position per the plan: dimensions are internredovisning metadata, not
BFL 5 kap 7§ verifikat content — this is strictly more conservative than
Fortnox/Visma (dimension-only diffs, open periods only, immutable log,
storno past locks — Tier 3 has no exceptions).
Mandatory pg suite (11 tests): GUC-less updates still blocked; amounts/
description can never change even under the GUC (transaction-local);
closed/locked/lock-date, role, registry, draft and cross-tenant rejections;
log immutability; gnubok.allow_delete bulk path unaffected.
UX (all writes through the ONE RPC): pencil on posted-voucher lines in
bookkeeping/[id] ("Påverkar endast internredovisningen, inte verifikatet")
+ retag-history card; BulkTagWorkbench at /dimensions/tagging (filters,
shift-select, merge vs "Ersätt tagg" replace mode, reversal-pair warning
with "Inkludera motverifikat" auto-selection, per-line failure display).
MCP: gnubok_tag_journal_lines (bookkeeping:write) — filter block resolved
via resolve-don't-select, ≤500 lines, staged via pending_operations (new
op type migration 20260702171000, medium risk tier, shared Zod validation
boundary between staging and commit; executor loops the RPC per line with
partial-success aggregation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): address #867 review — SQLSTATE classification, blocking storno confirm, documented divergence
- Retag route classifies RPC errors by SQLSTATE instead of message-regex:
P0001 (every rule violation in the RPC) → 409 verbatim, 42501 (tenant
guard) → 403, anything else → logged 500 with a generic message. No more
substring sniffing.
- The workbench's storno-pair warning escalates to a BLOCKING confirmation
naming the unselected counter-vouchers before apply (Srf U 14 gross
reporting — one-legged retags silently skew project P&L; the banner alone
was advisory).
- The empty-bag divergence is now documented on both schemas as intentional:
the direct dialog/workbench path allows {} (human untags phantom codes,
logged with reason), the MCP staged path rejects it (agents never
bulk-clear history).
Triage notes: the log's missing FKs are the point (behandlingshistorik must
survive undo_sie_import hard-deletes — a cascade would erase the trail);
SIE exports are generated fresh on demand, never cached, so post-retag
exports carry the new object lists automatically; date-scoped registry
values are deliberately not enforced at retag because entry creation does
not enforce them either — enforcing in one path only would be incoherent
(both belong to the PR10 rules engine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8bb49c07a2 |
feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry (#858)
* feat(dimensions): PR2 registry — CRUD API, register UI, settings toggle, SIE export on the new registry
Phase 2 of dev_docs/dimensions_implementation_plan.md. Companies with
dimensions_enabled=false (default) see zero change.
API:
- Dashboard CRUD: GET /api/dimensions (lazy-seeds system dims 1/6 via the
ensure_company_dimensions RPC), PATCH /api/dimensions/[id] (is_system
rename blocked), POST/PATCH/DELETE values (code immutable after creation;
strict Fortnox code format ^[A-Za-z0-9ÅÄÖåäö_+\-]{1,20}$ at the API layer;
retention-trigger deletes surface the Swedish "arkivera istället" message
as 409 DIMENSION_VALUE_REFERENCED).
- POST /api/dimensions/import-existing — scans journal_entry_lines.dimensions
for unregistered codes and mints inactive placeholder registry rows.
- v1 public API: GET dimensions + POST values (Idempotency-Key, dry-run),
registered in the OpenAPI spec (102→104 endpoints).
- dimensions_enabled boolean on company_settings (new migration,
UI-visibility only, never correctness-bearing) exposed through the
existing settings read/update path.
SIE export (lib/reports/sie-export.ts):
- Reads the new dimensions/dimension_values registry; legacy
cost_centers/projects tables now have zero readers (drop migration next).
- Fixes the latent Visma-rejection bug: #OBJEKT now declared for INACTIVE
values referenced by lines.
- Generic-N: #DIM/#UNDERDIM loop sorted by sie_dim_no; #TRANS object lists
serialize from the line JSONB map (sorted, '01'→'1' collapse); orphan
codes/dims synthesize declarations from the SIE reserved-number seed —
every referenced (dim, code) pair is guaranteed declared.
UI:
- /dimensions register (Register-recipe): tabs per dimension, search,
sortable table, value dialog (code immutable on edit, projekt dates on
dim 6), archive-not-delete affordances.
- DimensionCombobox shipped (mounts in the tagging PR).
- Settings toggle "Aktivera kostnadsställen & projekt" — toggle-on runs the
import-existing scan and links to the register.
- Nav row in redovisning, rendered only when dimensions_enabled (same
mechanism as pays_salaries).
- dimensions.* i18n namespace (51 keys, sv/en parity).
- Sandbox seed: demo dims + values, revenue line tagged {"1":"BUTIK","6":"P001"}.
Verified: 6328/6328 unit tests, guard + coverage gate green, tsc parity with
main (210=210), production build passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): PR2 review round — atomic archived-create, UNDERDIM ordering, import robustness, date semantics
- POST values accepts is_active so "create as archived" is atomic; the UI's
fragile create-then-PATCH fallback is deleted (PR Agent finding 1).
- DimensionCombobox blur revert reads the committed value/values through refs
so a selection landing inside the 150ms window always wins (finding 2).
- import-existing sanitizes candidate codes like the PR1 backfill and upserts
with ignoreDuplicates — one bad/duplicate code can no longer abort the
batch; created counted from returned rows (finding 3).
- SIE export emits all root #DIM before any #UNDERDIM so a parent always
precedes a lower-numbered child (SIE4 declaration order — Swedish review);
synthesized placeholder declarations now log one structured warning
(BFNAR 2013:2 behandlingshistorik) + defence-in-depth comment.
- Value dates rejected (400 DIMENSION_VALUE_DATES_NOT_ALLOWED) when the
parent dimension is flow-period (resets_annually=true); explicit null
still clears (Swedish review).
- Sandbox seed logs seeded dimension codes; GET /api/dimensions documents
the deliberate absence of dimensions_enabled gating (UI-visibility flag,
not a security boundary — compliance-swarm V8.2.1 rejected by design).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8cc2efb083 |
feat(dimensions): PR1 substrate — SIE-native registry + dimensions JSONB on journal lines (#857)
* feat(dimensions): substrate — SIE-native registry + dimensions JSONB on journal lines (PR1)
Implements phase 1 of dev_docs/dimensions_implementation_plan.md:
- New company-native registry tables: dimensions (= SIE #DIM/#UNDERDIM,
seeded is_system 1=Kostnadsställe / 6=Projekt via ensure_company_dimensions,
nullable bare firm_id) and dimension_values (= #OBJEKT), full RLS incl.
DELETE, audit + updated_at triggers, guard triggers (system dims undeletable,
sie_dim_no immutable, values referenced by posted lines archive-not-delete).
- journal_entry_lines.dimensions jsonb NOT NULL DEFAULT '{}' as the single
source of truth ({sie_dim_no: object_code}), CHECK object-typed, GIN
(jsonb_path_ops) + partial expression indexes on dims 1/6. Inherits posted-
line immutability from the existing trigger with zero new triggers.
- Backfill: representation copy of legacy cost_center/project text into the
JSONB map (trigger-disabled, schema_sync precedent); legacy cost_centers/
projects registry rows copied into dimension_values; inactive placeholder
values for orphaned free-text codes.
- Dual-write: engine buildLineInserts + storno/correction/date-move now derive
cost_center/project mirrors from the map via lib/bookkeeping/dimension-resolver.ts
(normalizeLineDimensions / lineDimensionColumns); reversal copies dims.
- CreateJournalEntryLineInput + shared Zod line schema gain a dimensions bag
(cost_center/project stay as deprecated aliases); pending-ops voucher lines
coerce it.
- CI ratchet: direct-jel-insert check in no-new-antipatterns.mjs — inserts into
journal_entry_lines outside sanctioned writers fail CI.
- pg-real suite: registry RLS/guards/retention, ensure_company_dimensions
tenant guard, dims frozen on posted lines, CHECK enforcement (13 tests).
Non-breaking: companies without dimensions see zero change; no UI yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): address review findings — canonical keys, boundary-validated staged bags, migration guidance
- normalizeLineDimensions canonicalizes numeric keys ('01' -> '1') so
leading-zero keys can't split values or miss the cost_center/project mirrors
(PR Agent finding).
- New coerceDimensionsBag() in dimension-resolver is the single boundary
validator for untyped staged payloads, enforcing the same constraints as the
Zod line schema (string-only values, 1-40 chars, no SIE-framing chars,
canonical keys). pending-operations normalizeVoucherLines now uses it —
staged payloads can no longer bypass API-layer validation via numeric
coercion (compliance-swarm V2.2/V1.2.5/PI1.1, Swedish review finding 4).
- Migration backfill comment now spells out the exact conditions under which
the trigger-disable pattern is defensible (BFL 5:5 / BFNAR 2013:2) and what
a future reviewer must verify before reusing it (Swedish review finding 2).
- 10 new resolver tests incl. reversal-parity (empty bag + aliases ==
alias-only) proving the reverseEntry and storno paths normalize identically
(PR Agent finding 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dimensions): round-2 review — shared Zod schema, transactional backfill, empty-string guard
- DimensionsBagSchema now lives in dimension-resolver as the single source of
truth; CreateJournalEntryLineSchema and coerceDimensionsBag both delegate to
it, so the API layer and the staged pending-operations path provably cannot
drift (compliance-swarm V2.2). coerceDimensionsBag switches to whole-bag
semantics: any invalid entry rejects the bag, exactly like the API schema.
- Migration backfill now runs DISABLE TRIGGER / UPDATE / ENABLE TRIGGER inside
one transaction — the ACCESS EXCLUSIVE lock from ALTER TABLE holds until
COMMIT, so no concurrent writer can slip an unguarded line write into the
window during a live apply (compliance-swarm V1.2, Swedish review finding 1).
- NULLIF guard: empty-string legacy mirrors can no longer mint {"n":""}
entries the resolver would interpret as "cleared" (PR Agent round-2 edge).
- COMMENT ON dimensions.resets_annually documenting the SIE4 #IB/#OIB
semantics the PR2+ export path must honour (Swedish review finding 2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f63d3e3100 |
Bug/open banking flow (#854)
* fix(enable-banking): pin Mobile BankID (decoupled) auth_method so Handelsbanken corporate connects We never sent auth_method to Enable Banking, so it fell back to the ASPSP's visible default — REDIRECT for Handelsbanken. For Handelsbanken *corporate* PSUs the redirect flow does not support Mobile BankID, so authorization failed right after the user approved in the BankID app. Mobile BankID at Handelsbanken is a DECOUPLED method flagged hidden_method=true, which Enable Banking only uses when requested explicitly. Resolve the bank's preferred auth method before /auth: query the ASPSP's auth_methods and pick the DECOUPLED (Mobile BankID) method when present, otherwise leave auth_method unset so banks that already work are untouched. The method name is read dynamically per psu_type, so it is robust across sandbox/production naming. - api-client: add approach/hidden_method to AuthMethod, fix ASPSP.auth_methods field name (was available_auth_methods, never populated), add getPreferredAuthMethod(), thread optional authMethod through startAuthorization - index: resolve authMethod in /connect and pass it on both fresh + reconnect - tests: cover method selection and request-body shaping Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(invoice-inbox): clean up bulk-selection toolbar UI Redesign the selection toolbar shown when inbox items are checked: one solid primary "Bokför valda" button with outlined secondary actions ("Fråga assistenten", "Ta bort") and a plain selection count. Removes the redundant "Avmarkera" button (users uncheck the still-visible box), fixes label clipping, and gives the toolbar more breathing room. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(entitlements): bypass paywall in local development Add isPaywallBypassed() so all gated capabilities are testable locally without a subscription. Fires only on NODE_ENV=development (npm run dev) or an explicit DISABLE_PAYWALL=true escape hatch — production builds run under NODE_ENV=production and the entitlement suite runs under 'test', so both keep exercising the real gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tic): resolve enskild firma bolagsuppgifter via 12-digit personnummer TIC's Lens search is fuzzy and only resolves an enskild firma from the 12-digit (century-prefixed) personnummer; a 10-digit form fuzzy-matched an unrelated entity. Expand personnummer to 12 digits before querying and reject hits whose registration number is unrelated to the request. Add a "Hämta" action to the settings Bolagsuppgifter panel to (re)fetch on demand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): implement categorize core for bank transaction categorization - Added `categorize-core.ts` to handle categorization of bank transactions, supporting single and bulk operations. - Introduced `categorizeMatchedTransaction` and `bulkBookMatchedInboxItems` functions for transaction processing. - Implemented fiscal period validation and duplicate booking detection. - Enhanced logging and error handling for transaction categorization. feat(scripts): add diagnostic script for Handelsbanken ASPSP metadata - Created `check-handelsbanken-aspsp.mjs` to fetch and display available authentication methods for Handelsbanken. - Outputs metadata for business and personal PSU types, including default authentication methods. fix(migrations): increase statement timeout for SIE bulk delete operations - Updated `20260629160000_sie_bulk_delete_statement_timeout.sql` to set a longer statement timeout for bulk delete RPCs to prevent cancellations during large imports. feat(migrations): add bulk book inbox items to pending operations - Expanded `pending_operations` table to include `bulk_book_inbox_items` operation type in `20260630120000_pending_operations_add_bulk_book_inbox_items.sql`. - Supports bulk booking of matched inbox items against bank transactions. test(pg): add tests for replace_period_opening_balance_link RPC - Implemented tests in `replace-period-opening-balance-link.pg.test.ts` to validate the functionality of the opening-balance correction flow. - Ensured immutability of opening balance links and proper handling of posted vs. non-posted entries. * fix(sie-export): update journal entries and lines handling in SIE export tests * fix(migrations): resolve version collision on 20260629160000 The SIE bulk-delete statement_timeout migration shared version 20260629160000 with journal_entries_list_series_filter (merged from main via #798/#823), causing a schema_migrations_pkey duplicate key error on apply. Rename the branch's migration to 20260629160100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(compliance): resolve compliance-swarm + review findings - opening-balance/correct: compensating rollback for the non-atomic storno+rebook so a mid-sequence failure never leaves two posted OB entries (ASVS V2.3); durable audit event on every failure path (V16); reference the original verifikationsnummer in the corrected entry per BFL 5 kap 5§; document that requireWrite already enforces write-role + membership (V8.2.1 was a false positive) - reports sources routes: validate the cursor date component as ISO (/^\d{4}-\d{2}-\d{2}$/) before use, 400 on malformed (ASVS V1.2), applied to both the VAT-declaration and trial-balance routes - AgentSessionList: await the rename PATCH, revert the optimistic title and toast on failure (ASVS V4.5) - bank booking: exclude same-batch siblings from the booking-time duplicate guard so bulk-booking distinct same-(date,amount) transactions no longer false-positives; pre-existing duplicate detection is preserved - BulkBookInboxDialog: drop the unsafe currency-based reverse_charge default, add an omvänd skattskyldighet advisory, and type VAT options to the backend VatTreatment union - OpeningBalanceRowEditor: hold onChange in a ref (synced in effect, not during render) so an unstable callback can't cause a render loop Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5b4cefe8ab |
feat(api): v1 endpoints to stamp invoice inbox items as consumed (#767)
* 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>
* fix(api): wrap stamp response in dataEnvelope and register route in load-routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Jonas Flodén <jonas@floden.nu>
---------
Signed-off-by: Jonas Flodén <jonas@floden.nu>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
809120c4b8 |
Bug/document linking (#688)
* feat: enhance supplier invoice payment process and settings handling - Implemented linking of invoice documents to journal entries for cash payments in the supplier invoice payment process. - Refactored settings fetching logic to improve loading states and error handling across various settings components. - Introduced a new SettingsLoadError component to handle cases where settings fetch fails or returns no data. - Updated useSettings hook to manage loading and error states more effectively, allowing for retries on failure. - Enhanced tests for supplier invoice creation to ensure document IDs are persisted correctly for cash method payments. * feat(salary): enable monthly salary edits in draft runs and handle zero-total declarations |
||
|
|
0ca9c25aba |
Add/user feedback (#679)
* feat(bookkeeping): make blocked fiscal-year creation actionable When creating a new räkenskapsår is blocked because a prior period is still open, the "Skapa räkenskapsår" dialog no longer dead-ends on an English toast. The API now returns the canonical bilingual error envelope with the blocking periods (id/name/dates) under details, and the dialog renders a Swedish panel that locks them inline (reversible locked_at) via the existing /lock endpoint and retries creation. The guard rule is unchanged and remains BFL-compliant: BFL 6 kap allows löpande bokföring of the new year in parallel with the prior year's bokslut, so a lock (not a full close) is sufficient and reversible. - Add PERIOD_CREATE_BLOCKED_BY_OPEN_PERIODS structured error code - Return envelope + details.blockingPeriods from the 409 (was English string) - CreatePeriodDialog: inline "lås och skapa" panel + lock-and-retry - Update route tests for the new envelope shape Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): prevent mouse wheel from mutating number inputs A focused <input type="number"> would change its value on scroll, silently turning e.g. a 20000 salary into 19998. Blur number inputs on wheel so the page scrolls instead of editing the value. Applied at the Input primitive so all number fields are protected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(salary): auto-derive skattetabell and kolumn for employees Replace the opaque manual "Skattetabell (29-42)" and "Kolumn (1-6)" inputs on the employee form with a self-deriving flow: the user picks their folkbokföringskommun from a searchable dropdown and the tax table fills itself in, while the column derives from the personnummer we already collect. - Add a searchable municipality picker (MunicipalityCombobox) backed by a new cached GET /api/salary/tax-tables/kommuner endpoint. - Wrap the whole "Skatt" card in a self-contained EmployeeTaxCard used by both the create and edit pages, with InfoTooltips and named column options. - deriveTaxColumn(): auto-select column 1 for under-66 employees; leave the ambiguous 66+ case (pension vs working senior) to a clearly-named manual choice. - Fix fetchKommunTaxRates() to page through all ~1300 församling rows instead of a single 500-row page (which silently dropped ~200 kommuner, incl. Göteborg) and normalize the uppercase names to title case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): correct CSV amount-column guess and surface skipped rows Manual CSV column-mapping auto-guess walked each data row right-to-left and picked the first numeric cell as the amount, so on the common ...;Belopp;Saldo layout it grabbed the trailing running-balance column. Extract the guess into a pure, tested suggestColumnMapping(): match header labels first (belopp/amount -> amount, saldo/balance -> balance), auto-fill the balance field, and fall back to value heuristics that skip the balance column and prefer a column carrying negative values. Also surface stats.skipped_rows + parse warnings in BankFileConfirmStep - the manual-mapping path skips the preview step that was the only place they showed, so skipped rows were silently dropped from view. Add a unit test reproducing the Saldo-as-amount regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: add "Save as draft" functionality for invoices - Implemented a new feature to allow users to save invoices as unnumbered drafts without generating an invoice number until finalized. - Added a `save_as_draft` flag to the CreateInvoiceInput schema to handle draft saving logic. - Updated the invoice creation API to skip number allocation when saving as a draft. - Introduced a new endpoint for finalizing drafts, which allocates an invoice number and emits an `invoice.created` event. - Enhanced the UI to include a "Save as draft" button, with loading states and tooltips. - Updated tests to cover the new draft saving and finalization logic, including race conditions for concurrent modifications. - Added relevant error handling for draft finalization and deletion scenarios. * feat(employee): add employment start and end date fields to employee forms * feat: enhance invoice and salary run handling with improved validation and event logging --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4a54467599 |
Bug/transaction date corruption (#668)
* fix(transaction): enforce valid date range for transactions and add database constraint * fix(transaction): implement server-side validation for transaction dates and enhance error handling |
||
|
|
3e42fc6f32 |
Feat/voucher docs (#664)
* feat: implement inbox document picker and linking functionality * feat: implement self-billing invoice functionality - Added support for registering self-billed invoices received from customers. - Updated the invoice schema to include fields for self-billing metadata such as `is_self_billed`, `external_invoice_number`, `self_billing_agreement_ref`, and `received_date`. - Created API route for handling self-billed invoice submissions, including validation and error handling. - Implemented database migrations to add necessary columns and constraints for self-billing invoices. - Developed tests to ensure correct behavior of self-billing invoice creation and validation rules. - Updated Swedish localization files to include new terms related to self-billing. * feat: enforce SIE import requirement for non-Fortnox providers in migration process * feat: streamline invoice processing and enhance error logging across APIs |
||
|
|
0b86901a2b |
Enforce MFA on critical mutation routes + post-audit foundation (A1) (#646)
* feat(lib): add canonical money + format + fetch primitives (audit Tier 0) Foundation for post-audit cleanup: shared primitives so subsequent refactors import one helper instead of reinventing (the duplication the audit found). - lib/money.ts: canonical roundOre/ORE_TOLERANCE (+ equalOre/isZeroOre/sumOre); lib/bokslut/rounding.ts re-exports for back-compat - lib/utils.ts: formatAmount, formatWholeKr, formatDateTime - lib/hooks/use-fetch.ts: generic client fetch hook (abort, bilingual errors, refetch) - components/common/DataState.tsx: loading/error/empty wrapper over Skeleton/EmptyState - messages: common.retry / common.load_error (sv+en) - tests: 16 tests incl. the 1.005 half-ore case and locale-robust format assertions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(guards): ratchet against new MFA-bypassing routes and naive ore-rounding Adds scripts/checks/no-new-antipatterns.mjs + committed baseline. Fails CI only when a PR ADDS a route hand-rolling supabase.auth.getUser() (which skips MFA AAL2 enforcement) or a new Math.round(x*100)/100. Baseline: 178 raw-auth routes, 668 naive rounds — ratchets down as the A1 (route-auth) and D1 (rounding) migrations land. Wired into core-build.yml; green at baseline. Note: scripts/ is gitignored (.gitignore:70 '/scripts') yet tracks 39 files via force-add; these two were force-added to match that existing pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api,errors): enforce MFA on journal-entry mutation routes via withRouteContext (A1) Migrates the 4 journal-entry mutation routes (commit, correct, reverse, recordate) off hand-rolled supabase.auth.getUser() onto withRouteContext, which enforces MFA AAL2 (requireAuth) + non-viewer role (requireWrite) and routes thrown errors through the canonical errorResponse envelope. Fixes audit finding A1 for the most compliance-critical mutations and folds in C8 for these routes (drops bookkeepingErrorResponse; they now emit message_en). Also fixes a latent bug: errorResponse()/extractBookkeepingDetails only handled 11 of 15 typed bookkeeping errors, so MeaninglessCorrection / NoOpenPeriodForDate / TargetPeriodClosed / TargetPeriodLocked silently degraded to a generic 500 (affecting existing v1 callers too). Adds the 4 missing registry codes + extract cases -> correct 400/409. Behavior change: untyped engine throws now return the canonical 500 envelope instead of 400+raw-string; typed errors keep their status (verified against the registry). Tests updated to the realistic typed-error contract + a 403 write-gate test on commit. Updates .claude/rules/api-routes.md to prescribe withRouteContext. Ratchets the antipattern guard 178 -> 174. Full unit suite green (5023); tsc: no new errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(api): enforce MFA on salary run authorization routes via withRouteContext (A1) Migrates the salary-run lifecycle write routes (approve, paid, revert) — the highest-PII A1 surface — off hand-rolled supabase.auth.getUser() onto withRouteContext (enforces MFA AAL2 + non-viewer role). Explicit { error } returns are preserved unchanged (passed through the wrapper); only auth changes, so no error-shape regression. Salary unit suite green (8). Ratchets the antipattern guard 174 -> 171. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * review: address PR #646 bot findings - guard: match withRouteContext/requireAuth at the CALL site (withRouteContext[<(]), not a bare import — closes the false-negative greptile flagged. It surfaced app/api/sandbox/seed (hand-rolled getUser; the loose regex had matched a code comment). Switched that route to requireAuth() — the documented stopgap for routes that can't use withRouteContext (it runs before a company exists; anonymous users, so MFA is a no-op but the auth path is now consistent). Guard stays at 171. - money.test: add the negative half-ore case roundOre(-1.005) === -1 to lock the rounding direction against regressions. - use-fetch: document keep-previous-data + deferred-loading (effect-tick) semantics. - structured-errors: drop the BFL 5 kap. 5 § citation from MEANINGLESS_CORRECTION per the swedish-compliance bot (5 § governs correction procedure, not the no-op precondition). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * review: enrich wrapper error logging + document sandbox GDPR controls (PR #646) - with-route-context: log unhandled errors and route errorResponse through the resolved { userId, companyId } logger, not just { requestId, operation } — closes the OWASP V16 audit-trail finding for all 82+ routes using the wrapper. Documented in the JSDoc. - sandbox/seed: document the GDPR Art.32 compensating controls for the anonymous write path (anonymous-only, /24 rate limit, synthetic demo data, own-company RLS scope). No functional change — the flagged behaviour is pre-existing by design; this records the reasoning inline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c74b19df1b |
Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f6ee0c2a82 |
Bug/customer invoice bug (#628)
* fix(supplier-invoices): self-assess reverse-charge VAT + link payments to vouchers Reverse-charge supplier invoices now carry a per-item reverse_charge_rate (0.06/0.12/0.25). Under omvänd skattskyldighet the supplier charges 0% VAT, so the line vat_rate stays 0 and the buyer self-assesses fiktiv moms at the statutory rate. Centralizes rate resolution (resolveReverseChargeRate) and the ruta 20-24 basis-account guard (isReverseChargeBasisAccount) in vat-entries so the booking engine and review-dialog preview can no longer drift. Adds the link_supplier_invoice_voucher pending operation: mark a leverantorsfaktura paid by linking an existing posted verifikat that debits 2440, with no new journal entry. Exposes find-candidates/link MCP tools and the bulk-reconcile helper, scoped under suppliers:read/write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(vat): report yearly VAT over the rakenskapsar, not the calendar year Annual VAT (helarsmoms) is filed per beskattningsar/rakenskapsar (SFL 26 kap), which can be extended or shortened up to 18 months. The previous Jan-Dec calendar span silently dropped part of an extended first year. calculateVatDeclaration now accepts a fiscalPeriodId and resolves the period's actual bounds for yearly; monthly/quarterly stay calendar. The reports UI passes the selected fiscal period, defaults the periodicity from the company's moms_period setting, and carries the period into the ruta drill-down. full-archive export threads the period id through too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migration): resolve supplier invoice status from payment amounts The provider's lifecycle status and its payment status are computed independently upstream and can contradict each other (e.g. a Fortnox invoice marked booked but fully paid). Both the arcim entity-mapper and the Fortnox mapper now let payment state win: fully paid -> paid, partial -> partially_paid, otherwise the mapped lifecycle status, with credit notes forced terminal. Balance is compared numerically (never strict === 0) so float drift or a residual ore resolves cleanly, and an absent Balance is treated as unpaid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(enable-banking): only ingest booked transactions to stop re-import drift Pending entries are skipped during sync: a pending row is unstable across syncs (a later 'synka nu' returns it still pending or finally booked, often with a different effective date). Because both the dedup external_id and the content-dedup key are date-derived, that drift minted a new id and re-imported a transaction that already existed - observed in production as the same amount+description landing twice with different dates. Gating the import set on a stable booking_date removes the drift at the source and leaves booked rows' ids byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(gitignore): ignore local SIE test fixtures tests/fixtures/sie/ may contain real or scrubbed company data and must never be committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoice): handle errors during registration journal entry creation and ensure invoice rollback feat(tests): add test for reverse charge rate handling on supplier invoice line items feat(fortnox): ensure paid status reflects zero balance for fully paid invoices chore(migrations): add reverse_charge_rate to supplier_invoice_items and backfill link_supplier_invoice_voucher --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
953980c875 |
Per-account bank reconciliation + overdue/inbox/privacy fixes (#619)
* feat(reconciliation): scope bank reconciliation per cash account via transactions.cash_account_id A company with two same-currency cash accounts (e.g. checking 1930 + a savings account) saw every SEK transaction on every account, and the status card summed across both — reconciliation filtered transactions by CURRENCY while filtering GL lines by ACCOUNT (issue #604). Bind each bank transaction to the cash_accounts row it settled on: - New nullable transactions.cash_account_id FK (ON DELETE SET NULL — a bank transaction is räkenskapsinformation, BFL 7 kap, and must survive cash-account deletion) + a best-effort 4-pass backfill. - All reconciliation/transaction queries scope to the selected account with a NULL->currency fallback, so legacy/un-backfilled rows never disappear mid-backfill. - ingestTransactions stamps cash_account_id from the batch's settlementAccount; categorize + manualLink resolve and use it. - Bank leg now books to the transaction's actual settlement account via applySettlementAccount (no-op for 1930), so interest/fees on a savings/EUR account reconcile instead of mis-booking to 1930. - manualLink cross-checks the transaction's account and requires a voucher line on the selected account (no silent cross-account links). - BankReconciliationView: quick-book menu for any settlement account, in-flight request abort on account/date switch, 500-row truncation notice, per-account state reset. - pg-real coverage for the FK, all backfill passes, account-scoped query isolation, and cross-company isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): stop marking paid invoices and credit notes as overdue update_overdue_supplier_invoices() (the daily pg_cron job) flipped every past-due 'registered'/'approved' row to 'overdue' without looking at the outstanding balance. Credit notes — created 'registered', remaining 0, due today — got flipped the next day, surfacing as "Förfallen" with "kvar att betala 0 kr"; so did any fully-paid invoice left in 'registered'/'approved'. Guard the cron on remaining_amount > 0.005 (the "fully paid" threshold used by the payment/match paths) and is_credit_note = false, and backfill the rows already mis-flagged (credit notes -> 'registered', paid -> 'paid' with paid_at stamped only when missing). pg-real coverage for the guarded function and the one-off backfill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): refresh dokumentinkorg on realtime row changes The InvoiceInboxWorkspace only refetched on mount and on explicit in-component actions. When an inbox item was resolved out of band — the in-app agent sheet committing a staged create_supplier_invoice_from_inbox / book-direct op, the /pending page approving one, or another tab booking it — none of those paths called fetchItems(), so the booked underlag stayed in "Att göra" until a manual reload (issue #600). Add invoice_inbox_items to the supabase_realtime publication (mirrors the /pending fix in 20260520120100) and subscribe in the workspace, refetching the whole list on any change so derived status/counts/ordering stay authoritative. RLS scopes the channel to the user's company. fetchItems now preserves optimistic upload placeholders so a refetch firing mid-upload can't drop an in-flight row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(privacy): disclose EU AI inference via Amazon Bedrock (eu-north-1) Update the privacy policy and DPA to state that AI inference, when AI features are enabled, runs inside the EU via Amazon Bedrock (eu-north-1, Stockholm) using Anthropic's Claude models — no transfer to a third country, prompts not retained after the call or used for model training. Add AWS as a subprocessor row and refresh the "last updated" dates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): rename invoice_inbox_realtime to avoid version collision main's #617 shipped 20260605120000_transactions_original_description.sql — the same version this branch used for the inbox-realtime publication. The Supabase migration tracker keys on the numeric version, not the filename, so the preview branch failed with a duplicate-key error on supabase_migrations.schema_migrations (version 20260605120000 already exists). Rename to the unique version 20260605120500; the body (ALTER PUBLICATION) is order-independent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): align run guard with status; harden filter interpolation Addresses PR review (greptile + compliance swarm): - The v1 and core bank/run routes rejected an unknown account uniformly, including the default '1930', while the status routes were lenient for '1930'. A company reconciling its primary SEK account without a cash_accounts row got 200 from status but 400 from run. Make run match status: '1930' falls back to currency-only scoping (cashAccountId undefined); non-default unknown accounts are still rejected. Adds a test. - /api/transactions accepts a user-supplied `currency` query param that was interpolated raw into a PostgREST .or() filter. Reject anything that isn't a 3-letter ISO code — RLS already scopes to the company, but an unsanitized value could otherwise malform/widen the filter. Assert currency/cashAccountId shape in scopeTransactionsToAccount as well. - categorize: log (instead of silently swallowing) a cash_accounts settlement-account lookup error, so a fall-back-to-1930 mis-booking is observable in the audit log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): correct backfill UPDATE..FROM join; idempotent realtime publication Two SQL errors that only surface on real Postgres (CI pg-real + Supabase preview) — the unit suite mocks Supabase, so neither was caught locally. - Backfill pass (a): `UPDATE transactions t ... FROM journal_entry_lines jel JOIN cash_accounts ca ON ca.company_id = t.company_id` referenced the UPDATE target `t` inside the FROM join's ON clause, which Postgres rejects ("invalid reference to FROM-clause entry for table t"). Move the company match to WHERE; the JOIN now relates jel<->ca only. Semantics unchanged. - invoice_inbox_realtime: `ALTER PUBLICATION ... ADD TABLE` is not idempotent (SQLSTATE 42710 if the table is already a member). The earlier version-collision push partially applied it on the Supabase preview branch, so the re-apply errored. Guard with a pg_publication_tables existence check. Both statements validated against a real Postgres: the single-line tx binds, the two-bank-line transfer stays NULL, and the publication add runs twice cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): backfill pass (c) uses array_agg, not min(uuid) Postgres has no min() aggregate for uuid, so pass (c)'s min(id) raised "function min(uuid) does not exist" on apply (CI pg-real + Supabase). The HAVING count(*) = 1 already guarantees one row per group, so (array_agg(id))[1] returns that single id. Validated the full backfill (all four passes) and the overdue migration against a real Postgres: every pass binds / falls through as intended, and the overdue guard + backfill produce the right statuses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(compliance): add RoPA entry for Amazon Bedrock AI inference (GDPR Art.30) The privacy policy now discloses AI inference (transaction categorization + document/receipt OCR) via Amazon Bedrock as a processing activity, but .compliance/ropa.yaml had no matching Art.30 record. Add it: opt-in consent basis, EU-region (eu-north-1) inference with no third-country transfer, prompts not retained or used for model training. Mirrors the privacy-page disclosure shipped 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> |
||
|
|
c6c86cded4 |
Mcp/template data feedback (#617)
* fix(booking-templates): scope template list to the active company GET /api/settings/booking-templates relied solely on the btl_select RLS policy, which is membership-wide (user_company_ids) and returns templates from every company the user belongs to. A user who owns multiple companies saw all their templates merged regardless of which company was active. Narrow the list in the API layer (mirroring counterparty-templates) to system + the active company + the active company's team. RLS stays the security backstop; this fixes the cross-company merge within a single user's own view (it was never a cross-tenant data leak). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): show proper message for duplicate bank file upload The bank file import page mis-parsed the structured error envelope ({ error: { code, message, details } }), so a BANK_FILE_DUPLICATE (409) fell through to the generic "Kunde inte läsa filen" fallback. The upload step also hardcoded that same string as the error heading, so duplicates were doubly misreported as parse failures. - Parse the structured envelope by error.code; surface error.message for all codes instead of rendering the error object. - Add a dedicated BANK_FILE_DUPLICATE message using the importedAt / importedCount details the route already returns. - Add an optional errorTitle prop to BankFileUploadStep (defaults to the previous text) and pass "Filen är redan importerad" for dupes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tests): add comprehensive tests for recordateEntry, inbox-linking, and external-id handling - Implemented unit tests for recordateEntry in the bookkeeping module to validate various scenarios including date changes, non-posted entries, and fiscal period restrictions. - Created tests for inbox-linking status in pending operations to ensure correct handling of invoice inbox items and supplier invoices, addressing historical bugs related to status updates. - Added tests for external-id utilities to ensure consistent handling of monetary amounts and deduplication keys across different transaction sources. - Introduced new functions in external-id.ts for stable external ID generation and normalization of imported descriptions, enhancing transaction deduplication reliability. feat(migrations): add new database migrations for transaction handling - Created migration to exclude storno and correction vouchers from unmatched GL lines, ensuring accurate reconciliation. - Added a migration to preserve original bank transaction descriptions in a new immutable column, allowing for user edits while maintaining audit trails and deduplication integrity. * feat(migrations): add function to exclude storno/correction vouchers from unmatched GL lines * feat(transactions): enhance transaction handling with improved description normalization and preloaded original entries --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2c59c3633f |
feat(invoices): cross-currency settlement + payment-status card (#615)
* feat(invoices): cross-currency settlement + payment-status card Two changes both surfaced by user feedback after PR #614: # 1. Invoice detail page: Betalningsstatus card The customer-invoice detail page now shows paid_amount + remaining_amount + the individual payment events whenever an invoice is partially_paid or paid (was previously only a single "Paid" line on fully-paid invoices, and nothing at all on partially_paid). Mirrors the supplier-invoice page's payment section. Each payment row links to its verifikat. # 2. Cross-currency match-invoice settlement Replaces the PR #614 round-9 block (MATCH_INVOICE_CURRENCY_MISMATCH) with proper FX-aware settlement. Flow: 1. Preview route detects tx.currency !== invoice.currency, fetches the Riksbanken spot rate for invoice.currency on tx.date (ML 8 kap 21–23§), and returns fx_conversion = { rate, rate_date, paid_in_invoice_currency }. When the lookup fails it returns fx_conversion.error = 'rate_unavailable'. 2. InvoiceMatchDialog renders a new Valutaomräkning card showing the rate + invoice-currency-equivalent + projected post-payment state + a one- line kursvinst/kursförlust note. When the lookup failed it swaps in a manual-rate input the user fills from their bank statement; the Confirm button blocks until a positive rate is supplied. 3. POST route does the same lookup (or accepts manual_exchange_rate from the request body), then: - paidInInvoiceCurrency = bankSek / rate (4dp precision) - invoice.paid_amount/remaining_amount accumulate in invoice currency - invoice_payments row records amount + currency = invoice.currency, exchange_rate = the rate actually used (not invoice.exchange_rate) - buildInvoicePaymentClearingLines gets paidInInvoiceCurrency so it credits 1510 by that × invoice.exchange_rate (booking rate) and posts the FX-diff line on 3960 (gain) or 7960 (loss) 4. buildInvoicePaymentClearingLines gains an optional fourth param. When supplied: proportional FX-aware AR-leg + balanced FX-diff. When omitted: pre-existing fallback (full-clear gets FX, partials defer). The change fixes the invoice.paid_amount accumulator bug that PR #614 round-9 worked around by blocking the case entirely. Now SEK→USD settlements actually work, with the verifikat balanced to the öre and the GL+sub-ledger in sync per BFL 5 kap 4–5§. Tests: - 3 new helper tests (paidInInvoiceCurrency happy path + edge cases) - 3 new route tests (Riksbanken happy path, lookup failure, manual rate) - All 4321 tests pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoices): align cross-currency match preview with commit + review cleanups Addresses PR #615 review feedback. Preview/commit divergence (Greptile P1): preview/route.ts computed paidAmount / isFullyPaid / useCashEntry from the raw SEK transaction.amount before the FX conversion ran. A 1 000 SEK payment against a 140 USD invoice made max(0, 140 − 1000) = 0 → is_fully_paid=true, so a cash-method unbooked invoice previewed a cash entry (Dr 1930 / Cr 30xx) while the POST handler — which converts first — commits the clearing entry (Dr 1930 / Cr 1510). The user approved one verifikat and a different one was booked. Move the FX lookup above the paid/remaining math so paidAmount derives from the invoice-currency conversion, mirroring the POST handler. Rate-unavailable stays non-fully-paid so the cash shape is never previewed on a guess. Add a preview-route regression test (cross-currency → clearing + not fully paid; same-currency cash path still previews the cash entry). Cleanups: - Bound manual_exchange_rate with .max(100000) as a sanity ceiling against pasted/garbage input corrupting the FX-diff posting (swarm V2.3). - Remove the invisible disabled placeholder retry button and its unused fx_manual_rate_retry i18n keys (Greptile P2). - Remove the now-unreachable MATCH_INVOICE_CURRENCY_MISMATCH error code (Greptile P2 dead code; confirmed zero references). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): record FX rate provenance + cover kursförlust path Follow-up to the PR #615 review (compliance swarm V16 / SOC 2 CC6.1 / GDPR Art.5(1)(f); Swedish accounting review). A manually-supplied cross-currency rate is a user-controlled money-path override of the ML 8 kap 21–23§ obligation and was indistinguishable from an automatic Riksbanken lookup in the audit trail. Tag the resolved rate with source: 'manual' | 'riksbanken' and: - write a "Manuell valutakurs <rate> <ccy>/SEK (betalningsdatum …)" note onto the existing invoice_payments.notes column when manual (BFL 5 kap 6–7§ — the verifikation must reflect the actual affärshändelse); - record rate_source + exchange_rate in payment_match_log.new_state. No schema change — both are existing columns/JSON. Tests: - cover the kursförlust (7960 Dr) branch of the cross-currency paidInInvoiceCurrency path — previously only the 3960 gain was asserted; - assert rate_source provenance ('manual' and 'riksbanken') reaches the match-log new_state on both FX paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
28f7cefc86 |
feat(bulk-book): manual booking mode + document inheritance (#610)
* feat(bulk-book): manual booking mode + document inheritance Two pieces of user feedback from PR #606: 1. "How come it is only mallar? Is it not possible to have manuell bokfoering?" - BulkBookDialog was template-only. Added a Tabs primitive with Mall / Manuell tabs. Manual tab pre-fills lines from the selected txs (one line per tx on 1930 + counterparty placeholder on 3001/5800 by direction), then the user edits Konto / Debet / Kredit / Beskrivning. Live balance + bank-leg checks drive the confirm button - same invariants the RPC enforces server-side. 2. "Documents attached does not follow into the bookkeeping. And if there are two different documents attached, none of them follow." The bulk_book_transactions RPC now propagates each tx's document onto the target verifikat (new in Branch B, existing in Branch A) as verifikationsunderlag. Per BFL 5 kap 6§ + BFNAR 2013:2 kap 4 a verifikat may have multiple underlag; every receipt that justified a tx is now retention-protected on the combined entry. The dialog shows a small count chip ("N bilagor foeljer med") so the user sees what will inherit. Also dropped p_user_id from the RPC signature (round-3 hardening pattern applied consistently across all multi-tx RPCs after PR #607). Caller resolves from auth.uid() inside the function. Schema: BulkBookSchema is now a 3-way XOR (existing_journal_entry_id | template_id+mode | manual_lines), with manual_lines validated as accountNumber + nonNegativeAmount per line. pg-real tests: - doc inheritance into a new combined verifikat (mixed: 2 of 3 txs have docs - docs_linked should be 2, not 3) - doc inheritance into an existing posted verifikat (link branch) - manual lines path (no template expansion artifacts in the resulting JE - just the 2 user lines) - unbalanced manual lines still rejected by BULK_BOOK_UNBALANCED Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 review - pg-real signature, account allowlist, account-number validity Three review findings on PR #610: 1. pg-real failure: 2 link-existing tests still used 5-arg SELECT bulk_book_transactions($1::uuid[], $2, $3, $4, $5) after the userId removal. My earlier replace_all caught only the patterns that had ::jsonb on $3; the link-existing tests pass null for new_entry and used a bare $3 so they slipped through. (Greptile P1) 2. Manual lines bypassed chart_of_accounts validation. A typo or adversarial caller could post to a BAS account that doesn't exist in this company's chart, corrupting the hauptbok and breaking SIE export. Both compliance-swarm (OWASP V2.3) and swedish-compliance flagged this. Added a single-roundtrip allowlist check in the route: query chart_of_accounts for distinct account_numbers in manual_lines and reject with BULK_BOOK_INVALID_ACCOUNT if any are missing or inactive. 3. UI canConfirm guard missed invalid account numbers. Account input allows 1-3 digits and JS string comparison '193' >= '1900' is false, so a 3-digit entry escapes bankLineNet, the bank match could pass via other lines, and the server returned 400 only after submit. Added previewLines.every(l => /^\d{4}$/.test(l.account_number)) to canConfirm so the Confirm button stays disabled inline. (Greptile P2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 round 2 - RPC chart-of-accounts, doc tenant isolation, GRANTs Seven compliance findings from the round-1 bot reviews: Migration (20260602121000_bulk_book_round2_fixes.sql): - RPC chart-of-accounts allowlist (defense-in-depth): every line in p_new_entry.lines is now verified to be an active BAS account for p_company_id. Closes the gap where the template branch and direct DB callers (psql, future MCP) bypassed the route's manual-branch check. Returns BULK_BOOK_INVALID_ACCOUNT with the offending list. (OWASP V8.2.1 + SOC 2 CC6.3) - Document inheritance CTE: added "AND d.company_id = p_company_id" to the UPDATE join so the tenant isolation is enforced on both sides (tx + doc), not just the tx side. Four bots converged on this finding (V1.2.5, A.8.2, CC6.6, swedish-compliance). - Bank-leg range check: "length(account_number) = 4 AND account_number BETWEEN '1900' AND '1999'" replaces the bare lexicographic comparison. Lexicographic-on-4-digit is safe today; the length guard is defense-in-depth against schema drift. (swedish-compliance) - Explicit role grants: REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO authenticated on both bulk_book_transactions and match_batch_allocate. (SOC 2 CC6.1) UI (BulkBookDialog): - Manual-mode prefill no longer suggests a hardcoded 3001/5800 counterpart. Reason (swedish-compliance): a user accepting the prefill could submit a verifikat with no VAT line (26xx), under-reporting utgaaende moms. The bank side stays pre-filled (unambiguous); the counterpart row scaffolds blank for the user to choose. Schema (BulkBookSchema): - manual_lines.debit_amount + credit_amount bounded at 99,999,999 SEK per line. Catches typos before the RPC. (compliance-swarm V4.5) i18n: - docs_inherit_hint terminology: "bilaga" -> "verifikationsunderlag" and an explicit "sparas i 7 ar enligt BFL 7 kap" reminder. swedish-compliance flagged that "bilaga" risks users treating the files as deletable attachments rather than retention-bound raekenskapsinformation. Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): seed chart_of_accounts in bulk-book pg-real seedTenant The round-2 RPC fix added a chart_of_accounts allowlist check inside bulk_book_transactions, but the test fixtures don't seed COA — so every existing test that submits lines (1930, 3001, 2611, etc.) now returns BULK_BOOK_INVALID_ACCOUNT instead of the expected error code. Seed the 8 accounts the suite actually uses directly in seedTenant (cheaper than calling seed_chart_of_accounts which inserts the full BAS 2026 chart). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ea1bf01f1e |
Fix/m sprint fixes (#613)
* fix(dashboard): exclude ignored and already-triaged transactions from stale count The "Gamla transaktioner" widget counted transactions that had been ignored or already marked as is_business=true but not yet booked, so users saw a nag for a row they had already dealt with — and the /transactions inbox correctly hid it. Align the count with the inbox criterion (is_business IS NULL, is_ignored = false) so the widget clears when the row leaves the inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): read entity_type from settings response wrapper The transactions page read entityRes.entity_type directly, but /api/settings returns { data: { entity_type, ... } }. The expression was always undefined, so setEntityType never fired and entityType stayed at its initial 'enskild_firma'. The template picker's entity_type filter then dropped every aktiebolag-tagged user template for AB customers — only entity_type='all' templates made it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale templates bank sync journal entry from transaction * fixed pr comments * fixed pr comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4da87e5e4c |
feat(transactions): bulk-book + is-booked predicate (#606)
* feat(transactions): bulk-book + is-booked predicate Closes the second of the two multi-tx ↔ multi-voucher flows from the original plan. Where PR #603's match_batch_allocate took 1 tx and spread it across N invoices (samlingsbetalning), this PR takes N bank transactions on the same day and rolls them up into ONE combined verifikat (samlingsverifikation per BFL 5 kap 6§ st 3) — the kiosk masshantering pattern the user explicitly asked for. ## Backend (Phase 3b) - **PL/pgSQL RPC** bulk_book_transactions: two branches, both atomic. 1. Link to existing posted verifikat (p_existing_journal_entry_id): no new JE. Validates the JE's 19xx net equals sum(tx.amount), inserts N transaction_voucher_links rows, and for N=1 also sets transactions.journal_entry_id (1:1 reader-path back-compat). 2. Create new combined verifikat (p_new_entry with pre-computed balanced lines): the route's applyTemplate() has already done ratio + VAT expansion per the chosen mode. The RPC validates the lines balance and the 1930 net matches sum(tx.amount), then commits via commit_journal_entry. Same security pattern as match_batch_allocate: company-member check via auth.uid(), SELECT … FOR UPDATE on each tx in id order, deterministic fiscal-period resolution (ORDER BY period_start DESC). - **Endpoint** POST /api/transactions/bulk-book — fetches template via RLS, expands per mode (one_line_per_tx | sum_per_account) using lib/bookkeeping/template-library.applyTemplate, passes the resulting lines to the RPC. On success emits one transaction.reconciled event per tx. - **22 new BULK_BOOK_* error codes** (sv + en) covering all guard paths. ## UI (Phase 5b) - **BulkBookDialog** — template picker + mode toggle (segmented control: en rad per transaktion / summera per konto) + live preview table with balance + bank-leg invariant indicators. Confirm only enabled when both pass. - **Multi-select inbox** — sticky action bar gains a "Bokför i klump" button gated by same-date + same-direction across selected txs. Tooltip explains the disabled state. ## Phase 6: is-booked predicate New lib/transactions/is-booked.ts. After multi-allocation and bulk- book, tx.journal_entry_id can be NULL even though the tx is anchored (via invoice_payments / supplier_invoice_payments / transaction_voucher_links). The helper checks all three storage locations so future readers don't falsely show multi-anchored txs as "unbooked". Companion getPrimaryJournalEntryId() resolves the best JE link to surface in UI. SQL mirror is_transaction_booked() exists from the PR #602 foundation migration. Existing readers (TransactionHistoryList, TransactionInboxCard) are not yet refactored to use the helper — that's a follow-up that touches per-tx JE links across multiple call sites. The helper is documented + tested so subsequent refactors are mechanical. ## Tests - tests/pg/bulk-book-transactions.pg.test.ts — 8 pg-real scenarios (happy path create-new with 3 txs, happy path link-existing, date mismatch, direction mismatch, amount mismatch, unbalanced lines, unauthorized). - app/api/transactions/bulk-book/__tests__/route.test.ts — 5 unit tests (schema XOR, link path, create-new with template fetch + applyTemplate, structured-error mapping). - lib/transactions/__tests__/is-booked.test.ts — 11 cases covering all three storage locations + primary-JE resolution. 26 unit tests pass on touched paths. RPC migration applied to remote via Supabase MCP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #606 review round 1 + CI fixes Closes the build failure and the two real Greptile findings. ## CI - **core-only + Vercel build fail**: I used useMemo for selectedTransactions and bulkBookEligible on the transactions page without importing it. TypeScript build (`next build`) caught it with "Cannot find name 'useMemo'". Fixed the import. ## Review findings - **(P1) Currency mismatch returned BULK_BOOK_DIRECTION_MISMATCH** whose user-facing message blames direction. Mixed SEK + EUR batches would show "All transactions must be the same direction" which is factually wrong. Introduced dedicated BULK_BOOK_MIXED_CURRENCY code (sv + en) explaining the actual constraint, and switched the route to use it. - **(P1) Branch B (create-new) N=1 missed reconciliation_method='manual'**. Branch A's N=1 UPDATE sets it alongside journal_entry_id; Branch B's didn't, leaving the reconciliation_method NULL even though the single tx was reconciled via the same flow. Downstream readers (reconciliation reports, status indicators) would treat the two N=1 paths differently. New follow-up migration patches Branch B's final UPDATE. RPC patch applied to remote via Supabase MCP. 26 unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7eb8715417 |
feat(transactions): split-payment allocator — 1 tx → N invoices (#603)
* fix(category-mapping): use leaf BAS accounts instead of group codes 3900, 5800, 6200 are BAS gruppkonton (header codes) and shouldn't carry postings. Switched the default mappings to the matching leaf accounts: - income_other: 3900 -> 3999 (Övriga rörelseintäkter) - expense_travel: 5800 -> 5890 (Övriga resekostnader) - expense_telecom: 6200 -> 6230 (Datakommunikation) The fallback for income_other inside getCategoryAccountMapping was also hardcoded to '3900'; updated to '3999' for consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(transactions): split-payment allocator — 1 tx → N invoices Closes one of the two flows that motivated PR #602's foundation: allocating a single bank transaction across multiple customer OR multiple supplier invoices, with one combined verifikat (samlingsverifikation per BFL 5 kap 6§ st 3). ## Backend (Phase 3a) - **PL/pgSQL RPC** match_batch_allocate (~400 lines): locks the tx + each target invoice with SELECT … FOR UPDATE in id order, validates status/currency/remaining/direction before any write, builds the combined verifikat via commit_journal_entry (atomically assigns voucher_number + flips draft→posted), inserts N rows in invoice_payments or supplier_invoice_payments pointing at the same JE, advances paid_amount/remaining_amount/status per invoice. Returns { ok, journal_entry_id, voucher_number, allocations: [...] } on success or { ok: false, code, details } on guard failure. Mixed customer+supplier kinds are rejected (v1 scope). - **Endpoint** POST /api/transactions/[id]/match-batch — thin wrapper around the RPC. Validates body via MatchBatchSchema (zod discriminatedUnion + superRefine to catch mixed-kinds at the schema layer). On RPC success, emits one invoice.match_confirmed or supplier_invoice.match_confirmed event per allocation so existing subscribers (reminders, automations, processing-history) keep working. Maps the structured RPC error envelope to errorResponseFromCode. - **16 new BATCH_* error codes** (sv+en): BATCH_TX_NOT_FOUND, BATCH_TX_ALREADY_BOOKED, BATCH_OVERSHOOT, BATCH_AMOUNT_EXCEEDS_TX, BATCH_MIXED_KINDS_UNSUPPORTED, BATCH_DIRECTION_MISMATCH, BATCH_CURRENCY_MISMATCH, BATCH_PERIOD_LOCKED, BATCH_RPC_FAILED, etc. ## UI (Phase 5a) - **MatchAllocationDialog** (components/transactions/) — direction- aware (positive tx → customer invoices, negative → supplier). Search + selectable list of open invoices. Per-row amount input with default = min(invoice.remaining, tx_remaining_budget). Live tally with green-check balanced state, red overshoot warning, gray leftover note. Confirm button disabled on overshoot. POSTs to /match-batch and on 200 triggers the same exit animation as single-tx match. - **Inbox row** gains a second outline icon button (Split icon) next to the existing 1:1 match button, gated by the same showInvoiceMatchButton predicate. Tooltip explains the direction- aware split. Opens MatchAllocationDialog. - **i18n** strings under tx_match_allocation namespace in sv.json and en.json (32 keys each). ## Tests - tests/pg/match-batch-allocate.pg.test.ts — 5 pg-real tests covering combined verifikat shape, overshoot guard, already-booked tx, direction mismatch, mixed-kinds rejection. - app/api/transactions/[id]/match-batch/__tests__/route.test.ts — 5 unit tests covering schema validation, mixed-kinds, happy path, structured-error mapping, raw-error → BATCH_RPC_FAILED. 63 unit tests pass across the touched paths. The RPC migration was already applied to remote in an earlier Phase 3a session (idempotent CREATE OR REPLACE FUNCTION; the next replay is a no-op). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-batch): PR #603 review round 1 + CI fixes Closes both CI failures and the three real review findings. ## CI fixes - **pg-real failure**: the RPC declared `v_journal_entry_id uuid := uuid_generate_v4()` which fails in the CI Postgres image (uuid-ossp extension is off). Switched to `gen_random_uuid()` — the codebase standard already used by supplier_invoices, invoice_inbox, etc. - **core-only failure**: my earlier BAS leaf-account commit (3900→3999, 5800→5890, 6200→6230) didn't update the matching `lib/bookkeeping/__tests__/category-mapping.test.ts` expectations, and `getDefaultAccountForCategory`'s fallback for `income_*` was still hardcoded to '3900'. Updated both. ## Review findings (greptile) - **P1 deadlock-stable locking** (`match_batch_allocate.sql:11`): the validation `FOR UPDATE` loop ran in caller-supplied array order. Two concurrent calls with overlapping invoice sets in opposite orders could deadlock and one would abort with `BATCH_RPC_FAILED`. Now all three loops (validate, build lines, advance invoices) iterate via `SELECT … FROM jsonb_array_elements(…) ORDER BY COALESCE(invoice_id, supplier_invoice_id)`, giving a stable global lock order regardless of how the caller ordered the JSON array. - **P1 duplicate-allocation detection** (`match_batch_allocate.sql:163`): the same invoice_id listed twice would pass the per-row overshoot guard (both iterations read the original `remaining_amount`) and the write loop would insert two `invoice_payments` rows for the same invoice. Added a `v_seen_ids text[]` check in the validation loop and a new `BATCH_DUPLICATE_ALLOCATION` error code (sv + en). The dialog already prevents this UI-side via `if (prev[candidate.id] return prev` — the RPC guard is the defense-in-depth layer. - **P2 zod `.positive()`** (`schemas.ts:544`): allocation amount was `nonNegativeAmount` (allowing 0), passing schema validation only to be rejected by the RPC with `BATCH_INVALID_AMOUNT`. Now `z.number().positive(…)` so 0-amount entries fail at the schema layer with a per-field path, cleaner 400. - **P2 strict `> 0` direction check** (`MatchAllocationDialog.tsx:82`): used `amount >= 0` to pick customer-side, but a zero-amount tx would load customer candidates only to hit `BATCH_TX_ZERO_AMOUNT` at submit time after the user has filled in allocations. Switched to `> 0` so 0-amount tx never reaches the dialog at all (it's rejected by the RPC immediately). The fourth Greptile comment (the schema P2 about amount validation) overlaps with the third; addressed in the same edit. ## Verification - 112 unit tests pass across touched paths - ESLint clean - New pg-real test `tests/pg/match-batch-allocate.pg.test.ts` covers the dedupe scenario (same supplier invoice listed twice with summing amounts that individually pass per-row overshoot) - RPC patch applied to remote via Supabase MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-batch): PR #603 review round 2 — compliance hardening Addresses the actionable findings from compliance-swarm and Swedish-accounting-compliance reviews. Six small RPC changes + two TS-side guards, all bundled in one follow-up migration. ## Security - **(GDPR Art.5(1)(f) / ISO A.8.2) Caller verification**: SECURITY DEFINER bypasses RLS, and the prior RPC accepted any (p_user_id, p_company_id) pair from the route. Now the function rejects with new `BATCH_UNAUTHORIZED` (sv+en, HTTP 403) if `auth.uid()` is not a member of `p_company_id`. Pattern lifted from `harden_invoice_number_rpcs` (#20260510140000). - **(OWASP V4.2) Allocation cap**: `MatchBatchSchema.allocations` now carries `.max(100)` to prevent DoS via unbounded FOR UPDATE locks. ## Swedish accounting correctness - **source_type per direction**: was hardcoded to `'invoice_paid'` for both customer + supplier batches, mis-routing behandlingshistorik filters. Customer batches keep `'invoice_paid'`, supplier batches now write `'supplier_invoice_paid'`. - **Fiscal-period determinism**: `LIMIT 1` on the period lookup was non-deterministic on overlap (e.g. corrected broken year). Added `ORDER BY period_start DESC` so the most recent matching period wins. - **Tolerance harmonisation**: cross-allocation sum used `+0.01` tolerance while per-row used `+0.005`. Both now `+0.005` so a multi-row batch can't drift ~0.01 SEK while each row passes individually. - **`transactions.category` no longer overwritten**: was forced to `'income_services'` (→ BAS 3001 at 25% VAT) for any customer batch, misrepresenting reduced-rate / export / EU-service invoices. The category is only meaningful 1:1 with a single invoice; batches now leave it as-is, mirroring the supplier-side `ELSE category` branch. ## Tests - `tests/pg/match-batch-allocate.pg.test.ts` now wraps every RPC call in `withUserContext(userId)` so `auth.uid()` resolves to the seeded owner. Without this the new membership check would have failed all existing tests. - New pg-real test: `rejects with BATCH_UNAUTHORIZED when caller is not a member of the company` — outsider user gets explicit refusal. - New happy-path assertion: `source_type = 'supplier_invoice_paid'` on the combined verifikat for supplier batches. 15 unit tests pass on the touched paths. RPC patch applied to remote via Supabase MCP. Out-of-scope mcp-server changes still parked locally. Skipped findings (documented in PR comment thread): - V8.2.1 ownership pre-check at route layer (RPC enforces it) - V4.5 / Art.5(1)(b) narrower API response and event payload — typed contracts require the full shapes - V2.4 rate-limiting — system-level, applies to all match endpoints - A.8.28 client-side RLS reliance — documented architectural choice - Direction pre-check at API layer (RPC catches with cleaner code) - V16 + Art.32 + Art.5(1)(b) low-severity logging nits Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |