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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:32:56 +01:00

232 lines
20 KiB
Markdown

Architecture Cleanup & SupabaseClient Injection
Context
External feedback identified the codebase as overengineered in some areas (unused sector extensions, dead event types) and
underengineered in one critical area (no abstraction boundary between core lib/ and the Supabase platform). The goal is to
slim down dead weight and refactor lib/ functions to accept SupabaseClient as a parameter instead of self-instantiating,
matching a pattern already used by bank-reconciliation.ts, ingest.ts, and other files.
Two PRs:
- PR1: Cleanup (delete sector/export extensions, prune 6 dead events)
- PR2: SupabaseClient injection refactor (reports first, then engine + core services)
---
PR1: Cleanup
1a. Delete sector & export extension directories
Delete these 6 directories entirely:
extensions/restaurant/
extensions/construction/
extensions/hotel/
extensions/tech/
extensions/ecommerce/
extensions/export/
Delete their workspace components:
components/extensions/restaurant/
components/extensions/construction/
components/extensions/hotel/
components/extensions/tech/
components/extensions/ecommerce/
components/extensions/export/
1b. Update sector registry
lib/extensions/types.ts:13 — reduce SectorSlug union:
// Before
export type SectorSlug = 'general' | 'restaurant' | 'construction' | 'hotel' | 'tech' | 'ecommerce' | 'export'
// After
export type SectorSlug = 'general'
lib/extensions/sectors.ts — remove 6 sector shells from SECTOR_SHELLS array (lines 21-57), keeping only the general entry.
1c. Update sectors test
lib/extensions/__tests__/sectors.test.ts — the test uses buildDefinitionsFromManifests() which walks extensions/ at runtime,
so counts auto-adjust. But hardcoded assertions need updating:
- Line 48: expect(SECTORS.length).toBe(7) → .toBe(1)
- Line 51: expect(getAllExtensions().length).toBe(25) → update to match remaining general extensions count (count manifests in
extensions/general/)
- Lines 63-66: "at least one extension per sector" — still valid for 1 sector
- Lines 69-73: Change getSector('restaurant') test to getSector('general')
- Lines 81-87: Change getExtensionDefinition('restaurant', 'food-cost') to a general extension
- Lines 94-96: Change getExtensionsBySector('restaurant') to getExtensionsBySector('general')
1d. Remove 6 dead event types
lib/events/types.ts — remove these 6 union members from CoreEvent:
- invoice.paid (line 32)
- invoice.overdue (line 33)
- bank.statement_received (line 39)
- bank.payment_notification (line 40)
- customer.pseudonymized (line 46)
- audit.security_event (line 72)
Remove from the import on line 1-16:
- CAMT053Statement
- CAMT054Notification
- AuditSecurityEvent
types/index.ts — delete the 3 placeholder interfaces (lines 1669-1685):
- CAMT053Statement
- CAMT054Notification
- AuditSecurityEvent
1e. Verification
npm run build # Confirm no broken imports
npm test # All tests pass
npm run setup:extensions # Codegen still works (manifests removed)
---
PR2: SupabaseClient Injection Refactor
Phase 1: Report generators (10 files)
These files self-instantiate createClient(). Refactor each to accept supabase: SupabaseClient as first parameter.
┌────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────┐
│ File │ Functions to change │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/trial-balance.ts │ generateTrialBalance(supabase, userId, periodId), │
│ │ generateTrialBalanceManual(supabase, userId, periodId) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/vat-declaration.ts │ calculateVatDeclaration(supabase, userId, ...) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/sie-export.ts │ generateSIEExport(supabase, userId, options) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/general-ledger.ts │ generateGeneralLedger(supabase, userId, periodId, ...) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/journal-register.ts │ generateJournalRegister(supabase, userId, periodId) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/monthly-breakdown.ts │ generateMonthlyBreakdown(supabase, userId, periodId) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/supplier-ledger.ts │ generateSupplierLedger(supabase, userId, asOfDate) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/supplier-reconciliation.ts │ generateReconciliation(supabase, userId, periodId) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/ar-ledger.ts │ generateARLedger(supabase, userId, asOfDate) │
├────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────┤
│ lib/reports/ar-reconciliation.ts │ generateARReconciliation(supabase, userId, periodId) │
└────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────┘
Not changed (no direct createClient call):
- income-statement.ts — delegates to generateTrialBalance(), which gets the client. Pass supabase through:
generateIncomeStatement(supabase, userId, periodId).
- balance-sheet.ts — same pattern, delegates to generateTrialBalance().
Sub-reports (also need injection):
- lib/reports/ne-bilaga/ne-engine.ts — generateNEDeclaration(supabase, userId, periodId)
- lib/reports/sru-export/sru-engine.ts — aggregateBalancesBySRU(supabase, userId, periodId), getSRUCoverage(supabase, userId)
Mechanical change per file:
1. Remove import { createClient } from '@/lib/supabase/server'
2. Add import type { SupabaseClient } from '@supabase/supabase-js'
3. Add supabase: SupabaseClient as first parameter
4. Delete the const supabase = await createClient() line
Update callers — each report API route already creates a client for auth. Pass it through:
// Before (app/api/reports/trial-balance/route.ts)
const result = await generateTrialBalance(user.id, periodId)
// After
const result = await generateTrialBalance(supabase, user.id, periodId)
12 API routes to update:
- app/api/reports/trial-balance/route.ts
- app/api/reports/income-statement/route.ts
- app/api/reports/balance-sheet/route.ts
- app/api/reports/vat-declaration/route.ts
- app/api/reports/sie-export/route.ts
- app/api/reports/general-ledger/route.ts
- app/api/reports/journal-register/route.ts
- app/api/reports/monthly-breakdown/route.ts
- app/api/reports/supplier-ledger/route.ts
- app/api/reports/ar-ledger/route.ts
- app/api/reports/ne-bilaga/route.ts
- app/api/reports/sru-export/route.ts (+ coverage/route.ts)
Phase 2: Core services (7 files)
Same mechanical pattern. Each function gets supabase: SupabaseClient as first parameter.
┌──────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┐
│ File │ Functions │
├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ lib/core/bookkeeping/period-service.ts │ lockPeriod, closePeriod, createNextPeriod, getPeriodStatus │
├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ lib/core/bookkeeping/storno-service.ts │ correctEntry │
├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ │ validateYearEndReadiness, previewYearEndClosing, generateOpeningBalances │
│ lib/core/bookkeeping/year-end-service.ts │ (note: executeYearEndClosing calls others that self-instantiate, so it also │
│ │ needs the param and must pass it through) │
├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ │ uploadDocument, createNewVersion, linkToJournalEntry, verifyIntegrity (keep │
│ lib/core/documents/document-service.ts │ ensureDocumentsBucket using createServiceClient — it needs service role for │
│ │ bucket ops) │
├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ lib/core/audit/audit-service.ts │ getAuditLog, getEntityHistory, getCorrectionChain │
├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ lib/core/tax/tax-code-service.ts │ getTaxCodes, getTaxCodeByCode, calculateMomsFromTaxCodes, seedTaxCodes │
├──────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┤
│ lib/invoices/invoice-matching.ts │ findMatchingInvoices │
└──────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┘
Phase 3: Bookkeeping engine + mapping (3 files)
┌───────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────┐
│ File │ Functions │
├───────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
│ │ getNextVoucherNumber, findFiscalPeriod, createDraftEntry, commitEntry, │
│ lib/bookkeeping/engine.ts │ createJournalEntry, reverseEntry (validateBalance stays pure, resolveAccountIds │
│ │ already takes client) │
├───────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
│ lib/bookkeeping/mapping-engine.ts │ evaluateMappingRules, saveUserMappingRule │
├───────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
│ lib/import/sie-import.ts │ checkDuplicateImport, importVouchers, saveMappings, loadMappings, executeSIEImport │
└───────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────┘
Engine.ts cascade: Since createJournalEntry calls createDraftEntry and commitEntry, and commitEntry calls
getNextVoucherNumber, the client flows through all internal calls. This eliminates the current pattern where each sub-function
creates its own independent client.
Special cases
- lib/bookkeeping/handlers/supplier-invoice-handler.ts — event handler, invoked by event bus. The handler creates its own
client because it has no caller to receive one from. Leave as-is — this is the edge of the system where the event bus must
bootstrap a client.
- lib/extensions/toggle-check.ts — uses createServiceClient() (service role, no cookies). Leave as-is — extension toggle
checks bypass RLS intentionally.
- lib/core/documents/document-service.ts ensureDocumentsBucket — uses createServiceClient(). Leave as-is for this one private
function.
Phase 2/3 caller updates
The API routes calling these functions need the same one-line change as the report routes: pass the existing supabase variable
as the first argument.
Verification
npm run build # No broken imports
npm test # All tests pass — mock pattern changes from module mock to direct client mock
npx vitest run lib/reports # Report tests specifically
npx vitest run lib/bookkeeping # Engine tests specifically
Tests currently mock @/lib/supabase/server. After the refactor, tests can pass a mock client directly via createMockSupabase()
from tests/helpers.ts — which is already the pattern used by the tests. The vi.mock('@/lib/supabase/server') calls in test
files can be removed for functions that now accept the client as a parameter.
---
What we're NOT changing
- engine.ts single write path
- DB enforcement triggers
- Extension toggle system
- Codegen from extensions.config.json
- Event bus design (lib/events/bus.ts)
- Report system structure
- Reconciliation 4-pass algorithm
- AI extension architecture