c0ecf2fa3bebd46bdfd0169efd73b89653d1dfed
46 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f266c386f3 |
chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n namespaces and 4 unused dependencies; fold byte-identical helper copies into one canonical home each (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body + v1ValidationError rolled out to ~55 v1 routes, booking-template schemas). No behaviour change: v1 bodies and status codes, MCP tool schemas, DB writes and money math are untouched. Naive ore rounding was deliberately not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list of things left alone on purpose. tsc, lint, 19588 unit tests and check:guards green; antipattern baseline ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(transactions): import RawTransaction from @/types after the ingest re-export removal CI's type ratchet (check:types, full tsconfig) caught the one test file that still imported the type through lib/transactions/ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
cd40127f0e |
feat(bank): expose bank-reported balance (booked + available) in UI, reconciliation, MCP and v1 API (#2118)
* feat(bank): expose bank-reported balance (booked + available) in UI, reconciliation, MCP and v1 API
The PSD2 sync has fetched the bank's reported balance for years but the
data was stranded (F7): the Bank-page source picker read a cash_accounts
column no sync ever updated (frozen at connect time), reconciliation
hard-coded external_balance to null for bank accounts, and neither MCP
nor the v1 API exposed any balance at all, so the only path to a current
bank balance was logging into the bank.
- getAccountBalance now returns booked + available from the same
quota-limited BALANCES response (previously all but one type discarded)
- every sync (manual + cron) mirrors balance, available_balance and
balance_updated_at into cash_accounts, fixing the stale picker
- new cash_accounts.available_balance column (additive migration)
- reconciliation bank kind: external_balance = bank-reported balance,
plus bank_reported_* fields and fetch timestamp in the bank block;
difference math stays movement-based and untouched
- reconciliation view shows "Saldo enligt banken ... hamtat {date}"
- MCP gnubok_list_cash_accounts returns the three balance fields; the
cash_today prompt now reports the bank's figure instead of teaching
agents to answer with the bookkept 19xx balance
- new GET /api/v1/companies/{companyId}/cash-accounts endpoint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
* fix(bank): keep external_balance null for bank sign-offs; never fabricate a zero balance; guard the mirror against stale writers
Post-review fixes from the skeptic pass + CodeRabbit on PR #2118:
- external_balance stays null for the bank reconciliation kind: sign-off
persists it into account_reconciliations and bokslutsbilagor computes
closing - external from that row, so a today-balance stored on a
balansdag sign-off printed a phantom warning-red differens in the
year-end appendix. The bank-reported figure lives only in the
timestamped bank_reported_* pair in the bank block, and only when its
fetch timestamp exists (a balance of unknown age is suppressed).
- AccountOverview no longer falls back to today's date when the balance
timestamp is missing; the line is omitted instead.
- getAccountBalance returns null on an empty BALANCES response instead
of fabricating amount 0 with a fresh timestamp; sync keeps the
previous stored value.
- updateBalancesFromSync only writes over an older-or-missing
balance_updated_at, so an older sync run finishing later cannot move
the mirrored balance backwards.
- The inline initial backfill (picker save) now mirrors fetched
balances into cash_accounts too (accounts_data is deliberately not
re-written there).
- cash_today MCP prompt mentions the gnubok_call_tool bridge for hosts
that only see the default catalog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
* fix(bank): express the stale-writer guard as two literal predicates for the schema guard
The .or() with a template literal pushed the no-phantom-columns
unresolvable-expression count over its ceiling. Same semantics, two
updates: one for rows with an older timestamp, one for rows with none.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
* fix(bank): rank interimBooked (ITBD) as a booked balance type before the generic fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewu46quXgh9LSr9UwYusxm
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d11d0a2e90 |
feat(reconciliation): match one bank event to several verifikationer (1:N) (#1553) (#2029)
One bank row can now settle several vouchers: journal_entry_id stays NULL and one transaction_voucher_links row per voucher carries a signed allocated_amount slice (sum must equal the row within the link tolerance, each slice bounded by the voucher's net line on the account). linkTransactionToVouchers does the locked transaction UPDATE first and rolls back on a failed junction insert; unlink and the re-booking guards understand junction-only rows; a storno of one of the N vouchers releases the row when the remaining slices no longer sum to its amount. The worksheet's right pane becomes multi-select when exactly one bank row is picked (Koppla only at difference 0); the v1/dashboard pair schemas accept allocations; the MCP reconcile resolver and executor carry 1:N pairs; skattekonto keeps single-pointer semantics. Closes #1553 |
||
|
|
4e1eb3d662 |
fix(cash-accounts): never propose or accept an orphaned twin ledger as counter-account; match and re-point across sibling ledgers (#1643) (#2010)
* fix(cash-accounts): never propose or accept an orphaned cash-account ledger as counter-account (#1643) A broken bank reconnect leaves cash_accounts rows that share the live account's IBAN (held by a revoked connection, or demoted to manual by the #916 fix). Three consequences are fixed here: - Problem 4 (silent mis-booking): the own-account transfer detector paired with such an orphan and proposed its ledger as the counter-account, and a counterparty template learned from that result replayed as 1940/1931 in the booking dialog. The detector now tolerates several rows on one IBAN, never pairs with the transaction's own row, a disabled row, or a revoked holder; the mapping engine drops a "transfer" whose counter equals the settlement account; suggest-categories withholds learned suggestions that reference an orphaned ledger; and both commit paths (POST /api/transactions/[id]/categorize, categorizeMatchedTransaction) reject with the new TX_CATEGORIZE_ORPHANED_COUNTER_ACCOUNT (400). Orphans are only refused in the COUNTER position: a stranded row still settles on its own ledger, and a manual account without a live IBAN twin is never treated as orphaned, so transfers between two live accounts keep booking. - Problem 1 (match dialog): the ranked unmatched-entries path also offers vouchers booked on sibling ledgers of the same IBAN, and manualLink accepts a voucher line on a sibling ledger. When it does, the same locked UPDATE re-points transactions.cash_account_id to the live sibling row (currency-gated, like PATCH /api/transactions/[id]/cash-account) so the account-keyed reconciliation does not count a cross-account link as an imbalance on both ledgers. - Problem 3 (naming): allocatePsd2LedgerAccount names the chart account BAS-style (BAS reference name for a standard slot, else "Bankkonto <CUR>") instead of the ASPSP-reported holder name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(cash-accounts): address review findings on the orphaned-ledger guards (#1643) One in-memory topology (cash_accounts rows + bank_connections status) now defines "live", "orphaned" and "same physical account" for the transfer detector, the match/link flows and every commit guard, so a proposal is never made that a guard later rejects. - Finding 1/4/6 (own IBAN as counterparty): findPairableCashAccountByIban treats the transaction's own IBAN as "not a transfer": every same-currency row on that IBAN is the same physical account, whichever is live, so interest stamped with the own IBAN never pairs with a twin (two active rows, a demoted-manual twin, or a live twin of a stranded row). Only a pocket in another currency on that IBAN can still pair. guardCounterLegs refuses a same-IBAN same-currency twin in the counter position on every commit path, even when both rows are active. - Finding 3: with several surviving candidates (currency pockets with no discriminator, or two active twins) the finder returns null instead of picking the lowest ledger, which is what the pre-PR lookup did. - Finding 5: the finder drops every row in the orphaned set, the same predicate the commit guards use (demoted-manual twins included). - Finding 9: "live" means enabled + connection status 'active'; an expired/error twin of a live row is orphaned, a lone expired connection (re-auth window) is not. - Finding 2: siblings are keyed on (normalized IBAN, currency) in describeCashAccountSiblings and the unmatched-entries route, so a SEK transaction can no longer link to a voucher whose only bank leg is on the EUR pocket of the same IBAN; manualLink rejects that as before. - Finding 8: manualLink re-points a row only when the voucher sits on the LIVE sibling and the own row is not live; the reverse direction links without moving the row. - Finding 7: the v1 REST categorize route runs the same guardCounterLegs check after account_override and returns TX_CATEGORIZE_ORPHANED_COUNTER_ACCOUNT. MCP stages through categorizeMatchedTransaction, already covered. - Finding 10: a learned template whose stale 19xx leg is a twin of the settlement row is rewritten to the settlement account (it is the bank leg, not the counter) instead of refused; suggest-categories exempts each transaction's own settlement ledger before withholding a suggestion. The error message now covers both the twin and the disconnected case. Tests pin each behavior (service, detector, manualLink, unmatched-entries, dashboard and v1 categorize routes, suggest-categories). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(cash-accounts): address round-2 review findings (#1643) 1+3. Orphan derivation keyed on (IBAN, currency): loadCashAccountTopology now keys the live twin on normalized IBAN plus currency (the rule every other "same physical account" check in the PR already used), so a manual or deselected GBP/EUR pocket beside a live SEK pocket of a multi-currency account is never orphaned, still pairs in the transfer detector and is accepted as counter at commit. Twin computation is shared (twinLedgersOf). 2. suggest-categories mirrors guardCounterLegs: a learned 19xx leg that is a twin of the transaction's own row is rewritten to the settlement ledger in the offered suggestion instead of being withheld; only a true counter-position orphan (or a twin that would book the settlement ledger against itself) is withheld. One topology load per batch (loadCounterLegTopology). 4. The free-form dialog path (POST /api/transactions/[id]/book) gets a line-level guard (guardBookedCounterLines): a 19xx line that is a twin of the transaction's own row or an orphaned ledger, alongside the settlement leg, is refused with TX_CATEGORIZE_ORPHANED_COUNTER_ACCOUNT. Only runs when the lines touch two distinct 19xx ledgers. The twin rewrite in suggest-categories (2) covers the both-active shape before the dialog is even opened. 5. manualLink re-points the row onto the sibling ledger the voucher was booked on whenever the sibling is live or the own row is not (both-live twins and both-dead rows included); only a live row whose voucher sits on a dead sibling links without moving. unmatched-entries now uses describeCashAccountSiblings and does not offer dead-sibling vouchers to a live row. DECISIONS.md: the PR's existing review follow-up line amended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(cash-accounts): address round-3 review findings (#1643) 1. Revoked-held rows are no longer orphaned unconditionally. A row whose connection is revoked is orphaned only under the twin rule (not live AND a live row shares its normalized IBAN + currency), so a disconnected-but-real account (the company's only 1930, or two real accounts on one revoked connection) stays pairable by the transfer detector and bookable as counter on every guarded path. Tests cover the no-twin case for getOrphanedCounterLedgers, findPairableCashAccountByIban, detectOwnAccountTransfer, guardCounterLegs and guardBookedCounterLines; the existing revoked tests now use a twin shape. 2. manualLink / unmatched-entries decide the re-point on the destination: a new shouldRepointToSibling moves onto a live sibling, or onto a dead one only when the own row's holder is gone (released: bank_connection_id null or revoked) and no sibling is live. An expired/error/pending own row links without moving. SiblingCashAccount gains `released`. Tests: expired own row + demoted twin links without moving and the twin's vouchers are not offered. 3. loadCounterLegTopology is exercised directly: settlement ledger and twins, other-currency pocket, null/unknown ids, cache, orphan set equal to guardCounterLegs' refusals on the same fixture, lookup failure. 4. guardBookedCounterLines docstring and the /book route comment now state that only the two-cash-legs shape is inspected; a single hand-typed 19xx line is not (covering it would cost a cash_accounts lookup on every ordinary booking). DECISIONS.md lines amended accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(cash-accounts): address round-4 review findings (#1643) 1. Same-connection re-registration twins (the dominant prod shape): two enabled rows on one active connection sharing (IBAN, currency) are now told apart by balance_updated_at; only the most recently synced row is live, the other is a stale twin (orphaned as a counter, never a re-point destination, and the transfer detector pairs with the syncing row alone). Rows with no stamp or the same stamp both stay live. 2. POST /book: a single 19xx line that is a sibling ledger the row should move to (the live twin of a stranded row) re-points cash_account_id in the same locked UPDATE that links the voucher, mirroring manualLink. guardBookedCounterLines returns { refusedLedger, repointCashAccountId }; an ordinary booking pays one PK read of the own row. 3. manualLink refuses the link (success:false, Swedish error) when the voucher sits only on a dead sibling instead of writing a cross-account link with a server-side warn; the REST and MCP link callers reach it without the unmatched-entries filter. 4. manualLink judges a voucher touching several sibling ledgers on the best of them (a live sibling, else the first the row may move to) instead of the first line PostgREST returns. Tests pinned in lib/cash-accounts, lib/reconciliation and the /book route; the two DECISIONS.md lines for #1643 amended in place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(cash-accounts): address round-5 review findings (#1643) 1/2/5. Same-connection twin liveness no longer ranks on cash_accounts.balance_updated_at (a connect-time snapshot the sync never refreshes, inverted on prod in 4 of 5 stamped groups). The live row is the one whose external_uid the bank still lists in bank_connections.accounts_data (rewritten on every sync); no listing, both listed or neither listed keeps both rows live (round-3 behavior). getConnectionStatuses selects accounts_data in the same query. 3. guardBookedCounterLines single-19xx-line shape: a twin the row may not move to (dead or disabled) is refused with TX_CATEGORIZE_ORPHANED_COUNTER_ACCOUNT instead of posting the only bank leg on the dead ledger; an unrelated 19xx line still posts as typed. Route test added. 4. Disabled cash_accounts rows are never siblings, so neither manualLink nor /book re-points a transaction onto a deselected row; a voucher booked only there is refused as a cross-account link. 6. PR body rewritten to the final rules; DECISIONS.md round-4 line amended (signal correction, /book refusal, disabled siblings). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(cash-accounts): never treat a null external_uid as listed by the bank (#1643) CashAccount.external_uid is nullable in the shared type; the same-connection twin rule now skips null uids instead of passing them to Set.has, which failed the strict type check in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(cash-accounts): drop the same-connection twin liveness rule; both rows stay live (#1643) Two enabled rows on one active bank connection sharing (IBAN, currency) are no longer ranked. Round 4 ranked on cash_accounts.balance_updated_at and round 5 on external_uid presence in bank_connections.accounts_data; each was verified against prod and each was contradicted by it (ingest routes by the accounts_data entry's ledger_account, which in two groups points at the OLD row, so the "stale" row is the one still being fed). Restores the round-3 behavior: neither twin is orphaned, the transfer finder returns null when both survive, no guard refuses either, and shouldRepointToSibling treats both as live siblings. No replacement signal; how to model the shape is a founder decision (PR #2010 review). getConnectionStatuses no longer selects accounts_data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1a8fe36bc4 |
fix(reconciliation): direction-aware NULL-link settlement for transfer legs (#2018)
* fix(reconciliation): direction-aware NULL-link settlement for transfer legs A linked transaction with no resolvable cash account counted as settling its voucher for every account. For an own-account transfer on a non-primary bankavstamning card this hid the near leg while the far leg stayed listed, producing a false oforklarat (reported: momskonto card with differens 0 kr but oforklarat -2 593,75). get_account_gl_lines_for_matching now discounts a NULL-attributed link (pointer or junction) only when ALL of: the card is a non-primary account, the voucher touches >= 2 of the company's cash-account ledgers, and the row's sign contradicts the voucher's net leg on the account. All other shapes keep byte-identical legacy behavior, protecting unbackfilled single-leg rows (measured -37 000 kr false-alarm risk under the naive primary-only rule, simulated per-card against prod before choosing this rule; see DECISIONS.md). Footprint measured on prod: 24 vouchers on 7 cards in 6 companies; 5 cards improve (3 to exactly 0,00), 2 surface a real user-fixable mislink that was previously hidden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HwDxhfhYbywQNr8y8a7pUo * test(reconciliation): isolate the primary-card exemption in pg coverage The existing primary-card assertion also passes via sign match; a reverse transfer (1931 -> 1930) whose NULL row contradicts the primary's +net leg pins condition 1 (legacy_null_ok) on its own. (CodeRabbit nitpick.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HwDxhfhYbywQNr8y8a7pUo --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c62321988b |
feat(reconciliation,bokslut): underlag on a balansdag + persisted closing checklist (Reko bilagor, PR 2 + PR 3) (#1873)
* feat(reconciliation): underlag on a balansdag, the files behind a sign-off (Reko bilagor, PR 2) A konsult attaches the kontoutdrag, engagemangsbesked or reskontralista an account was reconciled against to (account_key, through_date), before or after the sign-off, from every account body on the Avstämning page. Rows live in account_reconciliation_attachments (append-only, removal stamp by trigger, RLS like account_reconciliations), bytes in the documents bucket under the company prefix so its RLS applies unchanged, and the full archive copies them into bilagor/ with a hash manifest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(reconciliation): literal selects and payload in the attachments store so the phantom-column scanner can read them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3) (#1867) * feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3) The bokslut checklist is a catalogue in code with one state row per period (bokslut_checklist_items): the steps the system can judge (sign-offs through balansdagen, reskontra tie-outs, drafts, voucher gaps, trial balance) are computed live and a stored row only overrides them; the manual steps are the konsult's ticks, with who and when. It sits on the wizard's Kontroll step and is dumped into the full archive. A hole between fiscal years (one-file SIE migrations) is now named on the bokslut readiness screen and on the import result screen, where the next file is one click away. Non-adjacent period links are #1849's fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(bokslut): count unexplained voucher gaps, literal select and payload for the checklist store Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> 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> |
||
|
|
ebbdf96b74 |
feat(reconciliation): manual adapter for the whole balance sheet (Reko bilagor, PR 1) (#1854)
* feat(reconciliation): manual adapter so the whole balance sheet is reconcilable and signable (Reko bilagor, PR 1) Every class 1-2 account the bank and skattekonto adapters do not own now appears on the Avstämning page under "Övriga balanskonton" with IB, movement and UB through the balansdag, a system specification where one exists (1510 kundreskontra, 2440 leverantörsreskontra, 2920/2940 semesterlöneskuld) and, for every other account, the balance the signer states from their underlag at sign-off. Same three doors as before: dashboard routes, v1 API and the MCP tools take manual:<BAS> keys and an external_balance. The ledger side is computed per fiscal period via generateTrialBalance, never as an all-history sum: year-end re-books every balance account in an opening_balance verifikat, so an all-history sum counts a closed year twice. A stated external_balance is refused (EXTERNAL_BALANCE_NOT_ALLOWED) wherever the system already has an outside truth, so it can never hide a difference. No migration: account_reconciliations already accepts manual:NNNN keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * chore(api-skill): regenerate banking reference for the sign-off external_balance field Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d88df74b85 |
feat(reconciliation): residual booking + junction-aware bridge (#1862)
When the worksheet selection (N bank rows vs one verifikat) misses by a few kronor, 'Bokför mellanskillnaden som Bankavgift / Räntekostnad / Ränteintäkt / Öresavrundning och koppla' books the remainder on 6570 / 8410 / 8310 / 3740 against the bank account, links the rows to the main verifikat and anchors the residual verifikat through transaction_voucher_links. Bank accounts only (Skatteverket posts ränta and avgifter as rows of their own), capped at 5 000 kr, direction-checked against the kind; links are made first and undone if the booking is refused. Dashboard + v1 doors (transactions:write, Idempotency-Key, dry run), API skill regenerated. The bridge now treats transaction_voucher_links as links on both sides: migration 20260824190000 re-creates get_unlinked_gl_lines and get_account_gl_lines_for_matching to count junction-linked verifikat as matched (pg-real test), and the TS engine + items do the same for the transactions. This also stops bulk-booked samlingsverifikat from polluting the open buckets. 'Koppla bort' drops the junction rows too. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4fb449a474 |
fix(reconciliation): bank tile says 'Rörelse på banken' with the in/ut/antal split (#1858)
'Banktransaktioner i perioden (netto)' made a bookkeeper's abstraction do the explaining; Jakob read it as gross activity. The tile now reads 'Rörelse på banken i perioden' and its sub-line shows the gross split behind the net (in · ut · antal transaktioner), so 'netto' needs no label. The bank status carries bank_transaction_inflow / _outflow / _count for it; the bridge line uses the same wording. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
34ad1b1936 |
feat(reconciliation): manual N:1 matching: two-pane worksheet + group links (#1851)
'Matcha manuellt' as designed: outside rows on the left (multi-select), verifikat without an outside row on the right (single-select), the selection's arithmetic in the footer, and one Koppla that is enabled only when the difference is 0. Mode lives in the URL (?mode=match). Engine: a pair is now one OR MANY outside rows against one verifikat. Bank groups link per transaction (manualLink allows N:1 by design, so partial success is reported per row). Skattekonto groups go through the new linkSkattekontoRows: the verifikat's 1630 side must settle the sum, one guarded UPDATE links the whole group, and a partial hit is rolled back as LINK_RACE. 1:M stays UNSUPPORTED_PAIR_SHAPE until the residual link table (6c). v1 pitfalls + API skill regenerated. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
150e2a3f14 |
feat(reconciliation): agent surfaces, skattekonto notice, bank icons and fair sync order (#1836)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): the Avstämning page, one body for every account with an outside truth /reconciliation in Arbeta (after Transaktioner), on the approved layout: an account rail on the left (bank accounts and the skattekonto, logo or monogram, last fetch, status dot, URL-owned selection), and for the selected account four tiles (outside, ledger, difference, unexplained), the bridge that explains the difference, an actions row (link the proposed pairs, book the unbooked skattekonto events, run the bank matcher) and a full-width table banded by bucket with proposal rows linkable one by one. Every read and write goes through the PR 2 dashboard routes, so the page shows exactly what the v1 API and the MCP tools see. Also: nav item, command palette entry, sv/en strings. Period picker, manual match mode and sign-off are deliberately not here (PR 4/5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): sign-off, period picker, Hem row and the three doors for it "Markera som avstämd t.o.m. <datum>" as an append-only attestation: account_reconciliations (who signed which account through which date, with the numbers as they stood; reopen stamps instead of deletes; RLS members write as themselves, viewers read). Policy in one place (lib/reconciliation/signoff.ts): refused with an unexplained difference unless forced with a note, refused past today or past the skattekonto snapshot, refused at or before an active sign-off; reopen is the undo. Every status read now carries the latest active sign-off and the rail shows "avstämt t.o.m.". Three doors: dashboard routes (GET/POST .../signoff, POST .../reopen), v1 (same, scope reconciliation:signoff, Idempotency-Key, dry-run, registry + regenerated API skill), MCP gnubok_reconcile_signoff (search catalog, stages reconciliation_signoff after a policy dry run; executor + risk tier + op-type CHECK migration pair). Events reconciliation.signed_off / reconciliation.reopened, and the four reconciliation events join the public webhook set (additive; API version unchanged, changelog section added). Page: räkenskapsår + range picker in the header (own preset memory, opens on this month) scoping the bridge, the items and the default sign-off date; sign-off dialog with the forced-with-note path; reopen on hover. Hem: worklist category reconciliation_due ("Konton att stämma av"), zero until the company has signed anything off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): classify reconciliation:signoff as a tenant write for the MCP role guard gnubok_reconcile_signoff carries the deliberately separate reconciliation:signoff scope; the central viewer guard keys on the :write/:approve/:manage suffixes, so a viewer could reach the tool (RLS would still refuse the row, but the guard is the intended layer). Add :signoff to the classifier; the strictness test that caught it now passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(providers): serve local rate-limiter waiters in arrival order Two callers that both found the in-memory bucket empty each set their own timeout; the timeouts expired at the same instant from different timer lists and which woke first was platform-dependent. hydrateInvoices relies on "started first, requested first" to serve open invoices before paid ones, so lib/providers/__tests__/hydrate-invoices.test.ts flipped on CI (twice on #1817) while holding locally. A promise queue makes the local waiters FIFO without changing the rate; the Upstash path is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 14a7599bf2c6fa7f97de6ffab3dc4cf4d0e1827d) * feat(reconciliation): agent surfaces: summary resource, attention category, reconcile-month skill, skattekonto notice, fair sync order Accounted://reconciliation/summary: every reconcilable account with its state, unexplained difference, open counts, last fetch and latest sign-off, plus a next step; the rail as a resource, on the same service function the page and v1 use. Accounted://attention gains reconciliation_due (shared predicate with the Hem row). A reconcile-month workflow skill and the reconcile_month loadout describe the account-keyed flow (summary -> bridge -> buckets -> sign-off). The skattekonto sync persists its reconciliation summary (skattekonto_reconciliation_latest) so the new Hem notice skv_unexplained ("Skattekontot stämmer inte med bokföringen: X är oförklarat", link to /reconciliation?account=skattekonto) costs one small read instead of a bridge computation per render; it honours the drift tolerance and its id carries the whole-krona amount so öre noise never resurfaces a dismissal. The skattekonto sync cron orders eligible companies by stalest sync (never-synced first) before its per-run cap, so the tail is no longer starved by a fixed order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: retrigger preview build (builder OOM during Running TypeScript, not the diff) * fix(reconciliation): visual pass round 1: full-width table, bank tile shows the period sum From Jakob's first look at the page on real data: - The items table now spans the full page width (the approved layout); the rail + tiles + bridge + actions stay in the two-column grid above it, which now lives inside AccountOverview (the rail rides in as a prop) so the table can break out below. - The bank account's first tile said "okänt": it read external_balance (the reported bank balance, often unknown) while its label says Banktransaktioner i perioden. It now shows the bridge's period sum, matching the label, the difference and the bridge line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): bank brand icons in the rail The rail resolves each bank account's icon from its connection's bank_name (falling back to the account name) against square brand icons committed under public/logos/banks/: the set covers every bank with a live connection in prod as of 2026-08-24 (SEB, Lunar, Handelsbanken, Swedbank, Nordea, Svea, Länsförsäkringar, Revolut, Wise, Danske, Klarna, Northmill, PayPal, plus Stripe for named accounts). Word-boundary matching so lookalike names never hijack a logo; anything unmatched (the small sparbanker, file imports) keeps the monogram. The skattekonto already had its Skatteverket mark. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): label the bank period sum as netto Jakob read 'Banktransaktioner i perioden 399 941 kr' as gross activity (his is ~1,9 MSEK) and rightly asked why it was so low: the value is the net movement (in - out), which is what the bridge compares against the net booked movement on the ledger account. Verified against raw prod data (237 rows, 1 169 126,40 in, -769 185,04 out = 399 941,36). The tile and the bridge line now say '(netto)' / '(net)'. 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> |
||
|
|
f40795896f |
feat(reconciliation): sign-off, period picker, Hem row and the three doors for it (#1835)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): the Avstämning page, one body for every account with an outside truth /reconciliation in Arbeta (after Transaktioner), on the approved layout: an account rail on the left (bank accounts and the skattekonto, logo or monogram, last fetch, status dot, URL-owned selection), and for the selected account four tiles (outside, ledger, difference, unexplained), the bridge that explains the difference, an actions row (link the proposed pairs, book the unbooked skattekonto events, run the bank matcher) and a full-width table banded by bucket with proposal rows linkable one by one. Every read and write goes through the PR 2 dashboard routes, so the page shows exactly what the v1 API and the MCP tools see. Also: nav item, command palette entry, sv/en strings. Period picker, manual match mode and sign-off are deliberately not here (PR 4/5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): sign-off, period picker, Hem row and the three doors for it "Markera som avstämd t.o.m. <datum>" as an append-only attestation: account_reconciliations (who signed which account through which date, with the numbers as they stood; reopen stamps instead of deletes; RLS members write as themselves, viewers read). Policy in one place (lib/reconciliation/signoff.ts): refused with an unexplained difference unless forced with a note, refused past today or past the skattekonto snapshot, refused at or before an active sign-off; reopen is the undo. Every status read now carries the latest active sign-off and the rail shows "avstämt t.o.m.". Three doors: dashboard routes (GET/POST .../signoff, POST .../reopen), v1 (same, scope reconciliation:signoff, Idempotency-Key, dry-run, registry + regenerated API skill), MCP gnubok_reconcile_signoff (search catalog, stages reconciliation_signoff after a policy dry run; executor + risk tier + op-type CHECK migration pair). Events reconciliation.signed_off / reconciliation.reopened, and the four reconciliation events join the public webhook set (additive; API version unchanged, changelog section added). Page: räkenskapsår + range picker in the header (own preset memory, opens on this month) scoping the bridge, the items and the default sign-off date; sign-off dialog with the forced-with-note path; reopen on hover. Hem: worklist category reconciliation_due ("Konton att stämma av"), zero until the company has signed anything off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): classify reconciliation:signoff as a tenant write for the MCP role guard gnubok_reconcile_signoff carries the deliberately separate reconciliation:signoff scope; the central viewer guard keys on the :write/:approve/:manage suffixes, so a viewer could reach the tool (RLS would still refuse the row, but the guard is the intended layer). Add :signoff to the classifier; the strictness test that caught it now passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(providers): serve local rate-limiter waiters in arrival order Two callers that both found the in-memory bucket empty each set their own timeout; the timeouts expired at the same instant from different timer lists and which woke first was platform-dependent. hydrateInvoices relies on "started first, requested first" to serve open invoices before paid ones, so lib/providers/__tests__/hydrate-invoices.test.ts flipped on CI (twice on #1817) while holding locally. A promise queue makes the local waiters FIFO without changing the rate; the Upstash path is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 14a7599bf2c6fa7f97de6ffab3dc4cf4d0e1827d) --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3a62c5419e |
feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools (#1833)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. 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> |
||
|
|
0a8544e0cb |
feat(reconciliation): account-keyed engine: one bridge for bank and skattekonto (#1813)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. 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> |
||
|
|
5a8dd21931 |
feat(reconciliation): page-owned window, automatic matching, and a way out for unbookable rows (#1742)
Second half of the reconciliation redesign, on top of the bridge in #1737. **Toolbar.** The view hosted its own "Datum från / Datum till" inputs behind a Filtrera button: a second period control competing with the header's räkenskapsår picker (convention 8), and the source of a "typed but not applied" state that needed its own attention line to explain. The window is now owned by the page, narrowed through the shared ReportDateRange like every other report, and applied on change. The view holds no date state at all, which also removes the ref-synchronisation dance and the off-by-one it existed to prevent (a year switch fetching the previous year's window because the refs updated a commit late). Reconciliation opens on the FULL year, not the family default of YTD, and keeps its own preset memory: a reconciliation runs over a whole räkenskapsår, and inheriting a "Denna månad" last used on Resultatrapport would show an alarming difference for a window nobody chose here. ReportDateRange gained defaultPreset and storageKeyPrefix for that; every existing caller keeps its behaviour. **Automatic matching.** "Förhandsgranska" told the user nothing about what it did, and the ochre line above it existed only to point at it: people matched a whole migration row by row next to a button they never found. The matcher now runs by itself, once per window+account, whenever there is unmatched work. It is a dry run, so nothing is written and Tillämpa still requires an explicit click. The button stays as a re-run and is renamed to what it does. ?autorun=1 keeps a distinct meaning (run even on a clean window) so the transactions-inbox deep link still produces a result rather than silence. **A way out for rows that cannot be paired.** An unmatched bank row that no voucher on the account could settle is not reconciliation work, it is an unbooked affärshändelse, and the match picker held nothing for it. Those rows now offer "Bokför" into /transactions?highlight=<id>, with a bulk link in the section header. The rule (direction-compatible and equal to the öre) is extracted to lib/reconciliation/voucher-candidate.ts so it is testable and so the component never imports the server-only reconciliation module. Deliberately strict: a false negative offers booking on a row that could also have been paired, which is a legitimate outcome, while a false positive sends the user into an empty picker. 11 new tests for the candidate rule, covering direction, öre equality, float noise, PostgREST numeric strings and the foreign-account case where the candidate RPC projects no FX amount and no match may be claimed. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b5e908f9ea |
feat(reconciliation): explain the difference instead of just printing it (#1737)
The bankavstämning card showed three movement sums and a red difference, leaving the user to work out what the difference consisted of. The page already knew, exactly: every krona of it is (unmatched bank rows) minus (unmatched vouchers). Verified on prod for Arcim 1930 over 2025-07-17.. 2026-08-20: 403 565,42 bank, 332 680,93 booked, 70 884,49 difference, of which -277 799,92 sits in 74 unmatched transactions and -348 684,41 in 4 unmatched vouchers, leaving exactly 0,00 unexplained. Engine: getReconciliationStatus gains unmatched_transaction_total, unmatched_gl_line_total and unexplained_difference. The residual, not the raw difference, is the figure that can mean something is wrong: a difference is expected to be large mid-year and says nothing on its own. unmatched_gl_line_total is null rather than 0 on a foreign account, whose candidate lines carry no amount in that currency, and the card falls back to the flat figures there. Also fixes the candidate fetch's window: it used the caller's raw dateFrom while both other sides were clamped to the opening-balance floor, so a window opening before the account's IB (the v1 endpoint's default, or any multi-year range) counted vouchers from a period the reconciliation deliberately drops. UI: the card becomes a bridge whose two middle rows both explain the number and navigate to the list that resolves them, above a matched/total progress rule. Three stacked paragraphs of legal prose collapse into one line plus a tooltip, keeping the amounts on screen. The permanent destructive "Ej avstämd" badge is gone: being mid-year and unreconciled is the normal state, so it marked nothing (convention 5); Avstämd is now what gets the chip. The unmatched list becomes one line per transaction (convention 4). It rendered a ~230px card per row, each with an always-open, always-empty match field: for a real backlog that is thousands of pixels of empty search boxes, and it gave the rarest action the only visible affordance while bokför and ignorera hid behind the row menu. The picker, and its ranked-candidate fetch, now run for the one row the user opens. A non-zero residual is stated factually, never in destructive red: measured over the 206 single-1930-account companies with >=10 transactions, 136 are exactly 0,00 and 63 are >=100 kr out, dominated by ledger lines the candidate RPC hides (posted/storno on 127 companies) rather than user error. Surfacing those is follow-up work. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4e20c9dec4 |
fix(import): bulk-confirm the VAT-treatment review gate in account mapping (#1723)
* fix(import): bulk-confirm the VAT-treatment review gate in account mapping A Fortnox chart routinely puts 70+ class 3/4 accounts behind the vat-treatment review gate, and the only way through was one Bekräfta click per row across paginated 50-row pages. A live migration (2026-08-18) died exactly there, stuck at 50 kvar with Continue disabled and no way to see why. One outline button next to Continue now accepts the suggested default for every remaining row, with the exact semantics of the per-row button batched (defaults kept, rows marked reviewed). Wired in both the import wizard and the Arcim migration workspace. Strings in sv+en. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(guards): two naive-ore-rounds that stacked past the ratchet baseline #1700 and #1705 each added one Math.round(x*100)/100 and each passed CI alone against baseline 630; the first branch containing both trips the ratchet at 631. Convert both to roundOre (629, below baseline). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: place the roundOre import on its own line The previous commit inserted it inside a multi-line import block, breaking parsing in pdf-template.tsx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: give next build an explicit 8 GB heap The build worker OOMs on the runner's default Node heap since the bundle crossed the default old-space ceiling (first branch containing all of 2026-08-19's merges). Public-repo runners have 16 GB. 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> |
||
|
|
506d030bb1 |
fix(reconciliation): exclude ignored transactions from the bank total and bridge whitespace-drifted duplicate descriptions (#1705)
Bank reconciliation counted ignored transactions in bank_transaction_total while excluding them from the unmatched count, so after the sanctioned duplicate cleanup (ignore one twin) the differens showed the ignored sum forever and is_reconciled was unreachable: observed live as a permanent 116 367 kr differens on a fully booked enskild firma (78 867 kr ignored reconnect duplicates + 37 500 kr genuinely unbooked). The ignore toast already promised 'försvinner från avstämningen'; now the engine keeps that promise. Ignored rows are surfaced separately (count + sum) in the status object, the UI card, and the v1 API, mirroring the IB pattern. The duplicates themselves came from a PSD2 reconnect: the new connection re-rendered identical transactions with drifted whitespace (CRLF vs space, and a DROPPED space), so the prefix-containment content bridge missed every twin. descriptionsBridge now strips all whitespace before comparing: char-filtering preserves existing prefix relations, and the compare stays confined to a (date, öre) bucket. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7494274852 |
fix(reconciliation): stop counting a stornerad opening balance as the IB (#1647)
* fix(reconciliation): stop counting a stornerad opening balance as the IB getReconciliationStatus summed source_type='opening_balance' lines with no status filter while the GL fetch includes reversed entries. A reversed IB and its storno correctly net to zero inside gl_1930_balance (as on the balansräkning), but the reversed IB alone was still counted in gl_1930_opening_balance and subtracted from the period movement, so difference = (bank - gl) + reversed_ib: a phantom diff of exactly the cancelled amount after a perfectly correct rättelse. The IB-floor derivation had the same gap and could raise effectiveFrom to a stray reversed IB, silently dropping early-period movements. Only status='posted' opening_balance lines now count as the IB, for both the opening-balance figure and the floor: the same rule the canonical compute_prior_opening_balances RPC (20260421180000) already applies. Nothing else moves: the reversed pair stays in glBalance where it cancels. Reported via gnubok_feedback 2026-08-16 with the exact formula. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: retrigger preview build (Vercel runner hung in Running TypeScript for 45 min, BUILD_EXCEEDED_MAXIMUM_TIME) 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> |
||
|
|
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> |
||
|
|
4a38fa30ed |
fix(recon): share the cash-account scope and cover the no-1930 case (#1309)
PR #1295 (
|
||
|
|
f24b26a139 |
fix: similar-sweep currency remediation, security hardening and v1 API fixes (#1215)
* fix(security): gate replace_sie_import behind owner/admin membership The RPC was SECURITY DEFINER with EXECUTE granted to PUBLIC and anon, no company_members lookup, no auth.uid() reference and no unauthorized raise, while setting gnubok.allow_delete to disarm the BFL immutability and retention triggers. Any caller holding a company_id and an import id could hard delete another tenant's verifikationer. Confirmed live in production. Applies the same fail closed owner/admin guard that undo_sie_import already carries (migration 20260624120000), resolving the actor from COALESCE(p_user_id, auth.uid()) so it denies when the role is NULL, then revokes EXECUTE from PUBLIC and anon. search_path and the raised statement_timeout are restated, since CREATE OR REPLACE drops settings that are not repeated. userId is a required parameter on replaceSIEImport: the service client has a NULL auth.uid(), so a caller without an explicit actor now fails to compile rather than hitting the closed gate at runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): validate arcim OAuth callback state server side The callback route is skipAuth and decoded the state parameter as plain base64url JSON, trusting consentId and provider from it. A one time code was minted at flow start and never read. An unauthenticated attacker who learned a consent id could run an OAuth flow on their own provider account and post the callback with a forged state, landing their tokens on another tenant's consent, so the victim's next migration imported the attacker's ledger. State is now an opaque randomBytes(32) pointer to a provider_otc row, consumed by a single atomic UPDATE guarded on used_at IS NULL and expires_at, so a replay loses the row lock race and updates nothing. provider is read from provider_consents rather than trusted from the client. provider_otc already existed for exactly this purpose and was never wired up. Also scopes getConsent to an owning company, closing a cross tenant status oracle where the preview and migrate paths echoed a consent's status before the scoped check ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): scope documents storage to company_id (phase A) The documents bucket policies matched on auth.uid(), and upload keys were documents/{userId}/..., so company membership was never consulted. Removing a member revoked nothing: their session still authenticated and they kept direct Storage read access to every receipt, supplier invoice and bank statement they had uploaded. The same bug was fixed for sie-files in 20260416120000; this bucket was left behind. Phase A is additive. Company scoped policies are added alongside the uploader scoped ones, uploads move to documents/{companyId}/{userId}/..., and reads accept either layout so nothing breaks mid migration. Phase C, which drops the old policies, is gated on the backfill reporting zero remaining legacy prefix objects. The policy compares the company segment as text rather than casting to uuid the way sie-files does: this bucket holds keys whose second segment is not a uuid (MCP audit packages), and Postgres does not guarantee the bucket prefix qual runs before the cast, so a planner reordering would raise 22P02 and fail the whole query instead of filtering the row out. deleteDocument now removes both candidate keys. Removing only the stored pointer would leave a readable orphan copy of a document the user asked to erase. The backfill script is included but has never been run. It defaults to dry run, refuses .env.local by name, and verifies each copy is readable and SHA-256 identical before repointing the row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): enforce events:read scope and membership on /api/events This was the only one of the three validateApiKey call sites with no downstream guard: v1 and the MCP server both check scope and re-verify company membership, this route did neither. An events:read scope existed and was documented as gating the endpoint but was never called, so a legacy key falling back to DEFAULT_SCOPES read the full log. The bound company id went straight from the api_keys row into a service role query, so a key whose user had been removed from the company kept reading. Adds the scope check before any database access, re-verifies company_members with archived_at IS NULL, honours test mode by stamping X-Gnubok-Mode instead of ignoring it, applies minimisePayload so the pull surface can never return a wider payload than the push surface, and replaces the three flat error strings with the canonical envelope. Test key reads are served rather than blocked: TEST_KEY_WRITE_BLOCKED is gated on mutations in with-api-v1, so a read gets the same treatment as every other v1 read endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf(bookkeeping): sweep remaining journal_entries!inner embeds A previous refactor removed this pattern from lib/reports and introduced fetchEntryLines, but the class was never swept. Seventeen sites remained and had become the top application consumer of production database time: measured across the resulting query shapes, 32,694 calls and 25,848 seconds of execution, mean 790ms, with shapes averaging 2.6s and 3.0s and maxing at 7,962ms against the 8s statement_timeout, which surfaced to users as 500s on the booking path. PostgREST compiles an embed with filters on the embedded side into a correlated INNER JOIN LATERAL with a parameterized LIMIT, which stops Postgres reordering the join, so each query walked the whole journal_entry_lines table across all tenants. Driving from the entries side instead turns that into two indexed round trips. Converted sites keep their existing shape: the helper reattaches the parent entry under the same key the embed produced. Several conversions also remove a latent silent truncation where an unpaginated query was capped at PostgREST's 1000 row ceiling. Two deliberate exceptions. The free text ilike legs of the MCP display query stay on the embed, because each is capped at legLimit and that cap drives the truncation contract the tool reports, while the helper is unbounded. The accounts route moves to the existing get_account_usage_counts RPC instead, since its embed was a head count and the helper returns rows. commitEntry's write path is untouched: the change there is confined to the read query of the pre-commit dimension rule check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): anchor v1 list cursors on created_at Page two returned page one, forever, while still advertising a fresh next_cursor. The three routes sorted by and encoded a Postgres date column, which serializes as YYYY-MM-DD, but decodeDefaultCursor validates the cursor timestamp as full ISO-8601 and returned null, so the keyset filter was never applied and has_more never went false. An integrator syncing verifikat looped on the newest rows indefinitely. The transactions route already solved this and its comment names the trap; the fix was never ported. All three now order and encode on created_at with an id tie break, matching the transactions keyset predicate exactly. ISO_TIMESTAMP is deliberately left alone: relaxing it would silently change sort semantics on the route that currently works. Default ordering therefore moves from business date to insert order. Every business date is still on the row, and the invoices list gains date_from and date_to filters so a date range is still reachable; the other two already had them. The tests use an in-memory PostgREST that actually evaluates the filters, because the repo's pass-through mock cannot catch this class of bug: the bug is that the filter is never sent. They walk to exhaustion with a hard iteration cap, so an unterminated walk fails instead of hanging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): separate dry run from commit in the idempotency hash The request hash was built from url.pathname, which excludes the query string, so a dry run and its commit hashed identically. Following the flow documented in dry-run.ts, re-issuing the request with the same Idempotency-Key returned the cached preview with Idempotent-Replayed set and wrote nothing, while reporting 200. An agent or integrator saw success for a write that never happened. dry_run is folded into the hash only when true, not as an unconditional boolean. Including it as false would change the hash of every ordinary write, and with a 24h idempotency TTL any key in flight across the deploy would fail the request_hash comparison and 409 on a legitimate retry. Both hash call sites now go through one shared helper so they cannot drift into a permanent cache miss, and dry run responses are no longer stored at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: install the Bedrock SDK out of tree in the compliance review The Swedish accounting compliance gate had failed ten consecutive runs and so was posting nothing. With --no-package-lock npm discarded the lockfile and re-resolved the whole tree from package.json, floating @hookform/resolvers to 5.4.3, whose valibot ^1 peer conflicts with the pinned valibot 0.39.0. Installing into the parent of the checkout resolves only that one package, so an unrelated peer conflict can never take the gate down again. Node still finds it because ESM bare specifiers walk up parent node_modules; NODE_PATH would not have worked, as it is CommonJS only. --legacy-peer-deps was rejected because it masks future genuine peer conflicts and still reifies the full tree. The same step's SDK version is aligned from 0.31.0 back to the 0.29.1 that package.json and check:guards enforce after the streaming outage. That drift went unnoticed because the pin guard only inspects package.json and the lockfile, never workflow files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * build(docker): generate crontabs from vercel.json vercel.json defines 16 cron jobs; both Docker crontabs carried 9, and were byte identical to each other. Self hosted deployments therefore never sent recurring invoices, never dispatched webhooks and never cleaned up idempotency keys. tax-deadlines also ran once a year on 2 January instead of daily, and documents/verify weekly instead of daily. Extension crons are included rather than excluded. The Dockerfile copies the whole tree before building, so every extension cron route is compiled into the image regardless of the enabled preset, and each returns 200 when its extension is unconfigured, so curl -sf logs no failure. Two such entries were already present in the crontab for extensions absent from the preset, which settles the intent. documents/verify is treated as drift rather than a self hosted concession: the weekly cadence was present in the hosted crontab too, and the run is capped at 200 documents walking a nulls-first queue, so weekly drains the integrity queue seven times slower on a check that exists for BFL retention. webhooks/dispatch keeps its per minute cadence, adding 1,440 requests a day on self hosted. A gentler tick would silently stretch the first retry, since the retry ladder opens at 60 seconds. SCHEDULE_OVERRIDES is the one line place to change that. A parity test asserts the path sets match minus a documented exclusion list, and ratchets three cron routes that are currently scheduled nowhere so they are named rather than silently rotting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(observability): add a provider agnostic error sink There is no error tracking in this codebase: logs go to console and Vercel retention and nowhere else, nothing alerts on the 16 cron jobs, and seven code comments across lib, app, components and extensions asserted that Sentry captures errors when Sentry is not a dependency. The two most recent bug fixes on this repo were both discovered by customer email. This adds the sink, not a vendor. No dependency is taken: the interface has a no-op default and a registration point, so behaviour is unchanged until an adapter is registered. Releases are tagged from the build id already inlined by next.config.ts. Redaction moved out of lib/logger.ts into a leaf module that both the logger and the sink import, so there is one denylist and no path from application data to a third party can skip the personnummer regex, including direct sink calls that bypass the logger. That matters here because these logs carry personnummer and financial data. verifyCronSecret now reports its own 401s, which covers all 16 jobs without touching a route file and catches the case where CRON_SECRET is rotated without updating the scheduler and every job silently 401s forever. The threshold is one failure rather than the backup alert's three: suppressing the first occurrence is precisely how an outage stays invisible. The seven misleading comments are corrected to describe what the code actually does, including the two cases that still are not covered: the client side one, since the sink is server side, and a warn level call that is not forwarded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: remediate the 2026-07-26 similar-sweep findings across all surfaces Resolves the ~150-finding sweep (dev_docs/similar-sweep-2026-07-26.md) with one agent per finding; every behavioural fix carries a regression test proven to fail at HEAD. Full status, corrections to the sweep, refusals and open decisions in dev_docs/similar-sweep-2026-07-26-remediation-status.md. Structural roots closed: - resolveSekAmountOrNull(): honest SEK resolution refuses instead of booking 1:1; four duplicated toSek closures now refuse via INVOICE_FX_RATE_MISSING - ledger-line-amount.ts: journal_entry_lines.currency labels the document, not the amount; SQL pre-filter decoy proven and fixed - sparse-patch.ts: .partial() does not strip .default() in Zod 4.4.3; the exploitable salary payslip-line PATCH and KPI preferences sinks fixed - tests/schema: migration-replay phantom-column guard (13k+ refs, closed CHECK sets, onConflict targets); found 28 real defects, all fixed, all four baselines now empty - three new ratchet guards: sek-labelled-amount, cross-extension-import, ungated-extension-route Highlights: lawful VAT-rate set on all seven invoice surfaces (ML 6 kap), RC input VAT mismatch wired on web + both MCP callers, missing-underlag resource delegates to the shared RPC predicate, push-notifications consent polarity fail-closed, deadlines undo honours requested state, silent-failure and read-side-fabrication classes fixed across settings/KPI/inbox/Stripe/ Arcim/kassaflodesanalys, error-envelope stringification fixed at 10+ sites with isSwedishUserMessage extended. Also includes the parallel session's MCP invoice tools (update_invoice, recurring schedules, invoice deliveries) which share files with the sweep work and are verified green together. 13 new migrations are NOT applied anywhere; they apply via branch merge. 20260726120000 backfills 1247 supplier-invoice rows. pg tests for new DDL are written but unrun (no local Postgres). Verified: 11088 tests / 881 files green, tsc 0 non-test errors, lint 0 errors, check:guards passing, MCP payload 57475/57500. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(migrations): rename replace_sie_import migration off main's 20260726090000 version origin/main shipped 20260726090000_agent_quota_rpc_caller_guard.sql; keeping our replace_sie_import migration on the same version would abort the Supabase apply with a schema_migrations_pkey duplicate at merge time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): remediate pre-publish deep-review findings across all slices A 13-agent review of the full branch diff surfaced 1 critical, 5 high and ~45 further findings; this commit resolves them in one pass: - replace_sie_import / undo_sie_import: p_user_id honored only for service_role callers; any other caller is pinned to auth.uid() (impersonation gate bypass), authz raise errcode 42501 mapped to a Swedish 403 in the route, new caller-guard migration for undo - bulk_book_transactions refuses homogeneous non-SEK batches instead of writing foreign magnitudes into SEK ledger columns - credit-note cap trigger: company-match on credited_invoice_id, no cross-tenant figures in exception text - link_voucher RPCs resolve NULL invoice currency as SEK end to end - personal-number ciphertext CHECK split into NOT VALID + VALIDATE - same-currency foreign settlements clear 1510 at booking rate and book realized diff to 3960/7960; rate-less foreign write paths refuse - receivables revaluation covers partially_paid and outstanding amounts - period lock guard paginates candidates past the PostgREST 1000 cap - documents: service-client storage removals after authz, dual-layout reads in integrity cron and archive export, backfill delete-source sweep actually deletes with hash verification and shared-key grouping - invoice matching normalizes NULL/lowercase currencies (regression), duplicate candidates stop claiming amount matches they never ran - match-invoice aborts on any booking failure (no paid-without-verifikat) - refresh-exchange-rate reverts on concurrent booking (TOCTOU window) - KPI preferences upsert arbiter aligned to the company-scoped constraint - personnummer_last4 stripped from all salary responses incl. MCP tools - worked-hours batch restores destroyed rows on conflict and error paths - MCP: shared duplicate-claim builder (no more 'null kr'), short-circuit on tag_journal_lines overflow, auto_send schedules stage as high risk - observability sink redacts emails/IBANs/API keys and keeps redacted stacks in prod; assorted small guards (safe-return-to /@, dry_run=True, cursor helper off-by-one, OAuth state TTL 10 min, arcim saveMappings call removed) Full dispositions, deferred items and hand-verified accounting numbers are documented in the PR body and DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(personnummer): implement masking and encryption for personal numbers with tests * fix(review): address CI and compliance-bot findings for PR #1215 pg-real: the CI image's auth shim reads the legacy request.jwt.claim.role GUC, so both service-role simulations (runAsServiceRole and the invoice-delivery test's local helper) never satisfied auth.role() = 'service_role' and every legitimate p_user_id path failed closed; the shared helper now sets both GUC shapes plus SET LOCAL ROLE with a fail-loud sanity check, and the delivery test reuses it. The link-voucher migration had recreated both RPCs from pre-rewrite file text, reintroducing the NULL-unsafe membership pattern the null-safe-tenant-guards ratchet bans; both guards now use public.caller_is_company_member() with all currency changes preserved. Compliance bots: the customers export now emits the standard masked form instead of raw AES-256-GCM ciphertext in the Org-/personnummer column, and maskCustomerRow returns a non-round-trippable placeholder on decrypt failure instead of 500ing the list. MCP parity: gnubok_lock_period's staging pre-check now runs the exact countUnbookedInPeriod the commit path enforces (exported from period-service; local mirror deleted), and gnubok_agi_status resolves AGI state run-scoped so a correction run no longer renders as already filed. Declined with evidence: PR-Agent's opening-balances null-zeroing concern (all mergeable columns are NOT NULL with defaults per 20260713101000). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address codex review findings on PR #1215 - restore 20260726140000 to its preview-recorded content and restate the NULL-safe tenant guard under 20260727130000: a recorded migration version never re-runs, so the in-place edit could not reach the preview branch - replace toFixed() with sv-SE two-decimal formatting in the ROT/RUT cap warning texts and update the pinned test expectations - drop the em dash in the fiscal-periods route comment - strip trailing whitespace in import-existing.test.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): raise timeout on real PDF render tests renderToBuffer does real @react-pdf layout work and exceeds the 5s default when the full suite saturates the CPU; tests pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
288915c152 |
Fix/fdb fr usrs (#1125)
* fix(invoices): return attachment filename in delivery history summaries The 20260723003000 hardening dropped attachment_filename from list_invoice_delivery_summaries, so the delivery history UI always fell back to the generic "faktura.pdf" label. Recreate the RPC with the filename included: it is derived from company name, customer name, invoice number, and date, all already visible to every company member, so the minimization boundary is unchanged. Addresses stay masked and message content, BCC, and checksums stay server-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): surface own-account transfer legs in match-to-voucher by default The second (incoming) leg of a transfer between two of the company's own bank accounts was hidden in the 'Matcha mot befintlig verifikation' dialog because the voucher counted as 'already matched' once its outgoing leg was linked, even though the incoming account's line had no settling transaction. Users read the empty default list as 'the app won't let me link this'. get_account_gl_lines_for_matching now counts links per settlement account: a transaction provably on another cash account no longer marks the voucher as matched for the requested account, so the unsettled transfer leg surfaces by default (and auto-selects on an exact match). Same-account N:1 stays behind the 'Visa aven matchade verifikationer' opt-in, and transactions without a resolvable cash account conservatively keep counting everywhere. get_unlinked_gl_lines is deliberately untouched (feeds auto-reconcile). Companion guard: mark_entry_as_opening_balance now refuses entries with linked bank transactions, since half-settled transfer vouchers became reachable in the reconciliation view's unmatched table where 'Mark som IB' renders; re-tagging one would strand its transaction against a movement- excluded entry. getReconciliationStatus counts unmatched GL lines with the account-scoped RPC so the status card agrees with the table. Fixes #1026 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(api): cut prod p95 latency via local JWT auth, single-RT company resolution, and report aggregate RPCs Baseline 2026-07-23 (487 prod samples): p50 160ms, p95 480ms, 13% of requests over 300ms. Target: p95 under 300ms. - requireAuth: verify JWTs locally via getClaims (ES256/JWKS) instead of a second network getUser per request; getUser fallback keeps HS256 self-hosted and existing test mocks working; middleware still revocation-checks every /api request - resolve_active_company RPC (20260723161000): one round trip replaces 2-3 queries in getActiveCompanyId and middleware; PGRST202/42501 fall back to the legacy query path - arsredovisning build-data: ~33 sequential round trips down to ~7, output byte-identical (snapshot-proven) - currency rate route: stop bypassing the exchange_rates cache (missing supabase arg caused an external Riksbanken call on every request) - document.get: parallelize row fetch, signed URL and audit event - list_company_accounts RPC (20260723170000): accounts list in one round trip instead of paging past PostgREST's 1000-row cap - vat-declaration route: drop a dead sequential company_settings query - get_kpi_report_aggregates RPC (20260723180000): KPI report's three full-period line scans collapsed into one aggregate call; dimension- filtered path unchanged - lint: fix 9 baseline errors, downgrade 4 react-hooks compiler rules to warn, zero the eslint baseline ratchet All four gates green: lint 0 errors, 9163 tests, check:guards, build. Migrations applied idempotently to staging only; prod receives them via Supabase branching on merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): resolve PR review findings across auth, VAT declaration, and IB retag - requireAuth getClaims fast path: pin iss (project URL) and aud ('authenticated'), log every fallback to getUser (ASVS V9.1 finding) - remove the ignored accountingMethod parameter from calculateVatDeclaration and the dead company_settings.accounting_method reads in xlsx/pdf/eskd routes; v1 API keeps accepting the query param but documents it as a no-op - close the mark_entry_as_opening_balance TOCTOU race with a transactions trigger (20260723190000, FOR KEY SHARE on journal_entries) + pg tests; applied to staging and smoke-verified both directions - re-add the 42501 tenant guard to branch-local migration 20260723160000 (function body had silently reverted to the pre-20260619130100 definition) - document the buildK3Noter tbFullRows full-TB contract (uppskjuten skatt opening balance per BFNAR 2012:1 ch.29) - add KPI VAT-liability test covering reduced-rate output accounts 2621/2631 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(db): use NULL-safe caller_is_company_member in opening-balance retag guard The re-added tenant guard carried the pre-20260703180000 raw NOT IN (SELECT user_company_ids()) pattern, which the null-safe-tenant-guards ratchet blocks. Staging re-synced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f04dc4c4e0 |
fix(transactions): allow re-linking a bank tx stranded on a reversed verifikat (#988) (#1009)
* fix(transactions): allow re-linking a bank tx stranded on a reversed verifikat (#988) A transaction whose journal_entry_id points at a reversed/cancelled entry reads as "utan koppling" in the UI (the transactions page enriches only status='posted' links), yet the re-booking guards treated ANY non-null pointer as "already linked". So a storno'd/corrected transaction could never be linked to another verifikat or re-categorized: the exact symptom in issue #988. Add a shared hasLiveJournalEntryLink() predicate used by every re-booking guard (linkTransactionToJournalEntry, manualLink, categorize-core, and the MCP link stage-check): a pointer at a non-posted entry is treated as re-linkable, and the two optimistic-locked writes now lock on the exact previous pointer (null OR the stale id) instead of always .is(null), so the overwrite goes through race-safely. hasLiveJournalEntryLink fails closed on a read error so a transient blip can't detach a genuinely live link. The source was fixed in #726 (reverseEntry/correctEntry now detach/re-point the tx); this makes the guards self-heal for the pre-#726 backlog and any future best-effort miss. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transactions): detect 0-row CAS before invoice effects; fix categorize commit test Addresses PR review (CodeRabbit Critical + CI): - link-journal-entry.ts: the tx UPDATE now .select('id') and treats a 0-row result as LINK_TX_TX_ALREADY_LINKED, failing BEFORE any invoice settlement / invoice_payments insert. Without this, a concurrent re-link that lost the CAS would still mark the invoice paid against a transaction we didn't link (same optimistic-lock contract manualLink already enforces). - pending-operations commit route test: the categorize_transaction "already categorized" case now enqueues the hasLiveJournalEntryLink status read (posted = live) so it still returns 409. This was the core-only CI failure: the new liveness read in categorize-core consumed a queued response. - Updated the link happy-path / invoice-race test enqueues to return a row for the now-selecting tx UPDATE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a8801430f4 |
fix(reports): stop driving report queries from the unfiltered journal_entry_lines side (#971)
* fix(reports): drive report line queries from journal_entries, not the unfiltered lines side
Every report generator fetched journal_entry_lines with a
journal_entries!inner(...) embed and put the tenant filter on the
embedded side (.eq('journal_entries.company_id', ...)). PostgREST
compiles that to a correlated INNER JOIN LATERAL with a parameterized
LIMIT inside, which blocks join reordering: Postgres walked the ENTIRE
journal_entry_lines table (603k rows, all tenants) per report query.
Measured in production: 13.6 s vs 2.7 ms for the equivalent plain join,
against Supabase's 8 s statement_timeout; nightly cloud backups failed
for 5 of 11 companies on 2026-07-09 and a GL report 500'd.
Introduce lib/bookkeeping/entry-lines.ts with a shared two-step fetch:
1. fetch matching journal_entries (id + caller-selected columns)
filtered by company_id / fiscal_period_id / status / entry_date /
source_type, paginated via fetchAllRows;
2. fetch journal_entry_lines with .in('journal_entry_id', chunk) in
chunks of 100 ids (URL-length safety), paginated per chunk;
3. reattach the parent entry to each line under the embed's key shape
(line.journal_entries = {...}, aliasable) and sort lines by id
ascending to preserve the old .order('id') semantics.
Converted call sites (selected columns and filters preserved):
trial-balance (x2), general-ledger, journal-register, sie-export
(reuses its existing entry list via fetchLinesByEntryIds),
vat-declaration, dimension-pnl, opening-balances, monthly-breakdown,
periodisk-sammanstallning, rc-basis-gaps (sibling-line fetch now also
chunked), ar-reconciliation, supplier-reconciliation,
bank-reconciliation, asset-service (x2), bolagsskatt-calculator,
sarskild-loneskatt-calculator.
Tests: unit tests for the helper (chunk size, reattachment shape,
forced id/journal_entry_id columns, empty result, cross-chunk sort,
error propagation); existing report/reconciliation/bokslut test mocks
updated to the two-step query shape, preserving every assertion about
report output.
From the 2026-07-09 production log triage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(reports): stop echoing raw error messages from the general-ledger route
The catch handler returned err.message to the client in
details.reason; internal error strings (SQL fragments, table names,
timeout messages) must not reach the browser. The error is already
logged server-side with the request id, so the client envelope keeps
only the REPORT_GENERATION_FAILED code.
From the 2026-07-09 production log triage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9b126d22b9 |
fix(reconciliation): server-side confidence floor for unattended auto-apply (#903)
* fix(reconciliation): server-side confidence floor for unattended auto-apply runReconciliation applied every greedy match, including auto_fuzzy at confidence 0.75, with no server-side threshold. The UI is checkbox-gated, but the unattended callers (enable-banking nightly sync cron, the extension's post-sync sweep, and the v1 run endpoint) had no guardrail. - Add ReconciliationOptions.confidenceThreshold (0..1, clamped): the apply loop skips matches below it. Skipped matches stay in the result's matches array and are counted in the new skippedBelowThreshold field, so they are reported for review rather than silently dropped. Dry runs are unaffected; omitting the threshold preserves current behavior. - Both enable-banking sync callers now pass DEFAULT_UNATTENDED_CONFIDENCE_THRESHOLD (0.9, mirroring the gnubok_auto_match_period MCP default), so unattended runs never commit fuzzy (0.75) or date-range (0.85) matches. - v1 POST /reconciliation/bank/run accepts confidence_threshold (optional, 0..1, mirroring the MCP tool naming) and returns skipped_below_threshold; registry docs/pitfalls updated. Closes #880 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): surface skippedBelowThreshold in unattended sync logs Review finding on the first pass: the floor's 'reported, not silently dropped' guarantee never reached the two unattended callers, which discarded or under-logged the result. Also logs the DECISIONS.md line for the deliberate no-default choice on the v1 route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ec27228a8e |
style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
ea236cbcdf |
fix(reconciliation): Bankavstämning phase 0 — correctness + feedback batch (+ nav IA regrouping) (#879)
* feat(nav): interaction-mode sidebar grouping — Arbeta/Analys/Data/Skatt & bokslut Nav IA redesign phase 0 (dev_docs/nav_ia_redesign.md): same routes, regrouped by what the user is doing. CLAUDE.md restructured around Hard Rules (doc references updated); pending-page explainer removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): correctness + feedback batch for Bankavstämning (phase 0) Engine: fetchAllRows pagination on status/run/RPC fetches (silent 1000-row cap corrupted totals), optimistic-lock guards on manualLink + apply, unlink audit rows attributed to the acting user (was: company UUID), selected_matches partial apply intersected with a fresh match run. View: silent in-place refresh instead of a full-page skeleton per action, checkbox-gated apply with confidence badges (fuzzy unticked) in chunks of 500, honest result toasts, dry-run errors surfaced, ranked per-row picker candidates pinned to the applied date window, currency-correct amounts (bank side in account currency, GL side SEK), voucher links, translated source types, colored differens, dirty-date-filter guard. Discovery: year-end preflight 404 href fixed (/reconciliation/bank never existed), ⌘K palette entry, real links from the transactions page. v1: status registry schema now matches the actual ReconciliationStatus payload, errors documented as a count, false ~0.85-threshold pitfall replaced, route test mocks the real shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
027734ffc7 |
fix(reconciliation): scope bank avstämning to a fiscal period so the IB stops counting (#751) (#754)
* fix(reconciliation): scope bank reconciliation to a fiscal period so the IB stops counting (#751) The bank reconciliation widget defaulted its date window to "full history" (empty dateFrom). With no lower bound the GL side spans the fiscal-year boundary: a prior period's movements on the account net to exactly the opening balance, and the new period's IB entry adds another copy. The IB *summary* was excluded but the prior-period *detail* stayed in the period movement while the bank feed only covered the current period — a phantom difference equal to the IB (the "räknar med IB fast den säger borträknad" report in #751). - Server: getReconciliationStatus now floors the window at the most recent opening-balance date on the account (effectiveFrom = max(dateFrom, ibDate)) and clamps both the GL movement set and the bank-feed set identically. Derived from the already-fetched lines — no extra query. A no-op when the caller already passes period_start; a safety net otherwise. - UI: BankReconciliationView scopes to a fiscal period via FiscalYearSelector (defaults to the newest period), seeding dateFrom/dateTo and gating the initial fetch so the full-history numbers never flash. - Tests: two regression cases reproducing the cross-period scenario. Proven against prod: full-history -> difference -10 172,94 (matched the screenshot); period-bounded -> 0,00. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): re-fetch on fiscal-period switch; document IB-floor choice Review follow-up on #754: - BankReconciliationView: add selectedPeriodId to the gated fetch effect deps so switching räkenskapsår re-fetches with the new window, and a late period selection (selector signalling ready before the company context hydrates) still triggers the real period-scoped fetch instead of leaving the empty-window result. Manual date edits still stay on the explicit "Filtrera" action. - getReconciliationStatus: comment why ibFloor takes the LATEST opening-balance date (one IB per period invariant; across a multi-year window the most recent IB is the intended floor; same-date duplicates cancel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reconciliation): cover mid-period window (dateFrom after the IB date) Review follow-up on #754: documents that a per-month reconciliation window starting after the fiscal-year IB correctly excludes the IB and reconciles on the in-window movements alone (gl_1930_opening_balance = 0 by design). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e5f1d7a916 |
fix(reports): count reversed entries in bank/supplier/AR reconciliation (#734)
A reversed (storno-corrected) journal entry must be summed together with
its storno + correction, exactly as the trial balance and balance sheet
already do (.in('status', ['posted','reversed'])). The reconciliation
paths used posted-only and manufactured phantom differences.
- bank-reconciliation getReconciliationStatus: gl_1930_balance now counts
posted+reversed (so it equals the balansräkning for the account). Dropped
the correction_adjustment subtraction and the reversed-linked-tx drop — a
corrected/amount-corrected bank receipt now reconciles. The prior model
broke once correctEntry began re-pointing the bank transaction to the live
correction (the two changes were mutually inconsistent and produced a
difference equal to the corrected amount).
- supplier-reconciliation (2440) and ar-reconciliation (1510/1513): same
posted -> posted+reversed fix. Removes the false "Ej avstämd" gap a fully
paid, fully corrected company shows — the books net to 0 over posted+reversed
while a posted-only query double-counted the payment legs.
Also in this change:
- MCP gnubok_get_reconciliation_status: add account_number param (was hardcoded
to 1930; the lib already supported per-account reconciliation).
- counterparty-templates normalizeCounterpartyName: strip trailing month /
personal-initials tokens so "ngrok JW" / "Ngrok Mars" learn as one merchant.
- pending-operation reject 409 (route + MCP tool): clarify a resolved op was
approved explicitly (no auto-commit path exists) instead of a bare
"already committed".
- create_voucher: normalize description with String().trim() for consistency
with line_description.
- migration 20260625120000: backfill stripped diacritics on ~830 companies'
seeded chart-of-accounts names (Foretagskonto -> Företagskonto, etc.); the
seed function was fixed for new companies in 20260516130000 but never
backfilled. UPDATE-only and idempotent. Already applied to production.
Tests: 899 passing across the touched suites; lint + typecheck clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c74b19df1b |
Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
094bd85e81 |
fix(reconciliation): secondary-account scoping, dialog clipping, and N:1 matching (#624)
* fix(reconciliation): secondary-account scoping, dialog clipping, and N:1 matching Three follow-ups to per-account bank reconciliation (PR #623): - Secondary same-currency accounts (e.g. a 1931 savings account) double-counted the company's unassigned (NULL cash_account_id) transactions, inflating their bank total and showing a large bogus difference while 1930 still reconciled. Only the primary cash account now claims NULL rows; every other account scopes strictly to its own id. `includeUnassigned` is threaded through all status/run/list call sites from cash_accounts.is_primary. - The "Matcha mot befintlig verifikation" picker's dropdown was absolutely positioned inside the dialog's overflow-y-auto container and got clipped. Add an `inline` mode that renders the candidate list in normal flow; the dialog uses it, the reconciliation view keeps the compact overlay. - N:1 matching: several bank transactions can now settle one verifikat (a salary run paid in multiple transfers, an invoice paid in instalments). New get_account_gl_lines_for_matching RPC surfaces already-matched vouchers with a linked_transaction_count behind a "Visa även matchade verifikationer" toggle; manualLink's 1:1 guard is relaxed (the aggregate difference still catches mis-links). Tests: extended bank-reconciliation unit tests (strict scope + N:1), rewrote the cash_account_id isolation pg test to prove NULL rows land on the primary account only, and added a pg test for the new RPC. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): address PR review — accurate "att matcha mot" count - BankReconciliationView: the "N verifikationer att matcha mot" hint counted glLines (which includes already-matched vouchers when "Visa matchade" is on), overcounting the vouchers that still need a transaction. Use unmatchedGlLines so the label is correct regardless of the toggle (matches the table below). - MatchVerifikationPicker: document that `open` is overlay-only; the setOpen() writes are intentional no-ops in inline mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b5c3c3ec04 |
fix(reconciliation): surface bank tx + match to existing verifikat, delete UX, DPA links (#623)
* fix(reconciliation): surface bank transactions via robust cash_account_id scoping The per-account reconciliation scoping silently returned zero transactions for companies whose rows were NULL or mis-assigned mid-backfill (e.g. Arcim: 138 transactions, 101 unbooked, yet Bankavstämning showed "0 kr" while the 1930 GL movement and a large difference still displayed). Two causes, both fixed: - scopeTransactionsToAccount used a fragile nested or(...,and(is.null,...)) PostgREST filter. Replace it with a flat, reliable `currency = X AND (cash_account_id = id OR cash_account_id IS NULL)` and share the one implementation with /api/transactions so the status card and the lists can never drift. - The original best-effort backfill only touched NULL rows and an earlier revision mis-assigned cash_account_id (the since-fixed min(uuid) bug), which migrations cannot self-correct. Add an idempotent repair migration that re-seeds the default 1930 account and re-derives cash_account_id (correcting non-NULL mis-assignments) for booked rows and single-account companies. Also localise manualLink's user-facing errors to Swedish. Adds unit coverage for the new filter shape and pg-real coverage for the repair (incl. the Arcim single-account reproduction). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(transactions): match a bank transaction to an existing verifikat Adds a "Matcha mot befintlig verifikation" action to the Transactions inbox so a bank line that is already booked elsewhere (a salary run, a Fortnox/manual voucher, an invoice paid from the invoice page) can be linked to that existing verifikat with no new bokföring — the capability previously lived only in Reports → Bankavstämning. - Extract the searchable MatchVerifikationPicker into a shared client component. - New MatchVoucherDialog: resolves the tx's cash account, fetches candidates ranked server-side by reconciliation confidence, links via /api/reconciliation/bank/link (so the link is undoable in Bankavstämning). - unmatched-entries route gains an optional transaction_id that ranks candidates (ranking stays server-side; the recon lib is not client-safe). - Inbox row's overflow (⋯) menu gains the new action. Also fixes the Bankavstämning view: editing the date no longer auto-reloads (applies on Filtrera / account change only) and Datum till defaults to today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(transactions): actionable Swedish errors when a bank tx cannot be deleted The delete route returned a hardcoded English 409 that getErrorMessage mapped to the misleading generic "En konflikt uppstod. Ladda om sidan...". Return structured bilingual envelopes instead: - TRANSACTION_DELETE_BOOKED (409) for a booked/linked row — steers the user to unlink in Bankavstämning or storna the voucher. - TRANSACTION_DELETE_HAS_AUDIT_TRAIL (409) for the real, common case where an unbooked row carries payment_match_log rows: the cascade hits the audit-immutability trigger (P0001), previously surfaced as a bare 500. Steers the user to match-to-voucher or ignore instead. Updates the DELETE test suite and adds the P0001 case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): surface DPA and privacy policy links The /dpa page (personuppgiftsbiträdesavtal, GDPR Art. 28) was complete but linked from nowhere. Add a "Sekretess och avtal" card on Inställningar → Konto linking to /privacy and /dpa, and a reciprocal link to the DPA from the privacy policy's sub-processor section. (The DPA already links back to /privacy.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): address PR #623 feedback - unmatched-entries: when transaction_id is supplied but resolves to no row in the caller's company, return an empty candidate list instead of silently falling back to the full unranked list (Compliance Swarm V8.2.1, high). - MatchVoucherDialog: preserve a manually-picked voucher when the candidate list reloads (e.g. "Visa alla datum") instead of discarding it (Greptile P2). - DELETE /api/transactions/[id]: return the 404 as the structured { error: { code, message, message_en } } envelope like the handler's other errors, for a uniform contract (Greptile P2). Test updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): allow editing notes on a committed journal entry Saving a note on a posted verifikation failed with "Committed entries are immutable": enforce_journal_entry_immutability() had no posted→posted path, so a notes-only UPDATE fell through to the final RAISE. `notes` is internal annotation metadata (not verifikation content under BFL 5 kap. / BFNAR 2013:2), so add a narrow carve-out that permits a notes-only change on a committed entry — verified with a whole-row to_jsonb() diff so any other field change still raises, and only when status is unchanged. Period-lock enforcement is unaffected. CREATE OR REPLACE in a new migration (same pattern as 20260428160000_fix_journal_entry_immutability_delete_bypass); the migration-017 protections are extended, never weakened. Covered by a pg-real test asserting a notes edit succeeds while amount/description/account edits still fail. (Already applied to production; committing the file + test for repo consistency.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sandbox): make pre-staged pending operations executor-complete The seeded pending_operations existed only as display previews — approving them failed because commit executors in lib/pending-operations/commit.ts validate required fields on "Godkänn". Seed a backing invoice_inbox_items row and fill the supplier-invoice and categorize params with every field the executors require (inbox_item_id, full items array; real uncategorized transaction_id + category), so the sandbox approval queue is actually approvable end to end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
953980c875 |
Per-account bank reconciliation + overdue/inbox/privacy fixes (#619)
* feat(reconciliation): scope bank reconciliation per cash account via transactions.cash_account_id A company with two same-currency cash accounts (e.g. checking 1930 + a savings account) saw every SEK transaction on every account, and the status card summed across both — reconciliation filtered transactions by CURRENCY while filtering GL lines by ACCOUNT (issue #604). Bind each bank transaction to the cash_accounts row it settled on: - New nullable transactions.cash_account_id FK (ON DELETE SET NULL — a bank transaction is räkenskapsinformation, BFL 7 kap, and must survive cash-account deletion) + a best-effort 4-pass backfill. - All reconciliation/transaction queries scope to the selected account with a NULL->currency fallback, so legacy/un-backfilled rows never disappear mid-backfill. - ingestTransactions stamps cash_account_id from the batch's settlementAccount; categorize + manualLink resolve and use it. - Bank leg now books to the transaction's actual settlement account via applySettlementAccount (no-op for 1930), so interest/fees on a savings/EUR account reconcile instead of mis-booking to 1930. - manualLink cross-checks the transaction's account and requires a voucher line on the selected account (no silent cross-account links). - BankReconciliationView: quick-book menu for any settlement account, in-flight request abort on account/date switch, 500-row truncation notice, per-account state reset. - pg-real coverage for the FK, all backfill passes, account-scoped query isolation, and cross-company isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(supplier-invoices): stop marking paid invoices and credit notes as overdue update_overdue_supplier_invoices() (the daily pg_cron job) flipped every past-due 'registered'/'approved' row to 'overdue' without looking at the outstanding balance. Credit notes — created 'registered', remaining 0, due today — got flipped the next day, surfacing as "Förfallen" with "kvar att betala 0 kr"; so did any fully-paid invoice left in 'registered'/'approved'. Guard the cron on remaining_amount > 0.005 (the "fully paid" threshold used by the payment/match paths) and is_credit_note = false, and backfill the rows already mis-flagged (credit notes -> 'registered', paid -> 'paid' with paid_at stamped only when missing). pg-real coverage for the guarded function and the one-off backfill. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): refresh dokumentinkorg on realtime row changes The InvoiceInboxWorkspace only refetched on mount and on explicit in-component actions. When an inbox item was resolved out of band — the in-app agent sheet committing a staged create_supplier_invoice_from_inbox / book-direct op, the /pending page approving one, or another tab booking it — none of those paths called fetchItems(), so the booked underlag stayed in "Att göra" until a manual reload (issue #600). Add invoice_inbox_items to the supabase_realtime publication (mirrors the /pending fix in 20260520120100) and subscribe in the workspace, refetching the whole list on any change so derived status/counts/ordering stay authoritative. RLS scopes the channel to the user's company. fetchItems now preserves optimistic upload placeholders so a refetch firing mid-upload can't drop an in-flight row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(privacy): disclose EU AI inference via Amazon Bedrock (eu-north-1) Update the privacy policy and DPA to state that AI inference, when AI features are enabled, runs inside the EU via Amazon Bedrock (eu-north-1, Stockholm) using Anthropic's Claude models — no transfer to a third country, prompts not retained after the call or used for model training. Add AWS as a subprocessor row and refresh the "last updated" dates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): rename invoice_inbox_realtime to avoid version collision main's #617 shipped 20260605120000_transactions_original_description.sql — the same version this branch used for the inbox-realtime publication. The Supabase migration tracker keys on the numeric version, not the filename, so the preview branch failed with a duplicate-key error on supabase_migrations.schema_migrations (version 20260605120000 already exists). Rename to the unique version 20260605120500; the body (ALTER PUBLICATION) is order-independent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): align run guard with status; harden filter interpolation Addresses PR review (greptile + compliance swarm): - The v1 and core bank/run routes rejected an unknown account uniformly, including the default '1930', while the status routes were lenient for '1930'. A company reconciling its primary SEK account without a cash_accounts row got 200 from status but 400 from run. Make run match status: '1930' falls back to currency-only scoping (cashAccountId undefined); non-default unknown accounts are still rejected. Adds a test. - /api/transactions accepts a user-supplied `currency` query param that was interpolated raw into a PostgREST .or() filter. Reject anything that isn't a 3-letter ISO code — RLS already scopes to the company, but an unsanitized value could otherwise malform/widen the filter. Assert currency/cashAccountId shape in scopeTransactionsToAccount as well. - categorize: log (instead of silently swallowing) a cash_accounts settlement-account lookup error, so a fall-back-to-1930 mis-booking is observable in the audit log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): correct backfill UPDATE..FROM join; idempotent realtime publication Two SQL errors that only surface on real Postgres (CI pg-real + Supabase preview) — the unit suite mocks Supabase, so neither was caught locally. - Backfill pass (a): `UPDATE transactions t ... FROM journal_entry_lines jel JOIN cash_accounts ca ON ca.company_id = t.company_id` referenced the UPDATE target `t` inside the FROM join's ON clause, which Postgres rejects ("invalid reference to FROM-clause entry for table t"). Move the company match to WHERE; the JOIN now relates jel<->ca only. Semantics unchanged. - invoice_inbox_realtime: `ALTER PUBLICATION ... ADD TABLE` is not idempotent (SQLSTATE 42710 if the table is already a member). The earlier version-collision push partially applied it on the Supabase preview branch, so the re-apply errored. Guard with a pg_publication_tables existence check. Both statements validated against a real Postgres: the single-line tx binds, the two-bank-line transfer stays NULL, and the publication add runs twice cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migrations): backfill pass (c) uses array_agg, not min(uuid) Postgres has no min() aggregate for uuid, so pass (c)'s min(id) raised "function min(uuid) does not exist" on apply (CI pg-real + Supabase). The HAVING count(*) = 1 already guarantees one row per group, so (array_agg(id))[1] returns that single id. Validated the full backfill (all four passes) and the overdue migration against a real Postgres: every pass binds / falls through as intended, and the overdue guard + backfill produce the right statuses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(compliance): add RoPA entry for Amazon Bedrock AI inference (GDPR Art.30) The privacy policy now discloses AI inference (transaction categorization + document/receipt OCR) via Amazon Bedrock as a processing activity, but .compliance/ropa.yaml had no matching Art.30 record. Add it: opt-in consent basis, EU-region (eu-north-1) inference with no third-country transfer, prompts not retained or used for model training. Mirrors the privacy-page disclosure shipped in this PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c6c86cded4 |
Mcp/template data feedback (#617)
* fix(booking-templates): scope template list to the active company GET /api/settings/booking-templates relied solely on the btl_select RLS policy, which is membership-wide (user_company_ids) and returns templates from every company the user belongs to. A user who owns multiple companies saw all their templates merged regardless of which company was active. Narrow the list in the API layer (mirroring counterparty-templates) to system + the active company + the active company's team. RLS stays the security backstop; this fixes the cross-company merge within a single user's own view (it was never a cross-tenant data leak). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): show proper message for duplicate bank file upload The bank file import page mis-parsed the structured error envelope ({ error: { code, message, details } }), so a BANK_FILE_DUPLICATE (409) fell through to the generic "Kunde inte läsa filen" fallback. The upload step also hardcoded that same string as the error heading, so duplicates were doubly misreported as parse failures. - Parse the structured envelope by error.code; surface error.message for all codes instead of rendering the error object. - Add a dedicated BANK_FILE_DUPLICATE message using the importedAt / importedCount details the route already returns. - Add an optional errorTitle prop to BankFileUploadStep (defaults to the previous text) and pass "Filen är redan importerad" for dupes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tests): add comprehensive tests for recordateEntry, inbox-linking, and external-id handling - Implemented unit tests for recordateEntry in the bookkeeping module to validate various scenarios including date changes, non-posted entries, and fiscal period restrictions. - Created tests for inbox-linking status in pending operations to ensure correct handling of invoice inbox items and supplier invoices, addressing historical bugs related to status updates. - Added tests for external-id utilities to ensure consistent handling of monetary amounts and deduplication keys across different transaction sources. - Introduced new functions in external-id.ts for stable external ID generation and normalization of imported descriptions, enhancing transaction deduplication reliability. feat(migrations): add new database migrations for transaction handling - Created migration to exclude storno and correction vouchers from unmatched GL lines, ensuring accurate reconciliation. - Added a migration to preserve original bank transaction descriptions in a new immutable column, allowing for user edits while maintaining audit trails and deduplication integrity. * feat(migrations): add function to exclude storno/correction vouchers from unmatched GL lines * feat(transactions): enhance transaction handling with improved description normalization and preloaded original entries --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ea1bf01f1e |
Fix/m sprint fixes (#613)
* fix(dashboard): exclude ignored and already-triaged transactions from stale count The "Gamla transaktioner" widget counted transactions that had been ignored or already marked as is_business=true but not yet booked, so users saw a nag for a row they had already dealt with — and the /transactions inbox correctly hid it. Align the count with the inbox criterion (is_business IS NULL, is_ignored = false) so the widget clears when the row leaves the inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): read entity_type from settings response wrapper The transactions page read entityRes.entity_type directly, but /api/settings returns { data: { entity_type, ... } }. The expression was always undefined, so setEntityType never fired and entityType stayed at its initial 'enskild_firma'. The template picker's entity_type filter then dropped every aktiebolag-tagged user template for AB customers — only entity_type='all' templates made it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale templates bank sync journal entry from transaction * fixed pr comments * fixed pr comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8a6ce7093e |
feat: implement skattekonto drift detection and alerting (#525)
* feat: implement skattekonto drift detection and alerting - Add skattekonto drift computation logic to compare Skatteverket's saldo with GL 1630 sum. - Implement alerting mechanism for significant drift changes, with throttling to prevent alert spamming. - Introduce database functions to sum GL 1630 entries and list unbooked skattekonto rows. feat: create own account transfer detection - Develop logic to detect transfers between a company's own cash accounts based on counterparty IBAN. - Implement tests to validate detection logic under various scenarios, including matching and non-matching IBANs. feat: establish cash accounts as a first-class entity - Create cash_accounts table to manage routable cash accounts, replacing ad-hoc JSONB structures. - Implement functions for listing, upserting, and managing cash accounts, including primary account designation. feat: enhance GL line reconciliation functionality - Modify get_unlinked_1930_lines RPC to accept any account number for reconciliation, improving flexibility for different currencies. - Update related functions to ensure compatibility with the new cash_accounts structure. feat: capture counterparty IBAN in transactions - Add counterparty_iban column to transactions table to facilitate intra-account transfer detection. - Create index for efficient lookups based on counterparty IBAN. * feat: Enhance cash account handling and reconciliation processes - Updated reconciliation routes to enforce cash account validation for all account numbers, including '1930'. - Improved error handling for unknown cash accounts in reconciliation status and unmatched entries routes. - Changed CashAccountSelector to use sessionStorage instead of localStorage for better data privacy. - Fixed mapping for employer payroll taxes to route to the correct account (2730 instead of 2731). - Added safety checks for company IDs in the guessCounterAccount function to prevent injection vulnerabilities. - Introduced atomic RPC for setting primary cash accounts to avoid intermediate states during updates. - Seeded default cash accounts for new companies to ensure reconciliation routes are accessible from day one. - Updated email notifications for drift detection to avoid exposing sensitive financial data. - Enhanced bank reconciliation logic to handle multi-currency transactions correctly. - Renamed and updated tests to reflect changes in the underlying RPCs and ensure accurate coverage. - Migrated existing cash account rules to correct mappings in compliance with Swedish accounting standards. |
||
|
|
5eee06a56d |
fix(reconciliation): exclude opening balance from unmatched-1930 set (#443) (#485)
* fix(reconciliation): exclude opening balance from unmatched-1930 set (#443) Reconciliation incorrectly counted IB (source_type='opening_balance') vouchers on 1930 as unmatched bank transactions and included them in the GL period total. Result: an SIE-imported book always showed a phantom unmatched voucher and a difference equal to the IB amount, even when every real bank transaction was matched. This was the root of the "reconciliation says broken but numbers look right" complaint. The fix is two-sided: - get_unlinked_1930_lines RPC now skips source_type='opening_balance' rows. IB has no counterpart in the bank feed by definition. - getReconciliationStatus splits glOpeningBalance out of the period movement and computes difference = bankTotal − glPeriodMovement (was bankTotal − glBalance). gl_1930_balance is preserved unchanged for back-compat; new gl_1930_period_movement and gl_1930_opening_balance fields let the UI show both perspectives. - BankReconciliationView shows period movement vs bank in the diff and folds IB into a small caption ("Ingående balans … räknas inte i avstämningen") only when non-zero. Mirrors Fortnox's "Ingående saldo vid avstämningsstart" pattern — IB is verified separately, never surfaced as unmatched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(reconciliation): use IS DISTINCT FROM for NULL-safe source_type filter `source_type <> 'opening_balance'` evaluates to NULL (not TRUE) when `source_type IS NULL` under SQL three-valued logic. journal_entries.source_type is NOT NULL today so the practical difference is zero, but if that constraint is ever relaxed `<>` would silently drop NULL rows from the unmatched-1930 set, making them invisible to reconciliation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): use valid source_type 'bank_transaction' in pg-real fixture CI pg-real failed because 'bank_import' is not in the journal_entries source_type CHECK constraint. The valid value for bank-imported transactions is 'bank_transaction' (per migration 20260513170001 + earlier). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(reconciliation): deprecate gl_1930_balance on ReconciliationStatus Mark the IB-inclusive balance field as @deprecated on both the server-side ReconciliationStatus interface and the component-side mirror. Downstream consumers that read this field expecting it to drive the diff will be off by the IB amount whenever a SIE-imported opening balance exists on 1930. The new gl_1930_period_movement field is the correct basis for the diff and is what the difference field is computed against. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(reconciliation): tighten review feedback on PR #485 Three tweaks from the Swedish-accounting bot review: 1. Drop the gl_1930_period_movement ?? gl_1930_balance fallback in BankReconciliationView. gnubok has no persisted reconciliation snapshots and Vercel deploys atomically, so the old-server/new-client case can't occur — the fallback was defensive code for an impossible state. Per CLAUDE.md: don't add back-compat shims when you can just change the code. gl_1930_period_movement and gl_1930_opening_balance are now required on the type. 2. Clarify in the migration comment that source_type='opening_balance' is reserved for the fiscal-year IB voucher (always at period_start). The bot questioned whether the unconditional filter could hide a mid-year corrective IB entry; document the invariant — mid-year corrections use source_type='correction' or 'manual', never 'opening_balance'. 3. Test fixture 2099 → 2091. 2099 is årets resultat (current year); 2091 is balanserad vinst/förlust, the realistic counterpart for a carried-forward bank IB. RPC filters on 1930 only so this is cosmetic but matches what a real SIE import would produce. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9e1e13d388 |
Fix/bank and onboarding (#413)
* fix(onboarding): validate org number at step 2 instead of final submit Run normalizeOrgNumber (Luhn + 10/12-digit check) inside the Step 2 Zod schema and gate the duplicate-check and TIC lookup effects on it, so users get an inline error on the field they just typed instead of filling out two more steps and being bounced back from the server. Server-side check stays as defense in depth. Also fixes the old regex incorrectly rejecting valid 12-digit org numbers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(reconciliation): pass real userId to transaction.reconciled events runReconciliation and manualLink fell back to companyId when no userId was provided, causing FK violations on event_log.user_id (which references auth.users). Make userId a required arg in both functions, drop the fallback, and forward a real user id from every caller (user.id from authed routes, connection.user_id from the cron path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(enable-banking): use local companyId instead of reinlining ctx fallback Three sites in the enable-banking sync handler reinline `ctx?.companyId ?? user.id` instead of using the local `companyId` declared at the top of the block. Collapse all three to the local for consistency and to remove drift risk if the fallback expression ever changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2c03dac981 |
fix(reports): convert FX to SEK in supplier/AR ledger reconciliation (#396)
* fix(reports): convert FX to SEK in supplier/AR ledger reconciliation The supplier and AR ledger reports were summing remaining_amount directly without converting foreign-currency invoices, so a EUR/USD invoice would land in the aging total at face value while the corresponding 2440 / 1510 GL line was already posted in SEK. This produced false reconciliation discrepancies (e.g. 496,25 kr ledger vs 952,50 kr GL with four EUR/USD invoices). Apply resolveSekAmount(remaining, null, currency, exchange_rate) in supplier-ledger, supplier-reconciliation, ar-ledger, and ar-reconciliation. Per-invoice detail rows on the AR ledger keep the original currency for display; only aging buckets and totals become SEK. Adds mixed-currency test cases to all four files. Also bundles unrelated WIP from the working tree: - bank-reconciliation: log the swallowed catch error and drop the fallback path for the deleted get_unlinked_bank_lines RPC, using get_unlinked_1930_lines directly. - new GET /api/transactions list endpoint with unmatched/reconciled/ currency/date filters and full route tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(reports): address PR #396 review findings Compliance and Greptile review identified four issues; all four are addressed here. - Reconciliation: surface unconverted_fx_count on ReconciliationResult and ARReconciliationResult. When > 0 the difference field may be a data gap (FX invoice with no exchange_rate) rather than a true reconciliation break. UI now renders a Swedish caveat below the Avstämd / Ej avstämd badge so users understand the cause. New tests assert the count is set on legacy FX rows. - Reconciliation: document the invoice-date-rate assumption explicitly in the JSDoc of both reconciliation generators. Per ML 8 kap 21–23 §, the report uses each invoice's stored exchange_rate; partial payments settled at a different rate produce a delta correctly booked to 3960/7960 as valutakursvinst/-förlust, but the GL will diverge from the report by that amount until a subledger-derived total is wired up (deferred follow-up). - fetchUnlinkedGLLines: drop the misleading bankAccount parameter. It was advertised as configurable but the function silently returned [] for any value other than '1930'. Now the signature is honest: 1930-only until proper multi-account support is built. - /api/transactions: query MAX_ROWS+1 rows so the response can include has_more and limit fields. Callers can now detect truncation, which matters once a company crosses 500 unmatched transactions in the selected range. New test asserts has_more=true when 501 rows are returned by the DB and the response is sliced to MAX_ROWS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(reports): address PR #396 round-2 compliance review Compliance bot v2 review surfaced four findings on the previous commit; three are addressed here. The fourth (an fx_rate_diff_amount indicator distinguishing real reconciliation breaks from correctly-booked valutakursvinst/-förlust) requires a subledger-derived total against 3960/7960 — already documented as a deferred follow-up in the JSDoc. Changes: - Exclude unconvertible FX rows from SEK sums. resolveSekAmount's null-rate fallback returned the raw foreign amount, so a 100 EUR invoice with no rate was being added to a SEK total as if it were 100 SEK. All four generators (supplier-ledger, supplier-reconciliation, ar-ledger, ar-reconciliation) now skip rows where currency != SEK and exchange_rate is missing/zero, and count them in unconverted_fx_count. - Surface unconverted_fx_count on SupplierLedgerReport / ARLedgerReport (not just the reconciliation result). UI shows a Swedish caption beneath the "Totalt utestående" card whenever the count is positive, so users see the warning even if they don't enable the reconciliation panel. - Add outstanding_sek: number | null to ARInvoiceDetail. The per-invoice detail row keeps `outstanding` in invoice currency for display, but now also exposes the converted SEK value (or null when unconvertible). Defensive against future callers that sum across customers — they should use outstanding_sek to avoid mixing currencies (a real momsdeklaration foot-gun otherwise). - Add a 1930-only notice to BankReconciliationView. The reconciliation is scoped to account 1930; users with Plusgiro 1920, kreditkort 1940, or valutakonton now see a Swedish caption explaining those are reconciled separately. Tests: - supplier-ledger: previous "falls back to original amount" case flipped to assert the row is excluded and counted; assertion that the legacy supplier disappears from the list when their only row is unconvertible. - supplier-reconciliation: previous "1 100 ledger vs 1 000 GL" case flipped to assert ledger=1 000 and is_reconciled=true with unconverted_fx_count=1. - ar-reconciliation: same pattern. - ar-ledger: existing FX-mix test extended to assert outstanding_sek on each detail row; new test covering the null-rate exclusion path asserts the detail row is still pushed (with outstanding_sek=null) but excluded from buckets and the grand total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(reports): address PR #396 round-3 compliance review Compliance bot v3 surfaced four findings on the previous commit; two are addressed here, two are deliberately skipped (rationale in JSDoc / this message). Addressed: - is_reconciled now returns false whenever unconverted_fx_count > 0, even if the numeric difference is zero. Per BFL 5 kap, the reconciliation must cover all affärshändelser; if a row was excluded for a missing exchange rate, the calculation is incomplete by construction and the period cannot honestly be stamped Avstämd. The fix is for the user to fill in the missing rate, not for the system to claim balance on partial data. - AR reconciliation now sums account 1510 + 1513 in the GL balance comparison. Forward-looking defense for ROT/RUT fakturamodellen invoices that split AR receivables across the customer portion (1510) and the Skatteverket claim (1513). Today no production code posts to 1513 so the value is unchanged in practice; once fakturamodellen invoicing is added, the reconciliation will continue to balance without requiring another fix. UI label updated to "Kundfordringar (1510 + 1513) saldo" so the inclusion is visible. Field name and shape are unchanged for back-compat with the supplier-side parallel. Skipped: - "Block is_reconciled=true when any FX invoice exists in an open period" — overly aggressive; would block reconciliation for any FX-using company even when their books are correctly matched. The proper solution is the deferred subledger-derived total against 3960/7960 (already documented in JSDoc on both reconciliation generators), which can compute the *expected* FX rate difference and either subtract it from `difference` or expose it as `fx_rate_difference`. Not in scope for this PR. - Plusgiro 1920 wording in BankReconciliationView — bot itself marked this "not a hard finding". The 1920 reference is accurate per BAS 2026. Tests: - supplier-reconciliation: existing v3 exclusion test updated — asserts is_reconciled=false despite numbers matching, with comment explaining BFL rationale. - ar-reconciliation: same update + new test covering the 1510 + 1513 sum (1 200 on 1510, 300 on 1513, ledger total 1 500 → reconciled). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
566b9696f8 |
Feat/verification attachments (#371)
* feat(attachments): enhance document preview functionality for images and PDFs * feat(reconciliation): enhance transaction matching logic and clarify reconciliation process |
||
|
|
ade4ad5971 |
Fiscal period and multi bank (#228)
* feat: add fiscal period backward chaining and entry date validation Support creating fiscal periods before the earliest existing period (backward chaining) for backfill scenarios, alongside the existing forward chaining. The engine now validates that entry dates fall within the selected fiscal period, with a Swedish error message. The journal entry form auto-selects the matching period and shows a warning with a CreatePeriodDialog when no period covers the entry date. * feat: support multi-bank-account for imports and reconciliation Plumb a configurable settlement account through the entire bank import pipeline — mapping engine, transaction entries, ingest, and reconciliation — so secondary bank accounts (e.g. 1931, 1932) work correctly instead of hardcoding 1930. Adds a get_unlinked_bank_lines RPC that generalizes the existing get_unlinked_1930_lines with a fallback for backwards compatibility. The bank file import UI now shows a bank account selector when multiple 19xx accounts exist. Also adds default_vat_code/sru_code to account creation and fixes uploadDocument argument order in enable-banking sync. |
||
|
|
e89f2c402d |
feat: complete multi-tenant refactor + settings validation + fiscal period API (#156)
* feat: complete multi-tenant refactor for reconciliation, arcim, settings validation - Migrate bank-reconciliation to company_id (all functions + tests) - Migrate arcim-migration entity mappers and orchestrator to company_id - Fix enable-banking reconciliation calls to use companyId - Add Swedish law validation to settings schema: - VAT number required when VAT-registered (ML 11 kap. 8§) - Moms period required when VAT-registered (SFL 26 kap.) - Aktiebolag must use accrual accounting (BFNAR 2006:1) - Fix fiscal year period creation: always 12 months after first year (BFL 3 kap.) - Add plusgiro, website, pays_salaries fields to CompanySettings - Add plusgiro to invoice PDF template - Add fiscal period CRUD and opening balances API routes - Add frame-src CSP directive for future iframe embedding - Fix unlinked_1930_lines RPC to use company_id parameter - Update CLAUDE.md documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review findings (P1 + P2) - Fix reconciliation events emitting companyId as userId — thread actual userId through runReconciliation and manualLink - Move VAT cross-field validation (vat_number, moms_period) from schema refinements to route handler where effective stored state is available, preventing false rejection on partial updates - Add plusgiro format validation regex (N-N pattern) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
0dd1f5ebc1 |
feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19) Introduce companies table, company_members, and user_preferences to support multiple companies per user. All data scoping changes from user_id to company_id across the entire codebase. Key changes: - Database migration: new tables, company_id on 40+ tables, backfill, RLS rewrite from user_id to company-member-based, updated RPCs - Types: Company, CompanyMember, CompanyRole, UserPreferences types; company_id added to all entity interfaces; companyId on all events - Engine: all 7 core functions take companyId; storno, period, year-end services updated; 16 report generators updated - Middleware: company context resolution (cookie → prefs → first company) - API routes: ~120 routes updated with requireCompanyId() - Frontend: CompanyProvider context, layout/dashboard/onboarding updated - Extensions: context factory, 9 extensions, all lib files updated - Tests: 1880 tests passing, all helpers updated with company_id defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add database migrations for multi-tenant company and team system (GNU-19) Adds company_invitations, company creation RPC, team_members, account deletion RPC, and teams table refactor migrations. Updates base multi-tenant migration with cascading FKs and onboarding_step column. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team types and update core infrastructure for multi-tenancy (GNU-19) Adds TeamRole, MemberSource, and Team types. Refactors Supabase service client to be stateless, updates middleware for team-aware routing, extends CompanyContext with team/role fields, and updates extension service types to accept companyId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through business logic functions (GNU-19) Replaces user_id scoping with company_id across all lib modules: bookkeeping, documents, transactions, invoices, reconciliation, tax, deadlines, and import. Updates corresponding tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through API routes and extensions (GNU-19) Updates all existing API routes to extract and pass companyId. Updates enable-banking and arcim-migration extensions for company-scoped transaction ingestion and sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add company and team management API routes (GNU-19) Adds CRUD endpoints for company members, company invitations, team members, and team invitations. Includes invite token utilities, email templates, and company switch server action. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team/company UI components, pages, and dashboard updates (GNU-19) Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company members and team management panels. Updates dashboard layout for team-aware routing, onboarding for multi-step role choice, and auth callback for team invite acceptance. Ignores supabase/.branches/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in import page (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move appUrl declaration to outer scope in invite route (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for second company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in extension components (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update tests to use companyId instead of userId and improve type handling --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
7c3518a080 |
feat: payment matching audit trail and partial payments (#89)
* feat: payment matching audit trail, partial payments, and match improvements - Add payment_match_log table (append-only audit trail per BFL 7:1) with immutability triggers and proper RLS - Add invoice_payments table for partial payment tracking, mirroring supplier_invoice_payments pattern - Add remaining_amount column + partially_paid status to invoices - Add match-log.ts service for recording match/unmatch state transitions - Improve match-invoice route: support partial payments, record audit log, emit payment.matched events, handle storno conflicts - Improve match-supplier-invoice route: audit logging, partial payment support - Update invoice-entries.ts for partial payment journal entries - Update transaction ingest to detect and auto-suggest invoice matches - Update bank-reconciliation to handle partial payment state - Add invoice_payments, payment_match_log types and helpers - Extend tests for match-invoice route and transaction ingest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback — payment matching correctness bugs - P1: Return 500 on non-23505 payment insert failures instead of silently continuing with corrupted invoice state (both routes) - P1: Fix foreign-currency partial payment matching — use proportional remaining SEK amount instead of full total_sek - P2: Check error when clearing journal_entry_id after storno - P2: Add updated_at column + trigger to invoice_payments table - P2: Make supplier_invoice_payments.user_id NOT NULL after backfill Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
91e2c1705a |
feat: per-line VAT, invoice document types, ledger-based VAT declaration, bank reconciliation, and pagination
Per-line VAT rates: - Add generatePerRateLines() to group invoice items by vat_rate with separate revenue + VAT lines per rate group (invoice-entries.ts) - Add getAvailableVatRates() and getVatTreatmentForRate() (vat-rules.ts) - PDF template shows per-line VAT column and per-rate totals for mixed-rate invoices - Invoice create/review UI supports per-line rate selection - Types: add vat_rate/vat_amount to InvoiceItem, vat_rate to CreateInvoiceItemInput Invoice document types (proforma, delivery note): - Add InvoiceDocumentType, document_type and converted_from_id to Invoice type - PDF hides prices for delivery notes, adds proforma notice - Email templates support all document types - mark-paid skips journal entries for non-invoice document types - Migration 031: invoice_document_type Accounting method support: - Add AccountingMethod type (accrual/cash) - Migration 032: add_accounting_method column to company_settings VAT declaration rewrite: - Rewrite to read directly from general ledger (26xx/3xxx account lines) instead of aggregating invoices/transactions/receipts - ACCOUNT_RUTA mapping drives momsdeklaration boxes from GL balances Bank reconciliation: - Transaction ingest now pre-fetches unlinked GL lines and attempts auto-reconciliation during import - Add transaction.reconciled event type - Add ReconciliationMethod type and reconciliation_method on Transaction - Migration 030: bank_reconciliation - New reconciliation engine, API routes, and BankReconciliationView component Pagination (fetchAllRows): - New lib/supabase/fetch-all.ts overcomes PostgREST 1000-row limit - Adopted in all report generators, SIE/SRU export, account list APIs Fiscal period validation: - New validate-period-duration.ts enforces max 18 months per BFL 3 kap. - Applied in period-service.ts and fiscal-periods API Account mapper simplification: - Remove Levenshtein/fuzzy matching, use exact account number match only Swedbank parser improvements: - Support abbreviated headers (Clnr, Bokfdag, Radnr) - Use Referens column as counterparty Chart of accounts management: - Add DELETE endpoint with system account and usage protection - PUT uses partial updates - New AccountCombobox, AddAccountDialog, EditAccountDialog, ChartOfAccountsManager Tax deadline corrections: - Rewrite inkomstdeklaration_ab using Skatteverket lookup table - Rewrite arsredovisning deadline to 7 months after FY end per ÅRL 8:3 Onboarding first fiscal year: - Add first fiscal year toggle with date pickers and 18-month validation UI terminology: - Change "okategoriserad/kategorisera" to "obokförd/bokföra" throughout Report column fix: - Fix start_date/end_date to period_start/period_end in report queries Supplier invoice input: - CreateSupplierInvoiceItemInput uses amount field (legacy quantity/unit_price kept) Misc: - SIE import uses upsert for idempotent account creation - account-descriptions.ts falls back to BAS reference data - Add invoice_default_notes to CompanySettings - Update CLAUDE.md to reflect current project state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |