Commit Graph

11 Commits

Author SHA1 Message Date
Jakob Wennberg 5b8e3fa130 fix(vat): enforce decimal vat_rate on supplier invoice items and normalize MCP percent extraction (#1049)
Supplier invoice items store vat_rate as a decimal fraction (0.25) while
customer invoices use integer percent (25). The shared Zod schema accepted
0-100, so a percent-shaped vat_rate silently booked 2500 % VAT via
line_total * vat_rate, and the MCP inbox-conversion path staged the AI
extraction's percent-integer vatRate straight into the decimal column with
per-line vat_amount 0. Part of #310.

- CreateSupplierInvoiceItemSchema.vat_rate is now a literal union of the
  statutory decimal set (0, 0.06, 0.12, 0.25) with a unit-hint error,
  covering the cookie route, the invoice-inbox convert route, and /api/v1
  (whose runtime ALLOWED_SV_VAT_RATES guard stays as defense in depth).
- New shared normalizeVatRateToDecimal() in lib/vat: percent-shaped values
  (25, 12, 6) divide by 100, results snap to the legal Swedish set, and
  anything else (foreign 19/20, non-finite) maps to 0.
- gnubok_create_supplier_invoice_from_inbox normalizes vatRate at the
  extraction boundary and derives per-line vat_amount when the extraction
  carries none, so the staged header vat_amount is honest.
- The pending-operation executor normalizes staged vat_rate on insert, so
  rows staged before this fix cannot book percent-scaled VAT.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:28:36 +02:00
Jakob Wennberg a95872928e fix(supplier-invoices): warn on class 1/6 accounts for reverse charge lines (#1034)
* fix(supplier-invoices): warn on class 1/6 accounts for reverse charge lines (#863)

Item 2 of #863: when omvand skattskyldighet is on, lines booked on an
account starting with 1 (assets) or 6 draw a non-blocking warning banner
in the Kontering card naming the rows; reverse charge purchases normally
sit on 4xxx/5xxx cost accounts. Advisory only, since class 6 has
legitimate reverse charge uses (e.g. 6540 IT-tjanster for EU cloud
services), so submission is never blocked.

Item 1 (block VAT rates outside the legal set 25/12/6/0) already shipped
in PR #902; this change extracts that check plus the new one into a pure
tested helper, lib/vat/supplier-invoice-line-checks.ts, which is now also
the single source for the legal rate list used by the VAT rate preset
dropdown.

Item 3 (confirming the reason for a 0 % rate) is deferred: it is a UX
design question, not a validation gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(vat): note food-rate transition dates above LEGAL_VAT_RATES

Compliance-bot finding on PR #1034: the allow-list comment now records
that livsmedel moved 12 % to 6 % on 1 April 2026 (Prop. 2025/26:55,
ML 2023:200) and that the reduction is legislated to revert after
31 December 2027, when 6 % stays legal for books/transport but stops
being the food rate. The static list cannot express per-category
temporal validity; revisit at the reversion. Comment-only change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:14:23 +02:00
Jakob Wennberg 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>
2026-07-04 15:58:06 +02:00
Jakob Wennberg f8504f3bd0 fix: audit batch — pagination truncation, MFA/dead-code cleanup, mark-paid fail-closed (#841)
* fix(reports): paginate 8 more report/ledger queries (1000-row truncation)

Raw .select() without fetchAllRows() silently caps at PostgREST's 1000-row
limit, producing wrong statutory output for high-volume companies. Following
#806 (trial-balance/VAT), wrap the remaining offenders in
fetchAllRows + a stable .order('id') + dedupeBy:

- ink2-engine / ne-engine: INK2 & NE-bilaga tax declarations under-counted
- ar-reconciliation (1510/1513), supplier-reconciliation (2440): phantom
  "Ej avstämd" gaps
- full-archive-export: 7-year DR archive (added a unique total order so rows
  are not silently skipped/duplicated across pages)
- avgifter-basis, currency-revaluation, vat-declaration

Adds a regression guard test asserting >1000 ledger lines are summed, not
truncated at 1000.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(api): close extension-dispatcher MFA gap, scope /api/events to API key, sweep dead code

Security/correctness:
- ext/[...path] dispatcher now uses requireAuth() instead of inline
  supabase.auth.getUser(), enforcing MFA (AAL2) on hosted across the whole
  enabled-extension surface (banking sync, document upload/booking, supplier
  invoices, migration). Ratchets antipatterns-baseline raw-route-auth 168->165.
- /api/events now filters by the API key's bound company_id instead of the
  user's active company (was a cross-company read with a scoped key).
- enable-banking OAuth callback calls ensureInitialized() at module load so
  the PSD2 consent audit event (ASVS V16 / GDPR Art.30) isn't dropped on a
  cold-start instance.

Dead-code sweep (all confirmed zero importers):
- delete lib/tax/calculator.ts, lib/salary/engangsskatt.ts (+test),
  lib/email/resend.ts, lib/salary/salary-transaction-matcher.ts,
  lib/webhooks/diff.ts, lib/salary/effective-values.ts,
  lib/bookkeeping/template-prompt.ts
- trim unused lib/vat/eu-countries.ts helpers (keep EU_COUNTRIES)
- remove dead getAutomaticStatus() and the abandoned Activepieces CSP entry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(invoices): fail closed when a payment journal entry doesn't post

Three mark-paid paths (legacy route, v1 API, agent commit) diverged on the
"mark paid but the JE failed" case — two would flip the invoice to paid (or
leave an orphaned posted voucher) with no booking, silently diverging the GL
from the AR/AP sub-ledger. Unify on fail-closed:

- legacy + v1 + agent commitMarkInvoicePaid: never mark paid without a posted
  voucher; on a null/failed JE return INVOICE_PAID_BOOK_FAILED before any
  state mutation (v1 mirrors the match-invoice strict mode).
- agent path: add the .in('status',[...]).select('id') CAS guard and cancel
  the orphaned voucher (cancelOrphanedPaymentEntry) on a lost race or update
  error, matching the web route.
- legacy route: cancel the orphan on a non-race update error too (was only
  handled on the race branch).
- supplier mark-paid: stop swallowing a failed supplier_invoice_payments
  insert — that row drives the reversal amount in payment-sync; roll back the
  status flip and cancel the voucher instead.
- pending-ops orchestrator: error-check the terminal 'committed' write so an
  op stranded in 'committing' (the expire sweep only targets 'pending') is at
  least logged loudly.

Adds a guard test for the legacy fail-closed path. Full unit suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): unblock core build + address compliance-review findings

- avgifter-basis.ts: fix the core-build TypeScript error — PostgREST's
  type-level select parser models the salary_run embed as an array, which
  wasn't assignable to the object-typed generic. Type it `unknown` (rows are
  read via an explicit cast), making it robust across postgrest-js versions.
- /api/events: add a non-null companyId guard before the event_log query
  (defense-in-depth for the API-key-bound scope) — addresses ASVS V8.2.1 /
  ISO A.5.15.
- supplier mark-paid: add a CAS guard (.eq('status', newStatus)) to the
  payment-insert-failure rollback so a concurrent settlement can't be
  clobbered — addresses ASVS V2.3.
- dispatcher: add an AAL2 regression test asserting a non-MFA session is
  rejected (403) and the extension handler never runs — addresses the
  GDPR Art.32 review ask for the single extension chokepoint.

Verified deletions are safe: effective-values.ts was a dead duplicate — the
live AGI/payslip path inlines the same `?? override` coalescing
(generate-declaration.ts), so AGI correctness is unaffected.

next build: exit 0. Full unit suite: 6147 passing. ESLint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:34:23 +02:00
Jakob Wennberg 5bacda4839 fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14 (#796)
* fix(vat): drop personnummer century so enskild firma VAT number is SE+12 not SE+14

Onboarding derived the VAT number as SE${orgNumber}01. For an enskild firma the
org number is a 12-digit personnummer, producing SE + 14 digits, which fails the
^SE\d{12}$ validation — the pre-filled value is re-submitted on save and the tax
settings page becomes unsavable.

New shared helper lib/vat/vat-number.ts (normalize/validate/derive, reusing
normalizeOrgNumber to drop the century + Luhn-validate). UpdateSettingsSchema,
the onboarding wizard, the onboarding upsert in lib/company/actions.ts, and the
arcim-migration provider import all route through it. Backfill migration repairs
existing SE+14 rows to SE+12 (idempotent, scoped to ^SE\d{14}$ only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(arcim): warn when a provider VAT number is dropped as malformed

The provider VAT guard silently discarded a value that doesn't normalise to a
valid SE+12 momsregistreringsnummer. Emit a structured warn (provider +
company, no raw value — it can embed a personnummer) so consistently-bad
provider data is observable rather than invisible. Addresses the OWASP V16
logging finding on the arcim VAT-normalisation change in this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:29:58 +02:00
Mattsson da39eb2d43 Bug/momsdek overflow (#465)
* feat(settings): add option for company name position in invoice PDF

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* fix(ui): improve accessibility for company name position toggle in PDF settings

* fix(bookkeeping): align BAS 2026 reference data with official PDF

Reconciled lib/bookkeeping/bas-data/ against the BAS 2026 v1.1 official
chart (1286 accounts). All real discrepancies fixed:

- 2089 Fond för utvecklingsutgifter: k2_excluded → true
- 8417 Räntekostnader för dold räntekompensation: k2_excluded → true
- 1250, 1260 renamed to "(Fritt konto för Inventarier, verktyg och
  installationer)" — BAS 2026 freed these slots
- Periodiseringsfond 2120-2139: added year suffixes (2120 = "...2020"
  etc.) and added 8 missing accounts (2121-2127, 2129) for years
  2019, 2021-2027. Dropped phantom 2022/2024 prior-parser garbage.
- 4075-4078: EUland → EU-land
- 8411: förlagsoch → förlags- och

Verified: 1282 of 1286 PDF accounts match exactly after edits (remaining
4 are PDF-parser artifacts, not real data). Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(migrations): backfill BAS 2026 account labels in chart_of_accounts

Companion to the TS reference fix. Updates existing companies' rows where
they still carry seed-data typos or generic names that don't match BAS 2026:

- 4075-4078: EUland → EU-land (hyphenation)
- 8411: förlagsoch → förlags- och (hyphenation)
- 2120, 2130-2137, 2139: rename "Periodiseringsfond" (generic, no year) to
  the BAS 2026 canonical name with year suffix

Defensive: every WHERE clause matches an EXACT current value. Rows that
have been manually renamed by users — including those with a wrong year
that may reference legacy fonds from an earlier BAS numbering cycle — are
left untouched. No row is deleted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(migrations): fix wrong-year labels on Periodiseringsfond accounts

Follow-up to 20260513140000. The first backfill only renamed accounts whose
name was the generic "Periodiseringsfond" (no year). Many customers were
seeded from an older BAS numbering cycle where 2126 = "2016", 2127 = "2017",
etc. — BAS 2026 reuses those account numbers for years 2026/2027.

This migration aligns the year tag with the BAS 2026 meaning of each
account number across 2120-2127, 2129, 2130-2137, 2139. Only rows whose
name still starts with "Periodiseringsfond" are touched — customers who
renamed the account to something custom keep their name.

Verified on staging: all 18 accounts now carry BAS 2026 canonical names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(data): update account names and descriptions for clarity and consistency

* fix(migrations): backfill account names for BAS 2026 freed accounts and refine Periodiseringsfond name matching

* feat: enhance VAT handling with reverse charge logic and supplier type support

* feat: implement VAT declaration validation rules and enhance moms box mapping

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:17:32 +02:00
Jakob Wennberg 6ccd4f429c fix: Swedish VAT compliance — representation, domestic RC, full 26xx mapping, SIE (#206)
* 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>
2026-04-09 14:13:15 +02:00
Jakob Wennberg 66a4027f1e feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup
- Update BAS account catalog with comprehensive SRU codes and K2 flags
- Add currency revaluation service with tests and API route
- Add expenses page and account deletion API
- Enhance booking templates with new patterns and improved tests
- Improve transaction categorization with template picker and description matching
- Polish dashboard, onboarding, import, and transaction UIs
- Refactor year-end service for multi-step closing
- Move SRU generator to ne-bilaga, remove standalone SRU export
- Remove unused dev docs, mock data, and extension hooks
- Add invoice delivery note sequences migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:19:56 +01:00
Jakob Wennberg 03b569d708 refactor: consolidate extension system to general-only with manifest-driven architecture
- Remove all sector-specific extensions (construction, ecommerce, export,
  hotel, restaurant, tech) — only general-purpose extensions remain
- Move NE-bilaga and SRU export from extensions to core reports (lib/reports/)
- Move moms-box-mapping from extensions/export/shared to lib/vat/
- Replace per-extension API routes with catch-all dispatcher
  (app/api/extensions/ext/[...path]/route.ts)
- Add manifest.json for each extension with metadata, env vars, and deps
- Add api-routes.ts pattern for extension-defined API endpoints
- Add code generation scripts (generate-extension-registry, create-extension)
- Add extensions.config.json for opt-in extension loading
- Add extensions.schema.json for config validation
- Add email service interface with noop default (lib/email/service.ts)
- Add CI workflow (core-build.yml) to verify core builds with zero extensions
- Add migration 045: expand account_type CHECK for untaxed_reserves
- Update CLAUDE.md with comprehensive extension system documentation
- Update all report engines and bookkeeping services for new imports
- Clean up extensions.schema.json to only list existing extensions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:32:56 +01:00
Jakob Wennberg 6956a757f3 feat: consolidate booking templates from 100 to 48 and improve codebase
Reduce booking template library to eliminate duplicate suggestions when
users describe transactions. Templates with identical accounting treatment
(same account + VAT) are merged, keywords consolidated, and the entire
subscriptions group is eliminated. Also includes prior work on reports,
extensions, and transaction improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:21:10 +01:00
Emil e0b66fe397 Fixed extensions bugs 2026-02-24 16:06:36 +01:00