Two MCP-agent-reported bugs (feedback seq 254607, 261972; also reported
by mail):
- categorize_transaction validated vat_amount against the transaction-
currency gross but posted the raw figure as SEK: a 15.87 USD override
on a 79.34 USD Stripe payment booked 15.87 kr to 2611 instead of
~150.92 kr, and the entry still balanced so nothing could catch it.
buildMappingResultFromCategory now resolves the same SEK value the
line builder uses for the gross and books ALL VAT figures off it:
the override (scaled by the settlement ratio), the auto-derived rate
VAT, and reverse-charge fiktiv moms, which had the same defect for
every foreign-currency transaction. SEK transactions are unchanged.
The vat_amount schema now states the denomination; the override
bound error names the currency.
- complete_document_upload / upload_document declared
matched_supplier_id as a bare string while unmatched uploads
correctly return null, so strict clients failed every successful
unmatched upload and tripped the caller's circuit breaker. The
schema is now ['string', 'null'], matching the runtime.
Catalog token ceiling 59.9K -> 59.95K per the documented ratchet
protocol (prose trimmed to the floor first; headroom was ~19 tokens).
Claude-Session: https://claude.ai/code/session_01ScVhg6XsDtNXkiEQNV7LaZ
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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>
Incoming expense refunds (positive amount on an expense category) previously
booked with inverted debit/credit — crediting the bank and debiting the expense
account — which both imbalanced the book and reported negative bank flow.
getCategoryAccountMapping now detects amount > 0 on expense categories and
returns the reversed mapping: debit 1930, credit expense account, with 2641
as vatCreditAccount so ingående moms is correctly reversed on the VAT line.
The VAT line description is "Återföring ingående moms X%" rather than the
income-side "Utgående moms" label.
Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(bookkeeping): honor underlag VAT via vat_amount override in categorize flow
The categorize flow always derived VAT as rate × gross/(1+rate) from the
transaction amount, with no way to use the underlag's actual moms. On e.g.
a restaurant receipt with dricks (no VAT on the tip), the agent could see
the document's correct VAT but the staged booking recomputed the wrong
rate-based amount on every attempt.
- buildMappingResultFromCategory: optional vatAmountOverride replaces the
rate-derived VAT line ("Ingående/Utgående moms (enligt underlag)"; 0 =
no VAT line). Rejects negatives, amounts above the 25%-extraction bound,
and combination with reverse_charge / VAT-less treatments / private.
- gnubok_categorize_transaction: new vat_amount input, threaded into the
staged preview and persisted in the operation params.
- commitCategorizeTransaction: reads params.vat_amount so the approved
posting matches the staged preview exactly.
- PATCH /api/pending-operations/[id]: accepts vat_amount (null clears);
preserves a staged override across category edits while the treatment
still carries rate-based VAT, drops it when it no longer does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: guard order + agent guidance on vat_amount (PR #717 bots)
- Check treatment compatibility before the 25%-extraction bound so an
oversized override on reverse_charge reports the actual mistake (the
treatment), not the amount. Document why the typeof re-check stays:
commit-time params come from jsonb, so TS types don't hold at runtime.
- vat_amount property description now warns that foreign VAT is never
deductible as ingående moms and that a 0-moms document should use
vat_treatment="exempt" rather than vat_amount=0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): tools/list payload budget + reject vat_amount 0
core-only failed: the verbose vat_amount descriptions pushed the projected
tools/list payload to 36,051 tokens (ceiling 36,000; main is at 35,862).
Per the guard's own guidance, trim descriptions instead of bumping:
now 35,943.
Folds in the Swedish review's round-2 point while trimming: vat_amount 0
is now rejected with a pointer to vat_treatment "exempt". A 0-moms
document is an exempt supply — "exempt" produces the identical expense
booking and the correct income account (3004), so 0 had no use case and
only created a silent momsdeklaration misclassification path. Schema
declares exclusiveMinimum: 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(bookkeeping): use roundOre for vat_amount math (antipattern ratchet)
Second core-only failure: the naive-ore-round ratchet caught the new
Math.round(x*100)/100 lines (662 > baseline 661). Switch the override
path to roundOre from lib/money — including the pre-existing computed-VAT
line this PR touched — and ratchet the baseline down (659, raw-route-auth
168 locked in from main-side fixes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(category-mapping): use leaf BAS accounts instead of group codes
3900, 5800, 6200 are BAS gruppkonton (header codes) and shouldn't carry
postings. Switched the default mappings to the matching leaf accounts:
- income_other: 3900 -> 3999 (Övriga rörelseintäkter)
- expense_travel: 5800 -> 5890 (Övriga resekostnader)
- expense_telecom: 6200 -> 6230 (Datakommunikation)
The fallback for income_other inside getCategoryAccountMapping was also
hardcoded to '3900'; updated to '3999' for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(transactions): split-payment allocator — 1 tx → N invoices
Closes one of the two flows that motivated PR #602's foundation:
allocating a single bank transaction across multiple customer OR
multiple supplier invoices, with one combined verifikat
(samlingsverifikation per BFL 5 kap 6§ st 3).
## Backend (Phase 3a)
- **PL/pgSQL RPC** match_batch_allocate (~400 lines): locks the tx +
each target invoice with SELECT … FOR UPDATE in id order, validates
status/currency/remaining/direction before any write, builds the
combined verifikat via commit_journal_entry (atomically assigns
voucher_number + flips draft→posted), inserts N rows in
invoice_payments or supplier_invoice_payments pointing at the same
JE, advances paid_amount/remaining_amount/status per invoice. Returns
{ ok, journal_entry_id, voucher_number, allocations: [...] } on
success or { ok: false, code, details } on guard failure. Mixed
customer+supplier kinds are rejected (v1 scope).
- **Endpoint** POST /api/transactions/[id]/match-batch — thin wrapper
around the RPC. Validates body via MatchBatchSchema (zod
discriminatedUnion + superRefine to catch mixed-kinds at the schema
layer). On RPC success, emits one invoice.match_confirmed or
supplier_invoice.match_confirmed event per allocation so existing
subscribers (reminders, automations, processing-history) keep
working. Maps the structured RPC error envelope to
errorResponseFromCode.
- **16 new BATCH_* error codes** (sv+en): BATCH_TX_NOT_FOUND,
BATCH_TX_ALREADY_BOOKED, BATCH_OVERSHOOT, BATCH_AMOUNT_EXCEEDS_TX,
BATCH_MIXED_KINDS_UNSUPPORTED, BATCH_DIRECTION_MISMATCH,
BATCH_CURRENCY_MISMATCH, BATCH_PERIOD_LOCKED, BATCH_RPC_FAILED, etc.
## UI (Phase 5a)
- **MatchAllocationDialog** (components/transactions/) — direction-
aware (positive tx → customer invoices, negative → supplier). Search
+ selectable list of open invoices. Per-row amount input with default
= min(invoice.remaining, tx_remaining_budget). Live tally with
green-check balanced state, red overshoot warning, gray leftover
note. Confirm button disabled on overshoot. POSTs to /match-batch
and on 200 triggers the same exit animation as single-tx match.
- **Inbox row** gains a second outline icon button (Split icon) next
to the existing 1:1 match button, gated by the same
showInvoiceMatchButton predicate. Tooltip explains the direction-
aware split. Opens MatchAllocationDialog.
- **i18n** strings under tx_match_allocation namespace in sv.json
and en.json (32 keys each).
## Tests
- tests/pg/match-batch-allocate.pg.test.ts — 5 pg-real tests covering
combined verifikat shape, overshoot guard, already-booked tx,
direction mismatch, mixed-kinds rejection.
- app/api/transactions/[id]/match-batch/__tests__/route.test.ts — 5
unit tests covering schema validation, mixed-kinds, happy path,
structured-error mapping, raw-error → BATCH_RPC_FAILED.
63 unit tests pass across the touched paths. The RPC migration was
already applied to remote in an earlier Phase 3a session (idempotent
CREATE OR REPLACE FUNCTION; the next replay is a no-op).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(match-batch): PR #603 review round 1 + CI fixes
Closes both CI failures and the three real review findings.
## CI fixes
- **pg-real failure**: the RPC declared
`v_journal_entry_id uuid := uuid_generate_v4()` which fails in the CI
Postgres image (uuid-ossp extension is off). Switched to
`gen_random_uuid()` — the codebase standard already used by
supplier_invoices, invoice_inbox, etc.
- **core-only failure**: my earlier BAS leaf-account commit
(3900→3999, 5800→5890, 6200→6230) didn't update the matching
`lib/bookkeeping/__tests__/category-mapping.test.ts` expectations,
and `getDefaultAccountForCategory`'s fallback for `income_*` was
still hardcoded to '3900'. Updated both.
## Review findings (greptile)
- **P1 deadlock-stable locking** (`match_batch_allocate.sql:11`): the
validation `FOR UPDATE` loop ran in caller-supplied array order. Two
concurrent calls with overlapping invoice sets in opposite orders
could deadlock and one would abort with `BATCH_RPC_FAILED`. Now
all three loops (validate, build lines, advance invoices) iterate
via `SELECT … FROM jsonb_array_elements(…) ORDER BY
COALESCE(invoice_id, supplier_invoice_id)`, giving a stable global
lock order regardless of how the caller ordered the JSON array.
- **P1 duplicate-allocation detection** (`match_batch_allocate.sql:163`):
the same invoice_id listed twice would pass the per-row overshoot
guard (both iterations read the original `remaining_amount`) and
the write loop would insert two `invoice_payments` rows for the
same invoice. Added a `v_seen_ids text[]` check in the validation
loop and a new `BATCH_DUPLICATE_ALLOCATION` error code (sv + en).
The dialog already prevents this UI-side via `if (prev[candidate.id]
return prev` — the RPC guard is the defense-in-depth layer.
- **P2 zod `.positive()`** (`schemas.ts:544`): allocation amount was
`nonNegativeAmount` (allowing 0), passing schema validation only to
be rejected by the RPC with `BATCH_INVALID_AMOUNT`. Now
`z.number().positive(…)` so 0-amount entries fail at the schema
layer with a per-field path, cleaner 400.
- **P2 strict `> 0` direction check** (`MatchAllocationDialog.tsx:82`):
used `amount >= 0` to pick customer-side, but a zero-amount tx would
load customer candidates only to hit `BATCH_TX_ZERO_AMOUNT` at
submit time after the user has filled in allocations. Switched to
`> 0` so 0-amount tx never reaches the dialog at all (it's rejected
by the RPC immediately).
The fourth Greptile comment (the schema P2 about amount validation)
overlaps with the third; addressed in the same edit.
## Verification
- 112 unit tests pass across touched paths
- ESLint clean
- New pg-real test `tests/pg/match-batch-allocate.pg.test.ts` covers
the dedupe scenario (same supplier invoice listed twice with summing
amounts that individually pass per-row overshoot)
- RPC patch applied to remote via Supabase MCP
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(match-batch): PR #603 review round 2 — compliance hardening
Addresses the actionable findings from compliance-swarm and
Swedish-accounting-compliance reviews. Six small RPC changes + two
TS-side guards, all bundled in one follow-up migration.
## Security
- **(GDPR Art.5(1)(f) / ISO A.8.2) Caller verification**: SECURITY
DEFINER bypasses RLS, and the prior RPC accepted any
(p_user_id, p_company_id) pair from the route. Now the function
rejects with new `BATCH_UNAUTHORIZED` (sv+en, HTTP 403) if
`auth.uid()` is not a member of `p_company_id`. Pattern lifted from
`harden_invoice_number_rpcs` (#20260510140000).
- **(OWASP V4.2) Allocation cap**: `MatchBatchSchema.allocations` now
carries `.max(100)` to prevent DoS via unbounded FOR UPDATE locks.
## Swedish accounting correctness
- **source_type per direction**: was hardcoded to `'invoice_paid'` for
both customer + supplier batches, mis-routing behandlingshistorik
filters. Customer batches keep `'invoice_paid'`, supplier batches now
write `'supplier_invoice_paid'`.
- **Fiscal-period determinism**: `LIMIT 1` on the period lookup was
non-deterministic on overlap (e.g. corrected broken year). Added
`ORDER BY period_start DESC` so the most recent matching period
wins.
- **Tolerance harmonisation**: cross-allocation sum used `+0.01`
tolerance while per-row used `+0.005`. Both now `+0.005` so a
multi-row batch can't drift ~0.01 SEK while each row passes
individually.
- **`transactions.category` no longer overwritten**: was forced to
`'income_services'` (→ BAS 3001 at 25% VAT) for any customer batch,
misrepresenting reduced-rate / export / EU-service invoices. The
category is only meaningful 1:1 with a single invoice; batches now
leave it as-is, mirroring the supplier-side `ELSE category` branch.
## Tests
- `tests/pg/match-batch-allocate.pg.test.ts` now wraps every RPC call
in `withUserContext(userId)` so `auth.uid()` resolves to the seeded
owner. Without this the new membership check would have failed all
existing tests.
- New pg-real test: `rejects with BATCH_UNAUTHORIZED when caller is
not a member of the company` — outsider user gets explicit refusal.
- New happy-path assertion: `source_type = 'supplier_invoice_paid'`
on the combined verifikat for supplier batches.
15 unit tests pass on the touched paths. RPC patch applied to remote
via Supabase MCP. Out-of-scope mcp-server changes still parked locally.
Skipped findings (documented in PR comment thread):
- V8.2.1 ownership pre-check at route layer (RPC enforces it)
- V4.5 / Art.5(1)(b) narrower API response and event payload —
typed contracts require the full shapes
- V2.4 rate-limiting — system-level, applies to all match endpoints
- A.8.28 client-side RLS reliance — documented architectural choice
- Direction pre-check at API layer (RPC catches with cleaner code)
- V16 + Art.32 + Art.5(1)(b) low-severity logging nits
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add INK2 declaration improvements, invoice delivery date, and Swedish compliance skills
Expand INK2 engine with full INK2S/INK2R support and improved SRU generation.
Add delivery_date field to invoices and corresponding PDF/migration support.
Add Claude skills for Swedish asset accounting, invoice compliance, SIE import/export, SRU filing, and tax planning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review — map BAS 4500–4899, strip CRLF in SRU, document P3
- Map BAS accounts 4500–4599 (legoarbeten), 4700–4899 (diverse
varuinköpskostnader) to SRU 7512 so they are not silently dropped
from INK2R declarations
- Strip \r\n in sanitizeString to prevent CRLF injection in SRU fields
- Document P3 period suffix limitation for brutet räkenskapsår
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct BAS 4500-4599, 4700-4899 mapping from 7512 to 7511
Per the official BAS-to-SRU mapping, these account ranges are cost of
goods (legoarbeten, inkurans, svinn) and belong under 7511 (Råvaror
och förnödenheter), not 7512 (Handelsvaror). 7512 remains 4600-4699.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: Swedish VAT compliance — representation VAT, domestic RC, full BAS 26xx mapping, SIE encoding
- Representation expenses now default to reduced_12 VAT (ML 13 kap 24-25 §§);
income tax deduction was abolished 2017 but VAT deduction at 12% remains
- Domestic reverse charge (byggtjänster etc.) uses 2647 instead of 2645,
with distinct line descriptions for Swedish vs EU/non-EU RC
- VAT declaration maps all BAS 26xx variant accounts (egna uttag 2612/2622/2632,
uthyrning 2613/2623/2633, VMB 2616/2626/2636, import 2615/2625/2635,
domestic RC 2647, frivillig skattskyldighet 2642) and revenue variants
(3108/3105/3004/3100) to correct momsdeklaration rutor
- SIE parser: remove unreliable #FORMAT PC8 encoding detection (most software
exports UTF-8 with PC8 header), parse #FLAGGA for import-already-done warning,
default SIE type to 1 when absent, fix RTRANS/BTRANS documentation
- SIE export: add #RAR -1 (previous fiscal year), fix UB = IB + movements
- Error messages: add pattern matching for locked period trigger errors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Greptile review — update ruta49 JSDoc, use null sentinel in error map
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expand supplier invoice module with overdue cron job, credit note journal
entries, and event emissions on approve/mark-paid/create flows. Add entity
type (EF/AB) awareness to transaction categorization UI and category
mapping logic. Add comprehensive tests for supplier-invoice-entries,
transaction-entries, and expanded API route coverage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add currency-utils module for SEK conversion with exchange rates
- Refactor createJournalEntry to use draft+commit flow preventing voucher number gaps (BFL 5 kap. 7§)
- Add foreign currency support to invoice entries with per-line SEK conversion
- Centralize category-to-account mapping into single source of truth
- Refactor invoice inbox to use shared document analyzer with document type classification (receipt, supplier invoice, government letter)
- Update mapping engine, supplier invoice entries, and transaction entries
- Fix report component rendering issues
- Add new validation schemas and tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add journal entry preview, human-readable account names, auto-apply VAT,
fallback template suggestions, example prompts, invoice match comparison,
and batch result feedback. Also includes user-description-match extension,
describe/batch-describe API routes, improved AI categorization with multi-
suggestion support, and template embedding search.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add validation for non-empty debit/credit accounts before creating journal
entries. Generate fiktiv moms lines (2645/2614) for EU reverse charge expenses.
Change default unmapped expense account from 6900 to 6991. Add tests for
reverse charge handling and exhaustive category mapping coverage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace PSD2 bank integration as the default with file-based bank
import (CSV/XML), which better suits Swedish sole traders and small
companies. Enable Banking is now an opt-in extension.
- Phase 1: Extract generic transaction ingestion service (ingest.ts)
with dedup, auto-categorization, and OCR-based invoice matching
- Phase 2: Bank file parser library supporting Nordea, SEB, Swedbank,
Handelsbanken CSV formats and ISO 20022 camt.053 XML
- Phase 3: Database migration adding import_source, reference columns
and bank_file_imports tracking table
- Phase 4: Import wizard UI (5-step flow) and API routes for parse/execute
- Phase 5: Move Enable Banking to extensions/enable-banking/ with
commented-out loader entry for opt-in activation
- Phase 6: 104 new tests (ingestion + all parser formats), fixing
Nordea detection overlap and camt.053 XML tag collision bugs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove influencer-specific features (campaigns, TikTok, gifts, shadow ledger,
contracts, briefings) and consolidate into a clean ERP foundation with core
bookkeeping, invoicing, receipts, tax reporting, and calendar functionality.
Reorganize database migrations into a clean numbered sequence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>