Jakob Wennberg 8fd3f112f8 fix: surface active TIC companies + block duplicate org numbers (#344)
* fix: surface active TIC companies + block duplicate org numbers

Three fixes from live-prod testing:

1. Enrichment filter hid the user's directorships. Now accepts both
   Completed and PartiallyCompleted status from TIC (tenants without
   CompanyRoles enabled still get SPAR) and the /select-company role
   filter no longer requires companyStatus === 'Aktivt' — real TIC
   payloads have been observed with different values, and positionEnd
   alone is the authoritative "currently a director" signal. Added
   PII-free diagnostic logs so the next shape-mismatch is debuggable
   from Vercel logs without a round trip.

2. Manual wizard silently allowed duplicate org numbers. Added:
   - findExistingCompanyByOrgNumber helper in actions.ts (service role,
     bypasses RLS to see cross-tenant rows)
   - Server-side guard in createCompanyFromOnboarding — returns
     'org_number_exists' before the create RPC so we don't leave ghost
     companies
   - New /api/company/check-org-number endpoint for debounced client
     checks
   - Warning + disabled submit in Step2CompanyDetails
   - Friendly error toasts in WelcomeOnboarding + BankIdCompanyPicker
   - Mirror cleaned org_number onto companies.org_number on creation so
     future duplicate checks and lookups are reliable

3. /onboarding ignored ?org_number= when the picker routed there as a
   fallback. Now reads searchParams and pre-fills settings; also fixed
   a latent bug where Step1's entity-type change wiped the pre-fill on
   *first* selection (it should only reset on a genuine change).

Tests: duplicate-org guard (with formatted-input normalization),
check-org-number route (auth + 400 + exists true/false +
normalization).

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

* fix: address PR review feedback on duplicate-org guard

Greptile P1 findings + swedish-compliance feedback:

- findExistingCompanyByOrgNumber now throws on Supabase error instead
  of silently returning null. Previously a DB outage or RLS
  misconfiguration would bypass the entire duplicate guard and allow
  duplicates through.
- createCompanyFromOnboarding catches the throw and returns a
  user-facing error ("Kunde inte verifiera organisationsnummer"),
  failing closed instead of open.
- companies.update({ org_number }) error is now checked and triggers a
  rollback. Silent failure would leave the company without an
  org_number, breaking all future duplicate checks for that entity.
- New normalizeOrgNumber helper validates 10- or 12-digit input,
  strips the century prefix for 12-digit personnummer form, and
  rejects anything else. Malformed input would have corrupted SIE4
  (#ORGNR) and SRU (INFO.SRU) exports downstream.
- /select-company now uses loose `== null` for positionEnd — TIC has
  been observed returning `undefined` for open-ended positions, which
  strict `=== null` would silently filter out. Documented the two
  downstream isCeased guards so future maintainers don't remove one
  without the other.
- createCompanyFromTicRole refuses to provision when lookup.isCeased
  (BFL 2 kap — bokföringsskyldighet ends at avregistrering).
  BankIdCompanyPicker surfaces this client-side too.
- WelcomeOnboarding + BankIdCompanyPicker recognise new error codes:
  org_number_invalid, company_ceased.

Tests: +4 cases covering malformed input rejection, fail-closed
behaviour on DB error, 12-digit personnummer normalization, and the
ceased-company refusal path. Full suite: 2306 passing.

Out of scope for this PR (follow-up):
- Partial unique index on companies(org_number) WHERE archived_at IS
  NULL. Closes the race-condition window but needs a migration plus
  any existing-duplicate cleanup — too risky for this hotfix.
- Rate limiting on /api/company/check-org-number. Endpoint is
  auth-gated so not an immediate concern.

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

* fix: add Luhn validation and extract org-number normalization

Third round of PR review feedback (swedish-compliance):

- Add Luhn-10 check-digit validation to normalizeOrgNumber. Rejects
  structurally invalid org_numbers (wrong check digit) at the boundary
  instead of letting them propagate into SIE4 #ORGNR and SRU INFO.SRU,
  where Skatteverket and receiving accounting systems would reject
  them later anyway. Reuses the existing luhnValidate helper from
  lib/bankgiro/luhn.ts (Bankgirot 10-modulen — same algorithm applies
  to both Bolagsverket org numbers and Swedish personnummer).

- Extract normalizeOrgNumber into lib/company-lookup/normalize-org-number.ts
  so the server action and /api/company/check-org-number use the same
  rule. Previously the API route only stripped hyphens/spaces, so a
  12-digit input would miss a stored 10-digit duplicate and mislead the
  client debounce check ("not a duplicate" → submit → server rejects).

- /api/company/check-org-number now returns exists=false for
  Luhn-invalid input rather than querying the DB. The submit-time
  server action surfaces org_number_invalid, which is the right place
  for the error.

Test coverage: dedicated normalize-org-number.test.ts (10 cases
covering both-lengths, Luhn, whitespace tolerance, garbage). Updated
existing tests to use Luhn-valid numbers (real Volvo 5560125790,
synthetic personnummer 8001011231). New failing-Luhn test in
actions.test.ts. New 12-digit-normalization and
luhn-invalid-returns-false tests in route.test.ts.

Full suite: 2315 passing.

Not fixed (out of scope for this hotfix):
- 10↔12 digit round-trip fragility for personnummer born 2000+. This
  is a codebase-wide architectural choice (see lib/skatteverket/format.ts
  which uses a two-digit-year heuristic to choose 19/20 at export).
  Migrating to 12-digit storage is a separate refactor.
- Server-side re-fetch of TIC /lookup for isCeased. The trust boundary
  here is user-to-their-own-onboarding, not adversarial; doubling TIC
  API cost isn't proportionate.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:47:58 +02:00
2026-04-20 10:49:59 +02:00
2026-04-21 21:39:57 +02:00
2026-04-20 21:03:23 +02:00
2026-02-19 09:48:02 +01:00

gnubok

Open-source Swedish accounting software for sole traders (enskild firma) and limited companies (aktiebolag).

License: AGPL-3.0-or-later

What is gnubok?

gnubok implements double-entry bookkeeping compliant with Swedish accounting law (Bokforingslagen). It supports the BAS 2026 chart of accounts, handles VAT declarations (momsdeklaration), SIE import/export, and enforces 7-year document retention. Built for sole traders and limited companies operating in Sweden.

Features

  • Double-entry bookkeeping -- BAS 2026 chart of accounts, draft/commit workflow, sequential voucher numbering
  • Invoicing -- Create, send, and track invoices with mixed VAT rates and PDF generation
  • Bank reconciliation -- PSD2 bank connection via Enable Banking, 4-pass automatic matching
  • VAT declaration -- SKV 4700 form mapping, per-rate breakdown, EU/export handling
  • Tax reports -- NE-bilaga, INK2, SRU export for Skatteverket
  • Supplier invoices -- Registration, payment tracking, input VAT deduction
  • Document archive -- SHA-256 integrity, 7-year retention enforcement, full archive ZIP export
  • SIE import/export -- Standard Swedish accounting interchange format
  • Extension system -- Opt-in plugins for AI categorization, receipt OCR, email, calendar, and more

Self-Hosting

git clone https://github.com/erp-mafia/gnubok.git
cd gnubok
./setup.sh              # Prompts for Supabase credentials, generates .env
docker compose up -d

You need a Supabase project and must apply the database migrations before first use. See SELF-HOSTING.md for the full step-by-step guide, including Supabase setup, auth configuration, optional features (AI, email, push notifications), and troubleshooting.

Development Setup

Prerequisites: Node.js 20+, a Supabase project.

npm install
npm run dev       # Start dev server (auto-generates extension registry)
npm test          # Run tests
npm run build     # Production build
npm run lint      # ESLint

Tech Stack

  • Framework: Next.js 16 (App Router), React 19, TypeScript (strict)
  • Database: Supabase (PostgreSQL + Row Level Security + email/password auth + TOTP MFA)
  • Styling: Tailwind CSS 4 + shadcn/ui
  • Integrations: Enable Banking (PSD2), Anthropic SDK, LangChain, OpenAI, Resend, JSZip

Documentation

  • SELF-HOSTING.md -- Full self-hosting guide (Docker, Supabase setup, migrations, optional features)
  • CLAUDE.md -- Architecture, bookkeeping engine, database conventions, extension system
  • CONTRIBUTING.md -- Development workflow, code style, pull request process
  • SECURITY.md -- Vulnerability reporting policy

Contributing

Contributions are welcome. See CONTRIBUTING.md for the full guide.

All commits require a DCO sign-off (git commit -s).

License

AGPL-3.0-or-later with an extension exception: third-party extensions that interact solely through the documented Extension API may be licensed under any terms, including proprietary. See LICENSE for details and NOTICE for third-party attributions.

S
Description
Accounted — svensk bokföringsmotor (AGPL, BAS 2026, BFL-compliant, 150+ MCP tools). Finance-kapacitet brevet ERPNext. ADR-ENGAGEMENT-001-tillägg.
Readme AGPL-3.0 56 MiB
Languages
TypeScript 93%
PLpgSQL 5.8%
JavaScript 0.4%
HTML 0.3%
MDX 0.2%
Other 0.1%