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>