76b8d5c100a4171231b52def3ee355babfd3e105
601
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
76b8d5c100 |
fix(pending): show the staged kontering and bank currency on the bulk_book_transactions approval card (#1648)
The /pending card (and the chat ApprovalCard, same OperationPreview
dispatch) for bulk_book_transactions rendered only aggregates: tx_count,
tx_date, tx_sum, direction, mode. The staged journal lines sat unused in
params.new_entry.lines even though the executor's RPC posts them
verbatim, so the human approving an AI-staged samlingsverifikat could
not see which accounts were debited or credited: "-720, 2 tx, expense"
is compatible with both a correct booking and a wrong one.
- Staging now writes preview_data.lines (account_number, chart or BAS
account_name, debit/credit, line text) and entry_description, using
the same account-name lookup as gnubok_create_voucher, plus the bank
rows' currency. Nothing beyond what create_voucher already exposes;
still no per-tx descriptions or counterparty identifiers.
- New BulkBookPreview renders those lines with the create_voucher table
and totals, and shows the bank sum in the rows' own currency.
- CategorizePreview labels the source bank amount with its currency when
it is not SEK, next to the (always SEK) journal lines: a 2 500 USD
receipt booked as 24 292,50 kr read as a wrong SEK figure to an
approver who saw only one of the two numbers.
Reported via gnubok_feedback 2026-07-13 and 2026-07-14 ("the human-in-
the-loop control is the safety mechanism, and it is currently blind").
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e030393fe6 |
fix(rot-rut): payment-side booking, reminders and claim completeness (#1652)
* fix(rot-rut): payment-side booking, reminders and claim completeness Follow-ups from the 2026-08-17 ROT/RUT audit (dev_docs/rot_rut_audit_2026_08_17.md). Payment side (fakturamodellen: the customer pays total minus avdraget, the rest is a 1513 receivable on Skatteverket): - createInvoicePaymentJournalEntry without an explicit paymentAmount used to book invoice.total on 1930/1510. Every no-lines mark-paid path (MCP mark_invoice_as_paid, v1 API, no-body dashboard route, Stripe) settles the outstanding amount, so on a ROT/RUT invoice 1510 went negative by the deduction and 1930 was overstated; same defect for any previously part-paid invoice. It now books the outstanding amount (remaining_amount, else total minus paid_amount); a fully outstanding invoice keeps the total_sek path. - proposePaymentLines had no deduction awareness: the payment dialog pre-filled D1930 total / K1510 total, which the settlement plan rejected as an overpayment, so a ROT/RUT invoice could not be marked paid from the UI. Accrual: bank + 1510 carry total minus avdrag; cash method: bank gets the customer share, 1513 the avdrag, revenue + moms in full. Foreign invoices without a booking rate refuse (1513 is a kronor receivable). Dialog passes deduction_total. - Reminders and dröjsmålsränta were computed on invoice.total: a privatperson was dunned for the Skatteverket share and charged interest on it. New reminderPrincipal() = the invoice's "Att betala" (öre-rounded total minus avdrag) drives the processor's interest base and all three templates. Claim completeness (HUSFL 2009:194: art av arbete + antal arbetstimmar): - work_type and labor_hours were optional at creation but hard blockers at begäran-file time, when the invoice is numbered, booked and paid and cannot be edited. validateDeductionLines() now requires a same-kind arbetstyp and hours > 0 (schablontjänster exempt) on every deduction line; wired into validateInvoice, CreateInvoiceItemSchema (field-level issues) and the editor schema with inline errors under the ROT/RUT strip. Fixed the labor_hours register (valueAsNumber overrode setValueAs: an emptied field became NaN and failed validation with no visible error). The Underlag card now shows whenever any row is flagged, matching the payload/server predicate. Yearly ceilings: - COMBINED_MAX 75 000 kr: ROT + RUT share one ceiling per person (ROT capped at 50 000 inside it). deductionCapWarnings() carries the per-kind and the combined check plus optional prior-year totals; validateInvoice forwards them; the editor uses the same helper and fetches what the customer has already been granted in the invoice year (per customer, warning only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rot-rut): treat remaining_amount left at DEFAULT 0 as unmaintained when booking a payment Rows written by paths that bypass buildInvoiceWriteData (imports, sandbox seed, legacy migrations) carry remaining_amount = 0 while unpaid; prod has ~330 such open invoices. Booking 0 would have failed the engine's positive- amount rule, so the outstanding helper derives total - paid - deduction when the stored value is not positive. Test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rot-rut): review follow-ups on #1652 - ROT/RUT completeness moves to the invoice-level schema (CreateInvoiceSchema / UpdateInvoiceSchema share one refine) so it only applies to real invoices and skips text rows; the editor gates its mirror on the document type via a ref. Tests moved accordingly (CodeRabbit). - Prior-year deduction lookup follows the PAYMENT year (paid_at, else invoice_date for open invoices), paginates via fetchAllRows, and clears the total on a failed request instead of leaving a stale one. - rot-rut-file derives its schablon flags from SCHABLON_WORK_TYPES so the validator and the generator cannot drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rot-rut): pick the prior-year deductions client-side (phantom-columns ceiling) The runtime-built .or() filter counted as an unresolvable query expression for the no-phantom-columns guard. A customer has few deduction invoices, so fetch them all and select the payment year in code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
79240cb2ed |
fix(articles): article ROT/RUT prefill was dead for every dashboard-created article (#1651)
* fix(articles): article ROT/RUT prefill was dead for every dashboard-created article Follow-up to #1634. The user re-tested and picking a RUT article still left the line on "Ingen": the article form has always stored the bare kind ('ROT'/'RUT'), while the prefill only recognised Skatteverket work-type codes (BYGG, STAD, ...). On prod every dashboard-created ROT/RUT article holds the bare kind, so the fix in #1634 never fired for a real user, and worse, since the helper returned null for those values, picking such an article CLEARED a deduction the user had set manually on the row. - rot-rut-rules: parseArticleHouseworkType() understands both vocabularies (code -> kind + arbetstyp; bare ROT/RUT -> kind only), plus normalizeHouseworkType()/HOUSEWORK_TYPE_VALUES/workTypeLabel(). - InvoiceEditor.applyArticle: kind-only articles pre-fill the deduction and keep a same-kind arbetstyp already chosen on the row; "Spara som artikel" round-trips the code or, lacking one, the kind. - ArticleForm: the ROT/RUT select now offers the real Skatteverket arbetstyper in ROT/RUT groups (its own hint always promised "förifyller arbetstyp"); legacy kind-only values stay selectable as "RUT (arbetstyp ej vald)" so an edit never silently drops the flag. Article detail renders "RUT · Städning" instead of the raw code. - API + MCP commit schemas normalize housework_type (case-insensitive code or ROT/RUT, '' clears) and reject anything else; the CSV article import normalizes the column the same way. Prod holds 178 articles with '0'/'1' from a boolean "Rot" column that the keyword detector mapped straight through; those now read as no flag everywhere and can no longer be created. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(articles): review follow-ups on #1651 - InvoiceEditor: switching a row's skattereduktion ROT<->RUT clears an arbetstyp from the other list, and Spara som artikel only round-trips a work type that belongs to the row's kind (CodeRabbit). - MCP update_article: null / '' / whitespace now clear housework_type (commit drops only undefined keys, so the old undefined mapping made the flag un-clearable); create keeps treating them as unset. Tests. - Article CSV import warns when a non-empty ROT/RUT value is dropped as not-an-arbetstyp instead of dropping it silently. Test. - Hint wording: arbetstyp is pre-filled only when the article carries one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3841ab9f54 |
feat(mcp): bulk-link documents to vouchers in one staged approval (#1411)
gnubok_link_documents_to_vouchers stages up to 300 document-to-verifikat links as a single pending operation, addressed by voucher_series / voucher_number / fiscal_year instead of journal_entry_id UUIDs, for bulk receipt-migration jobs where N separate tools mean N separate approvals. Staging resolves every row server-side and returns a per-row hit or miss, so a systematic offset such as a wrong fiscal_year is visible before anything is approved rather than after N approvals. Only resolved rows enter the staged operation. The WORM precondition and the document lookup are shared with the single-document executor through precheckDocumentLink: a bulk call must enforce exactly the invariants N single calls would, and a second copy of a BFL 5 kap 6 § guard is a copy that keeps the old behaviour when the first is hardened. A batch that links nothing returns 409 instead of a committed no-op. Partial skips stay committed, but an approval-gated operation on räkenskapsinformation must not leave an audit record asserting a run that changed nothing. The tool is search-only: a one-off migration tool does not belong in the default catalog every session pays for in context, and keeping it there pushed the tools/list projection past the 58.5K token ceiling that payload-size.bench.test.ts guards. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
93541d7186 |
fix(ux): smoothness follow-ups - detail pages, batches, toasts, and the last edges (#1633)
* fix(ux): smoothness follow-ups - detail pages, batches, toasts, and the last edges Follow-up batch to #1629: the six documented deferred items from dev_docs/loading_states_analysis.md, in the same vocabulary (first-load-only takeovers, background reconcile behind mounted content, row/button-level pending, sequence guards). - Invoice detail pages: kundfaktura and leverantorsfaktura detail no longer blank the whole page for one-field changes. fetchInvoice shows the blocking spinner/skeleton only before the first paint (or when the pager steps to a different invoice); Bokfor / status / finalize / payment / send / Attestera / Markera betald / kreditera refetch behind the mounted page, the acting button shows a spinner-in-button, and the handlers await the refetch so pending covers until the content reflects the new state. The supplier detail's single isProcessing boolean became processingAction so the spinner lands on the clicked button only. (The leverantorsfakturor LIST try/catch/res.ok item was already fixed by #1629.) - useDestructiveConfirm: confirm(opts, action?) can now carry the destructive operation, so the dialog's existing isLoading spinner actually shows while it runs, dismissal is blocked meanwhile, and confirm resolves false if the action throws. Adopted at the /transactions row delete and the supplier- invoice detail delete (which previously permitted duplicate DELETEs with zero feedback). - Batch parallelization: new lib/concurrency.ts mapWithConcurrency (bounded worker pool, order-preserving, tested). /transactions batch categorize / ignore / delete run per-row requests 5 at a time instead of strictly sequentially; the bulkbar counter ticks per completed row. - Toast-spam reduction: batch categorize rows run silent (exit animation, count decrement and state patch stay; no per-row Bokford or generic failure toast) and ONE aggregate toast reports "N bokforda[, M misslyckades]" with a single Angra alla action that pools the same /uncategorize endpoint over every booked row (per-row undo is feasible today, so the aggregate is too). Interactive escalations (SI/CI match suggestions, duplicate warning, activate-account) deliberately keep their dialogs. - Underlag row-click flash: InvoiceInboxWorkspace handleSelect seeds the detail pane synchronously from the clicked list row and starts the document load in parallel with the detail GET (which hydrates on arrival), so a row click never flashes the onboarding/empty state, and a stale-response guard keeps a slow fetch from overwriting a newer selection. - #1629 round-2 edges: /pending holds the loading state when a fetch for a not-yet-loaded tab FAILS (never renders the previous tab's rows under the new tab's header, and never fakes an empty state); /transactions clears transactions/skvRows (+ count/paging) and bumps both fetch sequences on company switch, and loadSkvRows got the same sequence-guard pattern as fetchTransactions. Gates: full vitest suite green (14772 passed), tsc byte-identical to the origin/main baseline (stash-diffed), eslint 0 errors on touched files (warnings identical to baseline), check:guards green, package-lock untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): harden action feedback against stale responses and failures Address the seven CodeRabbit findings on #1633: - invoices/[id] + supplier-invoices/[id]: latest-request guard in fetchInvoice (sequence token) so a mutation refresh overlapping pager navigation can never commit invoice A's state under invoice B's URL; the deferred related-document writes are guarded too - supplier-invoices/[id]: try/catch/finally in approve/book/mark-paid/ credit/uncredit so a rejected fetch()/json() clears processingAction instead of leaving every invoice action disabled until reload - transactions: extend the skattekonto sequence guard to the connection-status write so a status response started under the previous company cannot flip the reconnect banner for the new one - transactions: runCategorize resolves { ok, journalEntryId } so the batch aggregate counts a 200-with-null-journal-entry booking (flag flip) as success instead of narrating it as misslyckades; Angra alla only targets rows with an actual verifikat, since the storno endpoint rejects rows without one - transactions: shared undoneIdsRef lets "Angra alla" cancel a pending finishBooking state patch; a fresh booking clears its row's entry so re-booked rows still get their delayed patch - InvoiceInboxWorkspace: monotonic request tokens for the detail and document reads so a same-item reload cannot resolve out of order and paint a stale snapshot or document URL - messages: ICU plural for the success part of both partial batch descriptions in sv and en (1 bokford, not 1 bokforda) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4921d1da5e |
feat(import): import skattekontoutdrag files into the skattekonto pipeline (#1637)
* feat(import): import skattekontoutdrag files into the skattekonto pipeline Users can now upload the kontohändelse export from Skatteverket's skattekonto e-service (current CSV layout, verified against a real 2026-08 export, plus legacy .skv files) instead of needing the paid API connection. Parsed rows land in skattekonto_transactions as booked file_import rows and inherit the existing 1630 rules engine, bulk booking, match-to-verifikat and both UIs unchanged. - Core parser lib/import/skattekonto-file/ with strict detection (orgnr header + saldo markers, or two distinct SKV vocabulary terms plus row shape), sum-integrity check (opening + rows must equal closing) and a wrong-company guard against company_settings. - computeDedupKey moves to core (lib/skatteverket/skattekonto-dedup); the extension re-imports it. File rows hash-key; content-signature partitioning skips rows already booked (either key form) and promotes matching upcoming rows in place. - syncSkattekonto gains a takeover step: an id-keyed API row adopts a matching hash-keyed imported row in place, so journal links survive connecting the API after a file import. Upcoming rows can no longer clobber a booked row on hash collision. - New skattekonto_file_imports table (company-scoped file-hash dedup) plus source/file_import_id provenance columns on skattekonto_transactions. - /import gains a Skattekontoutdrag wizard (upload/preview/result, deep link ?mode=skattekonto); the bank-file flow detects skattekonto files and redirects instead of importing them as bank rows. - /skattekonto renders imported rows for unconnected companies (attn line + import CTA) instead of discarding them behind the StartCard. - Free for everyone: the local-data booking/match routes were already ungated; only API sync/saldo stay capability-gated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skattekonto): align the EF F-skatt rule with the 2012 -> 2013 decision 20260810120000 established that 2012 is not standard BAS and moved the booking templates to 2013 (owner taxes in an enskild firma are an eget uttag), but the skattekonto_rules seed still booked EF preliminarskatt against 2012. The file importer makes this rule fire for every EF F-skatt row, so bring it onto 2013 too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(import): apply review findings on the skattekonto file import - Fix the takeover candidate comparator: the single-argument sort was an inconsistent relation and could adopt a stale upcoming row ahead of the booked file row in a 3+ candidate queue (regression test added), and page the candidate scan with fetchAllRows so a multi-year window is not silently capped at 1000 rows. - Fail parsing when a statement HAS saldo markers but not both readable balances: a file cut off before "Utgående saldo" previously skipped the sum check entirely. sum_valid stays null only for marker-less legacy files. - Count a promotion only when the UPDATE matched a row, so a concurrent sync cannot inflate promoted_count; log a failed finalize of the import record instead of discarding the error. - Migration (unshipped, edited in place): user_id is nullable with ON DELETE SET NULL so import records and their file-hash dedup survive user deletion, and the INSERT policy binds user_id to auth.uid() so a member cannot attribute an import to a colleague. pg tests cover both. - Make the upload drop zone keyboard-reachable (role, tabIndex, Enter/ Space) and give the six count-bearing strings ICU plural forms in both locales. Skipped with reasons on the PR: binding execute rows to file bytes and re-checking orgnr in execute (same client-trust model as the shipped bank-file execute; Zod + RLS scope writes to the caller's own company), a 404 test (the route has no not-found path), event-bus clearing in the route test (the route touches no events), and FK NOT VALID (new column referencing a brand-new empty table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dfb34a01d9 |
feat(invoices,year-end): four byrå-feedback fixes (validation feedback, moms gate, klarmarkera, article search) (#1641)
* fix(invoices): surface validation errors instead of a silent dead submit button A missing unit (or any other Zod failure) blocked both Granska & skapa and Spara som utkast with zero feedback: handleSubmit had no onInvalid callback, the buttons stayed enabled, and the unit field rendered no inline error. Reported by a byra user whose client could not save any invoice. - onInvalid handler on all three submit paths: destructive toast plus scroll to the first inline error - inline error text under the unit select and quantity input (the only line fields that had none) - same treatment in NewRecurringScheduleDialog, including inline errors on its item rows Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(supplier-invoices): stop defaulting 25 % moms for icke momsregistrerade companies The registration form hard-coded vat_rate 0.25 on the initial line, added rows, AI prefill fallback and konto defaults, regardless of company_settings.vat_registered. A non-VAT-registered business that missed the prefilled rate booked ingaende moms (2641) it has no right to deduct (ML 8 kap. 3 \u00a7). The customer-invoice side already gates on the same flag; the supplier side ignored it. - form: read vat_registered from /api/settings; when false, all moms controls (rate cells, per-line moms, totals rows) are hidden and every line is forced to 0 %, including late AI prefills - reverse charge keeps its rate controls: self-assessment is a separate obligation from deduction - route: 400 SI_CREATE_INVALID_INPUT when a non-registered company posts a line with vat_rate/vat_amount > 0 (API/MCP defense in depth), and an omitted vat_rate now defaults to 0 instead of 25 % for those companies - tests: guard rejection, reverse-charge pass-through, 0-default; existing POST tests updated for the new settings lookup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(year-end): klarmarkera imported years already closed in a previous system SIE-imported historical fiscal years land with is_closed = false and no closing entry, so the year-end page lists every migrated year as pending bokslut even though the bokslut was done in the old software. There was no sanctioned way to mark them done: closePeriod hard-requires locked_at and closing_entry_id. - migration: fiscal_periods.closed_externally boolean (audit clarity: distinguishes a year-end run here from a close done elsewhere) - markPeriodClosedExternally(): closes + locks without a closing entry; refuses already-closed periods, periods with their own closing entry, periods that have not ended, and periods with unbooked bank transactions (same stranding guard as lockPeriod); writes the immutable audit_log entry - POST /api/bookkeeping/fiscal-periods/[id]/close-external (requireWrite) - year-end page: one attn line on the preflight step with a confirm dialog describing the outcome; the marked year drops out of the eligible list Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): searchable article picker on invoice lines The article field was a plain Radix Select whose only matching is label-prefix typeahead: for numbered articles that means number-only lookup, and typing "skruv" found nothing. Byra feedback: name search would help a lot for users with real article catalogs. New ArticleCombobox (input-trigger dropdown, same pattern as AccountCombobox): free-text search over name + article number, diacritics-folded via foldText, keyboard navigation, pinned "Egen rad" free-text option, browse-all on focus like the Select it replaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: log klarmarkera pg-test decision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address skeptic and compliance-review findings on PR #1641 - ArticleCombobox: keyboard focus no longer auto-opens the list, opening highlights the committed selection, typing highlights the first match, and re-selecting the current value is a no-op. Previously Tab+Enter silently detached the article and wiped its revenue-account override. - Supplier invoice prefill for icke momsregistrerade: the zeroing effect now grosses the net amount up by the extracted rate before forcing 0 %, so the booked cost and 2440 keep the full att-betala amount instead of understating both by the moms. - markPeriodClosedExternally: only migrated periods qualify (must contain SIE-imported verifikat or no verifikat at all); the update carries an is_closed=false predicate so a concurrent normal close cannot be overwritten; confirm dialog now names the reporting consequences. - Route comment: honest scope (this route only; v1/inbox/MCP sweep is a follow-up) and current-law citation (13 kap. ML 2023:200). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: use roundOre for the icke-momsregistrerad gross-up (ratchet guard) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f8db38f989 |
fix(analytics): mask session replays by default, chrome-only unmask (#1639)
* fix(analytics): mask session replays by default, chrome-only unmask Invert PostHog session-replay masking from visible-by-default with pattern masking to deny-by-default: every input value is masked wholesale (rrweb maskAllInputs, no maskInputFn) and every text node is masked unless it sits under data-ph-unmask chrome or a table column header (th). Chrome tags live on the shared UI primitives (PageHeader, Label, Button except combobox triggers, TabsTrigger, Badge, Card/Dialog/Sheet titles, tooltips, help popovers, empty states, settings labels), and tagged chrome is still pattern-scrubbed for amounts and person-/organisationsnummer. data-ph-mask beats data-ph-unmask, so call sites that interpolate user data into chrome stay masked; a very-thorough audit swept every unmasked primitive and each found site got a call-site mask. Confirm-dialog wrappers and toasts stay masked centrally: their copy describes user objects by design. Untagged new UI over-masks instead of leaking. Privacy policy, RoPA and decision log updated in the same change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(analytics): tag detail-section chrome merged from main The register-detail primitives landed on main after the replay-masking audit ran: kickers and DefRow labels are static i18n chrome, values stay masked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(analytics): close skeptic and review findings on replay masking Explicit data-ph tags now resolve before the th chrome fallback, so a th nested inside a data-ph-mask container masks correctly (regression test added). Seven missed text-leak sites get call-site masks: delete-invoice and credit-page invoice numbers, IB-correction voucher reference, TIC orgnr (served unnormalized, so the separator-based scrub cannot be relied on), articles search-term empty state, dimension segment labels, and activate-account buttons. The attribute channel is closed with rrweb's blockClass: inputs whose placeholder carries an effective user value (salary overrides, correction description, danger-zone confirms, credit confirm) get ph-no-capture, removing the element from recordings while the prefill UX stays intact; the pivot-th title attribute is dropped. Privacy-policy effective date bumped to 2026-08-17. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1bb423b2b3 |
fix(salary): surface missing sender bankgiro/IBAN before betalfil download (#1640)
* fix(salary): surface missing sender bankgiro/IBAN before betalfil download Users see a bankgiro under BANKUPPGIFTER in settings (Bolagsverket snapshot, display only) while the payment-file routes read company_settings.bankgiro, so the LB download failed with an error that pointed at a page that looked correct. 153 companies have a registry bankgiro but an empty settings field. - PaymentFilePanel warns up front when the sender bankgiro (bg_lb) or IBAN (pain001) is missing, linking to Installningar -> Fakturering - betalkonton form offers a one-click prefill of the bankgiro from companies.tic_snapshot (Luhn-validated, user still saves) - bg-lb and skattekonto payment-file error copy now names the exact place to fix instead of 'foretagsinstallningar' Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): harden bankgiro prefill and warning per skeptic review - bankgiroFromTicSnapshot now requires the snapshot's orgNumber to match companies.org_number before suggesting anything: stale fuzzy-matched snapshots can hold another entity's profile, and this field becomes the payee account on invoices and Peppol e-invoices - salary run page refetches settings when the URL returns from the intercepting settings modal, so a bankgiro/IBAN saved there clears the missing-sender warning instead of leaving it stale Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
25524e1df4 |
fix(suppliers): stop requiring standardkonto that was never meant to be required (#1636)
* fix(suppliers): stop requiring standardkonto that was never meant to be required The supplier form initializes every optional field to '' and sent them as-is, while CreateSupplierSchema validates default_expense_account with the 4-digit account rule behind .optional(): an empty string is a present string, so saving a supplier with the field untouched failed with "Kontonummer måste vara 4 siffror" even though the field carries no required mark (reported by Björn with a screen recording; the edit page failed the same way for any supplier without a default account). Schemas now own the normalization, split by verb: on create '' becomes undefined (key dropped, column NULL), on update '' becomes null, because update routes pass fields straight into .update() where undefined means "leave unchanged" and clearing must actually write NULL. Email gets the same treatment and the form's old client-side email strip is removed; stripping empty strings client-side would break exactly the clear path. The free-text Standardkonto input is replaced with the shared AccountCombobox (browsable list filtered to cost classes 4-7, the same rule the agent-path expenseAccountField enforces), with the selected account name shown under the field and a clear button when set. Standardkonto itself stays optional: it only prefills supplier-invoice lines and the ledger-context suggestion covers the empty case. Verified end to end against the running app: saving a supplier without a default account succeeds on the update path, and the combobox search/select/clear cycle works inside the create dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api-spec): render preprocess pipes by output side, required-ness by undefined-acceptance The minimal Zod-to-JSON-schema walker described every pipe by its input side. For .transform() that is right (the caller sends the input), but z.preprocess() is the mirror image: the callable sits on the input side, so the supplier schemas' new empty-string normalization rendered email and default_expense_account as required untyped fields in the OpenAPI spec and the generated accounted-api skill. Describe the output side when the input is a transform. Required-ness now derives from schema.safeParse(undefined) instead of a top-level discriminator check: a field may be omitted exactly when the schema accepts undefined. Besides the preprocess pipes, this corrects several fields the old check misrendered as required (z.unknown() bodies, union-with-empty-string settings fields, preprocessed personal_number), so the regenerated skill references only flip required to optional where runtime validation already allowed omission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2eb3441244 |
fix(export): paginate the archive size estimate and explain scope counts (#1635)
The period branch of estimateArchiveSize ran a single unpaginated document read with one flat IN() over every posted entry id in the year: past the PostgREST row cap it silently undercounts, and past a few hundred entry ids the URL itself blows up. Chunk the id filter (CHILD_FK_CHUNK) and paginate every read with fetchAllRows, mirroring what writeDocuments already did (the ZIP content was never affected). The dialog now says per scope which documents are counted: full history includes unlinked inbox/receipt documents, a single year only those linked to posted vouchers. Without that line, a company with many unlinked receipts reads the count gap as a pagination bug. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
62c6fc44fe |
fix(invoices): article pre-fills ROT/RUT and kundkort personnummer covers the claim (#1634)
* fix(invoices): article pre-fills ROT/RUT and kundkort personnummer covers the claim Two gaps reported by a user invoicing RUT work: - Picking an article with a housework_type (arbetstypskod) left the line's skattereduktion on 'Ingen': the editor never fetched the field. applyArticle now derives deduction_type from the code's Skatteverket list (disjoint ROT/ RUT lists, new deductionTypeForWorkType helper) and sets work_type, with the same overwrite semantics as description/price: an article without a code clears the deduction so a material article never keeps claiming one. 'Spara som artikel' round-trips the code back onto the created article. - The customer card's personnummer was never used for the ROT/RUT claim; the user had to retype it per invoice. The browser only ever sees ciphertext or a mask, so the fix is a server-side fallback in buildInvoiceWriteData: typed > stored draft > kundkort. The kundkort value is decrypted, expanded to 12 digits (new expandPersonnummerTo12, century inference incl. '+' and samordningsnummer), Luhn-validated, and encrypted into the invoice; invalid or unreadable values fall through to the existing 'Personnummer krävs' error. The editor drops the required-mark and hints that the number comes from the kundkort when one exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): gate the kundkort personnummer fallback on individual customers ROT/RUT is a privatperson deduction; customers.personal_number is individual-only in the Zod schemas but not in the DB, so a stray value on a business row must never be claimed on implicitly. Typed values unaffected. Raised by the compliance review bot on #1634. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
44c3116357 |
feat(export): direct download of the complete archive from the Exportera tab (#1632)
The full-archive ZIP endpoint (SIE + reports + all documents) has existed since the settings/backup page, but lost its UI when that page became a redirect: the BackupDownloadForm component was orphaned and the download was API-only. Resurface it the way the export tab already works: a "Komplett arkiv" ImportRow (owner/admin only, matching the route's role gate) opening a small centered dialog like the SIE export next to it, with scope choice, fiscal-year picker, include-documents toggle, live size estimate, 413 handling, and a #full-archive deep link. The orphaned form and its dead settings_backup_download i18n namespace are deleted; its logic lives on in components/import/FullArchiveDialog. Over-limit copy now points at the existing cloud sync instead of promising it "in a later version". Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c897a906df |
fix(ux): actions update lists in place - no more takeovers, jumps and dead air (#1629)
* fix(ux): update lists in place on actions instead of takeover spinners and jumps Founder report: the app feels glitchy when clicking around, especially when deleting a row or booking something. The repo-wide anti-pattern behind it: single-row actions trigger whole-list skeleton/spinner takeovers (layout collapse, scroll jump, full stagger-enter replay), deletes give zero feedback then hard-jump, and the /transactions exit "animation" was filter-only and never animated. Per surface: - Never take over a rendered list for a background refresh. The skeleton/spinner swap is now reserved for an empty (or foreign) list on /transactions (fetchTransactions), /pending (fetchOperations, covering both listed Granskning findings, one file), kundfakturor (fetchInvoices), leverantörsfakturor (fetchInvoices, plus try/catch/finally so a failed fetch can no longer stick the skeleton or masquerade as an empty register) and the verifikat list (JournalEntryList now takes a refreshToken prop and refetches in place; /bookkeeping no longer key-remounts it into a spinner, so expansion/selection/pagination/scroll survive a created verifikat). Quiet inline Loader2 cues near the list headers on /transactions and /pending signal a background reconcile. - /transactions row exit: exiting rows (booked/ignored/deleted) stay rendered through the existing 350ms window with a real exit transition (.row-exit: fast fade, then the space closes by transitioning cell paddings/line metrics and a numeric max-height on the fixed-height cell spans) and pointer-events off. Instant removal under prefers-reduced-motion. Applied to the inbox cards, the skattekonto card and the history rows. - /transactions delete: routes through processingId (row spinner) and the exitingIds path, and decrements totalUncategorizedCount when the deleted row was pending (the realtime echo is not guaranteed for DELETE on a filtered subscription). - FyPicker double-fetch: the initial fetch now waits for FyPicker's onReady (fires after its persisted-scope restore), so mount does one correctly scoped fetch instead of racing an unscoped fetch against the restore refetch (list -> skeleton -> list on every visit). Period changes refetch background-only behind the client-filtered list. - Pagination survives realtime echoes: background refreshes re-fetch range(0, pagedCountRef) instead of resetting to the first 200 rows, so "Visa fler" pages no longer collapse after any action. Gates: full vitest suite green (14764 passed), tsc output byte-identical to the origin/main baseline, eslint 0 errors on touched files, check:guards green, package-lock untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): apply review round on action-feedback smoothness - /pending: sequence-guard fetchOperations so a stale previous-tab response can't overwrite the current tab's rows, counts, or loading cues - /pending: check res.ok on the pending fetch and both history fetches before applying payloads; failures keep current rows and surface the existing error toast - /transactions: reset fiscal scope (fyReady/fyPeriodId/fyPeriod) during render on company switch so FyPicker re-runs its persisted restore and stale bounds never scope a fetch for the wrong company - /transactions: drop a deleted row's id from selectedIds so the bulk bar can't act on a deleted row - row exit: add the inert attribute on exiting row wrappers alongside pointer-events so keyboard focus and activation are blocked too - JournalEntryList: preserve selection on refreshToken background refreshes (reconciled against the refreshed page); user-initiated reloads still clear it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4dbd19aeb0 |
fix(transactions): the underlag column is one surface, not a dropzone and an orphan button (#1628)
* fix(transactions): the underlag column is one surface, not a dropzone and an orphan button
The Bokfor transaktion dialog stretched its empty dropzone into a
45/72vh well and pinned "Valj befintligt underlag" alone at the very
bottom of the column, visually disconnected from the dropzone it
belongs to.
- Empty state: the underlag column now sizes to its content and
top-aligns; the inbox picker renders as a quiet full-width dashed
row directly under the dropzone ("eller valj befintligt underlag
fran inkorgen"), so drop-a-file and pick-from-inbox read as one
intake surface. The fixed-height sticky column returns as soon as
a document previews there (uploaded, picked, or pre-linked).
- Grid rebalanced from 1fr/520px to 2fr/3fr so the kontering side
dominates while nothing is being previewed on the left.
- QuickReviewDialog gets the same footer-row treatment for its picker
trigger inside the underlag collapsible, so both #1620 surfaces
present the affordance identically (disabled-while-booking kept).
Presentation only: upload path, select-mode picker held until booking,
linkDocuments with inbox_item_id, picked-state resets on close and
transaction change, and duplicate-match wiring are all unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: record the QuickReviewDialog picker-trigger consistency decision
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a977a67063 |
refactor(register): customer, supplier and article detail as documents, not card piles (#1624)
The three register detail pages rendered a handful of facts as a grid of sparse bordered cards floating in an empty page. They now read as one flowing document: serif entity name over a quiet type/status kicker, quiet ghost actions top right (delete turns terracotta only on hover), and hairline-kickered definition sections (KONTAKT, KUNDUPPGIFTER, PRIS, BOKFORING, BETALNINGSUPPGIFTER, FAKTUROR) with aligned label/value rows in a constrained column (max-w-2xl, supplier max-w-3xl for its invoice table). Sections land with the standard stagger-enter. New shared primitive components/ui/detail-section.tsx (DetailSection, DefRow, DefEmpty) carries the grammar. Empty values render a muted en dash for facts that matter (email, phone, expense account) and are omitted row-wise otherwise; a section with nothing to say is omitted. Behavior preserved: edit/deactivate/delete flows, confirm dialogs, personal-number reveal + AttnLine, account-activation retry, viewer lock states, routing and all existing i18n keys. New def_* label keys added line-wise to both sv.json and en.json; customer invoice_count gained ICU plural (1 faktura, not 1 fakturor). Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
51539b93ed |
fix(salary): one save per surface on the employee form (#1623)
* fix(salary): one save per surface on the employee form The employee edit page stacked two competing saves: the opening-balances Card ended in "Spara ingaende saldon" and the page ended in "Spara andringar" 80px below, with no visible boundary between their scopes. Worse, both self-saving panels lived INSIDE the page <form> and shadcn Button sets no default type, so every panel button (save opening balances, add/remove benefit) implicitly submitted the outer form too, firing the full employee PATCH alongside the panel's own request. Restructure so each surface owns exactly one save: - The employee <form> now closes right after the Bank card, with Avbryt + "Spara andringar" directly under the fields it actually saves. - Formaner and Ingaende saldon move below the form into a "Sparas separat" section (uppercase kicker + one-line scope hint) so the page save structurally cannot include them and their buttons can no longer leak submits into the employee form. - OpeningBalancesPanel becomes its own <form>: Enter saves the panel, and the save button enables only when its fields are actually dirty (fingerprint of loaded values, reset on successful save). - EmployeeBenefitsPanel buttons get explicit type="button". New strings in both messages/sv.json and messages/en.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): release the loading skeleton when the balances fetch fails CodeRabbit on #1623: a rejected fetch or JSON parse skipped the setLoading(false) line, holding the skeleton forever. The load now wraps in try/finally; a failed load falls back to the empty form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dd4ced1f93 |
feat(import): the constellation breathes between beats (#1622)
The theater canvas froze visually between spawn events; long holds like "Skriver till journalen..." read as stale. Add continuous ambient life inside the existing rAF loop, derived entirely from the clock (no extra timers), without inventing progress: motion means the system is alive, not that work completed. - Per-node breathing: radius +-10% (about 1px on the hub) plus up to 4% alpha, on two slow incommensurate clocks offset by each node's own position/wave phase so the field shimmers organically, not in sync. - Quiet ripple: every 7s a luminance wave travels hub to rim over 2.6s, brightening the hairline year rings (+0.18 alpha peak) and edges (+0.12) it passes. Alpha only: no color change, so it cannot be mistaken for the sage event pulse. - Settled mode (result reveals) rests at half breathing amplitude and gets no ripple; the reveal is a verdict. - prefers-reduced-motion: ambient scale is zero and the frame stays frozen as before. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
86f0b70fdd |
fix(vat): complete account treatment enforcement (#1593)
* fix(vat): complete account treatment enforcement * docs(api): refresh account endpoint skill * fix(mcp): preserve ruta 05 compatibility * test(vat): seed migration constraint fixtures * docs(vat): clarify treatment precedence --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
edfdbe2d2a |
fix(auth): move the BankID flow into a signed, user-gated, single-use cookie (#1625)
* fix(auth): move the BankID flow into a signed, single-use, confirm-on-resume cookie
A user's BankID signup identified successfully four times and created no
account. His screenshots show four tabs, one on the finished "Verifierad med
BankID, ange e-post" step, and the tab he was looking at showing the idle
button. Prod agreed: no bankid_identities row, no auth.users row.
On iOS outside plain Safari the BankID return URL is handed to the OS, which
opens a NEW tab. The session lived in per-tab sessionStorage, so that tab
started empty and rendered the start button while the completed flow sat
stranded. Login hid it (self-finishing, cookie-backed session); signup waits
for a human to type an e-mail into the stranded tab, so it dies there.
The session id is no longer handed to the browser. It lives in a signed
__Host- HttpOnly cookie set at /start; /poll, /complete, /link and /cancel
read it. Cookies are shared by every tab of the origin, which is what the
handoff needed. The id had to leave the client because it is an
unauthenticated bearer credential: /poll was skipAuth and returned
user.personalNumber, and /complete with mode 'login' returns a tokenHash that
verifyOtp turns into a session, MFA skipped for bankid_linked accounts.
A completed identification must never be consumed by whoever merely opens the
page. A shared cookie plus a shared machine means the tab that finds a
completed flow cannot prove the person at it is the one who made it, and no
client-side token can prove otherwise: nothing survives an iOS same-tab reload
yet dies on reopen-closed-tab / session restore / tab duplication. So a resume
is never automatic. The mount probe routes any found live flow to a confirm
card ("Fortsätt bara om det var du") that reveals no name, and only that click
polls and consumes. Auto-consume happens only inside the live component
instance that called startSession (desktop QR; the pre-navigation mobile
launch), which by construction is the originator. Cost: one tap after
returning from the BankID app on iOS, exactly where the reported bug lives;
desktop and Android never hit the resume path.
The rest is defence the four review rounds proved load-bearing:
- __Host- with Path=/ and unconditional Secure, so a script cannot plant the
same name at a longer path; readBankIdFlow fails closed on duplicates and on
a malformed percent-escape.
- Single-use is a unique index (bankid_consumed_sessions), claimed before
generateLink, not a Set-Cookie. Fail-closed on any non-23505 error, so the
migration MUST be applied before the code.
- A link flow requires auth at /start and pins userId; /link rejects a flow
owned by anyone else, before any TIC call. mode is pinned and /poll rejects a
body mode that does not match, so a login session cannot finish through the
signup panel. /poll withholds the holder name from a probe. The 900s
verified-step window is capped by MAX_TOTAL_LIFE from a signed startedAt.
/poll never clears the cookie (an untargeted Set-Cookie would delete a newer
flow); only /cancel and terminal /complete + /link exits clear. Avbryt holds
a 'cancelling' state until /cancel resolves so a new /start cannot race the
clear. Session id is logged only as an 8-char prefix.
The launch is untouched: iOS keeps its return URL, Android keeps redirect=null
(#194 closed that path deliberately).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(auth): bind BankID actions to the resumed flow
* docs: record BankID staging migration drift
* fix(auth): address BankID PR review
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2deea05d42 |
feat(import): attach underlag to SIE-migrated verifikat by filename (#1627)
* refactor(documents): lift the SIE voucher-ref resolver into core
The provider migration sweep resolved a source voucher reference to the
verifikat it became with an in-memory (period, series, number) index built
inside extensions/general/arcim-migration. The underlag filename import needs
the identical resolution, and core must never import from @/extensions, so the
index, its ambiguity handling and the two paged reads move to
lib/documents/voucher-ref-resolver.ts.
Behaviour-preserving for the extension: same index construction, same "drop
both when one key repeats inside a fiscal year" rule, same dateTo-window
resolution. The arcim tests pass unchanged.
Two deliberate additions on top of the lift:
- series comparison is now case-insensitive on both sides. SIE writes series
uppercase in practice but the spec does not require it, and a filename is
whatever the exporting tool produced.
- byNumber and fetchVouchersForNumbers serve the filename flow, which
resolves a handful of refs per request and must not pull every migrated
entry into memory to do it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(import): attach underlag to SIE-migrated verifikat by filename
A SIE file carries the ledger but not the underlag, so a migrating customer
brings the receipts over separately and today has to open every verifikat and
attach them by hand. Systems that export both name each receipt after its
verifikat (A31_<internal-id>.pdf), and the SIE import already preserves that
identity on every entry (source_voucher_series / source_voucher_number), so
the pairing is a lookup, not an interpretation: no AI, no amount matching, no
date windows.
Separate optional import mode (/import?mode=underlag), NOT a step inside the
SIE wizard: the receipts normally arrive later and from a different export, so
a migration must never be blocked on having them ready.
lib/documents/filename-voucher-ref.ts reads the ref out of a filename
lib/documents/underlag-import.ts builds the plan (reads only)
POST /api/import/documents/preview filenames in, match plan out
POST /api/import/documents/attach one file, archived and linked
components/import/UnderlagImportWizard review, adjust, run
Guards, because a document linked to a posted verifikat is
räkenskapsinformation and can never be re-pointed (BFL 7 kap):
- Matching keys on the SOURCE voucher number, never our own. The importer
renumbers per target series, so a file named after our number would land
on the wrong verifikat exactly when the import skipped a voucher.
- Nothing is uploaded until the whole plan has been shown: the preview
sends filenames only, the bytes stay in the browser.
- A ref that hits several migrated years is surfaced as a choice, never
resolved by guessing. So is a filename with a number but no series, which
is resolved but never pre-selected.
- A date-named file (20240131.pdf) is refused outright rather than read as
voucher 20240131.
- A target in a closed or locked period is shown but not selectable:
enforce_period_lock_documents would refuse the write anyway.
- The attach route re-resolves the filename server-side and 409s when it
does not name the target the client sent, so a stale plan cannot scatter
underlag permanently. An explicit manual assignment opts out of that check
and is flagged as such; company ownership of the entry is always verified.
- Idempotent per (verifikat, content): a re-run converges on the same
document row instead of archiving duplicates.
tests/pg/underlag-attach-period-lock.pg.test.ts pins the period-lock contract
the plan surface promises, including that the lock guards the LINK and still
lets an unlinked document be archived.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(import): scope underlag matching to a declared fiscal year
Adversarial review of #1627 refuted the resolver: it looked a ref up
company-wide and treated "exactly one candidate exists" as proof of identity.
Source systems restart voucher numbering every year and a filename carries no
year, so with a partial migration, or with that year's A31 among the vouchers
the importer routinely skips (empty, single-line, unbalanced), a 2023 receipt
was silently attached to a 2025 verifikat. Permanent under BFL 7 kap, and
invisible afterwards. Cardinality is not identity.
Every batch now declares its fiscal year and candidates outside it are dropped
before the index is built, so no downstream branch can see, count or propose
one. The attach route takes the year for its re-resolution from the TARGET
entry, never from the client, so the check cannot be widened by naming a
different year. Scoping cannot make the year inferable; it makes it asserted,
and the confirm dialog reads it back because it is the one input the files
cannot corroborate.
Four further defects from the same review:
- npm test went red: hoisting the column list into a VOUCHER_SELECT constant
hid it from the no-phantom-columns AST scan (ceiling 377 -> 379) and
dropped all eight journal_entries columns out of the guard on the one path
that writes irreversible links. Both selects are inline again, and split:
the provider sweep no longer fetches three display columns it never reads.
- The date guard only caught zero-padded hyphenated dates, so
`2024-1-31 kvitto.pdf`, `2024 01 31 ...`, `2024.1.31` and `24-01-31` all
parsed as voucher 2024 or 24. Widened to unpadded components, two-digit
years and space/slash separators; a bare year-shaped number is refused.
- `Verifikation 31.pdf` parsed as series ION: the alternation matched
`ifikat` and left `ion` for the series group. Reordering alone was not
enough (the engine backtracks into it), so the prefix now requires the
word to end.
- The manual-reference box was an unguarded write path: typing a date got
path-split down to a voucher number, marked the row selected, and posted
with override, which skips both server checks, while the row still showed
"Kan inte tolkas". Directory splitting is gone from the parser, the row
status is updated on resolve, and picking a server-proposed candidate no
longer counts as an override, which had disabled the filename check on
exactly the ambiguous rows it exists to protect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(import): enforce the declared fiscal year on the server
The second adversarial pass refuted the previous fix. The attach route took
the year for its re-resolution from the TARGET entry, which is tautological:
an entry is by construction inside its own fiscal_period_id, so the filter
could never drop it and the year axis was unfalsifiable. Server-side year
enforcement was zero; the declared year existed only as React state and was
never sent. The regression test that "proved" otherwise passed only because
the mock let one journal_entries row report two different fiscal_period_id
values to two different reads, a state Postgres cannot produce. A test that
could not fail.
The attach request now carries the year the user actually reviewed, echoed
back from the plan, and the route asserts it equals the target's own period
BEFORE any other check and including overrides: an override is a statement
about which verifikat, never about which year. Its test asserts that directly
instead of a mock artifact.
Also from the same pass, a UI race that made the confirm dialog lie: FyPicker
stayed interactive while a preview of up to 2000 filenames was in flight, so
the summary and the confirm text could read back a year the plan was not built
from, and a manually resolved row could join the batch from another year
entirely. The wizard snapshots the plan's year, every downstream read uses the
snapshot, manual re-resolution goes through the server's own echoed
plan.fiscal_period_id, and the picker is frozen while a preview runs.
Parser, from the corpus pass (~360 realistic filenames plus 200k random uuids,
no ReDoS found: 2000 hostile inputs in 26ms):
- Day-first and US dates parsed as voucher numbers: `31.01.2024` became
voucher 31, a number that always exists in the year. The guard now covers
both orders.
- `ver 31.pdf` parsed as series VER and came back auto-selectable, while
every spelled-out `Verifikat 31.pdf` correctly yielded a series-less
reference needing confirmation. Same filename, two trust levels, decided
by an abbreviation. `ver` is no longer a series.
Known residual, stated rather than papered over: a scanner's `A4.pdf` or a
`K10.pdf` blankett in the receipts folder still matches verifikat A4 or K10
when that year has them. No parser can separate those from a genuine
reference; they appear in the review table with the target's date and
description.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(import): make the user actually declare the fiscal year
The third adversarial pass found that the central guarantee of the previous
two commits was fiction. FyPicker auto-selects the newest fiscal period when
nothing is stored, and the wizard passes a page-specific storage key, so that
branch fired on every first use. A user migrating 2023 receipts who never
opened the picker resolved them against the newest year; A31 exists in
essentially every year, so those rows came back `matched`, pre-selected, with
only the confirm dialog between them and permanent links. Every commit message
and code comment claiming "the year the user named" described behaviour the UI
did not have.
FyPicker gains an opt-in `requireExplicitChoice` prop, default off so no other
caller changes, and the wizard uses it. The picker starts empty and the batch
cannot proceed until someone picks. A previously stored explicit choice for
this surface is still restored, which is what makes a multi-batch migration
bearable.
Also: a company with zero fiscal periods hit a disabled picker and a disabled
button with no explanation. There is now a line saying why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(import): close the restore-branch hole and demote collision-prone refs
Round four of adversarial review, two findings, both fixed.
1. `requireExplicitChoice` gated only the newest-period fallback, not the
localStorage restore branch above it, so the "user declares the year"
guarantee held only for a user's first-ever batch. From the second on, the
year was silently pre-filled from an earlier unrelated batch, and in a
multi-year migration last-used is the worst possible default: the user is
by definition moving to a different year each round. The prop now gates
FyPicker's ENTIRE auto-selection block with one outer condition (restore,
the ALL_YEARS-stored fallback, newest-period, preferLatestEnded), because a
per-branch gate already missed one branch once. It also suppresses the
localStorage write, which fired BEFORE onChange and so recorded picks the
wizard had rejected mid-preview. The wizard drops its storage prefix
entirely: within one sitting reset() carries the year in state, and
nothing survives the session.
2. The filename parser pre-ticked `A4 scan.pdf` and `K10.pdf` while requiring
a click for `31.pdf`, which carries MORE voucher evidence in a
single-series company. Two independent review passes flagged the same
inconsistency. Collision-famous refs (A0-A6 paper sizes, K2-K13/N1-N9/
T1-T2 blanketter, Q1-Q4 quarters) and three-letter series (IMG/DSC/DOC/
SCN are cameras; real SIE series are 1-2 chars) still parse and resolve
but are never auto-selected. Demoted, not refused: verifikat A4 genuinely
exists in every migrated ledger, and its real receipt costs one click.
Residual documented: an existing short series plus a small number in an
ad-hoc name (`B2 hyra.pdf`) is indistinguishable from a real ref by
filename alone.
Also: the attach route's multipart doc now names the required
fiscal_period_id field, and the stale reset() comment describes the actual
persistence model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(import): honor override only for unresolvable filenames + review round
Resolution pass for the PR #1627 review reports (CodeRabbit, Swedish
accounting review, compliance swarm).
The one substantive finding (CodeRabbit, major): `override: true` skipped the
filename consistency check entirely, so a crafted client could attach a
cleanly-named file to any same-year verifikat. The resolver now runs on every
request; an override is honored only when the filename is unresolvable in the
declared year (no parse, or no candidate) or already resolves to the requested
target. The shipped UI only overrides unresolvable rows, so nothing
user-facing changes. planAcceptsTarget is renamed planPermitsAttach and
carries the semantics in one place, with tests for both directions.
The Swedish review finding (BFNAR 2013:2 systemdokumentation): the
planPermitsAttach JSDoc still described the superseded derive-the-year-from-
the-target design. It now states the actual control: the route asserts the
caller-declared year equals the target's own period before this function runs.
CodeRabbit minors and nitpicks:
- underlag_confirm_body / underlag_run / underlag_locked_warning use ICU
plural forms in both locales; "1 filer arkiveras" was wrong Swedish.
- The attach and preview route tests mock @/lib/supabase/server per the
repo test guideline.
- fetchVouchersForNumbers narrows to the declared fiscal year at the DB;
the in-memory filter in buildUnderlagPlan remains the enforced truth.
- buildVoucherIndex appends into existing arrays instead of copying per
row: the provider sweep indexes every migrated entry in the company and
per-row copies made that O(n^2).
- The pg test reuses its insertDocument helper instead of a duplicated
INSERT; runAttach clears isLoading in a finally.
Declined, with reasons in DECISIONS.md: message-regex classification of
validateDocumentFile failures (established sibling pattern; validator
contract change is out of scope).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(import): attach only to posted or reversed verifikat
Second review cycle on PR #1627: the Swedish accounting review's re-run found
that nothing in the attach route verified the target entry's status. The SIE
import RPC posts every entry inside its own transaction, so a draft carrying a
source ref should be unobservable, but the link this route writes is
irreversible räkenskapsinformation, and an invariant enforced in another file
is not one this surface may lean on. Underlag references a verifikation
(BFL 5 kap 6-7 §), so the target must BE one.
Enforced twice: the route rejects non-posted targets with
UNDERLAG_ENTRY_NOT_POSTED (overrides included), and the resolver reads filter
to posted/reversed so a draft can never even become a candidate. Reversed
stays attachable: a storno'd original remains räkenskapsinformation and its
underlag belongs on it.
Also recorded as confirmed-intentional (review note, no code change): with
override and an unresolvable filename the endpoint links to any same-company,
same-declared-year, posted verifikat, migrated or not, which mirrors the
existing /api/documents/[id]/link capability. The period-lock error-string
regex note restates a disposition already recorded in DECISIONS.md.
The arcim test's Supabase double learns .in(), which the shared resolver read
now uses for the status filter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4362bffc0c |
fix(skattekonto): deep-link Skapa verifikat manuellt to a prefilled, auto-linked verifikat (#1621)
* fix(skattekonto): deep-link Skapa verifikat manuellt to a prefilled, auto-linked verifikat "Skapa verifikat manuellt" in the SkattekontoBookDialog routed to plain /bookkeeping: the user landed on the list with no form, no prefill and no link to the row (reported by a user for a Slutlig skatt event, which has no booking rule by design). The CTA now deep-links to /bookkeeping?skv_tx=... carrying the row's id, date, text and amount. The bookkeeping page opens the Nytt verifikat dialog prefilled (1630 on the correct side per the booking sign convention, balanced counter line with the motkonto left to pick, date and description set) and, once the verifikat is saved (posted or draft), links it back to the skattekonto row via the existing match endpoint. A failed link degrades to a destructive toast pointing at the manual "Matcha mot verifikat" path. The URL params are prefill convenience only: the match route re-validates ownership, ALREADY_BOOKED and ENTRY_ALREADY_LINKED server-side. The parse/build/line-shaping contract lives in core lib (lib/skatteverket/manual-verifikat-prefill.ts, unit-tested) because the bookkeeping page cannot import from the extension. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skattekonto): keep deep-link payload out of the URL + share the 1630 constant Resolves the PR #1621 review findings in one pass: - Compliance swarm (GDPR Art.5(1)(f), ISO A.8.12): the deep link no longer carries date, text and amount as query params, where they would persist in browser history, access logs and Referer headers. The row payload is staged in sessionStorage, consumed single-use and validated against the opaque skv_tx id, which is all the URL exposes. A missing or mismatched payload degrades to the plain /bookkeeping list; the auto-link itself is still validated server-side by the match route. - Swedish accounting review note: SKATTEKONTO_ACCOUNT ('1630') is now imported by the extension's booking and match libs from the core prefill lib instead of being duplicated, so prefill and server-side booking cannot drift. - CodeRabbit docstring warning: the new lib exports carry docstrings. Storage is injectable (PrefillStorage) so the node-env tests cover the round-trip, single-use semantics, id mismatch, malformed payloads and a throwing privacy-mode storage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skattekonto): record the sessionStorage staging window as accepted residual risk The compliance swarm's remaining LOW finding (ISO A.8.12) offers documentation as its remediation path: an XSS attacker already reads the full ledger via the session's authenticated APIs, so the sub-second sessionStorage staging window adds no capability worth a server-issued token roundtrip. Recorded in the lib header and DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6404591b89 |
fix(import): parse the SEB Transaktioner CSV layout (split Insättningar/Uttag) (#1616)
* fix(import): parse the SEB Transaktioner CSV layout (split Insättningar/Uttag) The SEB profile only understood the Kontoutdrag export layout. The Transaktioner page (the path most users find first) exports a different header: Bokförd;Valutadatum;Text;Typ;Insättningar;Uttag;Bokfört saldo, with dot decimals and the amount split across two columns. No profile detected it, so auto-detection found nothing and an explicit SEB choice failed on column detection. Teach the SEB profile the layout: detect on the Insättningar/Uttag pair (unique among supported formats), accept Bokförd as a booking-date column, and combine the split amount (Uttag carries its own minus; unsigned magnitudes are normalized to expenses). Fixture header and first data row are verbatim from a user-provided export. The import help text now lists both SEB export paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: decision log for SEB Transaktioner parser design Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3036c3d879 |
feat(transactions): pick underlag from the inkorg in quick review (#1620)
* feat(transactions): pick underlag from the inkorg in quick review The quick-review dialog reached from "Att göra -> Bokföra transaktioner" only offered a local-file dropzone, so a receipt already sitting in the underlagsinkorg had to be re-uploaded. That also left the inbox item unconsumed, leaving a duplicate in "Underlag att hantera". Wire InboxDocumentPicker (select mode) into the Underlag section, next to the upload zone, mirroring TransactionBookingDialog. Picked documents are held until the booking returns a verifikat, then linked through the existing linkDocuments call with their inbox_item_id, which stamps the inbox item as consumed. Picks reset on close and when the dialog moves to another transaction, so a document picked for one row can never attach to another row's verifikat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(transactions): clear inbox picks when the underlag link fails The partial-failure branch returned before the cleanup, so a pick that had already linked (and been consumed) stayed in state and would be re-linked on the next transaction the reused dialog opened. Picks are safe to drop: one that failed to link was never stamped, so it is still in the inkorg. Also record in DECISIONS.md that no "saknar underlag" predicate keys on transactions.document_id, which is what made leaving the pin off safe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(transactions): lock the inbox pick controls while booking handleConfirm captures pickedInboxDocs when it starts, so a document picked after the booking request went out was never linked, and the cleanup on completion then removed it from the list with no error. The pick and remove controls now follow the rest of the dialog and disable while isProcessing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0938646693 |
feat(onboarding): the branch question becomes its own journey step (#1615)
Founder feedback from a real signup: the done screen stacked the welcome, the 8-row company profile card AND the branch question, pushing the question below the fold, and the tiny favicons-in-ellipses provider chips looked bad. The done screen now ends in a revealed Fortsatt action; a new 'source' step at the existing KLART station (same station grammar as momsyn/moms under MOMSEN) shows only "Var fanns bokforingen innan?" with redesigned provider tiles: a 2-column grid of generously sized choices, each with the real logo on a small white bordered mark (the LogoMark grammar from NewUserChecklist), SIE-fil and new-business as equal-weight text tiles, flat hover, no lift. Everything fits one viewport. Behavior preserved exactly: mode='first' only (reducer-guarded DONE_CONTINUE), the single-choice latch, fire-and-forget keepalive PATCH /api/onboarding/state, captureBranch analytics, branchDestination routing, and the quiet skip escape. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fb3feaea12 |
refactor(import): de-box the migration wizard and give it a real ending (#1617)
The provider-migration wizard was a pile of cards in cards: a boxed progress rail with a fat bar, a bordered Card per step with more bordered boxes nested inside, amber warning panels, and a result step made of a verdict card, orange-badged per-year cards and a green-check card grid. All of it against the design conventions (no boxes doing hierarchy's job, attention is one ochre sentence, status colors are data not chrome). - Progress rail: quiet uppercase step row (done steps muted with a check, current in ink) over a hairline thread whose ink segment is the progress. - Every step de-boxed onto the page: serif step headlines, hairline-divided provider rows and option rows with the toggle on the right, per-year status as quiet muted lines, Verifikationsserie as one aligned row, connect/preview as headline + one quiet statline. - Amber boxes become AttnLine sentences (SIE-required gate, failed years); errors keep strong color as text, never tinted panels. - Step entry animates via the standard stagger-enter grammar (slideUp, reduced-motion safe); row lists stagger with data-no-stagger opt-out from the parent cascade. - Confirm dialog: one sentence naming what happens, compact muted selection list, replace-years info as muted prose; the ochre caution is the only colored element. - Result step rebuilt on the reveal grammar: settled TheaterCanvas beside a serif verdict derived from the real results, a compact statline, per-year outcomes as hairline lines with warnings as one ochre sentence each, Övriga data as a quiet two-column line list, next steps as quiet numbered lines. Document follow-up restyled to the same section grammar. - Removed dead code (MONTH_NAMES, InfoItem) and the now-unused accordion. No logic changes: handlers, fetch flows, OAuth/document reducers and all step gating are untouched. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b556475b01 |
fix(import): migration preview and theater read all fiscal years, not just the newest (#1614)
The /sie-data route parsed only the newest fiscal year's SIE file for the import preview and the returned SIEData.parsed. Mid-year provider exports have few or zero vouchers in the newest year, so the first real Fortnox migration (3 fiscal years, 4153 vouchers) previewed "0 verifikationer" and drew an almost-empty migration theater while the import itself landed all 4153 vouchers from the older files. - New mergeParsedSIEFiles (lib/import/sie-merge.ts): pure, browser-clean whole-dataset merge (accounts union first-wins, vouchers concatenated, fiscal years union oldest-first re-indexed newest=0, balances and issues concatenated, dimensions deduped), with unit tests. - /sie-data parses each file exactly once, builds the preview from the merged parse and returns parsed: merged; response shape unchanged. Validation stays newest-file-only so no previously accepted dataset is newly rejected. - /preview drops latestOnly and computes sieStats from the merged parse: the connect step's "Hittade X konton och Y verifikationer" line renders from THESE stats, so this is where the founder-visible count was lying. - The migration theater spreads its account waves across ~10s and births an additional wave on each real step label during the SIE phase (progress <= 55), through a shared rate-limited gate, so the canvas keeps performing over a multi-minute run. Narration labels and progress remain the wizard's real values; reduced motion unchanged. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ea85f4c084 |
feat(providers): use WINT's real logo instead of the placeholder wordmark (#1619)
public/logos/wint.svg was an explicit placeholder ("swap for WINT's official
logo asset before launch"): an Arial "WINT" text node. Replace it with the
official mark, supplied as PNG with transparency.
Downscaled from 1402x1122 / 771 KB to 256x205 / 10 KB (trimmed, 16-colour
palette) so it sits in the same weight class as the other provider logos,
which are 1.6-4 KB and render in the same 40px chip.
Both references updated: the wizard's PROVIDER_LOGOS map and the /import
LogoChip row. No wint.svg references remain.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5984652e47 |
fix(skatteverket): request the AGI kvittens scope and stop misdiagnosing the 403 (#1596)
* fix(skatteverket): request the AGI kvittens scope, not just the inlamning one
AGI is backed by two SKV APIs and each needs its own scope, but DEFAULT_SCOPES
only carried `agd`. That covers arbetsgivardeklaration/inlamning (POST underlag,
kontrollresultat, spara, skapaGranskningsunderlag), so a filing submits and
signs perfectly. The kvittens read lives on hanteraredovisningsperiod, which
requires `agdredovisningperiod`, so the flow died on its very last step with
403 {"error": "The required scopes are not authorized"}.
Confirmed against production: the same APIGW client id and secret reach the
bearer check on both APIs (401 "Token has been revoked" from each with a bogus
token), proving both subscriptions exist and the gateway contract was never the
problem, and SKV's application registration lists `agd`, `agdredovisningperiod`
and `momsdeklaration` as three separate scopes.
The body is identical to the APIGW subscription gap of #973, which is why
api-client's classification cannot distinguish the two from the string alone;
that split still needs the gateway-side check, so it is left as is.
Note the spelling `agdredovisningperiod`: no genitive s, exactly as SKV
registers it. Pinned with a scope-set regression test, since this is the third
time a scope has gone missing (#431 removed `ska` the same way) and the damage
is always invisible until a real filing fails.
The AGIPanel missing-scope banner now checks both scopes. It only looked for
`agd`, so the token shape that actually hurts, one that gets all the way
through signing before failing, produced no warning at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(skatteverket): stop blaming the APIGW subscription for a missing token scope
`{"error": "The required scopes are not authorized"}` has two causes and the
gateway never says which: our APIGW client has no subscription for the API
(#973), or the token lacks the scope that API requires. The message named only
the first, so a real production filing spent its debugging time in
Utvecklarportalen while the actual fix was a scope the code never requested.
The message now names both knobs, and says WHICH service refused. That last
part is what was really missing: the sibling inlamning API kept working
throughout, so "Skatteverket denied the call" pointed at nothing. Ruling the
two apart still needs the out-of-band test (call the API with a deliberately
invalid bearer and the same Client_Id: a subscription gap fails at the gateway
with this body, a scope gap reaches the bearer check and answers 401), which is
now documented at the predicate instead of living in someone's memory.
Classification is deliberately unchanged. ACCESS_DENIED stays out of
RECONSENT_ERROR_CODES: the scope case does need a reconnect, but only after the
scope exists, so an automatic reconsent would rebuild the self-perpetuating
banner of #1155.
This reverses one specific decision from #1250, which removed the
SKATTEVERKET_SYSTEM_SCOPES mention on the reasoning that the gateway rather
than the scope list had refused. That reasoning assumed the body could tell
them apart. It cannot. The test asserting the omission is inverted, with the
reason recorded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(salary): make the AGI kvittens-scope notice dismissible
The two AGI scopes get different treatments in AGIPanel: a token
without agd keeps the hard reconnect nudge, but a token missing only
agdredovisningperiod gets its own softer, dismissible notice. Every
existing hosted token lacks the new scope, and until Skatteverket's
application registration carries it a reconnect mints the same grant
again (SKV silently drops unregistered scope names), so a hard
reconnect demand would be the #1010 un-clearable banner, with each
futile attempt costing a BankID round-trip. Dismissal persists per
granted scope string: a futile reconnect stays quiet, a widened grant
re-evaluates from scratch.
Also asserts the shared prenumeration/scope message and the refused
API path on the 401 contract test, matching the 403 test (review nit).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>
* fix(salary): point the kvittens-scope notice at Skatteverket's own e-service
The compliance review flagged that dismissing the notice could leave a
filer without a software-side path to the receipt. Retrieval never
depended on the notice (the kvittens cron retries server-side and the
period's status row stays unconfirmed until a kvittens lands), but the
copy now also names the fallback that always works: verifying the
receipt in Skatteverket's Arbetsgivardeklaration e-service.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>
* fix(salary): use the ladder radius on the kvittens notice
The radius ladder guard landed on main after this notice was written;
rounded-md is dead vocabulary, bordered boxes are rounded-lg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(salary): scope the kvittens-notice dismissal to the employer
The localStorage key carried only the granted scope string, so on a
shared browser one company's dismissal hid the notice for every other
company with an identical grant. Skatteverket tokens are per company,
so the key now includes arbetsgivare alongside the scope string; the
same-grant reconnect behavior per company is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Emil <emilmattsson14@gmail.com>
|
||
|
|
315b94019b |
feat(providers): release the WINT card in the migration wizard (#1618)
WINT rendered as a disabled "Kommer snart" chip while the provider shipped dark. The provider is wired end to end, so empty COMING_SOON_PROVIDERS and let the card be selectable. WINT still needs WINT_MIGRATION_ENABLED=true in the environment: that server-side /connect gate is unchanged and is what actually authorizes the connection. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2d97fbf1bc |
fix: false popup-blocked toast on Visa dokument + scope SKV reconnect line to skattekonto source (#1613)
* fix(documents): stop false popup-blocked toast on Visa dokument window.open() returns null BY SPEC when 'noopener' is in the features string, even when the tab opens, so the destructive 'Tillåt popupfönster' toast fired on every successful open. Open without the features string and sever the reverse channel manually (tab.opener = null), the same pattern lib/browser/deferred-tab.ts already uses; the toast now fires only on a genuine popup block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): scope the SKV reconnect line to the skattekonto source The reconnect attn line rendered on /transactions whenever the SKV connection needed renewal, regardless of what the user was looking at, so it read as permanent noise. It now shows only when the source picker is on Skatteverket (the rows it actually explains); the skattekonto page keeps its own reconnect line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): keep Skatteverket source pickable while reconnect is needed In the reconnect-needed state the transaktioner fetch 401s, skvRows goes empty, the Skatteverket option left the source picker, and the stale-filter effect reset the filter to 'all': the source-gated reconnect line became unreachable exactly when it applied. Show the source whenever rows exist OR reconnect is needed (skvNeedsReconnect already requires connected=true, so never-connected companies get no phantom source). No component test: repo test scope is lib/ + app/api/ (no component tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9686b54b41 |
refactor(design): lock the border-radius ladder, one radius per role (#1607)
Seven radii were in circulation (4/5/6/8/12/16px + pill) with no rule for which went where; one toolbar row on /transactions mixed four shape languages. This locks a 4-tier ladder (design.md convention 16): - pill: interactive toolbar controls (buttons, chips, pickers, segmented controls, toolbar search, count nubs) - rounded-xl (12px): overlay tier: page panel, dialogs, slide-overs - rounded-lg (8px): cards, form fields, popover/menu content, boxes - rounded-sm (4px): nested leaves (menu items, checkboxes, kbd/code nubs) Changes: - New SegmentedControl primitive (pill-in-pill tablist, h-8) replaces the hand-rolled bg-muted/70 tablist copied across 11 files - New ToolbarSearch primitive (pill, h-8) adopted on 9 page toolbars; dialog/picker searches keep the rounded-lg Input - dialog.tsx 8px -> 12px, matching SettingsModal/slide-over/CommandPalette - ContextPicker chips at the shared h-8 toolbar height - ~300 rounded-md / bare rounded call sites remapped by role; auth icon tiles and the mobile nav sheet come down from 16px to 12px - rounded-md, bare rounded, rounded-2xl and rounded-[Npx] are dead vocabulary, enforced by a new off-ladder-radius check in check:guards Verified: lint 0 errors, 14422 unit tests pass, check:guards green, tsc clean on all changed files, sandbox screenshots of transactions/ bookkeeping/granskning toolbars and the Ny verifikation dialog. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18c20e68e6 |
fix(scoping): Skatteverket per företag + nåbara startkort + företags-scopade val (#1610)
* fix(scoping): skatteverket per company + true pristine gates + scoped dismissals Skatteverket connections become per (user, company): the token table carried BOTH UNIQUE(user_id) and UNIQUE(company_id) (two stacked half migrations), so one connection leaked "connected" onto every company the user belongs to, sync ran the token against the wrong orgnr (behorighet 403), and reconnecting from another company silently moved the row and went dark on the first company's crons. Token reads/writes are now scoped by company through the whole chain (token-store, api-client refresh coalescing, skvRequest and its 21 call sites, resolve-auth, crons, MCP), /skattekonto/saldo answers 401 NOT_CONNECTED for companies without their own row (which is what the page's startkort keys on), and the dashboard connect-nudge counts only the active company's row. Bookkeeping's pristine start card now keys on all-years emptiness via a count probe instead of "no active filters": the default fiscal-year selection counted as a filter, which made the card unreachable on brand-new companies (it showed "inga traffar" instead). Two browser-global localStorage keys become company-scoped with legacy fallbacks: the inbox onboarding dismissal (dismissing on one company hid the card everywhere) and the periodisering auto-detect toggle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(scoping): dedupe cron work per (user, company) + guard the ledger probe CodeRabbit findings on #1610: the skattekonto sync cron still deduped token rows by user_id alone, which would drop every company but one for multi-company operators (the exact scenario the PR fixes); and the all-years ledger probe could leave a stale false behind on a failed refetch, letting the pristine card render unconfirmed. The probe now resets to unknown in flight and carries the fetch generation guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4e14182a00 |
fix(salary): declare, book and pay AGI in whole kronor (SKV per-sats computation) (#1611)
* fix(salary): declare, book and pay AGI in whole kronor (SKV per-sats computation) A user's first lönekörning surfaced öre amounts in the AGI payable while Skatteverket deals in whole kronor. Three connected defects: - the AGI XML rounded amounts (Math.round); öretal bortfaller (SFF 2011:1261 22 kap. 1 §) requires truncation, and FK487 must be Skatteverket's own per-sats computation on the whole-krona underlag sums (IK587, kontroll B_006), not a truncation of the öre-exact engine sum - the salary booking credited 2731 with exact öre, leaving a residual after the whole-krona skattekonto draw; 2731 now carries the declared amount with the remainder on 3740 (Öres- och kronutjämning) - the LB payment file and TaxPaymentPanel paid/showed öre; they now use the declared whole-krona totals stored on agi_declarations (which also lets skattekonto auto-settlement match the draw); legacy öre rows keep paying öre-exact so pre-deploy bookings still clear 2731 New lib/salary/declared-avgifter.ts implements the SKV computation (per-IU whole-krona underlag, per-sats sums, youth/växa cap splits, exact integer math) shared by the AGI generator, the booking split and the preview. Review overrides route all legs through the same per-category truncation; basis overrides are inert on money totals (they never reach the filed IUs); the v1 book route gains override parity with book-run; F-skatt rows ignore avgifter overrides on every surface. Booked runs show their posted verifikat instead of a recomputed projection. tax_withheld_override requires whole kronor. Adversarially verified over three /skeptic rounds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: merge origin/main and re-ratchet the öre-round baseline The merge brought #1609 (net-pay öresavrundning) whose two new Math.round(x*100)/100 occurrences are counted against the baseline this branch had tightened from 637 to 629; 631 keeps the net -6 improvement without policing already-merged code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): address PR review (hybrid override computation, legacy youth cap, robustness) CodeRabbit round on #1611, all findings in one pass: - computeDeclaredAvgifterWithOverrides: one shared hybrid for the AGI generator AND the booking split. Overridden rows contribute their manual amounts per category; colleagues keep the SKV-exact per-sats underlag computation (a FoU override on one employee no longer costs the rest of the roster kronor of declared accuracy) - youth cap keys on the RESOLVED category so legacy null-category rows classified as youth by the rate heuristic still get the 25k split - F-skatt rows zero their avgifter_basis on both booking surfaces and in the preview, matching the AGI's isFSkattRow invariant - preview route: posted-voucher lookup errors return 500 instead of masquerading as a booked run with no vouchers; 400/500 tests added - run page clears stale AGI totals when the tax-payment fetch fails - SalaryOverridePanel truncates the tax override to whole kronor so the schema's .int() cannot bounce a decimal input with a 400 - v1 book route override parity pinned by a lifecycle test - DECISIONS.md format fixes + superseded entry marked; exempt category mapped explicitly; unified truncation-drift band with rationale Declined (recorded): dating the decision entries 2026-08-13 (bot assumed UTC; the decisions were made after midnight local time). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): round-2 review nits (shared F-skatt helper, test hygiene) - isFSkattStatus in declared-avgifter.ts: single source for the F-skatt exclusion, consumed by book-run, the v1 book route, the preview route and the AGI generator, per the Swedish review's drift-risk finding - declared-avgifter test suite gets the standard beforeEach cleanup Declined (recorded for the summary): auto-generated correction voucher for regenerated legacy periods (data-repair follow-up needing Emil's go); SFF 22 kap. 1 par. citation doubt (verified against lagen.nu and already shipped in tax-tables.ts); 3740 scope doubt (BAS generic utjamning account, Visma praxis, matches the user's reference voucher). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fbe4e18730 |
feat(mcp): book on custom accounts via account_override; fix kontoplan settings link (#1608)
* feat(mcp): book on custom accounts via account_override; fix kontoplan settings link gnubok_categorize_transaction only spoke a 19-category enum mapping to 21 hardcoded BAS accounts, so company-custom accounts (e.g. VMB) were unreachable from the agent surface even when active in the chart. - add account_override to gnubok_categorize_transaction with v1 REST semantics via a shared helper (lib/bookkeeping/account-override.ts): business-side replacement, class-2 auto-VAT drop with the 2610-2649 moms-line exception, plus a same-account degenerate guard; validated at staging and re-validated at commit - align the gnubok_create_voucher staging gate with the engine's seeding semantics: BAS 2026 accounts merely absent from the chart pass (the engine backfills them at commit) and the preview lists will_activate_accounts with BAS-name fallback; non-BAS unknown and inactive accounts still rejected - stop suggest_categories silently dropping mapping rules whose account is outside the fixed category maps; they surface with the rule's own account and an explanatory match_reason - correct the create_account next-step hint (categorize could never use the new account before; now true via account_override) - point the settings "Kontoplan (BAS)" link at /chart-of-accounts and redirect the orphaned /bookkeeping?tab=accounts URL (tab removed in #850; the deep link never worked after the #854 merge collision) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): address review findings on account_override - commit executor rejects a present-but-malformed stored account_override loudly instead of degrading to the category default (CodeRabbit major; the approver approved a preview showing the override account); with commitPendingOperation regression tests - accountToCategory returns null for unknown income accounts so custom income accounts get the same diagnostic as expenses (CodeRabbit minor), with income + reason-accumulation tests (CodeRabbit nit) - pin the class-2 VAT-drop balance invariant with a test through buildTransactionEntryLines (Swedish compliance review: gross booking, never an unbalanced net + missing VAT leg) - account_override description asks the agent to state the actual affärshändelse in notes when overriding (BFL 5 kap description concern) - eventBus.clear() in the two new test suites (CodeRabbit minor) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): never guess a moms leg onto an account_override without explicit VAT intent Round-2 Swedish compliance finding: the class-2 VAT drop did not cover margin-scheme (VMB) accounts in class 3/4, which are the override's flagship use case, so a forgotten vat_treatment attached the category default standard_25 and booked an ingående-moms deduction on a transaction where input VAT is not deductible (ML 2023:200). applyAccountOverride now takes explicit VAT intent (vat_treatment or vat_amount present) and books GROSS with no auto-VAT line without it: forgetting the flag under-deducts (lawful), never over-deducts. Both call sites (MCP staging preview, commit core) derive the flag the same way; the tool description states the enforced behavior. Deliberate divergence from v1 REST recorded in DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: move stray decision-log entry to the root DECISIONS.md The round-2 entry was appended from the wrong working directory and landed as lib/bookkeeping/__tests__/DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4bb0655e4a |
feat(salary): öresavrundning of net pay to whole kronor (#1609)
* feat(salary): öresavrundning of net pay to whole kronor Some banks reject salary payment files whose amounts carry öre. New company_settings.salary_net_rounding toggle (off by default): the engine rounds each net payout up to the next whole krona, never down, and emits a derived oresavrundning line item (semesterersattning pattern) that debits 3740 Öres- och kronutjämning so the salary entry stays balanced. Gross, tax and avgifter are untouched, so AGI/KU are unaffected. Payment files (pain.001 + Bankgirot LB) get whole-krona amounts via the rounded net_salary. Toggle in salary settings; payslip and run detail show the line item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): keep employer cost on the shared definition; block manual rounding lines Skeptic findings on the öresavrundning commit: (1) the engine included netRounding in totalEmployerCost while payslip summary, KPI cards and lönejournal recompute the figure from stored columns, printing two different totals on the same payslip; employer cost now stays on the shared definition and the öre cost is carried by the 3740 ledger line. (2) 'oresavrundning' is excluded from the line-item create/update schemas: it is the only item type the booking keeps out of the gross reconciliation, so a manually created row would structurally unbalance the salary verifikat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): add the item_type CHECK as NOT VALID, validate separately Compliance-swarm finding (SOC 2 CC8.1): the CHECK re-add scanned salary_line_items under the ADD's ACCESS EXCLUSIVE lock. Split per the house pattern (DECISIONS.md 2026-07-13): 20260813143000 re-adds the constraint NOT VALID, new 20260813143001 validates it under SHARE UPDATE EXCLUSIVE in its own transaction. The list is a strict superset of the previous CHECK, so validation cannot fail. Both files are branch-only, so editing in place is within the never-modify-shipped rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4a9fa5e6c5 |
feat(inbox): staged upload ack, HEIC/HEIF validation, WhatsApp silence fixes (#1605)
* fix(whatsapp): app-side unmute, close silent intake paths, health visibility - add POST /link/unmute and a Reactivate control on the Pausad state - company resolution: transient query errors release the row for sweep retry; genuine zero-options sends M19 instead of parking silently - media from unlinked senders bypasses the hourly greeting throttle (10 min burst window, daily cap kept) - GET /link returns 7-day failed-delivery and parked-inbound counts; sweep summary logs outboundFailed24h Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(documents): real HEIC/HEIF magic-byte validation, bilingual upload errors - detect ISO-BMFF ftyp brands (heic/heix/heim/heis/hevc/hevx/hevm/hevs, mif1/msf1) instead of exempting image/heic from validation; declared heic/heif accepts either family member (iOS labels vary) - new INBOX_UPLOAD_* structured error codes replace raw English strings on the inbox upload and attach-document routes - registry doc corrected to the real 10 MB cap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(inbox): staged upload with instant ack and deferred AI extraction - web uploads insert the inbox item as status processing and respond immediately; Bedrock extraction and supplier match run via after() with a CAS flip to received (email and WhatsApp channels keep the synchronous path) - widen invoice_inbox_items.status CHECK to include processing (migration 20260813180000, pg-real test included) - crash-recovery sweep cron (*/2) flips stale processing rows; bulk-book skips extraction_in_progress items - workspace: processing chip, in-flight rows disable actions, realtime flip, retry-extraction button for empty extractions - picker accept list drops HEIC/HEIF so iOS transcodes library photos to JPEG; server allowlists unchanged (supersedes 2026-08-01 HEIC decision, see DECISIONS.md) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(migrations): bump inbox processing-status migration past main's latest Main merged 20260813210000 while this PR was in flight; an inserted version older than the latest applied aborts the prod db push at merge. Renamed 20260813180000 to 20260813213000 and updated references. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(decisions): log preview-tracker orphan repair after migration rename Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
57d6651cfc |
feat(empty-states): startkort on six pages with strata imagery (#1603)
Replace the true-empty states on Kundfakturor, Transaktioner, Underlag, Loner, Bokforing and Skattekonto with StartCard: a self-contained dark hero (image-derived ground baked into the strata render, white primary CTA) that says what the page can do instead of what is missing. Primary CTAs lead with the connect/setup action per page (bank via PSD2 deep link, mailboxes, Skatteverket, migration import); filtered/search empty states and viewer fallbacks keep the old compact states. Design signed off in the Startkort prototype iterations 2026-08-13. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
08440fed94 |
feat(reconciliation): match migrated bank history against imported SIE verifikat (#1598)
* feat(reconciliation): match migrated bank history against imported SIE verifikat A first-class Fortnox/SIE migrator path: after SIE import plus bank connect or bank CSV upload, historical bank rows are auto-matched (>= 0.9) or suggestion-matched (0.75-0.89, persisted for review) against the imported verifikat, with a guided review surface, instead of landing as anonymous "Att bokfora" rows. Phase 0: per-cash-account unattended sweep (fixes #1298 cross-account pooling); widen payment_match_log action CHECK with linked_to_existing_voucher (silently unlogged since March). Phase 1: potential_journal_entry_id/method/confidence on transactions with CHECK + invalidation triggers; persistSuggestions in runReconciliation; sweep after bank CSV import with SIE overlap (suppressing auto-categorization); sweep summaries stamped on bank_connections and bank_file_imports; POST /api/reconciliation/bank/confirm-suggestions with per-pair server-side revalidation (voucher consumption + bank-leg amount and direction). Phase 2: "Granska forslag" review tab on Transactions with chunked bulk confirm, per-row fallbacks, "Kor matchning igen" (all_accounts sweep mode, mutually exclusive with dry_run), attn line, pre-migration row marker. Phase 3: ImportResultStep dual CTA (bank connect + CSV), migrator variant of the account-picker #917 nudge, sweep outcome on the onboarding checklist bank step. Non-selection apply runs on /api/reconciliation/bank/run now floor at 0.9 and persist the review band instead of auto-committing fuzzy matches. Migrations already applied to staging under the same versions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): resolve PR review findings in one pass Swedish accounting review (both previously-deferred holes closed): - runReconciliation's >= 0.9 auto-apply now writes 'matched' to payment_match_log (behandlingshistorik, BFNAR 2013:2 kap 8); the bus event alone lands in the 30-day event_log and is not an audit record. - The three match-route storno-conflict branches detach reconciliation links via unlinkReconciliation instead of storno-reversing the linked verifikat: a reconciliation link points at an independent verifikat that may evidence other affarshandelser, and a wholesale reversal is an over-broad rattelse (BFL 5 kap 5 §). - Historical gap quantified on prod (read-only, recorded in DECISIONS): 762 unlogged manual links across 52 companies since 2026-03-23. CodeRabbit: - confirm-suggestions route: maxDuration 300 for full 500-item batches. - AccountPickerDialog: migrator-nudge buttons set lookbackTouched so the async gap-fill probe cannot override an explicit choice. - enable-banking post-backfill sweep: persistSuggestions so the review band is not dropped. - bank-file execute: sie_sweep stamp errors are logged, not swallowed. - ImportResultStep: sandbox keeps the CSV CTA (file import works there). - payment_match_log CHECK swap: NOT VALID + VALIDATE, no table scan under ACCESS EXCLUSIVE. - logMatchEvent calls awaited (serverless can freeze unawaited work). - DECISIONS.md stale version reference annotated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): defer reconciliation-link detach until the match commits Round-2 review findings: - CodeRabbit: the eager unlinkReconciliation call could orphan a transaction if the match flow failed after it. All three match routes now persist NOTHING up front: the final transaction update overwrites journal_entry_id and clears reconciliation_method in the same write, so any failure in between leaves the existing link intact. The release is logged as 'unmatched' after the commit. - Swedish review: the auto_suggested logMatchEvent in runReconciliation is now awaited like every other audit write. - DECISIONS entry split into compliance/CodeRabbit lines and updated to describe the deferred detach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): literal reconciliation_method payloads for the phantom-column scanner The conditional spreads introduced with the deferred detach pushed the scanner's unresolvable-expression count past its ceiling (380 > 378). reconciliation_method: null is correct unconditionally on a confirmed invoice/supplier match (null is already the value on every row that was not reconciliation-linked), so the payloads become plain literals the guard can verify. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
05380ddf54 |
feat(bookkeeping): correction-chain depth guard + Bedrock stream retry (#1581)
* feat(bookkeeping): bypassable chain-depth guard on corrections and stornos Correcting or reversing an entry that already sits 3+ links deep in a rattelse chain (correction_of_id/reverses_id walked in the DB, never description matching) now throws CORRECTION_CHAIN_TOO_DEEP, steering the caller to book ONE correction expressing the chain's net effect. Agents looped storno+rattelse 10 deep on a live company (63/193 vouchers noise). The guard is advisory, never a dead end: allow_deep_chain bypasses it on every surface (correctEntry/reverseEntry option, REST body, MCP tool arg staged through pending_operations, and confirm dialogs with Ratta anda / Aterfor anda in the web UI). MCP staging pre-flight fires the guard at stage time so the agent reconsiders in the same turn, and the executor re-checks at commit. tools/list payload ceiling bumped 59K -> 59.5K for the two bypass properties (trimmed to one sentence first). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): retry the Bedrock stream once on transient failures A transient stream death (429/5xx, transport cut, or the two known stream-corruption signatures: 'Unexpected event order' and 'request ended without sending any chunks') killed the whole chat turn, stranding the user mid-answer. The turn now retries once per turn after a short backoff: safe because nothing is persisted until finalMessage() succeeds. A new stream_restart event carries the pre-attempt text snapshot so the chat client resets the partial bubble, drops uncompleted tool chips, and shows 'Forsoker igen...' until the retried stream produces text. Non-transient errors (403, 400) keep the existing immediate-error path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api): regenerate accounted-api skill and wire allow_deep_chain through v1 apiskill:check failed: CorrectJournalEntrySchema gained allow_deep_chain, making references/journal-entries.md stale. Regenerated (hand-applied: the generator output is deterministic from the registry). While wiring: the v1 correct route validated allow_deep_chain but dropped it, and the v1 reverse route's strict body schema would have rejected it outright, leaving API clients no bypass when the chain-depth guard fires. Both now forward the flag to the engine and document CORRECTION_CHAIN_TOO_DEEP as a pitfall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: re-trigger CI after Vercel infra hang The preview for e527e4044 compiled in 91s then hung 40 minutes in the TypeScript phase and was killed with no error output; a CLI redeploy of the identical code went Ready in 5m. Empty commit to refresh the git- triggered deployment status. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bookkeeping): address CodeRabbit review on the chain-depth guard - correction-chain: report rootVoucher only when the walk reached a genuine parentless root; a broken link, cycle, or hop-cap now yields null instead of presenting an intermediate voucher as the chain root. - recordate: propagate allow_deep_chain end-to-end (recordateEntry option, route schema, and a Flytta anda bypass confirm in the dialog); a date move is another storno+rattelse layer and carried the guard with no override path. - v1 correct/reverse: run the chain-depth guard before the dry-run return so a dry run gives the same verdict as the real execution. - dashboard reverse route: 400 on malformed JSON or a non-boolean allow_deep_chain instead of silently reversing without the override; empty body stays the supported no-body case. Tests added. - AgentChat stream_restart: discard the dead attempt's reasoning and re-arm the post-tool paragraph break so a retried turn doesn't render thinking twice or glue its continuation onto restored text. - v1 reverse route doc comment updated for allow_deep_chain. Not changed: the journal-list reverse flow (flagged as a dead end) can never receive CORRECTION_CHAIN_TOO_DEEP: the list renders Aterfor only for entries that are neither storno nor correction, and such entries have no backward chain links, so their depth is always 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(bookkeeping): recordate route test expects the new options arg recordateEntry now takes { allowDeepChain } as a sixth argument; the route test's called-with assertion predates it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d02fd82191 |
feat(vat): add per-account declaration treatments (#1588)
Closes #1457 |
||
|
|
ce6efdb3dc |
refactor(pending): one pending-op-owned preview for chat, /pending and flow views (#1537)
* refactor(pending): one pending-op-owned preview for chat, /pending and flow views A staged pending_operation was rendered three separate ways: the /pending page's OperationPreview switch (8 specialized renderers keyed on operation_type), ApprovalCard's own PreviewBlock (near-duplicate renderers keyed on 4 hardcoded MCP tool names), and AgentChat's toolNameFor() hack that mapped stored operation_types onto 'gnubok_'-prefixed tool names on hydration. This is the weakest seam ahead of flow-run views (plan seam 8.3): every new operation type had to be taught to render in two places and silently degraded in the third. Now there is one owner: - components/pending-operations/OperationPreview.tsx: the /pending renderers moved verbatim, dispatched on operation_type, consumed by /pending, ApprovalCard and future flow-run views. - components/pending-operations/vocabulary.ts: operation labels, single-action warnings and the one canonical rejection-category list (ApprovalCard's copy was byte-identical and is deleted). - lib/pending-operations/tool-name.ts: the single translation point between bare operation_types and 'gnubok_' tool names, with tests. toolNameFor gotcha fixed on the way: ApprovalCard's old dispatch only recognized 4 tool names, so a hydrated card for any other operation type (attach_document_to_transaction, match_transaction_invoice, ...) silently fell back to a raw generic preview. Hydration now passes the stored operation_type straight through attachStagedOperations to the card, and live streamed cards derive it from the event's tool name, so every operation type keeps its specialized preview on resume. Per-surface chrome (list row on /pending vs inline chat card) is deliberately kept: only the preview + vocabulary were the duplicated seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: drop a stray hunt_title copy rename that rode along 'Kvittojakten' -> 'Leta efter underlag' in messages/sv.json was uncommitted working-tree state from another session, swept into the extraction commit by git add breadth. It is a product-naming call with no en.json counterpart and does not belong in this refactor; preserved in this branch's first commit if it turns out to be wanted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pending): carry params to chat previews; guard preview amounts CodeRabbit round on #1537, both real. (1) AttachDocumentPreview renders its DocumentViewButton from params.document_id, which neither chat path carried: the staged_operation stream event now includes the tool-use input (the same values the staging tool stored as pending_operations.params) and hydration selects the params column, so an attach-document card in chat shows its evidence button live and on resume. (2) InvoicePreview and CreateTransactionPreview cast amounts straight into formatCurrency; a payload without one rendered 'NaN kr'. They now share the same show-the-gap guard the legacy summary already had. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6c64dd2312 |
fix(whatsapp): make every silent disposition observable, stop pure silence for linked senders (#1563)
* fix(whatsapp): make every silent disposition observable, stop pure silence for linked senders (#1552) Silence was a legitimate outcome in seven places and none left a trace a support question could be answered from. Now: - Unknown-sender declines (over quota, quota RPC failure, greeting throttle) persist content-free trace rows: wamid, phone hash, type, disposition. No body, media, raw payload, or profile name; capped at 20 rows per hash and day; deleted by the existing 30-day retention. The wamid dedupe also stops redelivered bad-code/greeting messages from earning a second reply. - Linked-sender deliberate silences (muted, stale tap, ignorable type) record their reason on the skipped row. - Non-policy silences reply: a row missing its media reference sends M18 through the link's reply address, a link revoked between arrival and processing sends the M1 unlinked copy (greeting-throttled). - Outbound rows keep WHY a send failed (Graph error detail), and Meta 'failed' delivery statuses store their error code and title. - The WhatsApp settings panel shows the last inbound event (closed enum, server-derived) and warns when the latest reply never left. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(whatsapp): include errorDetail in typed sendText mock results SendTextResult gained errorDetail; vi.mocked call sites must match the widened type or they raise fresh tsc errors over the repo baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(whatsapp): review fixes: fail-closed greeting throttle, cap only declined traces From CodeRabbit's pass on #1563: - greetingThrottled fails closed when the throttle window cannot be read, matching the unknown-sender quota's stance. - The decline-trace day cap applies only to 'skipped' rows (the one unbounded path); 'done' traces always insert so the wamid dedupe keeps preventing duplicate M1/M2 replies even past the cap. Their volume is already bounded upstream by the greeting throttle and the pre-binding quota. - company-question test mocks match the widened SendTextResult. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0d3ba5268d |
fix(transactions): close the booking duplicate guard's blind spots (#1573)
* fix(transactions): close booking duplicate guard blind spots G1-G3 The booking-time duplicate guard missed the most common bank-fee twin shapes: - G1: the sibling scan matched on the EXACT date only, so a duplicate import with a drifted date (CSV bokforingsdag vs PSD2 valutadag) was invisible. The scan now uses a +-3 day window with a deterministic ranking where exact-date candidates always outrank drifted ones (force=true re-detection stays bound to the reviewed candidate). - G2: booked-ness required transactions.journal_entry_id, so bulk-booked (transaction_voucher_links) and multi-allocated (invoice_payments / supplier_invoice_payments) siblings read as unbooked. The scan now batch-fetches the anchor rows and resolves the verifikat via getPrimaryJournalEntryId (is_transaction_booked semantics). - G3: the ledger scan excluded every voucher linked to any transaction, so a voucher booked from a date-drifted duplicate row escaped BOTH halves and the booking proceeded with no warning. A voucher whose linking transaction itself matches the target (same ore in the same currency, compatible cash account, date in the window) is now returned as the twin with transaction_id set. All candidate picks keep explicit total-order tiebreakers so a force re-detect returns the same candidate the user reviewed, and the SEK-or-null amount contract is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): offer match/ignore for sibling duplicates and route all 409s into the dialog The duplicate dialog hid its match action for sibling-transaction candidates (canMatch required transaction_id === null), so the user who most needed steering saw only 'Bokfor anda'. manualLink explicitly allows N:1 links, so the match action is now offered for both candidate kinds. Sibling candidates get question-form body copy ('vill du matcha mot verifikatet i stallet?') and an additional 'Ignorera transaktionen' action via the existing POST /api/transactions/[id]/ignore, which is the correct resolution when the row itself is a duplicate import (matching would double-count the bank side, booking the ledger side). Two clients dead-ended the TRANSACTION_BOOK_POSSIBLE_DUPLICATE 409 in a destructive toast with no way forward: - the counterparty-template branch of handleQuickReviewConfirm now sets the shared duplicateWarning state exactly like runCategorize, with the force retry bound to the reviewed candidate's voucher - BankReconciliationView's quick-book now opens the same dialog, with match/ignore refreshing the reconciliation lists New sv/en strings: dialog_duplicate_body_sibling, dialog_duplicate_ignore, dialog_duplicate_ignore_failed. File-level parity tests pin the 409 routing and the dialog affordances. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): duplicate guard on the bulk-book samlingsverifikation path /api/transactions/bulk-book never called detectBookingDuplicate, so a batch containing an already-booked twin minted a second verifikat with no warning. The route now runs the shared per-tx guard before the RPC, with intra-batch exclusions (the other selected txs are distinct events the user picked, and the link-existing target voucher is the batch's own destination), returning 409 TRANSACTION_BOOK_POSSIBLE_DUPLICATE with the candidate and the flagged tx id. BulkBookDialog routes the 409 into DuplicateBookingDialog for review (view voucher / cancel / book anyway) instead of a dead-end toast; 'Bokfor anda' re-runs the batch with force=true. On force the route re-detects and records each dismissed candidate as BankTransactionDuplicateDismissed in behandlingshistorik (BFNAR 2013:2 kap 8), parity with the /categorize bypass. Detection failures stay fail-open. Note: the MCP RPC twin (gnubok_bulk_book_transactions) bypasses this route and remains unguarded; guarding inside the RPC needs a migration and is out of scope here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): gate the duplicate-dialog ignore hint on the action being present The sibling body copy mentioned ignoring the row, but two render sites (the manual booking form and the bulk dialog) show sibling candidates without the ignore action. The guidance now lives in a separate dialog_duplicate_ignore_hint string rendered only when the Ignorera button itself renders, so copy never points at a button that is not there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1b829883ae |
feat(reconciliation): promote bulk matching and bridge it from the inbox (#1571)
* feat(reconciliation): accept confidence_threshold on the bank run route Mirror the v1 route: RunReconciliationSchema gains an optional confidence_threshold (0..1) that passes through to runReconciliation as the server-side floor on the apply path. The UI sends 0.85 with a strong-only apply so a pair the fresh re-run scores lower is skipped instead of committed; omitting it keeps the legacy behavior where every selected pair applies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): promote the bulk match flow and bridge it from the inbox The dry-run preview with pre-ticked strong matches existed but was never found: users matched whole migrations row by row. Three discoverability changes, no engine changes: - Bankavstamning: an attention line above the toolbar while unmatched transactions exist and no preview has run, with Forhandsgranska promoted to the filled variant. When every ticked preview pair is a strong match (>= 0.85) the apply button relabels to 'Matcha X starka traffar' and the apply sends confidence_threshold 0.85; mixed selections keep the plain label and omit the floor so manually ticked weaker pairs still apply. - Autorun bridge: ?autorun=1 on /reports/bank-reconciliation runs the preview once, only after appliedDates is set and not while datesDirty, so it can never cover a different window than the on-screen lists. - Transactions inbox: with >= 5 unbooked bank rows visible, an attention line links to the reconciliation with autorun (static text + count, no probe; the preview is the honest source of how many actually match). The review step stays: autorun lands on the preview table, one click from apply, and the server intersection guard is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1eebb75269 |
feat(transactions): move an unbooked transaction to another cash account (#1570)
A bank transaction that ingested under the wrong cash account (or with no
account at all: legacy connections, own-account transfers the backfills
deliberately skipped) surfaces under the primary account's reconciliation
and can never be matched on the account it belongs to, because
cross-account matching is deliberately blocked. There was no first-party
way to fix the binding.
New PATCH /api/transactions/[id]/cash-account moves a movable staging row
(not booked, not invoice/supplier-invoice matched, not anchored via
transaction_voucher_links) to another of the company's cash accounts,
addressed by its BAS 19xx ledger account. Cross-currency moves are
hard-rejected (the row would vanish from every report's currency scope),
and the movable gate is re-asserted atomically in the UPDATE filter
against a concurrent book/auto-match, mirroring the title route. The tvl
check runs as a pre-check query since PostgREST cannot express NOT EXISTS
in an update filter; a tvl row appearing concurrently implies the booking
flow, which sets its own transaction state.
UI: 'Flytta till annat konto' in the transaction inbox row menu (opens a
radio-list dialog of the enabled cash accounts, current one preselected
and disabled) and direct 'Flytta till {name}' items in the bank
reconciliation unmatched-row menu that PATCH and refetch the view.
New structured error codes: TRANSACTION_MOVE_BOOKED,
TRANSACTION_MOVE_UNKNOWN_ACCOUNT, TRANSACTION_MOVE_CURRENCY_MISMATCH.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
0643316ac8 |
feat(import): show SIE import history with undo on the import tab (#1574)
* feat(import): show SIE import history with undo on the import tab The list route (GET /api/import/sie) and the undo route (DELETE /api/import/sie/[id]/undo) both existed, but no UI ever called the list: once the post-import result screen was gone, past imports could not be seen or undone. Add a fold-open 'Tidigare SIE-importer' row on the Importera tab (same expanded pattern as the cloud-backup row) that lazy-loads a history table: filename, date, fiscal year, voucher count, status, and an undo button on completed rows. Undo confirms through DestructiveConfirmDialog (voucher count, IB cleared, documents detached but kept; plus a voucher-gap warning for large imports), keeps the dialog open for the long-running DELETE, and refetches on completion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(import): cover the SIE list and undo routes The base list route had no test file (its siblings all do) and the undo route was only covered indirectly. Add route tests through the real withRouteContext wrapper: 401, the { data, count, limit, offset } shape with company scoping and range math, the status filter, and the Swedish 500 path for the list; 401, 403 viewer, the { success, deletedEntries } passthrough, and the SIE_UNDO_FAILED envelope (reason in details) for undo, with undoSIEImport mocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e1f13f870a |
feat(import): warn about already-imported rows in the bank-file wizard (#1567)
* fix(transactions): paginate the ingest dedup maps past the 1000-row cap
buildExistingTransactionMaps issued un-paginated selects for the booked and
unbooked dedup maps, so PostgREST silently truncated each at 1000 rows: a
re-import over a wide date range in an active company deduped against a
partial map and inserted everything past the cap as duplicates. Both queries
now go through fetchAllRows with a stable .order('id') for range paging.
Also exports the function and its types for the upcoming read-only duplicate
preview, which must share the exact stored-row universe execute-side ingest
dedups against.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(import): add read-only duplicate preview endpoint for bank files
New POST /api/import/bank-file/check-duplicates (withRouteContext + Zod,
transactions capped at 20000) computes external_ids with the exact
generateExternalId(tx, format, index) derivation execute uses and runs
previewDuplicates: Layer-1 id collisions plus the Layer-2 text bridge with
counting semantics and the currency guard, against the same stored-row maps
ingest builds (buildExistingTransactionMaps). The result is advisory; execute
stays authoritative and mirrors/settlement-account guards are documented
preview/execute differences.
A dedicated endpoint because the generic_csv path re-parses client-side and
never re-hits /parse. Also removes the dead existing_transaction_count field
from the parse response (a raw date-range count consumed by nothing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(import): surface duplicate rows in the bank-file import wizard
Overlapping bank imports used to dedup silently: the wizard promised
'Importera N transaktioner', ingest skipped the twins, and the user saw fewer
rows than parsed with zero explanation. The wizard now calls check-duplicates
after a successful parse AND inside handleColumnMappingConfirm (the
generic_csv path never re-hits parse), and:
- BankFilePreviewStep: warning card in the AlertTriangle pattern ('{count}
rader finns redan', skipped automatically) plus a 'Finns redan' badge on
flagged rows in the 50-row table
- BankFileConfirmStep: repeats the summary card (generic path skips preview)
and the CTA counts 'Importera {parsed - duplicates} transaktioner'
- BankFileResultStep: renders result.duplicates when > 0, closing the loop
ingest.ts documents as unrendered
Execute semantics unchanged: all rows are sent, ingest skips; the preview is
advisory and never promises an exact final number. New strings in both
messages/sv.json and messages/en.json next to the import_psd2 anchors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
7ccaab7a08 |
fix(agent): contain the floating assistant panel on open and resize (#1575)
A persisted float rect saved at the viewport edge, or on a larger monitor, passes clampFloatRect (which only keeps 48px reachable so a live drag may deliberately hang off an edge) and renders the panel as a 48px sliver on every open. Add containFloatRect, which snaps the whole window inside the viewport, and an AgentSheet effect that validates the persisted rect on sheet mount and on viewport resize and persists the corrected position. The rect is read through a ref so drag commits do not re-trigger containment: parking the window half off-screen still works within a session. The live drag path and clampFloatRect are unchanged, and containFloatRect's output is always a fixpoint of clampFloatRect, so the render clamp stays a no-op. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f8507d38ae |
fix(settings): land Medlemmar clicks on the members section (#1566)
The user-menu link pointed at /settings/team, but in-app navigation is intercepted by the settings modal, whose section map has no team entry: unknown sections fall back to Företag, leaving the user to scroll and find Medlemmar themselves. Link to /settings/company#members instead, and scroll the members section into view when it mounts (ref callback, since the content mounts after the settings fetch). The hash is cleared after scrolling so tab-switching back to Företag stays put. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |