1583e302da
* 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.
1720 lines
114 KiB
Markdown
1720 lines
114 KiB
Markdown
# 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`](https://github.com/erp-mafia/gnubok/issues). 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](https://github.com/erp-mafia/gnubok/issues/243)
|
||
|
||
**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](https://github.com/erp-mafia/gnubok/issues/193)
|
||
|
||
**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](https://github.com/erp-mafia/gnubok/issues/107)
|
||
|
||
**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](https://github.com/erp-mafia/gnubok/issues/243).*
|
||
|
||
**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 <title> causes double-reading ("Log out Logga ut") on some screen readers.
|
||
- Fix: Add aria-hidden="true" to decorative icons paired with visible text. Wrapper component or ESLint rule.
|
||
|
||
**A15. Dialog close button label is "Close" (English) not "Stäng"** [low]
|
||
- File: `components/ui/dialog.tsx:48`
|
||
- Jarring in sv-lang app. SandboxBanner and ApiKeysPanel close buttons use Swedish — this one is inconsistent.
|
||
- Fix: Change `<span className="sr-only">Close</span>` to `Stäng`.
|
||
|
||
**A16. MFA enroll copy-secret button has no aria-label** [low]
|
||
- File: `app/(auth)/mfa/enroll/page.tsx:236`
|
||
- Icon-only button (Check/Copy) with no aria-label.
|
||
- Fix: `aria-label={copied ? 'Kopierat!' : 'Kopiera hemlig nyckel'}`.
|
||
|
||
**A17. --border token at 90% lightness on white card fails 3:1 UI contrast** [low]
|
||
- File: `app/globals.css:33`
|
||
- `#E6E6E6` on `#FCFCFC` ≈ 1.45:1; inputs/selects/textareas borders hard to see for low-vision users.
|
||
- Fix: Increase to `0 0% 60%` (~3.08:1 on white) or `0 0% 70%` (~2.27:1 borderline).
|
||
|
||
**A18. toast TOAST_REMOVE_DELAY effectively infinite but ToastClose X has no aria-label** [low]
|
||
- File: `components/ui/use-toast.tsx:7`, `components/ui/toast.tsx:81`
|
||
- Radix ToastPrimitive.Close may provide default but should verify.
|
||
- Fix: Add `aria-label="Stäng"` explicitly consistent with other close buttons.
|
||
|
||
### Mobile-UX agent
|
||
|
||
**M1. Batch-mode floating action bar ignores safe-area-inset-bottom** [high]
|
||
- File: `app/(dashboard)/transactions/page.tsx:864`
|
||
- bottom-20 (80px) above bottom nav (64px) but doesn't add env(safe-area-inset-bottom). iPhone home indicator overlap possible.
|
||
- Fix: `style={{ bottom: 'calc(5rem + env(safe-area-inset-bottom, 0px))' }}`.
|
||
|
||
**M2. Multiple financial number inputs missing inputMode="decimal"** [high]
|
||
- File: Key sites — `app/(dashboard)/expenses/new/page.tsx:492,601,700`, `expenses/[id]/page.tsx:478`, `supplier-invoices/new/page.tsx:343`, `CorrectionEntryDialog.tsx:245,254`, `CustomerForm.tsx:293`, `InvoiceSettingsForm.tsx:34,42`, `salary/employees/new/page.tsx:174,202,207,255,264`, `salary/runs/new/page.tsx:76,80`, `TransactionForm.tsx:116`
|
||
- iOS shows default alphanumeric keyboard instead of numeric/decimal pad. Invoice-new does this correctly — apply same pattern.
|
||
- Fix: Add `inputMode="decimal"` to currency fields, `inputMode="numeric"` to integer fields (invoice numbers, payment terms).
|
||
|
||
**M3. Inline badge-button touch targets below 44px in transaction history** [high]
|
||
- File: `components/transactions/TransactionHistoryList.tsx:146,158`
|
||
- "Ej bokförd" / "Möjlig match: Faktura X" styled as badges (`px-2.5 py-0.5 text-xs`) ≈ 20px height.
|
||
- Fix: Replace with `<Button size="sm">` (h-9 = 36px) or add `py-2` or wrap in `min-h-[44px] flex items-center` container.
|
||
|
||
**M4. h-8 w-8 (32px) delete buttons without 44px expansion** [medium]
|
||
- File: `components/settings/CompanyMembersSection.tsx:252,301`
|
||
- Remove-member / revoke-invite lack `min-h-[44px] min-w-[44px]` overrides used elsewhere (e.g. JournalEntryForm).
|
||
- Fix: Add min-44px overrides OR use default `size="icon"` which renders h-10 w-10.
|
||
|
||
**M5. PaymentBookingDialog desktop table lacks inputMode and sub-44px delete** [medium]
|
||
- File: `components/invoices/PaymentBookingDialog.tsx:317,326,337`
|
||
- Desktop table type="number" inputs without inputMode="decimal"; delete button h-8 w-8 without 44px minimum.
|
||
- Fix: Add inputMode="decimal"; delete button h-10 w-10 or min-44px.
|
||
|
||
**M6. PWA manifest locks orientation to portrait-primary** [medium]
|
||
- File: `public/manifest.json:9`
|
||
- Tablets/landscape data entry blocked; a11y guidelines discourage orientation lock.
|
||
- Fix: Remove orientation key or set to "any".
|
||
|
||
**M7. AccountCombobox dropdown items ~28-32px height** [medium]
|
||
- File: `components/bookkeeping/AccountCombobox.tsx:201`
|
||
- py-1.5 too close together for reliable mobile touch.
|
||
- Fix: `py-3` on mobile or conditional `py-2 sm:py-1.5`; consider bottom-sheet on mobile.
|
||
|
||
**M8. Batch-select Checkbox 16×16px without touch target expansion** [medium]
|
||
- File: `components/transactions/TransactionInboxCard.tsx:100`
|
||
- Default h-4 w-4; card tap area works but checkbox itself unclear.
|
||
- Fix: Wrap in `<label>` with min-h-[44px] min-w-[44px] flex items-center justify-center -m-3 p-3. Or h-5 w-5.
|
||
|
||
**M9. Mobile bottom nav only shows 3 items leaving Reports and Expenses behind drawer** [low]
|
||
- File: `components/dashboard/DashboardNav.tsx:174`
|
||
- Pinned tabs: Översikt, Fakturor, Transaktioner, Meny. Reports/Expenses/Bookkeeping require drawer.
|
||
- Fix: Consider user-configurable or MRU-based tabs; or add Reports as 4th tab.
|
||
|
||
### Logging agent
|
||
|
||
**L1. Journal entry commit has no info-level log** [high]
|
||
- File: `lib/bookkeeping/engine.ts:244-281`
|
||
- Most critical operation silent; debugging "where did voucher A-2341 come from?" requires DB reconstruction.
|
||
- Fix: `createLogger('bookkeeping-engine')`; log info on commit with `{ companyId, entryId, voucherNumber, voucherSeries }`.
|
||
|
||
**L2. API key creation has no success log** [high]
|
||
- File: `app/api/settings/api-keys/route.ts:80-105`
|
||
- Security-sensitive; no trace of creation or revocation.
|
||
- Fix: `log.info({ userId, companyId, keyPrefix, name }, 'API key created')` after successful insert.
|
||
|
||
**L3. Company creation has no success log** [high]
|
||
- File: `lib/company/actions.ts:60-147`
|
||
- Significant business event untraced.
|
||
- Fix: After revalidatePath, `log.info({ companyId, userId, entityType }, 'Company created via onboarding')`.
|
||
|
||
**L4. Send invoice has no info log on success** [high]
|
||
- File: `app/api/invoices/[id]/send/route.ts:158-233`
|
||
- No production trace of who sent what to whom; errors logged but not success.
|
||
- Fix: After eventBus.emit, `log.info({ invoiceId, invoiceNumber, to, companyId }, 'Invoice sent')`.
|
||
|
||
**L5. Missing login success/failure structured log** [high]
|
||
- File: `app/(auth)/auth/callback/route.ts:1-140`
|
||
- Security-sensitive; no server trace of auth callback outcomes.
|
||
- Fix: `createLogger('auth/callback')`; `log.info({ userId }, 'Auth callback success')` and `log.warn({ type, error }, 'Auth callback failed')`.
|
||
|
||
**L6. Logger missing debug level — all verbose logs forced to console.log** [medium]
|
||
- File: `lib/logger.ts:9`
|
||
- `LogLevel = 'info' | 'warn' | 'error'`; no debug. Hot-path files promote trace to info (noise) or fall back to console.log.
|
||
- Fix: Add `debug(message, ...args)` gated by `NODE_ENV === 'development'` or `LOG_LEVEL` env var.
|
||
|
||
**L7. Logger uses varargs instead of structured context objects** [medium]
|
||
- File: `lib/logger.ts:21-29`
|
||
- Space-separated text output; log aggregators (Vercel/Axiom/Datadog) can't index structured fields. Pino/Bunyan convention impossible.
|
||
- Fix: Change signature to `info(context: Record<string, unknown> | string, message?: string)` emitting JSON in production, or integrate pino.
|
||
|
||
**L8. EventBus error handler uses console.error directly** [medium]
|
||
- File: `lib/events/bus.ts:54`
|
||
- High-priority signal bypasses logger; missing module prefix / Sentry scope.
|
||
- Fix: `const log = createLogger('event-bus')` at module level; replace console.error.
|
||
|
||
**L9. Storno service uses console.error instead of logger** [medium]
|
||
- File: `lib/core/bookkeeping/storno-service.ts:31,38`
|
||
- Errors lose company/entry context.
|
||
- Fix: `const log = createLogger('storno')` with structured `{ entryId, error }`.
|
||
|
||
**L10. document-matcher — 15+ chatty console.log calls at info-equivalent level** [medium]
|
||
- File: `lib/documents/document-matcher.ts:55-309`
|
||
- Per-document-per-candidate lines; hundreds per sweep in production.
|
||
- Fix: Move to debug level once L6 added; use `createLogger('document-matcher')`.
|
||
|
||
**L11. batch-match.ts — console.log sweep progress at info level** [medium]
|
||
- File: `lib/documents/batch-match.ts:47-107`
|
||
- Per-item skip logs noise; summary at line 107 correctly info.
|
||
- Fix: `createLogger('batch-match')`; promote summary; demote per-item to debug.
|
||
|
||
**L12. sie-import.ts — console.log/info/error throughout** [medium]
|
||
- File: `lib/import/sie-import.ts:863,939,1368,1844,1856,1935,1938,1965`
|
||
- console.info bypasses module prefix + env filtering. Error cases lack structured context.
|
||
- Fix: `createLogger('sie-import')`; pass `{ companyId, batchNumber, attempt }` to error calls.
|
||
|
||
**L13. Cron jobs — console.log/error without module prefix** [medium]
|
||
- File: `app/api/invoices/reminders/cron/route.ts:12,20,24,39`; `events/cleanup/cron/route.ts:31,35`; `deadlines/status/cron/route.ts:33,45`; `sandbox/cleanup/cron/route.ts:34,38`; `tax-deadlines/cron/route.ts:33,41`
|
||
- Reminders cron has no module tag; errors lack `durationMs` for performance audit.
|
||
- Fix: `createLogger` per cron; `{ durationMs, processed, sent, failed }`.
|
||
|
||
**L14. enable-banking/lib/sync.ts — console.log for sync start/result** [medium]
|
||
- File: `extensions/general/enable-banking/lib/sync.ts:52,66,94,112`
|
||
- Critical operational path unfilterable.
|
||
- Fix: `createLogger('enable-banking/sync')`; log.info/log.error.
|
||
|
||
**L15. Invoice send failure missing companyId and invoiceId context in error logs** [medium]
|
||
- File: `app/api/invoices/[id]/send/route.ts:159,174,198,216,232`
|
||
- No correlation to specific invoice from Vercel log.
|
||
- Fix: Include `{ invoiceId, companyId }` in every error log; `createLogger('api/invoices/send')`.
|
||
|
||
**L16. CompanyMembersSection logs invite URL in DEV** [low]
|
||
- File: `components/settings/CompanyMembersSection.tsx:93`
|
||
- `console.log('[DEV] Company invite URL:', data.data.inviteUrl)` exposes `gnubok_inv_` token plaintext in browser console.
|
||
- Fix: Truncate token: `inviteUrl.replace(/gnubok_inv_\S+/, '[TOKEN]')` or use server-side log.
|
||
|
||
**L17. /api/log lacks rate limiting and uses JSON.stringify(extra)** [low]
|
||
- File: `app/api/log/route.ts:8`
|
||
- Unauthenticated; malicious actor can flood logs. `JSON.stringify(extra)` on Error returns `{}` (non-enumerable props).
|
||
- Fix: Add auth check + rate limiting; use `createLogger('onboarding-log')` with structured context.
|
||
|
||
**L18. Settings tax deadline logs — no module prefix** [low]
|
||
- File: `app/api/settings/route.ts:136,138`
|
||
- Indistinguishable in logs from other routes.
|
||
- Fix: `createLogger('api/settings')`; include companyId.
|
||
|
||
**L19. provider-data-fetcher.ts — logs object keys (potential data leak)** [low]
|
||
- File: `lib/providers/provider-data-fetcher.ts:153,251`
|
||
- Diagnostic dead debug code.
|
||
- Fix: Remove or gate behind debug with `createLogger('provider-data-fetcher')`.
|
||
|
||
**L20. register page logs supabaseUrl and user metadata in browser console** [low]
|
||
- File: `app/(auth)/register/page.tsx:189,223`
|
||
- Client-side debug traces outputting supabaseUrl/userId/email/confirmationSentAt etc.; not guarded by NODE_ENV.
|
||
- Fix: Wrap with `if (process.env.NODE_ENV === 'development')` or remove.
|
||
|
||
### Testing agent
|
||
|
||
**T1. Fiscal period lifecycle API routes have no route-level tests** [high]
|
||
- File: `app/api/bookkeeping/fiscal-periods/[id]/close/route.ts:1` + 4 others (`lock`, `year-end`, `opening-balances`, `currency-revaluation`)
|
||
- Most sensitive operations; 401/400/500 unvalidated. year-end GET runs validate+preview in parallel — untested.
|
||
- Fix: Add `__tests__/route.test.ts` for each: 401 unauth, 400 service throw, 200 success. `vi.mock` period/year-end services.
|
||
|
||
**T2. All report API routes (/api/reports/*) have no route-level tests** [high]
|
||
- File: `app/api/reports/vat-declaration/route.ts:1` + 15 others
|
||
- Generators tested in `lib/reports/__tests__/` but route-level 401 bypass or schema mis-mapping undetected. Legal risk.
|
||
- Fix: Start with vat-declaration, balance-sheet, income-statement: 401, query param 400, happy-path shape `{ data: { rutor: {...} } }`.
|
||
|
||
**T3. All salary API routes except /agi/submit have no tests** [high]
|
||
- File: `app/api/salary/runs/[id]/book/route.ts:1` + ~20 others
|
||
- `/book` integrates salary-entries producing multi-line journal entries — zero coverage at any level.
|
||
- Fix: Unit test salary-entries.ts; route tests for book/approve/calculate: 401, 400 (wrong status), 200.
|
||
|
||
**T4. lib/salary/salary-entries.ts has zero tests despite bookkeeping integration** [high]
|
||
- File: `lib/salary/salary-entries.ts:1`
|
||
- Errors produce unbalanced or mis-posted entries across 7210/2710/2731/2741/2742/2920/2940.
|
||
- Fix: `lib/salary/__tests__/salary-entries.test.ts`: balanced journal assertions, account mappings, gross/net/vacation/pension paths.
|
||
|
||
**T5. reverseEntry in lib/bookkeeping/engine.ts has no unit test** [high]
|
||
- File: `lib/bookkeeping/engine.ts:333`
|
||
- Only storno path from API; swap-debit-credit / not-posted guard / reverses_id link all untested. Route test mocks reverseEntry.
|
||
- Fix: `describe('reverseEntry')` in engine.test.ts: throws on non-posted, creates swapped lines, sets reverses_id.
|
||
|
||
**T6. Salary report files (avgifter-basis, salary-journal, vacation-liability) have zero tests** [high]
|
||
- File: `lib/reports/avgifter-basis.ts:1` + 2 siblings
|
||
- Legally required for AGI reconciliation + year-end semesterlöneskuld (2920/2940).
|
||
- Fix: Test standard/youth/reduced rates; zero-row case. Pattern from `lib/reports/__tests__/vat-declaration.test.ts` with sequential mock queue.
|
||
|
||
**T7. engine.test.ts omits eventBus.clear() in beforeEach** [medium]
|
||
- File: `lib/bookkeeping/__tests__/engine.test.ts:1`
|
||
- emit mock call count persists across tests; voucher-atomicity split file compounds issue.
|
||
- Fix: `beforeEach(() => { vi.clearAllMocks() })` at top; `eventBus.clear()` if real bus used.
|
||
|
||
**T8. match-invoice route test omits eventBus.clear** [medium]
|
||
- File: `app/api/transactions/[id]/match-invoice/__tests__/route.test.ts:60`
|
||
- Mock call count accumulates across tests.
|
||
- Fix: `eventBus.emit.mockClear()` or `vi.clearAllMocks()` in beforeEach.
|
||
|
||
**T9. reverse route test is missing 500 error path** [medium]
|
||
- File: `app/api/bookkeeping/journal-entries/[id]/reverse/__tests__/route.test.ts:1`
|
||
- 401, 400 (engine throws Error), 200 covered but 500 (non-Error throw) untested.
|
||
- Fix: Add `it("returns 500 on unexpected error")` with non-string reject.
|
||
|
||
**T10. No coverage threshold configured in vitest.config.ts** [medium]
|
||
- File: `vitest.config.ts:1`
|
||
- PR dropping lib/bookkeeping from 80% → 40% passes silently; CI has no --coverage flag.
|
||
- Fix: Add `coverage: { provider: 'v8', thresholds: { lines: 80, functions: 80 }, include: ['lib/**', 'app/api/**'], exclude: [...] }`. Add --coverage to npm test in CI.
|
||
|
||
**T11. lib/transactions/category-suggestions.ts has zero tests** [medium]
|
||
- File: `lib/transactions/category-suggestions.ts:1`
|
||
- Pure-function priority logic (rule > pattern > history); untested.
|
||
- Fix: Test priority ordering, all-zero uncategorized.
|
||
|
||
**T12. lib/bookkeeping/propose-send-lines.ts has zero tests** [medium]
|
||
- File: `lib/bookkeeping/propose-send-lines.ts:1`
|
||
- Pure function; ideal for unit testing; handles mixed VAT + multi-currency.
|
||
- Fix: Tests for single-rate, mixed-rate, export, EUR invoice. Assert FormLine account_number + amount.
|
||
|
||
**T13. booking-templates.test.ts weak toBeDefined assertions** [low]
|
||
- File: `lib/bookkeeping/__tests__/booking-templates.test.ts:92,491,499,506,514,523,532,538,545`
|
||
- `expect(t).toBeDefined()` then `t!.debit_account` — safe but confusing TypeError on failure.
|
||
- Fix: `if (!t) throw new Error('Template not found: ...')` OR use `expect(t?.debit_account).toBe(...)` optional chaining.
|
||
|
||
**T14. makeBankConnection fixture listed in CLAUDE.md but absent** [low]
|
||
- File: `tests/helpers.ts:762`
|
||
- Import `makeBankConnection` from @/tests/helpers returns undefined or TypeScript error.
|
||
- Fix: Add minimal BankConnection factory.
|
||
|
||
**T15. describe.skipIf in income-statement-bokio integration test** [low]
|
||
- File: `lib/reports/__tests__/income-statement-bokio-integration.test.ts:112`
|
||
- Silently skipped in CI; fixtures from `dev_docs/example_sie/` (gitignored).
|
||
- Fix: Add comment documenting intentional skip; commit synthetic SIE fragment for 8999 edge case.
|
||
|
||
### Performance agent
|
||
|
||
**PE1. Dashboard fires 20 parallel DB queries on every page load** [high]
|
||
- File: `app/(dashboard)/page.tsx:57-104`
|
||
- 6 separate COUNTs + journal_entry_lines YTD join + unbounded document_attachments scan.
|
||
- Fix: Replace document_attachments count with RPC aggregate; cap YTD fetch with `.limit()`; combine related COUNTs into single RPC.
|
||
|
||
**PE2. document_attachments unbounded fetch on dashboard** [high]
|
||
- File: `app/(dashboard)/page.tsx:99`
|
||
- No `.limit()`; materializes tens of thousands of rows to build a Set.
|
||
- Fix: `COUNT(DISTINCT journal_entry_id)` via RPC; or `.limit(10000)` safety cap.
|
||
|
||
**PE3. No partial index on transactions(company_id) WHERE is_business IS NULL** [high]
|
||
- File: `supabase/migrations/20260330130000...:1258-1259`
|
||
- Uncategorized count on every page load scans all company transactions.
|
||
- Fix: `CREATE INDEX idx_transactions_company_uncategorized ON transactions (company_id) WHERE is_business IS NULL`.
|
||
|
||
**PE4. journal_entry_lines lacks company_id — RLS join required for every report query** [high]
|
||
- File: `supabase/migrations/20240101000002_bookkeeping.sql:135-186`
|
||
- Every report nested-loop joins across millions of lines; no composite index on (journal_entry_id, account_number).
|
||
- Fix: Add company_id with backfill from journal_entries; composite index (company_id, account_number) OR covering index on (journal_entry_id, account_number, debit_amount, credit_amount).
|
||
|
||
**PE5. Transactions page is 988-line client component with no code splitting** [medium]
|
||
- File: `app/(dashboard)/transactions/page.tsx:1-988`
|
||
- Imports Framer Motion + 12+ sub-components + 1,623-line booking-templates data + template-library + category-mapping + counterparty-templates. All on first render.
|
||
- Fix: Server component shell; `dynamic()` for on-demand dialogs (SwipeCategorizationView, TransactionBookingDialog); fetch booking-templates via API instead of bundling.
|
||
|
||
**PE6. BAS_REFERENCE (14k-line static dataset) imported in client component** [medium]
|
||
- File: `components/import/OpeningBalanceEditStep.tsx:10`
|
||
- Entire BAS 2026 chart (8 files, 14,274 lines) serialized into import-page JS bundle.
|
||
- Fix: API endpoint `/api/bookkeeping/accounts/reference` + lazy fetch; OR `dynamic(() => import('./OpeningBalanceEditStep'), { ssr: false })`.
|
||
|
||
**PE7. TransactionHistoryList — no useMemo, no virtualization** [medium]
|
||
- File: `components/transactions/TransactionHistoryList.tsx:33-40`
|
||
- `filter()` every render; 200+ row Cards in DOM.
|
||
- Fix: `useMemo([transactions, searchTerm, filter])`. For 200+ items: `@tanstack/react-virtual`. React.memo on row component.
|
||
|
||
**PE8. pdfjs-dist in production bundle but unused** [medium]
|
||
- File: `package.json:49`
|
||
- `pdfjs-dist@5.4.530` (~3 MB); no imports anywhere in source.
|
||
- Fix: Remove; or move to disabled extension's own package.json if conditionally used.
|
||
|
||
**PE9. Recharts components not lazy-loaded on reports page** [medium]
|
||
- File: `app/(dashboard)/reports/page.tsx:16-21`
|
||
- TrialBalanceChart + VatCompositionChart + IncomeExpenseChart eager-loaded; recharts ~200 KB gzipped.
|
||
- Fix: `dynamic(() => import('@/components/reports/TrialBalanceChart'), { ssr: false })` for all three.
|
||
|
||
**PE10. Dashboard layout runs 5 sequential DB queries (2 chained)** [medium]
|
||
- File: `app/(dashboard)/layout.tsx:48-63`
|
||
- team_members then teams (if team_id exists). Every page load for consultant accounts.
|
||
- Fix: Join via `.select('team_id, role, teams:team_id(*)').eq('user_id', user.id).limit(1).maybeSingle()` — single query.
|
||
|
||
**PE11. VIES VAT validation has no cache** [low] (dupe of Rate-limits RL3)
|
||
- File: `lib/vat/vies-client.ts:112-119`
|
||
- 10s timeout on every EU invoice creation; VIES commonly slow.
|
||
- Fix: `next: { revalidate: 86400 }` on fetch OR vat_validation_cache table.
|
||
|
||
**PE12. TransactionInboxCard re-renders on every parent state change** [low]
|
||
- File: `components/transactions/TransactionInboxCard.tsx:36`
|
||
- Not wrapped in React.memo; parent passes inline callbacks. 50+ motion.div re-renders per categorization.
|
||
- Fix: `React.memo(TransactionInboxCard)`; stabilize inline callbacks with `useCallback` in parent.
|