Files
accounted/findings.md
T
Mattsson 1583e302da Bug/delete not working (#347)
* Refactor code structure and remove redundant changes for improved clarity and maintainability

* feat: add booking template usage tracking and related policies

* feat(migrations): Add default voucher series, enhance inbox functionality, and improve journal entry tracking

- Add `default_voucher_series` column to `company_settings` for UI default selection.
- Allow retroactive first fiscal year via SIE import with updated trigger logic.
- Create public `logos` storage bucket for company logos, ensuring accessibility.
- Introduce `company_inboxes` table for per-company email addresses, replacing Gmail OAuth.
- Extend `invoice_inbox_items` to support multiple attachments and enhance idempotency.
- Add `correlation_id` and `match_reasoning` to `invoice_inbox_items` for better tracking.
- Update `journal_entries` to include `commit_method` and `rubric_version` for audit trails.
- Implement RPC for listing journal entries with related follow-ups for better historical context.
- Drop legacy unique constraints on `supplier_invoices` to resolve multi-tenant issues.
- Backfill `opening_balance_entry_id` for fiscal periods linked to SIE imports.
- Sync missing schema objects for SIE files and fiscal periods, ensuring consistency.
- Add immutability trigger to `processing_history` to prevent deletions.
- Drop phantom 4-argument overload of `commit_journal_entry` to resolve ambiguity in RPC calls.

* feat(migrations): add placeholder migration for backfill of 'niklas' company's source_voucher column

* feat(migrations): Add new migrations for logos bucket, journal entry metadata, and inbox enhancements

- Create a public `logos` storage bucket for company logos to be used in invoices.
- Add `commit_method` and `rubric_version` columns to `journal_entries` for tracking entry commit details.
- Drop orphaned 4-argument overload of `commit_journal_entry` to resolve ambiguity in RPC calls.
- Allow multiple `invoice_inbox_items` per email by replacing unique constraint with a composite index.
- Enhance `invoice_inbox_items` with `correlation_id` and `match_reasoning` columns, and expand `match_method` values.
- Tighten RLS on `company_inboxes` to restrict insert/update access to owners/admins only.
- Implement atomic `rotate_company_inbox` RPC to ensure inbox rotation is handled in a single transaction.
- Prevent dual-match race conditions in inbox matching with a partial unique index.
- Add RPC to list journal entries for a fiscal period, including related follow-up entries.
- Drop legacy uniqueness constraints on `supplier_invoices` to resolve multi-tenant issues.
- Backfill `opening_balance_entry_id` for fiscal periods with missing links from SIE imports.
- Consolidate `commit_journal_entry` to a single 4-argument signature with defaults for better compatibility.
- Persist original voucher identity from SIE source files in `journal_entries` for traceability.
- Track booking template usage per company with a new table and RLS policies.
- Add `updated_at` column to `booking_template_usage` for audit consistency.
- Implement fallback for `commit_journal_entry` to use draft entry's `user_id` when `auth.uid()` is NULL.
- Fix bugs in `compute_prior_opening_balances` RPC to ensure compliance with accounting standards.

* Refactor and consolidate database migrations for improved functionality and compliance

- Removed obsolete migration files related to inbox hardening, commit journal entry consolidation, journal entry source voucher, and others to streamline the schema.
- Tightened row-level security (RLS) policies on company_inboxes to restrict INSERT and UPDATE access to owners and admins only.
- Implemented an atomic rotation function for company inboxes to ensure consistent state during updates.
- Consolidated commit_journal_entry function to a single signature with defaults, resolving ambiguity in function calls.
- Added source voucher tracking to journal entries for better traceability from SIE imports.
- Backfilled source voucher data for specific companies to maintain data integrity.
- Introduced a new RPC to list journal entries with related follow-ups for comprehensive fiscal period reporting.
- Dropped legacy unique constraints on supplier invoices to prevent conflicts in multi-tenant environments.
- Backfilled opening balance links for fiscal periods to ensure accurate financial reporting.
- Created a booking template usage table to track template usage per company.
- Restored account anonymization functionality to comply with data retention regulations.
- Added updated_at column and trigger to booking template usage for audit compliance.
2026-04-22 15:33:04 +02:00

114 KiB
Raw Blame History

Audit findings (non-critical)

Generated by /swarm audit on 2026-04-22 (run 20260422-125911).

Total non-critical findings: 324 (~113 high, ~141 medium, ~73 low)

The 35 critical findings from this audit were filed as GitHub issues on erp-mafia/gnubok. The full report including criticals is at .swarm/20260422-125911/findings.md (gitignored).

Per-agent non-critical breakdown:

Agent High Medium Low Total
VAT 6 4 1 11
Invoice-compliance 7 5 2 14
Payroll 3 7 0 10
SIE 3 3 8 14
SRU 9 8 2 19
Year-end 7 6 7 20
Asset-accounting 4 1 1 6
Financial-reporting 4 5 2 11
Tax-planning 5 5 1 11
Project-accounting 3 4 2 9
Bookkeeping-engine 4 4 1 9
Provider-connections 8 5 1 14
Security 0 4 8 12
RLS-multitenancy 3 4 1 8
Auth-MFA 3 8 4 15
Error-handling 5 6 5 16
Event-bus 3 5 1 9
Document-retention 4 6 2 12
Rate-limits 3 6 3 12
UI-UX 7 5 6 18
A11y 4 9 5 18
Mobile-UX 3 5 1 9
Logging 4 10 6 20
Testing 6 6 3 15
Performance 4 6 2 12

Domain agents

VAT agent

V1. getMomsRutaDescription returns wrong ruta 05/06/07 labels [high]

  • File: lib/invoices/vat-rules.ts:191
  • Claims 05/06/07 are "Utgående moms 25%/12%/6%"; per SKV 4700 those are actually rutor 10/11/12. Actual labels are 05 = Momspliktig försäljning ej i annan ruta, 06 = Momspliktiga uttag, 07 = Vinstmarginalbeskattning. Canonical version exists in moms-box-mapping.ts.
  • Fix: Delete function or correct labels; migrate call sites to getBoxLabel() from moms-box-mapping.ts.

V2. 300 SEK representation VAT cap mentioned in comments but never enforced [high]

  • File: lib/bookkeeping/category-mapping.ts:139
  • Comment says // Representation defaults to reduced_12 (max 300 SEK/person) but no code computes cap. Users over-deduct VAT and face 20% skattetillägg on reassessment.
  • Fix: Add participant_count field; compute min(actualVat, headcount × 36 SEK) for food, or block the input-VAT booking until headcount confirmed.

V3. VIES error messages are English [high]

  • File: lib/vat/vies-client.ts:126
  • 'VAT validation service unavailable. Please try again later.' propagates to Swedish customer-creation flow.
  • Fix: Return Swedish, e.g. 'Momsnummerskontrollen (VIES) är tillfälligt otillgänglig. Försök igen om en stund.'

V4. Reverse-charge revenue hardcoded to services account 3308/3305 [high]

  • File: lib/bookkeeping/invoice-entries.ts:529
  • Intra-EU goods sale classifies as service → Ruta 39 instead of 35. Per ML 5 kap 22-25§ distinction is material; mismatches produce SFL 52 kap 10§ penalties.
  • Fix: Add sale_type: 'goods' | 'services'; branch 3108/3308 for RC, 3105/3305 for export.

V5. Foreign-currency invoices use today's FX rate, not invoice-date rate [high]

  • File: app/api/invoices/route.ts:159
  • Per ML 7 kap 7§ and EU VAT Directive Art. 91, conversion must use rate on invoice date. Backdated invoices book wrong SEK VAT. Same bug in app/api/pending-operations/[id]/commit/route.ts:328.
  • Fix: Pass invoice date: fetchExchangeRate(invoiceInput.currency, new Date(invoiceInput.invoice_date)).

V6. No jämkning / mixed verksamhet / frivillig skattskyldighet workflow [high]

  • File: lib/vat/ (missing module)
  • (a) No capital-goods jämkning tracking per ML 15 kap, (b) no avdragsandel for blandad verksamhet per ML 13 kap 29§, (c) no frivillig skattskyldighet för lokalhyra per ML 12 kap. Accounts exist but no logic populates them.
  • Fix: Three tracking tickets. Minimum viable: capital_goods table + avdragsandel on company_settings + frivillig flag per fastighet.

V7. Ruta 50 (import VAT base) always 0 [medium]

  • File: lib/reports/vat-declaration.ts:201
  • Skatteverket e-deklaration cross-validates Ruta 60 ≈ Ruta 50 × 25%; empty Ruta 50 with non-zero Ruta 60 generates validation error on upload.
  • Fix: Add import_tax_base field; populate from 4545/4546/4547 when 2615/2625/2635 booked.

V8. generateReverseChargeLines defaults to 25% when rate omitted [medium]

  • File: lib/bookkeeping/vat-entries.ts:88
  • Callers in booking-templates.ts:1567, mapping-engine.ts:225, counterparty-templates.ts:251 omit rate. Books 25% fiktiv moms to 2614 for 6%/12% EU-RC purchases.
  • Fix: Make vatRate required; throw on unsupported rate instead of falling through to 2614.

V9. Per-item vat_rate schema accepts any number 0-100 [medium]

  • File: lib/api/schemas.ts:153
  • Schema allows 18 which is rejected later with misleading message.
  • Fix: z.union([z.literal(0), z.literal(6), z.literal(12), z.literal(25)]) with Swedish error.

V10. Reverse-charge path doesn't distinguish goods vs services (ruta 21/22 vs 20/23) [medium]

  • File: lib/reports/vat-declaration.ts:373
  • Code comment admits ruta20/23 (goods) → 0 for now; EU goods acquisitions entirely missing.
  • Fix: Add reverse_charge_type; branch ruta assignment per goods/services.

V11. calculateVat has no unit guard and non-canonical rounding [low]

  • File: lib/invoices/vat-rules.ts:121
  • No JSDoc; Math.round(x) / 100 unusual vs canonical Math.round(x * 100) / 100.
  • Fix: Add JSDoc + range assertion; rewrite using canonical pattern.

Invoice-compliance agent

IC1. Credit note numbering risks collisions; not a separate series [high]

  • File: app/api/invoices/route.ts:309
  • KR- prefix concatenation; per ML 17 kap 24§ p.2 numbers must be unique and from dedicated sequential series. Also KR-KR-... possible on correction-of-correction.
  • Fix: Add next_credit_note_number + prefix to company_settings; new RPC generate_credit_note_number with atomic UPDATE...RETURNING.

IC2. Foreign-currency invoice persisted without SEK VAT when Riksbanken returns null [high]

  • File: app/api/invoices/route.ts:158-167
  • ML 17 kap 29§ requires SEK VAT amount on invoice charging Swedish VAT; currently persisted as null silently.
  • Fix: 503 Swedish error or force-populate with fallback rate + manual review flag.

IC3. Exchange rate fetched for creation date, not invoice date [high]

  • File: app/api/invoices/route.ts:159
  • Per ML 8 kap 21-23§ tax point date must be used. Same fix needed in app/api/invoices/[id]/convert/route.ts.
  • Fix: Pass new Date(invoiceInput.invoice_date) to fetchExchangeRate(); persist exchange_rate_date.

IC4. Mandatory seller/buyer address (ML 17:24 p.5) not enforced [high]

  • File: lib/invoices/pdf-template.tsx:324-332
  • No validation; new users with incomplete settings can send invoices with only company name. Zod schema doesn't check addresses.
  • Fix: Validate address presence on seller + buyer at POST and /send.

IC5. Buyer VAT number not verified on reverse-charge invoices [high]

  • File: app/api/invoices/route.ts:120-122
  • Only validated flag checked; not vat_number non-empty. PDF silently omits when empty.
  • Fix: Assert customer.vat_number && /^[A-Z]{2}\d{2,}/.test(...) when treatment is reverse_charge.

IC6. No domestic reverse-charge support (byggtjänster/electronics) [high]

  • File: lib/invoices/vat-rules.ts:72-116
  • No path for ML 16 kap 13§ byggtjänster or 17§ electronics; can't issue compliant construction invoices.
  • Fix: Add domestic_reverse_charge treatment with sub-types; wire to account 3231 + specific text + ruta 41.

IC7. No ROT/RUT fakturamodellen support [high]

  • File: types/index.ts + engine (feature gap)
  • Per HUSFL 2009:194 6-9§§: no flag, no fastighetsbeteckning, no personnummer, no 1513/3740 split, no consumption cap tracking.
  • Fix: Add fields for personnummer/fastighet/labor-cost/deduction; split AR to SKV portion on 1513 + revenue reduction on 3740; print mandatory split on PDF.

IC8. No Peppol BIS 3.0 / e-invoicing support [medium]

  • File: (feature gap)
  • B2G mandatory since 2019 (Lag 2018:1277); no UBL 2.1 generation, no Peppol endpoint, no sending.
  • Fix: Plan Peppol extension producing UBL 2.1 BIS 3.0 XML; send via access point (Pagero/Qvalia/etc.).

IC9. No självfakturering (self-billing) support [medium]

  • File: lib/invoices/pdf-template.tsx:264-270
  • InvoiceDocumentType lacks self_billing; no notation rendering.
  • Fix: Add to type; render mandatory notation on PDF; store agreement reference.

IC10. OCR reference collides between credit note and original [medium]

  • File: lib/bankgiro/luhn.ts:65-70
  • Invoice 2026001 and its credit note KR-2026001 produce identical OCR → bank reconciliation confusion.
  • Fix: Inject prefix digit (e.g. 9) before numeric payload on credit notes.

IC11. Credit note items default vat_rate to 0 instead of original rate [medium]

  • File: app/api/invoices/route.ts:352-362
  • vat_rate: item.vat_rate ?? 0 drops legacy items to 0% VAT; PDF mis-renders.
  • Fix: Fall back to originalInvoice.vat_rate ?? 25; back-fill data migration.

IC12. invoice_items.vat_rate/vat_amount columns missing from migrations [medium]

  • File: supabase/migrations/20240101000001_core_schema.sql:246-256
  • Columns added out-of-band to prod; fresh staging breaks mixed-rate invoicing.
  • Fix: Author migration ALTER TABLE invoice_items ADD COLUMN IF NOT EXISTS vat_rate numeric, vat_amount numeric DEFAULT 0; NOTIFY pgrst, 'reload schema'; + data back-fill.

IC13. Public invoice-action reminder token has no expiry, leaks via GET URL [low]

  • File: supabase/migrations/20240101000005_invoice_reminders.sql:16-17
  • 256-bit entropy but no expires_at; GET query logged in Vercel / browser history / Referer headers.
  • Fix: Add expires_at timestamptz DEFAULT sent_at + interval '30 days'; move to POST with token in body.

IC14. Invoice reminder cron uses console.log [low]

  • File: app/api/invoices/reminders/cron/route.ts:12
  • Inconsistent with reminder-processor.ts using createLogger.
  • Fix: Use createLogger('invoice-reminders-cron').

Payroll agent

P1. Karensavdrag per-year cap (10) not enforced [high]

  • File: lib/salary/absence-calculator.ts:63-79
  • config.maxKarensavdragPerYear = 10 defined but never referenced. Per Sjuklönelagen §7a the 11th karens must be refunded.
  • Fix: Look up prior karensavdrag count in last 12 months; pass skipKarens when ≥10.

P2. Jämkning silently dropped when validTo is missing [high]

  • File: lib/salary/calculation-engine.ts:271
  • isJamkningValid returns false if validTo null; Skatteverket jämkning commonly has open-ended end dates.
  • Fix: Treat missing validTo as indefinite (default 9999-12-31); require dates when jamkning_percentage set.

P3. No Skatteverket F-skatt verification flow [high]

  • File: app/(dashboard)/salary/employees/[id]/page.tsx:306-310
  • f_skatt_status free dropdown; no Företagsuppgifter API lookup; employer strictly liable.
  • Fix: Add "Verifiera F-skatt mot Skatteverket" action via skatteverket extension; block f_skatt on stale/missing verification.

P4. Vacation-day range restricted to 25-40 in schema [medium]

  • File: lib/api/schemas.ts:659
  • Hourly workers and short-term contracts need <25 days.
  • Fix: Allow 0 with salary_type === 'hourly' bypass.

P5. sick_day15_plus account mapping implies employer bears cost [medium]

  • File: lib/salary/account-mapping.ts:28
  • Maps day-15+ to 7281; Försäkringskassan pays employee directly; distorts FK499 TotalSjuklonekostnad.
  • Fix: Map to 7210 as unpaid absence, OR split into sick_day15_plus_employer (CBA top-up → 7281) vs sick_day15_plus_fk (pure reduction → 7210).

P6. Net deduction line items mapped to 7210 instead of liability accounts [medium]

  • File: lib/salary/account-mapping.ts:38-41
  • Union/advance/other net deductions all → 7210 (wrong); should credit liability/receivable (2890/1680/2690).
  • Fix: Map each net-deduction type to conceptually-correct credit account; couples with the balance fix (P8 area).

P7. Engångsskatt brackets are approximations, not Skatteverket-published rates [medium]

  • File: lib/salary/engangsskatt.ts:25-37
  • Self-documented "simplified brackets"; state tax kicks in at 660,400 (2026), not simplified 500,000.
  • Fix: Fetch from Skatteverkets open-data API keyed on (year, kommun, annual_income); brackets as fallback.

P8. No tests for salary-entries.ts (505 lines, 0 tests) [medium]

  • File: lib/salary/salary-entries.ts (production booking code)
  • Critical net-deduction and löneväxling bugs (ticketed) would have been caught.
  • Fix: Cover single-employee basic run, net-deduction balance, löneväxling booking, mixed employment types, ensureSalaryAccountsExist.

P9. AGI FK499 TotalSjuklonekostnad may include wrong amount [medium]

  • File: app/api/salary/runs/[id]/agi/xml/route.ts:146-154
  • Uses Math.abs(li.amount); distinction between sjuklonAmount / totalDeduction / karensavdrag ambiguous.
  • Fix: Store sjuklon_paid_amount as separate column; document sign convention.

P10. Phase 4 work gaps (F-skatt verification UI, tax-table refresh, KU10 delivery) [medium]

  • File: Project-wide
  • Phase 4 plan partially implemented; verification UI and annual refresh missing.
  • Fix: File tickets for remaining Phase 4 gaps.

SIE agent

S1. saveMappings called with user.id instead of companyId [high]

  • File: app/api/import/sie/mappings/route.ts:66
  • saveMappings(supabase, user.id, mappings) should be companyId. Mappings saved under user UUID in company_id column; won't be readable via GET/PUT/DELETE endpoints in same file.
  • Fix: Change to saveMappings(supabase, companyId, mappings); add round-trip test.

S2. #TRANS dimension/object-list data silently discarded on import [high]

  • File: lib/import/sie-parser.ts:620-627
  • Object list skipped; no #DIM/#OBJEKT handlers; project/kostnadsställe tags lost during Fortnox/Visma/BL migration.
  • Fix: Add #DIM + #OBJEKT handlers; parse object list into SIETransactionLine.objects; map dim=1→cost_center, dim=6→project on journal_entry_lines.
  • 🔁 Possibly tracked by #243 Projektbaserad

S3. Export declares #RAR -1 but emits no balances for year -1 [high]

  • File: lib/reports/sie-export.ts:98-100
  • Writes #RAR -1 but only emits #IB 0 / #UB 0 / #RES 0. Importers may surface continuity warnings or reject.
  • Fix: Compute and emit #UB -1 / #RES -1, OR omit #RAR -1 when no year -1 balances.

S4. #TRANS allows missing object list, losing strict-spec detection [medium]

  • File: lib/import/sie-parser.ts:625-635
  • Object list {} mandatory per spec; silent skip masks source corruption.
  • Fix: Emit warning when missing; treat as error in strict mode.

S5. #KSUMMA neither generated on export nor validated on import [medium]

  • File: lib/reports/sie-export.ts:217, lib/import/sie-parser.ts:662
  • Tamper detection forfeited; CRC-32 never computed.
  • Fix: Validate on import when present and CP437; optionally emit on export.

S6. Export declares #FORMAT PC8 but emits UTF-8 [medium]

  • File: lib/reports/sie-export.ts:82
  • Strict receivers expecting CP437 will mojibake.
  • Fix: Emit actual CP437-encoded bytes (Uint8Array response).

S7. #KTYP parsed but ignored during import [low]

  • File: lib/import/sie-parser.ts:498-507
  • Account type hint dropped; edge cases like 1680 (used as liability) lose type information.
  • Fix: Use accountType from #KTYP when creating accounts.

S8. No #ADRESS, #VALUTA SEK, or #KPTYP emitted on export [low]

  • File: lib/reports/sie-export.ts:81-100
  • Compulsory (#VALUTA) / recommended fields missing.
  • Fix: Add #VALUTA SEK, #KPTYP BAS2014, #ADRESS from company_settings.

S9. #FLAGGA 1 never written on archived import (no anti-duplication) [low]

  • File: lib/import/sie-import.ts:1360-1375
  • Archive leaves #FLAGGA 0 → usable elsewhere as if never imported.
  • Fix: Rewrite first #FLAGGA 0 to 1 in content before storage upload.

S10. Tab-prefixed #TRANS on export is non-standard [low]

  • File: lib/reports/sie-export.ts:186
  • Some strict parsers reject indented record labels.
  • Fix: Drop leading tab for maximum interop.

S11. Voucher balance check uses 0.01 SEK tolerance [low]

  • File: lib/import/sie-parser.ts:378-386
  • 0.01 is smallest non-zero; unbalanced-by-1-öre sources slide through silently.
  • Fix: Tighten check to >0.005 or emit warning.

S12. Unknown-tag info spam for common tags [low]

  • File: lib/import/sie-parser.ts:660-664
  • Info-level noise ("Okänd tagg: #DIMENSION") on standard SIE4 files.
  • Fix: Extend ignore list; add #DIM/#OBJEKT handlers (also fixes S2).

S13. No detection of SIE4I (subsystem imports with empty series/verno) [low]

  • File: lib/import/sie-parser.ts:337-345
  • checkDuplicatePeriodImport wrongly blocks additive 4I imports.
  • Fix: Detect by empty #VER series or absence of #KONTO; skip period-duplicate check.

S14. Import rejects #FLAGGA 1 files with warning, but doesn't block [low]

  • File: lib/import/sie-parser.ts:728-731
  • Accepts already-imported file as warning only.
  • Fix: Surface #FLAGGA 1 as validation error requiring explicit user override flag.

SRU agent

SR1. NE-bilaga amount formatter uses Math.round — violates SFL 22:1 [high]

  • File: lib/reports/ne-bilaga/sru-generator.ts:166-168
  • SFL 22 kap. 1 § mandates öre truncated toward zero (INK2 generator correctly uses Math.trunc).
  • Fix: Replace with Math.trunc(amount).toString().

SR2. NE-bilaga field code 7000 for fiscal year range is malformed [high]

  • File: lib/reports/ne-bilaga/sru-generator.ts:77-81
  • Emits #UPPGIFT 7000 20250101-20251231 (single hyphen-joined range); spec requires separate 7011/7012 lines. 7000 not a valid SRU field.
  • Fix: Emit #UPPGIFT 7011 <startYYYYMMDD> and #UPPGIFT 7012 <endYYYYMMDD> separately. Remove formatSRUDateRange.

SR3. NE-bilaga BLANKETT type has no year/period suffix [high]

  • File: lib/reports/ne-bilaga/sru-generator.ts:65
  • Emits #BLANKETT NE; spec requires #BLANKETT NE-<year><Px> (e.g. NE-2024P4). SKV will reject as invalid blankett type.
  • Fix: Derive incomeYear + Px; concatenate.

SR4. NE-bilaga #IDENTITET missing timestamp, wrong format [high]

  • File: lib/reports/ne-bilaga/sru-generator.ts:67-75
  • Spec requires #IDENTITET <OrgNr> <YYYYMMDD> <HHMMSS>; org number not guaranteed 12-digit. NE-bilaga is typically filed by a fysisk person — format should be YYYYMMDDNNNK (personnummer without hyphen), not century-prefixed.
  • Fix: Emit proper format with date + time; don't prepend century 16 for EF; add formatPersonOrPJuridNumber(orgNumber, entityType) helper.

SR5. NE-bilaga block is missing #NAMN and #FIL_SLUT [high]

  • File: lib/reports/ne-bilaga/sru-generator.ts:50-116
  • No #NAMN <taxpayer name> inside blankett block; no #FIL_SLUT terminator. Missing #FIL_SLUT is on SKV's Level 1 rejection list.
  • Fix: Add #NAMN after #IDENTITET; append #FIL_SLUT as last line.

SR6. SIE export writes wrong SRU codes in #SRU records [high]

  • File: lib/reports/sie-export.ts:126-128
  • #SRU <account> <sru_code> uses seeded codes directly; inherits the obsolete/wrong codes from migration.
  • Fix: Fix seeded codes (critical, already ticketed) OR compute correct INK2R code at export from INK2R_ACCOUNT_MAPPINGS.

SR7. computeSRUCode() uses legacy/incorrect ranges [high]

  • File: lib/bookkeeping/bas-data/sru-mapping.ts:17-107
  • Returns legacy codes (7201/7202/7203/7210-7212/7220-7231/7310-7380) that don't match current INK2R räkenskapsschema.
  • Fix: Rebuild against current INK2R table; for juridiska personer use INK2R_ACCOUNT_MAPPINGS; verify NE codes for enskilda firmor against current spec.

SR8. INK2R mapping covers BAS 4500-4599 and 4700-4899 but self-describes as "no standard SRU mapping" [medium]

  • File: lib/reports/ink2/ink2-engine.ts:464-471,819-822
  • Code commits to 7511 while comment claims the opposite. BAS recommends 7513 for sidokostnader.
  • Fix: Split into 7513 if sidoposter; document choice explicitly; warn in INK2 view on non-zero balances.

SR9. BAS 2300-2319 mapped to 7350 (Obligationslån) — over-broad [medium]

  • File: lib/reports/ink2/ink2-engine.ts:317-319
  • 7350 should only cover 2320-2329; 2300-2319 are kreditinstitutskulder → 7352.
  • Fix: Move range to new 7352 entry alongside existing 2340-2359.

SR10. BAS 2100-2109 folded into 7321 (Periodiseringsfonder) — may overlap säkerhetsreserv [medium]

  • File: lib/reports/ink2/ink2-engine.ts:268-271
  • 2100-2109 can be custom obeskattad reserv (säkerhetsreserv i försäkringsbolag); spec maps such reserves to 7323.
  • Fix: Remove 2100-2109 from 7321; fold into existing 7323 entry (with 2130-2149 and 2160-2199).

SR11. INK2 7651 emitted even when tax booked (ej avdragsgill) [medium]

  • File: lib/reports/ink2/ink2-engine.ts:883-902
  • Auto-derivation assumes 100% of booked tax flows into 7651; ignores periodiseringsfond/koncernbidrag/representation/ränteavdrag.
  • Fix: Expose editable INK2S form with fields 7650-7770 OR surface blocking warning listing unhandled adjustments before SRU download.
  • 🔁 Possibly tracked by #193 INKS2 balanserar inte

SR12. INK2 7113/7114 derived from bookkeeping result + tax — missing INK2S round-trip [medium]

  • File: lib/reports/ink2/ink2-engine.ts:883-891
  • Per spec 7113/7114 must equal INK2S 8020/8021; computed independently; future divergence risk.
  • Fix: Compute taxableResult once in INK2S; derive 7113/7114 FROM ink2s['8020']/['8021']. Test asserting equality.

SR13. computePeriodSuffix cannot produce P3 [medium]

  • File: lib/reports/ink2/sru-generator.ts:35-43
  • Short first fiscal year silently falls back to P4; rejected with "Invalid blankett type string".
  • Fix: Track period_type / is_short_year in fiscal_periods; select P3 when under 12 months. Or expose picker in UI.

SR14. No validation of company postal_code / org_number before SRU generation [medium]

  • File: lib/reports/ink2/sru-generator.ts:99-120
  • Silently writes '000000000000' or '00000'; SKV rejects Level 2 with no UI warning.
  • Fix: Validate at generation time; return 400 Swedish: "Organisationsnummer saknas — fyll i under Inställningar > Företag".

SR15. No parsing/handling of Skatteverket validation response [medium]

  • File: app/api/reports/ink2/route.ts; entire repo
  • User downloads ZIP, manually uploads; no mottagningskvittens parsing, no submission-id → period-id status store.
  • Fix: Future Skatteverket extension to POST and parse receipt.
  • 🔁 Possibly tracked by #107 Skatteverket API

SR16. INK2 UI and engine silently ignore failure to map account 8999 [low]

  • File: lib/reports/ink2/ink2-engine.ts:774-775
  • Only literal string '8999' check; class 9 silently dropped.
  • Fix: Configurable skip list; unconditional warn on unmapped accounts.

SR17. Amount truncation applied twice (engine + sru-generator) [low]

  • File: lib/reports/ink2/ink2-engine.ts:632-636; sru-generator.ts:89-92
  • No-op for integers; unclear for reviewer; future float input could hide rounding bug.
  • Fix: Remove Math.trunc from formatAmount; assert integer input or centralize truncation.

SR18. No handling of N9 ränteavdragsbegränsningar anywhere [low]

  • File: entire repo (no matches)
  • Only matters for companies with net interest costs >5 MSEK.
  • Fix: Document as out of scope; refer affected users to accountant.

SR19. Hard-coded "Okänd" postort placeholder [low]

  • File: lib/reports/ink2/sru-generator.ts:120
  • Silently masks data-quality issue.
  • Fix: Reject SRU generation when any mandatory INFO.SRU post missing; Swedish user-facing error. Don't substitute placeholders.

Year-end agent

YE1. Year-end closing entry bypasses account 8999 (Årets resultat) [high]

  • File: lib/core/bookkeeping/year-end-service.ts:299-360
  • Zeros class 3-8 directly to 2099 (AB) or 2010 (EF); standard Swedish practice nets P&L to 8999 first, then 8999 → 2099/2019. Inconsistent with SIE4 export conventions.
  • Fix: Build two-leg close: (a) zero class 3-8 against 8999; (b) move 8999 balance to 2099 (AB) or 2019 (EF).

YE2. Enskild firma year-end credits 2010 directly instead of 2019 [high]

  • File: lib/core/bookkeeping/year-end-service.ts:283-287
  • EF closing should post to 2019 "Årets resultat"; only next-year opening consolidates 2011-2019 into 2010.
  • Fix: Use 2019; add separate new-year opening step consolidating 2011-2019 into 2010 on period_start of next FY.

YE3. Resultatdisposition chain (2099→2098→2091) for AB not implemented [high]

  • File: lib/core/bookkeeping/year-end-service.ts:446-482 (absent)
  • 2099 permanently accumulates each year's result; no bolagsstämma / utdelning flow anywhere.
  • Fix: On next-period creation, post Debit 2099 / Credit 2098; expose "Bokför resultatdisposition" UI action generating 2098 → 2091 / 2898.

YE4. NE-bilaga covers only R1-R11; R14/B1-B22/R15-R46 missing [high]

  • File: lib/reports/ne-bilaga/types.ts:1-14, ne-engine.ts:42-128
  • Real NE-bilaga 2161 requires R12/R13 (avgifter/pension), R14 (bokfört resultat), B1-B22 (balance sheet), R15-R46 (skattemässiga justeringar). Code reuses R11 for "Årets resultat" while actual R11 is depreciation.
  • Fix: Extend NEDeclarationRutor to match official blankett; fix R11/R14 mapping.

YE5. No bokslutstransaktioner driven before closing (depreciation, accruals, tax provision, SLP, periodiseringsfond) [high]

  • File: lib/core/bookkeeping/year-end-service.ts:27-262
  • Validation only checks balance/drafts/gaps/continuity/FX; no pre-close prompts for required bokslut phases.
  • Fix: Add pre-closing "bokslutsplan" step with per-phase checklists and generators.

YE6. Periodiseringsfond cap is not enforced and not calculated [high]

  • File: (no implementation)
  • AB 25% cap, EF 30%, 6-year reversal — all absent. BAS accounts exist but no validation.
  • Fix: Add periodiseringsfonder table keyed by year; validate 25% cap at year-end; emit 6-year reversal deadlines.

YE7. executeYearEndClosing is not transactional — half-closed state on failure [high]

  • File: lib/core/bookkeeping/year-end-service.ts:403-523
  • 8-step sequence across separate DB calls; mid-flight failure leaves partial state (revaluation committed but no closing entry; period locked with no next period; OB in next period with failed continuity check).
  • Fix: Wrap sequence in single RPC; advisory lock on fiscal period; atomic rollback on continuity failure.

YE8. entity_type defaults silently to 'aktiebolag' — wrong closing account for EF [high]

  • File: lib/core/bookkeeping/year-end-service.ts:282
  • const entityType = settings?.entity_type ?? 'aktiebolag'; EF with missing settings would post to non-existent 2099.
  • Fix: Fall back to companies.entity_type (NOT NULL) if settings missing; throw if both null.

YE9. TOCTOU between validateYearEndReadiness and closing entry creation [medium]

  • File: lib/core/bookkeeping/year-end-service.ts:410-453
  • Concurrent writes between validation (line 410) and close (line 446) can miss entries; preview hash not captured for idempotency.
  • Fix: Hash-based idempotency token on POST; advisory lock on fiscal period for duration of RPC.

YE10. No depreciation automation; no inventarieregister [medium]

  • File: (no implementation)
  • Phase 3 depreciation postings must be manual; no räkenskapsenlig 30%/20% vs restvärde 25% choice.
  • Fix: Out of scope (asset module); readiness check should at minimum warn on 12xx activity without 78xx counterpart.

YE11. lockPeriod has business check for un-bookkept transactions, not un-booked supplier/customer invoices or receipts [medium]

  • File: lib/core/bookkeeping/period-service.ts:38-51
  • Only checks uncategorized bank transactions; draft invoices / unmatched receipts / unreconciled bank accounts pass.
  • Fix: Add checks for registered-but-unposted supplier_invoices, draft invoices, unmatched receipts, unreconciled 1930.

YE12. Year-end UI is "Coming soon" stub [medium]

  • File: app/(dashboard)/bookkeeping/year-end/page.tsx:7-37
  • Feature gated behind API access; no guided flow, no preview, no validation surface.
  • Fix: Build UI per closing-process phases 1-8 (reconciliations → accruals → depreciation → inventory → untaxed reserves → tax provision → resultatdisposition → final close).

YE13. INK2 auto-derivation ignores periodiseringsfond/överavskrivningar for taxable result [medium]

  • File: lib/reports/ink2/ink2-engine.ts:881-891
  • taxableResult = resultAfterFinancial + taxAmount ignores all skattemässiga justeringar.
  • Fix: Populate 7113/7114 from user-entered INK2S values (not auto-derive); OR add full INK2S ruta codes to type and compute correctly.

YE14. No validation EF closing doesn't touch AB-only reserved accounts (2110-2129, 2150-2159, 2512) [medium]

  • File: lib/core/bookkeeping/year-end-service.ts:303-331
  • EF mistakenly posting to periodiseringsfond carried forward without warning.
  • Fix: In validateYearEndReadiness for EF, warn on any posted balance in AB-only ranges; suggest reversal.

YE15. resultAccountSummary uses unrounded balances; may diverge from closing lines [low]

  • File: lib/core/bookkeeping/year-end-service.ts:308-331
  • UI shows sub-öre differences from posted amounts.
  • Fix: Round summary amounts the same way: Math.round(netBalance * 100) / 100.

YE16. Readiness check silently allows closing period with zero posted entries [low]

  • File: lib/core/bookkeeping/year-end-service.ts:201-211
  • Warning-then-throw mismatch; confusing "No result accounts to close" error despite activity.
  • Fix: Make "no activity" a hard error at validation OR allow closing with rolled-forward balances only, skipping the closing entry.

YE17. previewYearEndClosing computes netResult but never uses it [low]

  • File: lib/core/bookkeeping/year-end-service.ts:290-291
  • No cross-check between income statement and implicit closing balance.
  • Fix: Compare netResult against implicit balancing amount; warn (or block) on divergence.

YE18. No reopen / undo path for year-end closing [low]

  • File: (no implementation)
  • Misskapad bokslut requires manual SQL intervention; enforce_opening_balance_immutability + enforce_journal_entry_immutability block ordinary recovery.
  • Fix: Provide "öppna om bokslut" admin action: storno closing entry + unlock period + delete OB entry in next period + audit log.

YE19. createNextPeriod always forces 12-month periods — no fiscal-year change support [low]

  • File: lib/core/bookkeeping/period-service.ts:152-169
  • Ignores BFL 3 kap. 3 § (6-18 months transition year after SKV approval).
  • Fix: Accept optional nextPeriodMonths parameter (bounded 6-18).

YE20. Executed year-end closing uses voucher_series 'A' — not a dedicated bokslut series [low]

  • File: lib/core/bookkeeping/year-end-service.ts:451
  • Swedish practice uses dedicated series (commonly 'B' or 'I'); mixing complicates voucher-gap analysis.
  • Fix: Use configurable bokslut series (default 'B'); seed voucher_sequences row for (company, period, 'B') before creating closing entry.

Asset-accounting agent

Note: the entire fixed-asset module is missing and has been filed as critical tickets #100-104. The following findings refine scope.

AA1. Förbrukningsinventarier threshold (half PBB) not enforced programmatically [high]

  • File: lib/bookkeeping/booking-templates.ts:1306-1322 (equipment_consumable) and :1323-1346 (equipment_capital)
  • Per IL 18 kap. 4 §: threshold 29,600 SEK (2026) + grouping rule. Classification depends on user-chosen template; 40k SEK laptop silently expensed.
  • Fix: On transaction/supplier-invoice lines landing on 5410/5411/5412, compare amount_ex_vat to prisbasbelopp / 2; warn over-threshold; sum same-supplier-same-day-same-account for grouping.

AA2. Komponentavskrivning (K3) not supported; K2/K3 gate absent [high]

  • File: lib/assets/components.ts (missing)
  • K3 (BFNAR 2012:1 17.4-17.5) mandates component depreciation; from 2026 fastighetsbolag/BRF must use K3. No asset register so component absent.
  • Fix: When asset module is built, include component decomposition with per-component fields; gate K2 vs K3 on company.

AA3. No jämkning of input VAT on capital goods [high]

  • File: lib/vat/jamkning.ts (missing)
  • ML 15 kap correction unenforceable without asset register (buildings 10yr/≥100k, maskiner 5yr/≥50k).
  • Fix: As part of asset register schema, store acquisition_input_vat_amount per asset; offer jämkning posting on disposal or business-use change.

AA4. equipment_capital template dead-ends — no follow-through [high]

  • File: lib/bookkeeping/booking-templates.ts:1323-1346
  • Books Dr 1250 / Cr 1930 but asset invisible to system afterward; no register entry; no year-end depreciation reminder. special_rules_sv mentions 7832 but nothing posts automatically.
  • Fix: Prompt user to create inventarieregister entry (or auto-seed draft from transaction); don't let 12xx grow without linked asset record.

AA5. No handling of finansiell leasing (K3) — capitalization path missing [medium]

  • File: lib/bookkeeping/booking-templates.ts:204-225; app/api/assets/leases/** (missing)
  • K3 BFNAR 2012:1 kap. 20 IAS 17 classification absent. K3 20.29 juridisk person exemption undocumented. 2024 Skatteverket VAT ställningstagande on finansiell leasing-as-goods not reflected.
  • Fix: Document K2-only scope in templates. For K3 capitalization: add leasingavtal entity with effective-interest payment split, ROU depreciation, and VAT rule change flag.

AA6. Reporting paths for 78xx exist but cannot be populated [low]

  • File: lib/reports/ne-bilaga/ne-engine.ts:112-127; ink2-engine.ts:278,593-597; balance-sheet.ts:28-30
  • Report infrastructure is a partial shell without the engine.
  • Fix: Fixed indirectly by building the asset module (critical tickets).

Financial-reporting agent

FR1. No årsredovisning document generator (förvaltningsberättelse, noter, signatures) [high]

  • File: app/(dashboard)/bookkeeping/year-end/page.tsx:25-27 + absent generators
  • No förvaltningsberättelse, noter, underskriftssida, or fastställelseintyg. PDF is explicitly "Arbetsutkast — ej undertecknat".
  • Fix: Build template + noter generator + signature manifest (styrelseledamoter table prerequisite) OR document scope as "bokföringsverktyg".

FR2. Balance sheet does not split bundet/fritt eget kapital [high]

  • File: lib/reports/balance-sheet.ts:45-47
  • ÅRL 3:10a-10b § and K2 4.7 require AB to split EK. Uses single '20': 'Eget kapital' bucket.
  • Fix: Replace '20' entry with two: Bundet (2010-2089) and Fritt (2090-2099).

FR3. No K2/K3 accounting framework persistence [high]

  • File: supabase/migrations/20240101000001_core_schema.sql:63-94, types/index.ts
  • No accounting_framework column. K2/K3 distinction materially changes format, noter, depreciation, allowed assets.
  • Fix: Add accounting_framework text check in ('k2','k3','k_ifrs') to company_settings; default 'k2' for AB; ask during onboarding; branch K2-specific restrictions.

FR4. Lämnade gottgörelser mapped into INK2R as Avsättning periodiseringsfond [high]

  • File: lib/reports/ink2/ink2-engine.ts:585-591
  • BAS 8840 maps to SRU 7525 (wrong; gottgörelser are separate bokslutsdisposition).
  • Fix: Re-map 7525 to BAS 8811; route 8840 to SRU 7422 (or accept out of scope).

FR5. INK2S skattemässiga justeringar exposes only 4.1/4.2/4.3a/4.15/4.16 [high]

  • File: lib/reports/ink2/ink2-engine.ts:893-902
  • Missing 4.3b (SRU 7652), 4.3c (7653), 4.6a schablonintäkt (7654), 4.6d (7667), 4.7f aktiefållan (7757), 4.14a underskott (7763). taxableResult wrong for any AB with periodiseringsfond/representation/nedskrivning/underskott.
  • Fix: Add editor for INK2S fields with BAS-suggested defaults OR zero out INK2 p. 1.1/1.2 with blocking warning.

FR6. INK2 p. 1.4 (SLP) never populated [medium]

  • File: lib/reports/ink2/types.ts:95-100
  • SLP rate 24.26% on pension costs (BAS 7410-7419); never computed.
  • Fix: Add fields to INK2Rutor; sum pensionspremier; compute SLP = base × 0.2426; allow override; warn on pensionskostnader with zero SLP.

FR7. No Bolagsverket filing / förseningsavgifter / iXBRL [medium]

  • File: lib/tax/deadline-config.ts:242-289 (deadline only)
  • Förseningsavgifter escalation, tvångslikvidation warnings, iXBRL generator, fastställelseintyg, Bolagsverket API — all absent.
  • Fix: Scope decision — document as non-goal OR implement iXBRL + fastställelseintyg + filing flow.

FR8. Kassaflödesanalys generator absent [medium]

  • File: (absent from lib/reports/)
  • Mandatory for K3 större and brf per ÅRL 2:1 § and SFS 2022:1028.
  • Fix: Lower priority for K2 target. For K3/större: implement indirekt metod.

FR9. No revisionsplikt computation / tracking [medium]

  • File: (absent)
  • ABL 9:1 § thresholds uncomputed; audit-required companies file without revisionsberättelse.
  • Fix: Add requires_audit + auditor_name to company_settings; auto-compute from KPI (two of three thresholds); dashboard warning on transition.

FR10. Income statement uses non-ÅRL rubriker [low]

  • File: lib/reports/income-statement.ts:30
  • 'Huvudintäkter' instead of ÅRL's 'Nettoomsättning'; K2 uppställningsform review would fail.
  • Fix: Rename rubriker to ÅRL Bilaga 2 exact wording.

FR11. Balance sheet kortfristiga skulder conflates trade payables [low]

  • File: lib/reports/balance-sheet.ts:52-55
  • Lumps leverantörsskulder (2440) with förskott (2420) and checkräkningskredit (2410) into single "Kortfristiga skulder" row.
  • Fix: Replace '24' prefix with 2410/2420/2440/2450/2460/2470/2480/2490 splits matching INK2R mapping.

Tax-planning agent

TP1. No periodiseringsfond planner (cap + schablonintäkt + 6-year auto-reversal) [high]

  • File: lib/tax/ (absent)
  • 25% cap uncomputed; 6-year auto-reversal untracked; schablonintäkt (SLR × avsättning) never calculated.
  • Fix: Add lib/tax/periodiseringsfond.ts with calculateMaxPeriodiseringsfondAB, FIFO reversal tracker, calculateSchablonintakt. Surface on year-end wizard.

TP2. No överavskrivningar planner (2150/8850 slack vs plan) [high]

  • File: lib/tax/ + lib/bookkeeping/engine.ts (no asset register hooks)
  • No 30-regeln / 20-regeln calculator showing headroom.
  • Fix: Add lib/tax/overavskrivningar.ts; depends on asset register.

TP3. Koncernbidrag validation absent [high]

  • File: lib/bookkeeping/engine.ts (BAS 8810/8820 postings unchecked)
  • No IL 35 kap enforcement: ≥90% ownership, direct/indirect chain, K2/K3 consistency, underskottsspärr. No koncern data model.
  • Fix: Modal on 8810/8820 postings asking counterparty + ownership + documentation; company_relationships table for automation.

TP4. 3:12-reglerna entirely absent [high]

  • File: lib/tax/famansbolag/ (absent)
  • No gränsbelopp, förenklingsregeln, löneunderlag, K10, sparat utdelningsutrymme. Most important tool for target fåmansbolag user.
  • Fix: Build famansbolag/ module with K10 tracker + löneunderlag validator + optimization; shareholders table + settings page.

TP5. 2026 3:12 reform not addressed [high]

  • File: lib/tax/ (absent since 3:12 absent)
  • Regeringen's 2026 reform changes löneunderlag / gränsbelopp / lönekrav.
  • Fix: Key the rule table by fiscal year in whatever 3:12 module is built.

TP6. No fåmansbolag detection (≤4 owners holding ≥50%) [medium]

  • File: types/index.ts (no shareholder fields)
  • IL 56:2 test can't be evaluated.
  • Fix: Add shareholders table; derive is_famansbolag from DB view/function.

TP7. No ränteavdragsbegränsningar calculator (EBITDA 30% + 5 MSEK + N9) [medium]

  • File: lib/tax/ (absent)
  • IL 24:21-29 limitation uncomputed; no N9 generator; carry-forward up to 6 years untracked.
  • Fix: Add lib/tax/ranteavdrag.ts with taxEbitda computation; warn as 8400-8499 approaches 5 MSEK.

TP8. No lön vs utdelning optimization (brytpunkt, "max utdelningsutrymme") [medium]

  • File: app/(dashboard)/salary/ + lib/tax/famansbolag/optimization.ts (absent)
  • Most common fåmansbolag question unanswered.
  • Fix: Add optimization.ts with recommendedLonForMaxUtdelningsutrymme; show on salary settings for AB users.

TP9. Year-end service has no planning scenarios (preview before vs after of bokslutsdispositioner) [medium]

  • File: lib/core/bookkeeping/year-end-service.ts:268-390
  • previewYearEndClosing returns single netResult; no variability analysis.
  • Fix: Add year-end-planner.ts returning N scenarios (no disposition / max periodiseringsfond / max + överavskrivning) with bolagsskatt impact per scenario.

TP10. Tax calculator is unused dead code (not wired into any UI or API) [medium]

  • File: lib/tax/calculator.ts:69,129
  • calculateEFTax / calculateABTax imported nowhere. Also simplistic — ignores arbetsgivaravgifter / periodiseringsfond / estimated slutlig skatt.
  • Fix: Delete OR wire into "Beräknad skatt" card after filling gaps.

TP11. No kapitalförsäkring-i-bolaget warning [low]

  • File: lib/bookkeeping/bas-data/class-1-assets.ts:1359 (account exists, no guard rail)
  • No notice when premium booked as personalkostnad; no check on excluded income.
  • Fix: Add expense-warnings rule flagging premium booked to 7xxx instead of 138x.

Project-accounting agent

Most findings below possibly tracked by #243 Projektbaserad.

PA1. No project or cost-center CRUD endpoints or UI [high]

  • File: app/api/ (no projects/ or cost-centers/ subdirectory)
  • Tables exist (supabase/migrations/20240101000015_dimensions.sql:7-68) and types exist (types/index.ts:2095-2117) but no API or dashboard page. Only reader is SIE export.
  • Fix: Add /api/projects/* and /api/cost-centers/* CRUD routes + settings page.

PA2. No UI to tag journal entries, invoices, or expenses with project/cost_center [high]

  • File: app/(dashboard)/bookkeeping/[id]/page.tsx, components/ (no dimension-picker)
  • journal_entry_lines.cost_center/project engine-accepted but never populated by app-created entries.
  • Fix: Add dimension picker to journal entry line editor, invoice line items, supplier invoice line items, categorization drawer, expense form.

PA3. SIE import discards #TRANS object list [high] (dupes SIE S2)

  • File: lib/import/sie-parser.ts:625-627
  • Fortnox/Visma/Bokio migration strips project/cost-center tags.
  • Fix: Parse #DIM/#OBJEKT + object list; map to journal_entry_lines during import.

PA4. No project-filtered reports [medium]

  • File: lib/reports/ (no filters)
  • Reports don't accept project_id/project_code/cost_center parameter.
  • Fix: Add optional projectCode/costCenterCode filters to general-ledger, trial-balance, income-statement, monthly-breakdown. Per Srf U 14 kvittningsförbud project BS views must be reported gross per project.

PA5. No WIP / pågående arbeten posting logic [medium]

  • File: lib/bookkeeping/ (no WIP module); lib/core/bookkeeping/ (no service)
  • BAS 1470/1620/2420/2450/4970 exist but no code books; färdigställandegrad absent; befarade förluster (K3 BFNAR 2012:1 23.32) unmodeled.
  • Fix: Year-end closing step for WIP entries; project_contracts table with cumulative-cost + invoiced-amount tracking.

PA6. Engine entry generators don't propagate project or cost center [medium]

  • File: lib/bookkeeping/invoice-entries.ts, supplier-invoice-entries.ts, transaction-entries.ts, vat-entries.ts, lib/salary/salary-entries.ts
  • Engine buildLineInserts passes through when given, but upstream generators never set the fields. SalaryRunEmployee.cost_center/project declared but never read.
  • Fix: Propagate on revenue/cost/wage lines; leave VAT lines null.

PA7. Invoice items and expenses have no project column [medium]

  • File: supabase/migrations/20240101000001_core_schema.sql:246-256; supplier_invoices.sql:190
  • No way to associate line to project before journal entry generation.
  • Fix: Add project_id uuid references projects(id) + cost_center_id to invoice_items, supplier_invoice_items, expenses.

PA8. cost_centers / projects tables nearly identical, no SIE dimension number [low]

  • File: supabase/migrations/20240101000015_dimensions.sql:7-68
  • Neither carries dimension number; export hardcodes dim 1 / dim 6.
  • Fix: Add dimension_number int not null default 1/default 6; optional cost_center_id uuid FK on projects.

PA9. Dimension string columns unconstrained to known codes [low]

  • File: supabase/migrations/20240101000011_alter_existing_tables.sql:58-64
  • Free-text columns on journal_entry_lines; typos land in immutable posted entries; SIE import accepts undefined codes.
  • Fix: Validation trigger (company-scoped) OR migrate to FK columns with ON DELETE SET NULL.

Bookkeeping-engine agent

BE1. reverseEntry and correctEntry reserve voucher numbers that become undocumented gaps on cleanup [high]

  • File: lib/bookkeeping/engine.ts:378-462, lib/core/bookkeeping/storno-service.ts:81-268
  • Pre-reserve + INSERT + post-UPDATE pattern; failure leaves sequence increment + no voucher_gap_explanations row. Violates BFNAR 2013:2 punkt 5.8 and blocks year-end readiness.
  • Fix: Rework to use commit_journal_entry RPC pattern (atomic), OR insert voucher_gap_explanations row on every cancel branch per mark-paid convention.

BE2. reverseEntry places reversal in original.fiscal_period_id with entry_date outside that period [high]

  • File: lib/bookkeeping/engine.ts:375,403-404; lib/core/bookkeeping/storno-service.ts:96,193
  • Reversal today on 2024 entry lands with entry_date=2026-04-22 but fiscal_period_id=2024. Violates BFL 5 kap 1§.
  • Fix: Place reversal in period containing entryDate via findFiscalPeriod; fail if no open period covers it.

BE3. correctEntry emits journal_entry.corrected but never journal_entry.committed [medium]

  • File: lib/core/bookkeeping/storno-service.ts:289-298
  • Extensions subscribed to .committed miss correction-induced postings (inconsistent with reverseEntry emitting both).
  • Fix: Also emit .committed twice from correctEntry OR document .corrected as compound superseding event.

BE4. Sandbox seed bypasses the engine with a comment that invites the pattern [medium]

  • File: app/api/sandbox/seed/route.ts:295-351
  • Direct status: 'posted' inserts + hand-picked voucher numbers; comment "inserted directly, not via engine, to avoid event emission" normalizes anti-pattern.
  • Fix: Route through createJournalEntry with sandbox: true flag that handlers ignore. Remove "avoid event emission" justification.

BE5. PATCH /journal-entries/[id]/notes accepts posted entries; outcome depends on trigger version [medium]

  • File: app/api/bookkeeping/journal-entries/[id]/notes/route.ts:32-36
  • Works today because schema_sync trigger whitelists notes; breaks if earlier trigger version is deployed (forks, downgrades, self-hosted).
  • Fix: Add explicit status check; document dependency in comment.

BE6. mark-paid routes create entry, then payment; failure leaves orphaned posted entry [medium]

  • File: app/api/supplier-invoices/[id]/mark-paid/route.ts:165-181
  • Three-step sequence: entry → status CAS → payment insert. Step 3 failure silently succeeds; reverseEntry lookup later no-ops on amount sync.
  • Fix: Insert payments row first, OR wrap in DB RPC, OR storno entry on step 3 failure. At minimum return 500.

BE7. Mapping rule regex uses user-provided patterns with no catastrophic-backtracking guard [medium]

  • File: lib/bookkeeping/mapping-engine.ts:160,175
  • ReDoS on every categorization; mapping_rules is user-writable.
  • Fix: Validate at rule-create time (reject nested quantifiers) OR use safe regex engine OR cap execution via worker with timeout.

BE8. Mapping rule priority tie results in "first row returned" — not deterministic [low]

  • File: lib/bookkeeping/mapping-engine.ts:64
  • Two rules with equal priority return in Postgres storage order; can produce different categorizations across calls.
  • Fix: .order('priority', asc).order('created_at', asc) as tiebreaker.

BE9. template-library.applyTemplate writes amounts as strings via toFixed(2) [low]

  • File: lib/bookkeeping/template-library.ts:60-61
  • Output feeds UI form; violates CLAUDE.md guard rail 9 as a pattern.
  • Fix: Keep numbers as numbers in output; convert at UI boundary. Use Math.round(amount * 100) / 100.

Cross-cutting Opus

Provider-connections agent

PC1. Invoice reminder cron has no idempotency — duplicate reminders on retry [high]

  • File: app/api/invoices/reminders/cron/route.ts:6-45, lib/invoices/reminder-processor.ts
  • Vercel retry duplicates reminders; level-3 "Tredje påminnelse" doubled destroys trust.
  • Fix: Write reminder record before sending with status: 'pending'; idempotency key reminder-{invoiceId}-{level}; pass Resend Idempotency-Key header.

PC2. Invoice-inbox Resend attachment download has no timeout or size cap [high]

  • File: extensions/general/invoice-inbox/lib/resend-inbound.ts:70
  • Arbitrary provider URL; 1 GB PDF blows memory via arrayBuffer().
  • Fix: 30s timeout; MAX_DOCUMENT_SIZE enforcement; streaming abort when bytes exceed cap.

PC3. Enable Banking callback/sync error messages are English-only [high]

  • File: app/api/extensions/enable-banking/callback/route.ts:72,83,105,206; extensions/general/enable-banking/index.ts:216,381,435
  • Raw tokens bank_error=invalid_state / 'Sync failed' surface in Swedish UI.
  • Fix: Map all codes to Swedish in lib/errors/get-error-message.ts new bank_connection context; consume in settings/banking page.

PC4. VIES validation error returned to user is English [high]

  • File: lib/vat/vies-client.ts:94,105,127,144
  • English errors via /api/vat/validate to customer-creation UI.
  • Fix: Return Swedish; add canRetry boolean and fallbackAccepted flag for UI behavior.

PC5. VIES client has no retry on 5xx or transient network errors [high]

  • File: lib/vat/vies-client.ts:108-147
  • Notoriously unstable backends; one-shot + 10s timeout blocks user.
  • Fix: 2-attempt retry with 500ms + jitter for 502/503/504/AbortError. Keep 4xx non-retried.

PC6. Skatteverket-issued 401/403 errors not surfaced uniformly to client [high]

  • File: extensions/general/skatteverket/lib/api-client.ts:140-161
  • Handlers catch generically with err.message; loses code; 429 unmapped and hits user as raw response text.
  • Fix: instanceof SkatteverketAuthError check in every handler; return { error, code }. Add 429 branch with Swedish retry message.

PC7. Resend webhook signature verification only fires if RESEND_INBOUND_WEBHOOK_SECRET is set [high]

  • File: extensions/general/invoice-inbox/lib/resend-inbound.ts:30-46
  • Missing secret → 500 every request; no svix-id dedup.
  • Fix: svix_event_ids table with UNIQUE(svix_id); insert first, conflict returns 200 without processing; fail loudly on startup if secret missing.

PC8. Google Drive upload has no timeout and uploads entire buffer in RAM [high]

  • File: extensions/general/cloud-backup/lib/google-drive.ts:126
  • Multipart recommended only ≤5 MB; archives grow to 100 MB+; Buffer.concat in memory.
  • Fix: 5-min AbortController timeout; switch to resumable uploads for >5 MB via /upload/drive/v3/files?uploadType=resumable; stream from source.

PC9. Enable Banking retry on POST /sessions is not retried [medium]

  • File: extensions/general/enable-banking/lib/api-client.ts:340-360
  • Transient network failure forces user to restart 90-day BankID consent flow.
  • Fix: Single retry on pure network failure (AbortError / TypeError thrown before any response). Don't retry on HTTP errors — code is one-time.

PC10. Riksbanken currency API fallback rates are stale and silent [medium]

  • File: lib/currency/riksbanken.ts:107-122
  • Hard-coded rates (~11.5 SEK/EUR) drift; caller has no signal a fallback was used.
  • Fix: Add source: 'riksbanken' | 'fallback' to ExchangeRate; warn UI on fallback; decline automatic booking for large amounts (>10k SEK equivalent).

PC11. Several provider error paths use console. instead of structured logger* [medium]

  • File: extensions/general/enable-banking/lib/api-client.ts:191,206,236,248-488 and others
  • Inconsistent prefixes; no Sentry scope; no level filtering.
  • Fix: Use createLogger('enable-banking') and siblings; keep structured metadata.

PC12. skatteverket_tokens decryption failure silently returns null [medium]

  • File: extensions/general/skatteverket/lib/token-store.ts:94-98
  • SKATTEVERKET_TOKEN_ENCRYPTION_KEY rotation masks as "Inte ansluten"; reconnect may loop.
  • Fix: Log warn level on decryption failure; auto-delete undecryptable row OR distinct error TOKEN_UNREADABLE surfacing "Återställ anslutning" UI.

PC13. Enable Banking consent expiry warning only fires in cron [medium]

  • File: app/api/extensions/enable-banking/sync/cron/route.ts:125-132
  • User-triggered "Synka nu" doesn't check expiry; 90-day consent dies silently.
  • Fix: Check isConsentExpiringSoon on /sync load; surface consent_expiring_in_days in response.

PC14. getSupportedBanks fallback list does not log upstream error [low]

  • File: extensions/general/enable-banking/lib/api-client.ts:266-275
  • Ålands/Norwegian users see Swedish-only fallback silently.
  • Fix: Delete fallback or return provider_unavailable error; mark legacy export @deprecated.

Security agent

SEC1. MCP OAuth grants ALL_SCOPES on every token exchange [medium]

  • File: app/api/mcp-oauth/token/route.ts:118
  • Consent page lists prose permissions; no scope selection; every OAuth key gets write access to everything.
  • Fix: Accept scope param; validate against ALL_SCOPES; persist into stateless auth code; render checkboxes on consent page; default read-only.

SEC2. HTML injection in outbound invoice email templates [medium]

  • File: lib/email/invoice-templates.ts:21-154, reminder-templates.ts, invite-templates.ts, consent-notification-templates.ts
  • Interpolates customer.name / company.bank_name etc. without escape. Attacker-owned company can embed phishing links rendered in Swedish customer's email.
  • Fix: Add escapeHtml() helper; wrap every interpolated value; reject HTML in input validation for company_settings/customer name.

SEC3. Two cron endpoints use non-constant-time secret comparison [medium]

  • File: app/api/events/cleanup/cron/route.ts:13, app/api/extensions/push-notifications/cron/route.ts:25
  • Plain !==; bypasses existing verifyCronSecret() helper with crypto.timingSafeEqual on SHA-256.
  • Fix: Replace with verifyCronSecret(request).

SEC4. Error messages leak PostgREST internals to client [medium]

  • File: app/api/bookkeeping/journal-entries/route.ts:99 and dozens
  • Raw error.message exposes table/column/constraint names.
  • Fix: Route through getErrorMessage(); log raw server-side; strip regex of relation "..." / column "..." / constraint "..." / policy "...".

SEC5. Missing company_id defense-in-depth filter on journal_entry_lines query [low]

  • File: app/api/bookkeeping/accounts/[number]/route.ts:45-48
  • Relies on RLS only; deviates from "belt + suspenders" rule. Count leak if RLS relaxed.
  • Fix: Join through journal_entries explicitly: .select('id, journal_entries!inner(company_id)').eq('account_number', number).eq('journal_entries.company_id', companyId).

SEC6. Redirect URI host allowlist has localhost permanently open [low]

  • File: app/api/mcp-oauth/authorize/route.ts:17-22, register/route.ts:5-10
  • Production accepts http://localhost / 127.0.0.1; widens attack surface combined with malicious local services.
  • Fix: Gate localhost/127.0.0.1 patterns behind process.env.NODE_ENV === 'development'.

SEC7. PDF generation error details leak to client [low]

  • File: app/api/invoices/[id]/pdf/route.ts:97-101
  • @react-pdf/renderer errors include file paths and stack fragments.
  • Fix: Log server-side; return generic Swedish message; include internal id in response header for support.

SEC8. /api/log endpoint has no auth, no rate limit, writes to Vercel logs [low]

  • File: app/api/log/route.ts:3-14
  • Trivial log bloat / noise vector.
  • Fix: requireAuth(); cap body size (8 KiB); per-IP rate limit; Zod validation.

SEC9. /api/sandbox/seed error message suppression swallows stack [low]

  • File: app/api/sandbox/seed/route.ts:526-531
  • Outer catch{} swallows error; no server-side logs.
  • Fix: catch (err) { log.error('sandbox seed failed', { err }); ... }.

SEC10. TIC BankID start cooldown uses naive x-forwarded-for [low]

  • File: extensions/general/tic/index.ts:406-408
  • Self-hosted reverse-proxy misconfigurations allow spoofing; unlimited billable BankID sessions.
  • Fix: Document self-hosted reverse-proxy requirement; use CF-Connecting-IP behind Cloudflare or platform-provided peer address.

SEC11. middleware.ts config does not run on /api — per-route auth is the only line of defense [low]

  • File: middleware.ts:8-22
  • New route forgetting requireAuth() gets no protection.
  • Fix: CI check / grep that every app/api/**/route.ts calls requireAuth()/verifyCronSecret()/validateApiKey() or is in an allowlist.

SEC12. Extension API PATCH /settings deep-merges user-controlled JSON [low]

  • File: app/api/extensions/[sector]/[slug]/settings/route.ts:50-77
  • Object-spread allows user to overwrite any setting key (e.g., emailTemplate, redirectUri); role check limits to company writers but member role can tamper.
  • Fix: Zod-validate body against extension schema before merging; reject __proto__/constructor/prototype keys; gate settings writes to owner/admin.

RLS-multitenancy agent

RLS1. API key auth in /api/events discards the key-bound company_id [high]

  • File: app/api/events/route.ts:29-44
  • Uses requireCompanyId() (active_company_id) instead of authResult.companyId. External automation with API key scoped to X sees company Y's events after user switch.
  • Fix: Use authResult.companyId when token branch fires; audit every other dual-auth endpoint (MCP server, /api/events is outlier).

RLS2. /api/mcp-oauth/token binds the new API key to the user's current active company [high]

  • File: app/api/mcp-oauth/token/route.ts:105
  • Authorize consents on X; user switches tab to Y; token binds Y. Scope drift across handshake.
  • Fix: Extend AuthCodePayload with companyId; set at POST time; use payload.companyId in /token; re-verify membership at exchange.

RLS3. Document storage paths keyed by uploader user_id, not company_id [high]

  • File: lib/core/documents/document-service.ts:121,194; supabase/migrations/20240101000024_storage_bucket_policies.sql:30-52
  • Teammates can't access colleagues' uploads; BFL räkenskapsinformation blocked. Contrast with sie-files bucket which was correctly updated to user_company_ids().
  • Fix: Switch storage paths to documents/{company_id}/...; rewrite RLS to check (storage.foldername(name))[1]::uuid IN (SELECT user_company_ids()). Backfill via service role or view unifying both prefixes.

RLS4. event_log rows written by bus handler have NULL company_id [medium]

  • File: lib/events/handlers/event-log-handler.ts:102-109,133-141
  • persistEvent omits company_id; RLS excludes; /api/events returns zero events. (Also filed as event-bus critical.)
  • Fix: Extract companyId from payload; backfill NULLs via user_id → active_company_id mapping if TTL hasn't swept.

RLS5. notification_log_select policy reads company_id IS NULL rows — any authenticated user sees them [medium]

  • File: supabase/migrations/20260330130000_multi_tenant_company_refactor.sql:775-776
  • OR company_id IS NULL legitimate for shared BAS mappings only; push-notifications currently broken writer creates NULL rows.
  • Fix: Remove OR company_id IS NULL from notification_log_select; backfill or purge NULL rows; fix push-notifications sender.

RLS6. push-notifications extension queries .eq('company_id', userId) [medium]

  • File: extensions/general/push-notifications/notification-sender.ts:241,273,286
  • Wrong filter key; wasNotificationSent returns false (resends every notification); subscriptions empty.
  • Fix: Either revert to .eq('user_id', userId) OR migrate push_subscriptions/notification_settings to company scope and update extension.

RLS7. Tic BankID signup extension_data upsert uses old onConflict target [medium]

  • File: extensions/general/tic/index.ts:669-676
  • Unique constraint is (company_id, extension_id, key); upsert errors or inserts with NULL company_id (NOT NULL column → fails). Onboarding pre-fill breaks.
  • Fix: Defer enrichment caching until company exists OR companyless cache table (user_signup_data).

RLS8. Changing companies.team_id doesn't re-sync company_members [low]

  • File: supabase/migrations/20260331010000_teams_table_refactor.sql:252-309
  • Team A members keep source='team' rows after move; Team B members not auto-added. No UI path exposes this currently but invariant is unsafe.
  • Fix: AFTER UPDATE trigger on companies; delete stale team source rows; call sync_team_to_company(NEW.id, NEW.team_id). Alternately prohibit direct updates, require dedicated RPC.

Auth-MFA agent

AM1. Two cron endpoints compare CRON_SECRET with === instead of verifyCronSecret() [high]

  • File: app/api/extensions/push-notifications/cron/route.ts:25, app/api/events/cleanup/cron/route.ts:13
  • (Also flagged by security + rate-limits agents.) Once extracted, attacker can trigger events/cleanup or push-spam.
  • Fix: Use verifyCronSecret() helper.

AM2. OAuth token endpoint grants ALL_SCOPES regardless of consent [high]

  • File: app/api/mcp-oauth/token/route.ts:118 + authorize/route.ts:133-138
  • Consent bullet list silently narrower than actual scopes granted; users unknowingly grant payroll/invoices write.
  • Fix: Accept scope param, render individual toggles, persist through auth code, pass to api_keys.insert. Default read-only.

AM3. OAuth consent + token exchange reachable on AAL1 [high]

  • File: app/api/mcp-oauth/authorize/route.ts:86-92,180-187
  • AAL1 attacker (stolen password, no TOTP) can grant themselves full-scope API key that bypasses MFA forever (API key auth doesn't check AAL).
  • Fix: requireAuth() in both GET and POST; require AAL2 on hosted for any flow that issues long-lived credential.

AM4. /auth/callback honours unvalidated next query parameter — open redirect via protocol-relative URL [medium]

  • File: app/(auth)/auth/callback/route.ts:10,55,173
  • new URL('//evil.com/path', origin) resolves to https://evil.com/path; phishing vector after genuine email confirmation.
  • Fix: Apply same startsWith('/') && !startsWith('//') allowlist as MFA enroll page; reject data:/javascript:.

AM5. MFA verify rate-limit is client-side React state — direct-API attacker faces no throttle [medium]

  • File: app/(auth)/mfa/verify/page.tsx:82-90
  • Brute-force over an hour is practical without server-side throttle.
  • Fix: Server-side mfa_verify_failures table keyed by user_id; enforce in API route wrapping supabase.auth.mfa.verify(). Verify Supabase project-level rate limits.

AM6. createAuthCode doesn't validate non-empty code_challenge [medium]

  • File: app/api/mcp-oauth/authorize/route.ts:51,165
  • Only validates code_challenge_method === 'S256' (default passes with empty challenge); today close but future verifyPkce change could bypass.
  • Fix: Return invalid_request when code_challenge is missing or <43 base64url chars.

AM7. OAuth consent CSRF-reachable — no CSRF token on POST /authorize [medium]

  • File: app/api/mcp-oauth/authorize/route.ts:140-147,161-211
  • SameSite=Lax allows top-level POST via window.open auto-submit. Combined with ALL_SCOPES + no AAL2 gate silently exfiltrates API key.
  • Fix: CSRF token in GET stored in HttpOnly cookie; validate in POST; SameSite=Strict on OAuth cookies.

AM8. Anonymous sandbox users are not exempt from shouldEnforceMfa — trapped on /mfa/enroll [medium]

  • File: lib/auth/mfa.ts:17-21
  • Sandbox flow breaks on hosted production.
  • Fix: if (user.is_anonymous) return false in shouldEnforceMfa(). Lock-in test in lib/auth/__tests__/mfa.test.ts.

AM9. Logout does not revoke refresh token or clear company-context cookie [medium]

  • File: components/dashboard/DashboardNav.tsx:129 and 4 others
  • signOut() defaults to scope: 'local'; refresh token continues issuing access tokens. Company cookie leaks across user sessions.
  • Fix: signOut({ scope: 'global' }); clear gnubok-company-id and gnubok-invite-token cookies with maxAge=0.

AM10. BankID linking overwrites app_metadata rather than merging [medium]

  • File: extensions/general/tic/index.ts:798-800,844-846
  • updateUserById with app_metadata: { bankid_linked: true } replaces entire object; wipes Supabase-set provider/providers and any future subsystem metadata.
  • Fix: Fetch current app_metadata first, spread; OR use user_metadata.bankid_linked that doesn't collide with Supabase bookkeeping.

AM11. verifyPkce uses === string compare, not constant-time [low]

  • File: lib/auth/oauth-codes.ts:81
  • Not exploitable (challenge is public) but defense-in-depth.
  • Fix: crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(codeChallenge)) with length guard.

AM12. Dynamic client registration endpoint has no rate limit [low]

  • File: app/api/mcp-oauth/register/route.ts:21-50
  • Public; spammable (consumes Vercel function time); redirect_uris array unbounded.
  • Fix: IP-based rate limit (10 req/min); cap redirect_uris.length at 5.

AM13. Authorize GET renders code_challenge into action attribute without HTML escape [low]

  • File: app/api/mcp-oauth/authorize/route.ts:140,144
  • WHATWG URL parser percent-encodes dangerous chars so practically safe, but escapeHtml is available and inconsistent with companyName treatment.
  • Fix: Wrap url.pathname + url.search in escapeHtml().

AM14. Invite acceptance endpoint has no rate limit — status leak [low]

  • File: app/api/team/accept/route.ts:11-48
  • 256-bit entropy; brute-force infeasible today but no throttle; 404/410 distinction leaks lifecycle state.
  • Fix: IP-based rate limit (20 req/min); uniform response for not-found / expired / used.

AM15. requireAuth() and middleware fail-open on getAuthenticatorAssuranceLevel error [low]

  • File: lib/auth/require-auth.ts:29-36; lib/supabase/middleware.ts:102-107
  • Destructures data on error; aal undefined; MFA check passes silently.
  • Fix: Capture error; fail-closed with 503 or redirect to /mfa/verify.

Error-handling agent

EH1. Raw Postgres error.message returned in >70 API routes (info-disclosure) [high]

  • File: app/api/deadlines/[id]/route.ts:39 and ~70 others
  • Exposes table/column/constraint names; English jargon to Swedish users.
  • Fix: Route 500 responses through getErrorMessage(); log raw error server-side. At minimum strip /relation "[^"]+"|column "[^"]+"|constraint "[^"]+"/.

EH2. 203 'Unauthorized' literals returned unchanged [high]

  • File: ~155 API route files
  • UI displays data.error raw across most surfaces; English "Unauthorized" in Swedish app.
  • Fix: Replace literal at API layer with Swedish message (simpler, 155 files) OR enforce getErrorMessage convention on client.

EH3. 31 'Failed to X' API error strings displayed to users [high]

  • File: app/api/transactions/[id]/match-invoice/route.ts:69 and many
  • Hardcoded English user-facing business errors displayed raw on categorize/match/book pages.
  • Fix: Translate each at source — these are user-facing business-logic errors, not engine internals.

EH4. Only one route segment has an error boundary [high]

  • File: app/(dashboard)/error.tsx exists; /(auth), /(onboarding), /(public), /companies, /invite, /sandbox do not
  • Render errors fall through to app/global-error.tsx — full-page white screen with generic "Något gick fel".
  • Fix: Add error.tsx to (auth), (onboarding), companies at minimum. Pattern from (dashboard)/error.tsx.

EH5. global-error.tsx and (dashboard)/error.tsx don't capture to Sentry [high]

  • File: app/global-error.tsx:1-33, app/(dashboard)/error.tsx:14-16
  • Only console.error; no Sentry trace despite SENTRY_DSN being configured.
  • Fix: Sentry.captureException(error, { tags: { digest: error.digest } }) inside useEffect. Surface error.digest as "Referens: {digest}" for support correlation.

EH6. Silent swallowed errors in non-blocking operations [medium]

  • File: app/api/invoices/[id]/send/route.ts:198,216, lib/bookkeeping/handlers/supplier-invoice-handler.ts:42,72, app/api/transactions/[id]/categorize/route.ts:318,328,349,378, app/api/transactions/[id]/match-invoice/route.ts:115, lib/bookkeeping/engine.ts:314-316
  • Background journal entry / document upload failures only log to stderr; user sees success but books incomplete.
  • Fix: Enqueue failures in pending_operations with visible "1 operation failed — retry" indicator, OR return warnings: [{ type, message }] array in success response.

EH7. Zod validation messages are mostly English [medium]

  • File: lib/api/schemas.ts:11,14,20,149,150,151,168,193,195,215,244,256,305,310,314,335,336,406,428,444,474,494,504,551 — ~60%
  • Messages like "Expected YYYY-MM-DD date format" propagate via validateBody through getErrorMessage as English field-prefixed strings.
  • Fix: Convert every Zod message to Swedish; set global error map via z.setErrorMap for defaults like "Expected string, received number"; translate 'Validation failed' + 'Invalid JSON' + 'Invalid query parameters' in lib/api/validate.ts.

EH8. Correction dialog fallback is English [medium]

  • File: components/bookkeeping/CorrectionEntryDialog.tsx:112
  • throw new Error(result.error || 'Failed to create correction') — fallback displayed in Swedish toast.
  • Fix: Change to 'Kunde inte skapa ändringsverifikation'. Same pattern in components/deadlines/UpcomingDeadlinesWidget.tsx:74, TaxTodoWidget.tsx:85, transactions/page.tsx:255.

EH9. Bank callback error messages redirect with English URL params [medium]

  • File: app/api/extensions/enable-banking/callback/route.ts:77,83,104,206
  • URL params missing_parameters / invalid_state / Connection failed surface raw in banking settings toast description.
  • Fix: Swedish phrases in URL param OR map tokens client-side (replicate handleTicError pattern).

EH10. Enable Banking sync errors leak English to toast [medium]

  • File: extensions/general/enable-banking/components/BankingSettingsPanel.tsx:104,159,211
  • Failed to get transactions (403): {...} shown raw; provider 403 (expired consent) / 429 / 500 confusing.
  • Fix: Add extensions/general/enable-banking/lib/error-messages.ts Swedish mapper similar to invoice-inbox.

EH11. Health endpoint leaks raw DB error to public [medium]

  • File: app/api/health/route.ts:29,41
  • Public (no auth); raw Supabase error message includes infrastructure details.
  • Fix: Return generic error: 'database_error'; log full error server-side.

EH12. MCP-oauth and extension settings routes leak English [low]

  • File: app/api/mcp-oauth/*, app/api/extensions/[sector]/[slug]/settings/route.ts:80, data/route.ts:42,90,130, ext/[...path]/route.ts:60
  • OAuth acceptable (machine-to-machine per RFC); extension data/settings surface to user via workspaces.
  • Fix: Keep OAuth English; translate extension data/settings errors.

EH13. Invoice inbox error mapper exists but pattern is one-off [low]

  • File: extensions/general/invoice-inbox/lib/error-messages.ts:1-45
  • Well-structured pattern-based mapper but duplicated rather than shared.
  • Fix: Expand ERROR_PATTERN_MAP in lib/errors/get-error-message.ts; introduce registerErrorPatterns(patterns) API for domain modules.

EH14. getResponseErrorMessage helper exists but is never imported [low]

  • File: lib/errors/get-error-message.ts:253-263
  • Dead code; callers do manual unwrapping.
  • Fix: Refactor the 15 getErrorMessage call sites to use getResponseErrorMessage; promote in CLAUDE.md. Or delete.

EH15. isSwedishUserMessage heuristic may false-positive on mixed-language errors [low]

  • File: lib/errors/get-error-message.ts:111-127
  • /kunde inte/ or /försök igen/ match pass English prefix through.
  • Fix: Tighten heuristic to require message START with Swedish word OR drop heuristic, rely on pattern map.

EH16. ValidateBody returns English top-level 'Validation failed' [low]

  • File: lib/api/validate.ts:41,62,104
  • UIs reading data.error only show literal English.
  • Fix: Swedish summary like 'Valideringen misslyckades. Kontrollera fälten och försök igen.'; translate 'Invalid JSON' / 'Invalid query parameters' siblings.

Event-bus agent

EB1. Supplier invoice handler queries company_settings by wrong key [high] (dupe of VAT #2 / Engine #143)

  • File: lib/bookkeeping/handlers/supplier-invoice-handler.ts:28
  • Mock test passes because createQueuedMockSupabase ignores filter args.
  • Fix: Change filter; assert filter args in test.

EB2. Fire-and-forget eventBus.emit wrapped in synchronous try/catch [high]

  • File: app/api/transactions/[id]/match-invoice/route.ts:299-311, match-supplier-invoice/route.ts:211-223, lib/reconciliation/bank-reconciliation.ts:181-194,364-377
  • No await; sync try/catch can't catch promise rejection; serverless may kill work mid-flight before HTTP response returns.
  • Fix: Add await to all four sites; keep try/catch as defensive safety.

EB3. bank-file import emits transaction.synced differently from enable-banking [medium]

  • File: app/api/import/bank-file/execute/route.ts:115-120, extensions/general/enable-banking/index.ts:356-360
  • bank-file uses eventBus.emit directly; enable-banking uses ctx?.emit ?? eventBus.emit.bind(...).
  • Fix: Standardize extension code to always use ctx.emit; document webhook/cron exceptions.

EB4. Salary and lifecycle events not persisted to event_log [medium]

  • File: lib/events/handlers/event-log-handler.ts:12-36
  • PERSISTED_EVENT_TYPES missing salary_run.created/approved/booked, agi.generated/submitted, journal_entry.deleted, company.deleted, account.deleted.
  • Fix: Add salary + AGI events + journal_entry.deleted; document excluded ones in comment block.

EB5. Dead event types never emitted (receipt.extracted, receipt.matched, receipt.confirmed) [medium]

  • File: lib/events/types.ts:43-64
  • Receipt-ocr extension doesn't exist in extensions/general/; handlers/settings are dead configuration.
  • Fix: Remove types + handlers + settings until receipt-ocr exists, OR add TODO comment noting missing emission source.

EB6. Bus logs handler failures without handler identity and uses console [medium]

  • File: lib/events/bus.ts:52-58
  • Can't tell which handler in a 4-subscriber chain failed; bypasses structured logger.
  • Fix: Track handler ids via wrapper; log via createLogger('event-bus') with structured fields { eventType, handlerId, error }.

EB7. No per-handler timeout — slow handler blocks HTTP response [medium]

  • File: lib/events/bus.ts:44-60
  • await Promise.allSettled means single slow extension delays every API route that emits; Vercel 10s limit risk.
  • Fix: Optional timeoutMs per handler via Promise.race; emitAsync() variant (no await) for non-critical events; document which must block response.

EB8. invoice-inbox emits supplier_invoice.registered even if registration entry creation failed [medium]

  • File: extensions/general/invoice-inbox/index.ts:838-876
  • Try/catch logs to stderr but flow emits registered → confirmed → may double-post via core handler.
  • Fix: On creation failure either fail request with Swedish error OR skip lifecycle emits and mark inbox item as needing attention.

EB9. account.deleted payload missing companyId breaks extension context [low]

  • File: lib/events/types.ts:87; lib/extensions/registry.ts:62-64
  • Registry fallback makes ctx.companyId = userId; DB queries silently fail.
  • Fix: Add companyId: string | null to account.deleted; document absence intent.

Document-retention agent

DR1. anonymize_user_account RPC called from API but no migration in repo [high]

  • File: app/api/account/delete/route.ts:51
  • RPC applied out-of-band; fresh/preview branches 500 on delete; retention posture undefined. Violates CLAUDE.md "Always use migration files".
  • Fix: Commit migration defining public.anonymize_user_account(target_user_id uuid). Verify it scrubs PII from profiles but doesn't touch bookkeeping / document_attachments / audit_log.

DR2. Documents storage bucket RLS scoped to uploader user_id, not company_id [high] (dupe of RLS3)

  • File: supabase/migrations/20240101000024_storage_bucket_policies.sql:30-52
  • Teammates blocked from downloading colleagues' uploads; BFL räkenskapsinformation access broken.
  • Fix: Switch storage path to documents/{company_id}/...; RLS check (storage.foldername(name))[1]::uuid IN (SELECT user_company_ids()).

DR3. Verify cron does not flag missing storage files [high]

  • File: app/api/documents/verify/cron/route.ts:59-63
  • Download failure only logged to stderr; no audit_log entry; no storage_missing_at flag.
  • Fix: Insert audit_log INTEGRITY_FAILURE with action + description; add storage_missing_at timestamptz column.

DR4. delete_user_account audit-trigger disabling wipes audit history on CASCADE [high]

  • File: supabase/migrations/20260330170000_delete_user_account_rpc.sql:29-36; schema_sync.sql:804-812,820-822
  • Explicit DELETE FROM public.audit_log WHERE company_id = ANY(v_company_ids); BFL-relevant audit trail destroyed.
  • Fix: Don't delete audit_log rows; redact JSONB PII fields only. Retain with user_id pointing at tombstone.

DR5. document.storage_path can be used to enumerate other users' files if bucket RLS changes [medium]

  • File: app/api/documents/[id]/route.ts:42-44
  • Signed URL depends solely on DB-side filter; rogue storage_path insert could leak.
  • Fix: When fixing DR2, ensure RLS verifies company_id via folder prefix; don't rely solely on DB-side filtering.

DR6. receipts table has no immutability trigger; linked receipts can be deleted [medium]

  • File: supabase/migrations/20240101000004_receipts.sql:7-52; schema_sync.sql:873-888
  • Matched receipts (supplying VAT split / underlag) deletable via receipts_delete policy; ON DELETE SET NULL silently unlinks from transaction.
  • Fix: Add BEFORE DELETE trigger block_receipt_deletion rejecting when matched_transaction_id + journal_entry_id non-null. Consider immutability on extracted fields post-confirmation.

DR7. Archive export silently drops documents when storage download fails [medium]

  • File: lib/reports/full-archive-export.ts:365-394
  • ZIP returns 200 with partial manifest; no HTTP signal; GDPR portability degraded.
  • Fix: Include X-Archive-Partial: true header + partial: true in body when any manifest entry errored. Surface count on UI before download.

DR8. Archive export doesn't include superseded document versions' content when unlinked [medium]

  • File: lib/reports/full-archive-export.ts:327-336
  • Unlink-then-delete path renders version invisible; audit doesn't track unlink specifically.
  • Fix: Fix metadata-immutability trigger (critical) to block unlinking; add DOCUMENT_UNLINKED audit action.

DR9. No enforcement that journal entries have supporting documents (underlag) [medium]

  • File: lib/bookkeeping/engine.ts and related
  • BFL 5 kap 6§ requires räkenskapsinformation; gnubok treats documents as best-effort.
  • Fix: Periodic report listing posted entries without any linked document as first step.

DR10. Verify cron schedule in vercel.json vs docstring mismatch [low]

  • File: vercel.json:20-22 (daily) vs app/api/documents/verify/cron/route.ts:7-8 (weekly in doc) vs CLAUDE.md (weekly)
  • Reviewers can't trust either artifact.
  • Fix: Reconcile; daily is more conservative for integrity.

DR11. block_document_deletion uses SECURITY DEFINER but audit_log insert can fail silently [low]

  • File: supabase/migrations/20240101000017_enforcement_triggers.sql:164-213, 20260330130000...:1165-1207
  • Insert before RAISE; transaction aborts rolls back the audit insert; blocked deletion attempts leave no trace.
  • Fix: Use pg_notify or out-of-band logger with autonomous transactions for the audit insert.

DR12. Sandbox trigger bypass uses ambiguous user_id lookup [low]

  • File: supabase/migrations/20260311120000_sandbox_support.sql:18-112,149-203
  • Checks company_settings.user_id which was dropped in multi-tenant refactor; sandbox bypass silently broken (never triggers).
  • Fix: Rewrite to company_id = COALESCE(OLD.company_id, NEW.company_id); verify sandbox tests actually exercise the bypass branch.

Rate-limits agent

RL1. MFA verify lockout is client-side only — brute force not prevented server-side [high]

  • File: app/(auth)/mfa/verify/page.tsx:85-90 (dupe of Auth #AM5)
  • Suggested fix same as AM5.

RL2. Two cron endpoints use non-timing-safe CRON_SECRET comparison [high]

  • (Dupe of Security SEC3 / Auth AM1.)

RL3. /api/vat/validate proxies every request straight to VIES, no cache, no rate limit [high]

  • File: app/api/vat/validate/route.ts:8-39
  • SSRF-through-proxy DoS pattern; gnubok's Vercel egress IP shared across customers → easy to get flagged.
  • Fix: Cache results keyed by {country_code, vat_number} for ≥24h (vat_validation_cache table); rate-limit 30/hour per user; debounce UI button.

RL4. Bank file execute accepts unbounded transactions[] array [medium]

  • File: app/api/import/bank-file/execute/route.ts:43-48
  • Parse caps at 10 MB but execute accepts synthetic parsed transactions; 10M fake transactions possible.
  • Fix: Cap transactions.length at 10,000; Zod per-entry validation (no amount >10^12, no absurd dates); return 413 on exceed.

RL5. SIE execute route has no file-size guard [medium]

  • File: app/api/import/sie/execute/route.ts:36-66
  • Parse has 50 MB cap but execute re-accepts FormData without check; direct POST blows memory.
  • Fix: Apply same 50 MB guard at top of execute. Extract to lib/import/validate-upload.ts shared with parse.

RL6. API key rate limit window is fixed (1 minute) — allows 2x burst [medium]

  • File: supabase/migrations/20260326130000_api_key_scopes.sql:27-42
  • 200 requests across minute boundary with precise timing.
  • Fix: Sliding window or token bucket; OR document effective "up to 2x RPM across minute boundaries".

RL7. Invoice reminder cron sends emails in serial for-loop with no Resend quota awareness or backoff [medium]

  • File: lib/invoices/reminder-processor.ts:162-263; extensions/general/email/lib/resend-service.ts:31-80
  • No 429 retry, no daily quota awareness, no inter-send delay.
  • Fix: Honor Retry-After on 429; withRetry from lib/providers/retry.ts; 100-200ms inter-send delay over 20 reminders; long-term queue via event_log + worker.

RL8. Company invite endpoint has no per-user rate limit [medium]

  • File: app/api/company/members/invite/route.ts:25-184
  • Free email-send service / email-harvest vector; burns Resend quota.
  • Fix: Cap 20 invites per company per 24h via count against company_invitations.created_at. Apply to app/api/team/invite/route.ts too.

RL9. /api/invoice-action/[token] has no rate limit [medium]

  • File: app/api/invoices/reminders/action/route.ts:18-173
  • Public endpoint; token brute-force or mass-GET DoS; timing-based enumeration possible via .eq('action_token', token).single().
  • Fix: Per-IP 10 req/min; timing-safe token lookup; validate format (length/charset) before DB round-trip.

RL10. Enable Banking retry uses linear backoff and ignores Retry-After [medium]

  • File: extensions/general/enable-banking/lib/api-client.ts:183-217
  • Linear backoff RETRY_DELAY_MS * (attempt + 1); may get gnubok app ID flagged by banks.
  • Fix: Parse response.headers.get('retry-after'); honor if present; else exponential backoff initialDelay * 2^attempt with jitter capped 30s.

RL11. fetchAllRows has no iteration cap [low]

  • File: lib/supabase/fetch-all.ts:29-37
  • while (true) loop with no maximum; 28 call sites; malicious/massive company can OOM Vercel 1GB.
  • Fix: Configurable maxRows parameter (default 100,000); explicit throw on exceed; log warn above 10,000.

RL12. /api/health is public and runs a live DB query on every request [low]

  • File: app/api/health/route.ts:9-45
  • 100 RPS flood = 100 Supabase queries/sec hitting service role pool.
  • Fix: Cache healthy response in-process 10-30s; lighter query or ping Supabase /health; do DB check periodically not per request.

Cross-cutting Sonnet

UI-UX agent

UX1. Saturated blue/purple/gray badges on supplier-invoice and expense status — off-brand [high]

  • File: app/(dashboard)/supplier-invoices/[id]/page.tsx:23-31; app/(dashboard)/expenses/[id]/page.tsx:19-26; suppliers/[id]/page.tsx:125-131
  • Raw Tailwind bg-blue-100 text-blue-800 / bg-purple-100 / bg-orange-100 / bg-gray-100 outside palette.
  • Fix: Replace with token-based: registered → bg-muted / variant="secondary"; disputed/partially_paid → bg-warning/15 text-warning-foreground; credited → bg-secondary.

UX2. Salary run status uses blue/emerald/amber raw classes [high]

  • File: app/(dashboard)/salary/page.tsx:20-26; app/(dashboard)/salary/runs/[id]/page.tsx:28-35
  • STATUS_COLORS uses bg-blue-100 / bg-emerald-100 / bg-green-100 / bg-amber-100 instead of design tokens.
  • Fix: Swap to tokens: approved → primary/10; paid → success/10; booked → success/15; review → warning/15.

UX3. font-bold on financial totals — should be font-semibold or font-medium [high]

  • File: app/(dashboard)/reports/page.tsx:666,694,808,1177,1182 and 6 others
  • Design baseline documents font-medium default, font-semibold for emphasis, font-bold rare.
  • Fix: Replace font-bold with font-semibold on totals; reserve font-bold only for net-result card.

UX4. Supplier-invoice table amounts use font-mono instead of tabular-nums [high]

  • File: app/(dashboard)/supplier-invoices/page.tsx:174-175
  • Applies monospace (Geist Mono) vs body Geist Sans — inconsistent with invoices/journal entries/transactions/KPI.
  • Fix: Replace font-mono with tabular-nums.

UX5. Reports page financial amounts missing tabular-nums in ReportSectionTable [high]

  • File: app/(dashboard)/reports/page.tsx:858-868
  • Income statement, balance sheet, NE-bilaga render without fixed-width numerals.
  • Fix: Add tabular-nums to amount <td> + subtotal <span>; also IncomeStatementView and BalanceSheetView totals.

UX6. VAT declaration result badge uses bg-orange-100 — raw Tailwind, not a token [high]

  • File: app/(dashboard)/reports/page.tsx:1032,1184
  • Orange not in palette; should be ochre warning token.
  • Fix: Replace with bg-warning/15 text-warning-foreground and text-warning-foreground.

UX7. Public pages use font-bold text-gray-900 and slate gradient — off-brand [high]

  • File: app/(public)/dpa/page.tsx:14, privacy/page.tsx:13, invoice-action/[token]/page.tsx:147,151,186
  • Headings bypass font-display / font-medium / foreground token; bg-gradient-to-b from-slate-50 to-white is raw Tailwind slate palette.
  • Fix: Change headings to font-display text-3xl font-medium tracking-tight text-foreground; replace gradient with bg-background.

UX8. Login and register pages use border-blue-200 bg-blue-50 text-blue- info boxes* [medium]

  • File: app/(auth)/login/page.tsx:354-365,387-391, register/page.tsx:361-362
  • Blue not a design system semantic.
  • Fix: border-warning/30 bg-warning/5 text-foreground OR border-border bg-muted text-muted-foreground for neutral info.

UX9. Help page category badges use blue, purple, cyan, pink, orange — Tailwind rainbow [medium]

  • File: app/(dashboard)/help/page.tsx:203-208
  • Five saturated hues on single page violates "restrained palette".
  • Fix: Collapse to 2-3 token-based tints (primary/success/warning); vary icon shape if differentiation needed.

UX10. MFA/2FA "enabled" banner uses bg-green-50 border-green-200 text-green- — raw green* [medium]

  • File: components/settings/SecuritySettings.tsx:215-223
  • --success token exists (sage green) exactly for this purpose.
  • Fix: bg-success/8 border-success/20 text-success.

UX11. viewportThemeColor is #304D83 — saturated indigo blue [medium]

  • File: app/layout.tsx:37
  • Brand is grayscale foundation; blue bar in mobile chrome/PWA splash is off-brand.
  • Fix: Change to primary dark (#3f3f46 zinc-700) or foreground (#171717).

UX12. global-error.tsx h2 uses font-semibold and a raw button [medium]

  • File: app/global-error.tsx:17-28
  • Inline-classed <button> instead of Button component; text-xl font-semibold instead of font-display.
  • Fix: Use Button; change heading to font-display font-medium.

UX13. Dashboard error page has Swedish typo "Nagot gick fel" [medium]

  • File: app/(dashboard)/error.tsx:20
  • Missing å.
  • Fix: Change to "Något gick fel".

UX14. Invoice list empty state (tab-filtered) does not use EmptyState component [medium]

  • File: app/(dashboard)/invoices/page.tsx:254-261
  • Ad-hoc <div> with bare icon + <h3>; inconsistent with shared EmptyState pattern (muted circle background + action).
  • Fix: Replace with <EmptyState icon={Receipt} title="Inga fakturor..." description="Prova att byta flik..." />.

UX15. Supplier-invoice table row not fully clickable — inconsistent with invoices/expenses [medium]

  • File: app/(dashboard)/supplier-invoices/page.tsx:160-183
  • Invoice-number link is inside cell; clicking row outside link has no action.
  • Fix: <tr onClick> + cursor-pointer OR extract to card-list pattern.

UX16. Dashboard income/expense detail cards use p-5 — off rhythm [low]

  • File: components/dashboard/DashboardContent.tsx:519,538
  • Other cards use p-4; 4/6/8 step rhythm documented.
  • Fix: Change to p-4 or p-6.

UX17. InvoiceInboxWorkspace uses border-red-500 text-red-700 — raw red, not destructive token [low]

  • File: components/extensions/general/InvoiceInboxWorkspace.tsx:212,221,235
  • Confidence level badges should use design-system --destructive.
  • Fix: border-destructive/25 bg-destructive/5 text-destructive.

UX18. InvoiceInboxWorkspace EU validation info box uses raw blue [low]

  • File: components/extensions/general/InvoiceInboxWorkspace.tsx:1201-1208
  • No blue semantic in palette.
  • Fix: border-border bg-muted/40 text-foreground OR subtle primary tint (primary/15, primary/5, text-primary).

A11y agent

A1. Icon-only destructive "Delete" buttons missing aria-label [high]

  • File: app/(dashboard)/supplier-invoices/[id]/page.tsx:202, suppliers/[id]/page.tsx:167, invoices/new/page.tsx:581, expenses/new/page.tsx:534, components/bookkeeping/ChartOfAccountsManager.tsx:430, CorrectionEntryDialog.tsx:227, settings/CompanyMembersSection.tsx:250,299, CalendarHeader.tsx:59,62
  • Only 9 of ~35 icon buttons have aria-label; screen readers announce no meaningful name.
  • Fix: Add Swedish aria-label to every icon-only button: "Ta bort rad", "Redigera konto", "Gå tillbaka", etc.

A2. Framer Motion animations not gated by prefers-reduced-motion [high]

  • File: components/transactions/TransactionInboxCard.tsx:74, SwipeCategorizationView.tsx:631
  • JS-driven animations bypass CSS @media (prefers-reduced-motion: reduce) override.
  • Fix: Import useReducedMotion from framer-motion. transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }}. Disable drag on SwipeCategorizationView.

A3. Progress bars lack accessible label — loading invisible to SR [high]

  • File: components/import/SIEUploadStep.tsx:97, BankFileUploadStep.tsx:166, BatchCategorySelector.tsx:56, ArcimMigrationWorkspace.tsx:988, app/(dashboard)/import/page.tsx:239
  • "progress bar, 0 percent" with no context.
  • Fix: Pass aria-label="Importerar SIE-fil" etc. Consider outer <div role="status" aria-live="polite"> for dynamic updates.

A4. Tag-input remove button has no focus ring and no accessible name [high]

  • File: components/ui/tag-input.tsx:94
  • focus:outline-none removes ring with no replacement; only content is <X> icon.
  • Fix: Replace with focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1; add aria-label={Ta bort ${tag}}.

A5. Data tables missing scope="col" on th headers [medium]

  • File: app/(dashboard)/supplier-invoices/page.tsx:148, suppliers/[id]/page.tsx:260, supplier-invoices/[id]/page.tsx:340, supplier-invoices/new/page.tsx:394
  • Screen readers can't reliably associate cells with headers. shadcn TableHead doesn't default-add either.
  • Fix: Add scope="col" to all raw <th>; include as default in TableHead component.

A6. Collapsible "Övrigt" nav section button missing aria-expanded [medium]

  • File: components/dashboard/DashboardNav.tsx:307
  • Rotates chevron visually but screen readers can't know open/closed state.
  • Fix: Add aria-expanded={isOvrigtExpanded}; optional aria-controls.

A7. Mobile navigation bottom sheet missing aria-modal [medium]

  • File: components/dashboard/DashboardNav.tsx:504
  • role="dialog" + aria-label present but no aria-modal; JAWS/iOS VoiceOver may not restrict reading; no explicit focus trap.
  • Fix: Add aria-modal="true"; use Radix Dialog primitive for proper focus trap.

A8. --muted-foreground at 40% likely fails 4.5:1 on white/card [medium]

  • File: app/globals.css:25; components/dashboard/DashboardNav.tsx:221
  • #666666 on #FCFCFC ≈ 4.48:1 borderline fail. Placeholder/60 variant ≈ 2.2:1 fails WCAG 1.4.3.
  • Fix: Increase to 0 0% 38% (#616161, ~4.74:1 on white). Dedicated placeholder token.

A9. SelectTrigger uses focus: instead of focus-visible: [medium]

  • File: components/ui/select.tsx:21
  • Focus ring on mouse click; inconsistent with inputs/buttons/tabs using focus-visible. ring-primary/35 also may be too low contrast.
  • Fix: focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-primary/60.

A10. Loading spinners lack accessible label across ~20+ pages [medium]

  • File: app/(public)/invoice-action/[token]/page.tsx:91, customers/[id]/page.tsx:176, invoices/[id]/page.tsx:338, companies/new/page.tsx:199 and ~20 others
  • <Loader2 className="animate-spin" /> inside unannotated container.
  • Fix: Wrap in <div role="status" aria-label="Laddar innehåll"> or add aria-live="polite".

A11. Status badges use raw Tailwind colors without icon/text pairing check [medium]

  • File: app/(dashboard)/supplier-invoices/[id]/page.tsx:23, suppliers/[id]/page.tsx:124
  • bg-blue-100 text-blue-800 ≈ 4.3:1; bg-yellow-100 text-yellow-800 ≈ 3.5:1 — likely fails 4.5:1 for normal text.
  • Fix: Use design system Badge with semantic variants (success/warning/destructive/secondary). Also fixes dark-mode.

A12. Skip-to-content link absent on 2 layout fallback branches [medium]

  • File: app/(dashboard)/layout.tsx:86,134
  • Only third render path includes "Hoppa till innehåll"; fallback branches force keyboard users to tab through nav.
  • Fix: Extract skip link component; repeat in all three paths before DashboardNav.

A13. animate-slide-up CSS class under prefers-reduced-motion [medium]

  • File: app/globals.css:269
  • Global @media wildcard * covers most but transition-all on Button and other effects need manual audit.
  • Fix: Verify in browser with prefers-reduced-motion: reduce that auth slide-up / button transitions / modal animations all resolve immediately.

A14. Lucide icons inside icon+text buttons not marked aria-hidden [low]

  • File: Broadly across DashboardNav, invoices/new/page, login, etc. (100+ instances)
  • Implicit