feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup
- Update BAS account catalog with comprehensive SRU codes and K2 flags - Add currency revaluation service with tests and API route - Add expenses page and account deletion API - Enhance booking templates with new patterns and improved tests - Improve transaction categorization with template picker and description matching - Polish dashboard, onboarding, import, and transaction UIs - Refactor year-end service for multi-step closing - Move SRU generator to ne-bilaga, remove standalone SRU export - Remove unused dev docs, mock data, and extension hooks - Add invoice delivery note sequences migration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a9bf24ce0b
commit
66a4027f1e
@@ -1,51 +0,0 @@
|
||||
ERP-Base: Gap-analys mot svensk bokföringsmarknad
|
||||
|
||||
1. SAKNAS HELT — Kritiska luckor
|
||||
1.1 Leverantörsreskontra
|
||||
Alla etablerade system har fullständig leverantörsreskontra: registrering av inkommande fakturor, förfallodatum, betalningsstatus, automatisk bokföring vid betalning. Ditt system saknar tabeller och flöden för leverantörsfakturor. Detta är ett absolut krav för att kunna kallas bokföringssystem.
|
||||
Behövs: suppliers-tabell, supplier_invoices-tabell, flöde för registrering/betalning/bokföring, leverantörsreskontra-rapport, stöd för både kontant- och fakturametoden.
|
||||
1.2 Kundreskontra (formellt)
|
||||
Du har invoices och customers, men det saknas en explicit kundreskontra-vy som visar utestående fordringar, förfallna fakturor, och avstämning mot konto 1510. Alla konkurrenter har detta som standardfunktion.
|
||||
1.3 Lönehantering
|
||||
salary_payments finns men alla konkurrenter (Fortnox, Bokio, Visma) erbjuder komplett lönehantering: lönespecifikationer, arbetsgivaravgifter, skattetabeller (FOS-förfrågan mot Skatteverket), AGI-rapportering, semesterhantering. Detta är en separat modul som de flesta SME-kunder förväntar sig.
|
||||
Behövs: Skattetabellhantering, lönespec-generering (PDF), arbetsgivaravgiftsberäkning, AGI-rapportering, semesterskuld, förmånsberäkning (bil, etc).
|
||||
1.4 Årsredovisning (K2/K3)
|
||||
Aktiebolag måste lämna årsredovisning till Bolagsverket. Fortnox och Björn Lundén genererar detta. Din plattform har årsbokslut men saknar årsredovisningsgenerering med förvaltningsberättelse, noter, och formell K2/K3-struktur.
|
||||
Behövs: Generering av förvaltningsberättelse, resultaträkning (K2-format), balansräkning (K2-format), noter, digital inlämning till Bolagsverket (XBRL).
|
||||
1.5 Kontantmetod-stöd
|
||||
Många enskilda firmor bokför med kontantmetoden (bokslutsmetoden). Ditt system verkar byggt kring faktureringsmetoden. Båda måste stödjas, med automatisk övergång till fakturametod vid bokslut för kontantmetoden.
|
||||
1.6 Anläggningsregister
|
||||
Inventarier, maskiner, fastigheter — med avskrivningsplaner (linjär/degressiv), restvärden, och automatisk avskrivningsbokföring. Saknas helt. Krävs för AB med tillgångar.
|
||||
1.7 Offert/Order-flöde
|
||||
Fortnox och Visma har offert → order → faktura-kedja. Inte nödvändigt för MVP men förväntat i ett komplett system.
|
||||
|
||||
2. FINNS MEN OTILLRÄCKLIGT — Behöver utökas
|
||||
2.1 Bokföringsmallar / Konteringshjälp
|
||||
Bokio's stora USP är smart konteringshjälp: användaren väljer "IT-tjänst 25% moms" och systemet konterar automatiskt. Du har AI-kategorisering, men saknar troligen ett bibliotek av färdiga bokföringsmallar för vanliga affärshändelser som en nybörjare kan välja mellan.
|
||||
Behövs: 50-100 vanliga transaktionsmallar (kontorsmateriell, IT-tjänst, bensin, representation, etc) med korrekt moms och kontering.
|
||||
2.2 Bankavstämning
|
||||
Du har PSD2-transaktionssynk, men behöver explicit bankavstämning: matcha banktransaktioner mot bokförda poster, markera avstämda, visa differenser. Alla konkurrenter har detta.
|
||||
2.3 Momsdeklaration
|
||||
Du nämner "10 rutor" men verifierar att den genererar korrekt SKV 4820-underlag? Behöver också stödja: EU-handel (omvänd skattskyldighet), import/export-moms, olika momssatser (25/12/6/0%), tröskelbelopp (120 000 SEK från 2025).
|
||||
2.4 SIE-export
|
||||
Du har SIE4-export. Verifiera att SIE-import också fungerar korrekt (ingående balanser, verifikationer, kontoplan) — detta är kritiskt för att kunder ska kunna byta till ditt system från Fortnox/Bokio.
|
||||
2.5 Rapporter
|
||||
Du har saldobalans, resultat, balans, moms. Saknar troligen:
|
||||
Huvudbok (alla transaktioner per konto)
|
||||
Grundbok (verifikationslista i datumordning)
|
||||
Kundreskontra-rapport
|
||||
Leverantörsreskontra-rapport
|
||||
Periodrapporter (jämförelse mellan perioder)
|
||||
Kassaflödesanalys
|
||||
|
||||
3. HYGIEN-FUNKTIONER — Förväntas av alla
|
||||
3.1 Autentisering
|
||||
BankID-inloggning förväntas av svenska användare. Inte nödvändigt dag 1, men e-post + lösenord + 2FA via TOTP är minimum.
|
||||
3.2 Mobilapp / Responsivt
|
||||
Alla konkurrenter har mobilapp eller fullt responsivt gränssnitt. Kvittofotografering från mobil är en hygienfaktor.
|
||||
3.3 Periodlåsning
|
||||
Bokföringslagen kräver att bokföring är "varaktig" — du behöver kunna låsa perioder så att poster inte kan ändras i efterhand utan att det syns. Du har WORM-arkiv, verifiera att periodlåsning är implementerad.
|
||||
3.4 Fleranvändarstöd
|
||||
Roller: ägare, redovisningskonsult (extern), anställd. Behörigheter per modul. Alla konkurrenter har detta. Redovisningskonsult-access är affärskritiskt — byråer är den viktigaste distributionskanalen.
|
||||
3.5 Verifikationskedja
|
||||
Varje verifikation behöver: löpnummer utan luckor, datum, belopp, motkonto, beskrivning, bifogat underlag. Du har detta delvis via WORM + voucher numbering, men verifiera fullständigt BFL-compliance.
|
||||
@@ -1,232 +0,0 @@
|
||||
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
|
||||
@@ -1,472 +0,0 @@
|
||||
|
||||
Here is Claude's plan:
|
||||
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
|
||||
Plan A: Extension Opt-In Foundation (~3 days)
|
||||
|
||||
Context
|
||||
|
||||
erp-base is being open-sourced. The core (Swedish bookkeeping) should work standalone with only Supabase. Extensions (AI,
|
||||
PSD2, push, email, sector tools) ship in the same repo as an "extensions library" that developers explicitly opt into via a
|
||||
JSON config file. Everything is open-source — monetization happens on the hosted platform.
|
||||
|
||||
This plan establishes the architectural boundary. After this plan:
|
||||
- Core builds and runs with an empty config (zero extensions)
|
||||
- Extensions are enabled by adding their ID to extensions.config.json
|
||||
- CI prevents anyone from accidentally importing extension code in core
|
||||
- Every extension has a manifest describing its dependencies
|
||||
|
||||
Plan B (separate, future) handles the full cleanup: email extraction, API route migration, template-embeddings relocation,
|
||||
SRU/NE-bilaga merge, cross-extension cleanup, and documentation.
|
||||
|
||||
Current Coupling (What We're Fixing)
|
||||
|
||||
Only 3 files in lib/ directly import from extensions/:
|
||||
|
||||
┌───────────────────────────────────────┬───────────────────────────────────────────────────────────┐
|
||||
│ File │ Coupling │
|
||||
├───────────────────────────────────────┼───────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/loader.ts │ 12 hardcoded static imports from @/extensions/ │
|
||||
├───────────────────────────────────────┼───────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/workspace-registry.tsx │ 24 hardcoded next/dynamic() imports │
|
||||
├───────────────────────────────────────┼───────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/sectors.ts │ Hardcoded extension metadata (data only, no code imports) │
|
||||
└───────────────────────────────────────┴───────────────────────────────────────────────────────────┘
|
||||
|
||||
Everything else is already clean — event bus, registry, context factory, types, core API routes, core components.
|
||||
|
||||
Implementation
|
||||
|
||||
Step 1: Create the config file and JSON schema
|
||||
|
||||
New file: extensions.config.json
|
||||
{
|
||||
"$schema": "./extensions.schema.json",
|
||||
"extensions": []
|
||||
}
|
||||
|
||||
New file: extensions.schema.json
|
||||
|
||||
JSON Schema listing all valid extension IDs with descriptions, giving IDE autocompletion. Generated from manifest files (or
|
||||
hand-maintained initially).
|
||||
|
||||
Step 2: Add manifest.json to every extension
|
||||
|
||||
Each extension directory gets a manifest describing its metadata, imports, and requirements.
|
||||
|
||||
Format:
|
||||
{
|
||||
"id": "receipt-ocr",
|
||||
"sector": "general",
|
||||
"exportName": "receiptOcrExtension",
|
||||
"workspace": "@/components/extensions/general/ReceiptOcrWorkspace",
|
||||
"requiredEnvVars": ["ANTHROPIC_API_KEY"],
|
||||
"optionalEnvVars": [],
|
||||
"npmDependencies": ["@anthropic-ai/sdk"],
|
||||
"definition": {
|
||||
"name": "Receipt OCR",
|
||||
"category": "import",
|
||||
"icon": "Camera",
|
||||
"dataPattern": "both",
|
||||
"description": "Scan and process receipts with AI",
|
||||
"longDescription": "..."
|
||||
}
|
||||
}
|
||||
|
||||
Extensions to manifest (24 total):
|
||||
|
||||
┌────────────────────────┬────────────────────────────────────────────┬───────────────────────────────────────────────────┐
|
||||
│ Extension │ Path │ Required Env Vars │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ receipt-ocr │ extensions/general/receipt-ocr/ │ ANTHROPIC_API_KEY │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ ai-categorization │ extensions/general/ai-categorization/ │ ANTHROPIC_API_KEY, OPENAI_API_KEY │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ ai-chat │ extensions/general/ai-chat/ │ ANTHROPIC_API_KEY, OPENAI_API_KEY │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ invoice-inbox │ extensions/general/invoice-inbox/ │ ANTHROPIC_API_KEY │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ enable-banking │ extensions/general/enable-banking/ │ ENABLE_BANKING_APP_ID, ENABLE_BANKING_PRIVATE_KEY │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ push-notifications │ extensions/general/push-notifications/ │ VAPID_PRIVATE_KEY, NEXT_PUBLIC_VAPID_PUBLIC_KEY │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ calendar │ extensions/general/calendar/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ eu-sales-list │ extensions/export/eu-sales-list/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ vat-monitor │ extensions/export/vat-monitor/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ intrastat │ extensions/export/intrastat/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ currency-receivables │ extensions/export/currency-receivables/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ food-cost │ extensions/restaurant/food-cost/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ earnings-per-liter │ extensions/restaurant/earnings-per-liter/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ pos-import │ extensions/restaurant/pos-import/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ tip-tracking │ extensions/restaurant/tip-tracking/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ rot-calculator │ extensions/construction/rot-calculator/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ project-cost │ extensions/construction/project-cost/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ revpar │ extensions/hotel/revpar/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ occupancy │ extensions/hotel/occupancy/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ billable-hours │ extensions/tech/billable-hours/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ project-billing │ extensions/tech/project-billing/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ shopify-import │ extensions/ecommerce/shopify-import/ │ (none) │
|
||||
├────────────────────────┼────────────────────────────────────────────┼───────────────────────────────────────────────────┤
|
||||
│ multichannel-revenue │ extensions/ecommerce/multichannel-revenue/ │ (none) │
|
||||
└────────────────────────┴────────────────────────────────────────────┴───────────────────────────────────────────────────┘
|
||||
|
||||
Step 3: Build the generator script
|
||||
|
||||
New file: scripts/generate-extension-registry.ts
|
||||
|
||||
The generator:
|
||||
1. Reads extensions.config.json to get enabled extension IDs
|
||||
2. For each ID, finds and reads extensions/**/manifest.json matching that ID
|
||||
3. Generates 3 files under lib/extensions/_generated/:
|
||||
|
||||
lib/extensions/_generated/extension-list.ts — When config has ["receipt-ocr", "ai-categorization"]:
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
import type { Extension } from '../types'
|
||||
import { receiptOcrExtension } from '@/extensions/general/receipt-ocr'
|
||||
import { aiCategorizationExtension } from '@/extensions/general/ai-categorization'
|
||||
|
||||
export const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
receiptOcrExtension,
|
||||
aiCategorizationExtension,
|
||||
]
|
||||
|
||||
lib/extensions/_generated/workspace-map.tsx — Dynamic import map:
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { ComponentType } from 'react'
|
||||
import type { WorkspaceComponentProps } from '../workspace-registry'
|
||||
|
||||
export const WORKSPACES: Record<string, ComponentType<WorkspaceComponentProps>> = {
|
||||
'general/receipt-ocr': dynamic(() => import('@/components/extensions/general/ReceiptOcrWorkspace')),
|
||||
'general/ai-categorization': dynamic(() => import('@/components/extensions/general/AiCategorizationWorkspace')),
|
||||
}
|
||||
|
||||
lib/extensions/_generated/sector-definitions.ts — Extension metadata:
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
import type { ExtensionDefinition } from '../types'
|
||||
export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {
|
||||
general: [
|
||||
{ slug: 'receipt-ocr', name: 'Receipt OCR', ... },
|
||||
{ slug: 'ai-categorization', name: 'AI Categorization', ... },
|
||||
],
|
||||
}
|
||||
|
||||
When config is empty ("extensions": []):
|
||||
export const FIRST_PARTY_EXTENSIONS: Extension[] = []
|
||||
export const WORKSPACES: Record<string, ComponentType<WorkspaceComponentProps>> = {}
|
||||
export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {}
|
||||
|
||||
Generator features:
|
||||
- npm run setup:extensions — Generate + validate env vars (warn if missing)
|
||||
- npm run setup:extensions -- --list — Print all available extensions with descriptions
|
||||
- Outputs: "Enabled: receipt-ocr, ai-categorization. Warning: OPENAI_API_KEY not set (required by ai-categorization)"
|
||||
|
||||
Step 4: Modify loader, workspace-registry, and sectors to use generated files
|
||||
|
||||
lib/extensions/loader.ts — Replace hardcoded imports:
|
||||
import { extensionRegistry } from './registry'
|
||||
import { FIRST_PARTY_EXTENSIONS } from './_generated/extension-list'
|
||||
|
||||
let loaded = false
|
||||
|
||||
export function loadExtensions(): void {
|
||||
if (loaded) return
|
||||
loaded = true
|
||||
for (const extension of FIRST_PARTY_EXTENSIONS) {
|
||||
extensionRegistry.register(extension)
|
||||
}
|
||||
}
|
||||
|
||||
lib/extensions/workspace-registry.tsx — Replace hardcoded map:
|
||||
import type { ComponentType } from 'react'
|
||||
import { WORKSPACES } from './_generated/workspace-map'
|
||||
|
||||
export interface WorkspaceComponentProps {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export function getWorkspaceComponent(
|
||||
sector: string,
|
||||
slug: string
|
||||
): ComponentType<WorkspaceComponentProps> | null {
|
||||
return WORKSPACES[`${sector}/${slug}`] ?? null
|
||||
}
|
||||
|
||||
lib/extensions/sectors.ts — Replace hardcoded extension definitions:
|
||||
|
||||
The sector shells (general, restaurant, construction, etc.) stay hardcoded since they are structural. The extension
|
||||
definitions per sector come from the generated file. Merge them at runtime.
|
||||
|
||||
Step 5: Commit empty defaults for generated files
|
||||
|
||||
These are committed so core compiles out of the box without running the generator:
|
||||
|
||||
- lib/extensions/_generated/extension-list.ts → Empty FIRST_PARTY_EXTENSIONS
|
||||
- lib/extensions/_generated/workspace-map.tsx → Empty WORKSPACES
|
||||
- lib/extensions/_generated/sector-definitions.ts → Empty EXTENSION_DEFINITIONS
|
||||
|
||||
Add to .gitignore a comment explaining these files are auto-generated but the defaults are committed.
|
||||
|
||||
Step 6: npm scripts
|
||||
|
||||
Add to package.json:
|
||||
{
|
||||
"setup:extensions": "tsx scripts/generate-extension-registry.ts",
|
||||
"prebuild": "npm run setup:extensions",
|
||||
"predev": "npm run setup:extensions"
|
||||
}
|
||||
|
||||
This ensures the generated files are always up-to-date before build/dev.
|
||||
|
||||
Step 7: CI regression guard
|
||||
|
||||
New file: .github/workflows/core-build.yml
|
||||
|
||||
name: Core Build (no extensions)
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
core-only:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20 }
|
||||
- run: npm ci
|
||||
- name: Reset extensions config
|
||||
run: echo '{"extensions":[]}' > extensions.config.json
|
||||
- run: npm run setup:extensions
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
- name: Check no core imports from extensions
|
||||
run: |
|
||||
VIOLATIONS=$(grep -r "from '@/extensions/" lib/ app/api/ components/ --include="*.ts" --include="*.tsx" \
|
||||
| grep -v "app/api/extensions/" \
|
||||
| grep -v "components/extensions/" \
|
||||
| grep -v "lib/extensions/_generated/" \
|
||||
| grep -v "lib/extensions/loader.ts" || true)
|
||||
if [ -n "$VIOLATIONS" ]; then
|
||||
echo "ERROR: Core code imports from @/extensions/:"
|
||||
echo "$VIOLATIONS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Note: After this plan, lib/extensions/loader.ts will no longer import from @/extensions/ (it imports from _generated/), so the
|
||||
exclusion for loader.ts is just a safety measure during transition.
|
||||
|
||||
Verification Criteria
|
||||
|
||||
1. Empty config builds: echo '{"extensions":[]}' > extensions.config.json && npm run setup:extensions && npm run build →
|
||||
succeeds
|
||||
2. Single extension works: Add "calendar" to config → npm run setup:extensions && npm run build → calendar extension available
|
||||
at /e/general/calendar
|
||||
3. Full config works: Add all 12 currently-loaded extension IDs → npm run setup:extensions && npm run build → identical to
|
||||
current behavior
|
||||
4. CI catches violations: If someone adds import { x } from '@/extensions/foo' in lib/utils.ts, the CI job fails
|
||||
5. All tests pass: npm test with both empty and full config
|
||||
6. Generator warns about missing env vars: Enable ai-categorization without OPENAI_API_KEY → warning printed, build still
|
||||
succeeds
|
||||
|
||||
Critical Files
|
||||
|
||||
┌─────────────────────────────────────────────────┬────────────────────────────────────────────────────────┐
|
||||
│ File │ Action │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ extensions.config.json │ Create │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ extensions.schema.json │ Create │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ scripts/generate-extension-registry.ts │ Create │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/_generated/extension-list.ts │ Create (empty default) │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/_generated/workspace-map.tsx │ Create (empty default) │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/_generated/sector-definitions.ts │ Create (empty default) │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ .github/workflows/core-build.yml │ Create │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/loader.ts │ Modify: import from generated file │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/workspace-registry.tsx │ Modify: import from generated file │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ lib/extensions/sectors.ts │ Modify: import definitions from generated file │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ extensions/*/manifest.json (24 files) │ Create │
|
||||
├─────────────────────────────────────────────────┼────────────────────────────────────────────────────────┤
|
||||
│ package.json │ Modify: add setup:extensions, prebuild, predev scripts │
|
||||
└─────────────────────────────────────────────────┴────────────────────────────────────────────────────────┘
|
||||
|
||||
---
|
||||
Plan B: Full Decoupling (Reference — Execute Later)
|
||||
|
||||
This plan is for after Plan A is complete. Context preserved here so nothing is lost.
|
||||
|
||||
Prerequisites
|
||||
|
||||
Plan A complete: config system works, manifests exist, CI guard in place.
|
||||
|
||||
Phase 3: Create the email extension (~1.5 days)
|
||||
|
||||
Extract email from core into an extension. Core only needs Supabase.
|
||||
|
||||
1. Create extensions/general/email/:
|
||||
- index.ts — Extension definition, subscribes to invoice.created, invoice.overdue
|
||||
- lib/email-service.ts — Resend integration (moved from lib/email/)
|
||||
- lib/templates/ — Invoice, reminder, notification templates
|
||||
- manifest.json — requires RESEND_API_KEY, RESEND_FROM_EMAIL
|
||||
2. Create NoopEmailAdapter in core (lib/email/service.ts):
|
||||
- Core defines EmailService interface + no-op default
|
||||
- Email extension registers real implementation via services pattern on the registry
|
||||
- Invoice flows check if email service is available; if not, skip sending (no crash)
|
||||
3. Event-driven: Core emits invoice.created, invoice.overdue. Email extension subscribes, sends emails. If not loaded, events
|
||||
fire but nothing sends.
|
||||
4. Move cron: /api/invoices/reminders/cron becomes a thin proxy or moves into email extension's apiRoutes.
|
||||
|
||||
Current email files to move:
|
||||
- lib/email/ → Review what's here, extract Resend-specific code into extension
|
||||
- Invoice template generation stays in core (PDF generation), email delivery moves to extension
|
||||
|
||||
Phase 4: Move extension API routes into extensions (~5-7 days)
|
||||
|
||||
Move handler logic from app/api/extensions/<name>/ route files into each extension's apiRoutes array. The catch-all at
|
||||
app/api/extensions/ext/[...path]/route.ts dispatches.
|
||||
|
||||
Frontend URL change: /api/extensions/<name>/<action> → /api/extensions/ext/<name>/<action>
|
||||
|
||||
Routes to convert (move handler into extension apiRoutes):
|
||||
- ai-categorization/suggestions/, ai-categorization/settings/
|
||||
- ai-chat/, ai-chat/stream/, ai-chat/sessions/
|
||||
- invoice-inbox/inbox/, invoice-inbox/inbox/[id]/*, invoice-inbox/settings/
|
||||
- receipt-ocr/upload/, receipt-ocr/[id]/*, receipt-ocr/settings/, receipt-ocr/queue/
|
||||
- push-notifications/subscribe/, push-notifications/settings/
|
||||
- All export/* routes
|
||||
|
||||
Thin proxy routes (external callbacks / cron — keep but make extension-agnostic):
|
||||
- invoice-inbox/webhook/ — Resend webhook: delegates to extensionRegistry.get('invoice-inbox')?.apiRoutes
|
||||
- enable-banking/callback/ — PSD2 OAuth: delegates to registry
|
||||
- enable-banking/sync/cron/ — Vercel cron: delegates to registry
|
||||
- push-notifications/cron/ — Vercel cron: delegates to registry
|
||||
|
||||
Keep as-is (core framework):
|
||||
- toggles/, [sector]/[slug]/data/, [sector]/[slug]/settings/, ext/[...path]/
|
||||
|
||||
Delete all other dedicated routes after moving logic.
|
||||
|
||||
Phase 5: Move template-embeddings.ts out of core (~1 day)
|
||||
|
||||
lib/bookkeeping/template-embeddings.ts imports @langchain/openai.
|
||||
|
||||
1. Move to extensions/general/ai-categorization/lib/template-embeddings.ts
|
||||
2. Add services field to Extension interface (lib/extensions/types.ts):
|
||||
services?: Record<string, (...args: unknown[]) => Promise<unknown>>
|
||||
3. ai-categorization registers: services: { findSimilarTemplates: ... }
|
||||
4. app/api/transactions/suggest-categories/route.ts uses registry:
|
||||
const aiExt = extensionRegistry.get('ai-categorization')
|
||||
const templateSuggestions = aiExt?.services?.findSimilarTemplates
|
||||
? await aiExt.services.findSimilarTemplates(transaction, entityType)
|
||||
: []
|
||||
// Rule-based suggestions from mapping-engine.ts always available
|
||||
|
||||
Phase 6: Merge SRU export and NE-bilaga into core (~1 day)
|
||||
|
||||
Tax compliance features, no external deps, always available:
|
||||
- Move extensions/sru-export/ → lib/reports/sru-export/
|
||||
- Move extensions/ne-bilaga/ → lib/reports/ne-bilaga/
|
||||
- Move workspace components → components/reports/
|
||||
- Move API routes → app/api/reports/sru-export/, app/api/reports/ne-bilaga/
|
||||
- Remove from extension system (no manifest, not in loader)
|
||||
|
||||
Phase 7: Cross-extension dependency cleanup (~0.5 days)
|
||||
|
||||
invoice-inbox imports processReceiptFromDocument from receipt-ocr.
|
||||
|
||||
Use services pattern:
|
||||
- receipt-ocr registers: services: { processReceiptFromDocument }
|
||||
- invoice-inbox calls: extensionRegistry.get('receipt-ocr')?.services?.processReceiptFromDocument(...)
|
||||
- Gracefully skips if receipt-ocr not enabled
|
||||
|
||||
Phase 8: Documentation (~1 day)
|
||||
|
||||
1. README.md: Self-hosting guide — core setup (just Supabase), extension opt-in
|
||||
2. EXTENSIONS.md: Extension interface, events, context API, how to build extensions
|
||||
3. scripts/create-extension.ts: Scaffolds new extension (manifest, index.ts, types, workspace)
|
||||
|
||||
Plan B Effort Summary
|
||||
|
||||
┌──────────────────────────────────┬─────────────┐
|
||||
│ Phase │ Effort │
|
||||
├──────────────────────────────────┼─────────────┤
|
||||
│ Phase 3: Email extension │ 1.5 days │
|
||||
├──────────────────────────────────┼─────────────┤
|
||||
│ Phase 4: API route migration │ 5-7 days │
|
||||
├──────────────────────────────────┼─────────────┤
|
||||
│ Phase 5: Template-embeddings │ 1 day │
|
||||
├──────────────────────────────────┼─────────────┤
|
||||
│ Phase 6: SRU/NE-bilaga merge │ 1 day │
|
||||
├──────────────────────────────────┼─────────────┤
|
||||
│ Phase 7: Cross-extension cleanup │ 0.5 days │
|
||||
├──────────────────────────────────┼─────────────┤
|
||||
│ Phase 8: Documentation │ 1 day │
|
||||
├──────────────────────────────────┼─────────────┤
|
||||
│ Total │ ~10-12 days │
|
||||
└──────────────────────────────────┴─────────────┘
|
||||
|
||||
Environment Variable Reference
|
||||
|
||||
Core (required):
|
||||
- NEXT_PUBLIC_SUPABASE_URL — Supabase project URL
|
||||
- NEXT_PUBLIC_SUPABASE_ANON_KEY — Supabase anonymous key
|
||||
- SUPABASE_SERVICE_ROLE_KEY — Supabase service role key
|
||||
- NEXT_PUBLIC_APP_URL — App base URL
|
||||
- CRON_SECRET — Auth for core cron jobs (deadlines, tax deadlines, document verification)
|
||||
|
||||
Extension env vars:
|
||||
|
||||
┌────────────────────────┬───────────────────────────────────────────────────┬────────────────────────┐
|
||||
│ Extension │ Required │ Optional │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ email │ RESEND_API_KEY, RESEND_FROM_EMAIL │ RESEND_WEBHOOK_SECRET │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ receipt-ocr │ ANTHROPIC_API_KEY │ │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ ai-categorization │ ANTHROPIC_API_KEY, OPENAI_API_KEY │ │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ ai-chat │ ANTHROPIC_API_KEY, OPENAI_API_KEY │ │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ invoice-inbox │ ANTHROPIC_API_KEY │ │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ enable-banking │ ENABLE_BANKING_APP_ID, ENABLE_BANKING_PRIVATE_KEY │ ENABLE_BANKING_SANDBOX │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ push-notifications │ VAPID_PRIVATE_KEY, NEXT_PUBLIC_VAPID_PUBLIC_KEY │ VAPID_SUBJECT │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ calendar │ (none) │ │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ All export extensions │ (none) │ │
|
||||
├────────────────────────┼───────────────────────────────────────────────────┼────────────────────────┤
|
||||
│ All sector extensions │ (none) │ │
|
||||
└────────────────────────┴───────────────────────────────────────────────────┴────────────────────────┘
|
||||
|
||||
Key Architectural Decisions
|
||||
|
||||
1. Config is JSON — No TypeScript in config. Generator reads it without a compiler. CI validates trivially.
|
||||
2. Core never imports from @/extensions/ — Enforced by CI. The only bridge is the generated _generated/ files.
|
||||
3. Extensions communicate via events and services — Event bus for async reactions, services record for synchronous
|
||||
capabilities.
|
||||
4. Extension tables stay in shared DB — Empty when extension isn't enabled. RLS prevents access issues. No migration splitting
|
||||
needed.
|
||||
5. CRON_SECRET is core — 3 core cron jobs need it (deadlines, tax deadlines, document verification).
|
||||
6. Email is an extension — Core works without email. Invoices can be created/downloaded but not sent.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
Reference in New Issue
Block a user