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>
This commit is contained in:
Jakob Wennberg
2026-02-26 14:32:56 +01:00
co-authored by Claude Opus 4.6
parent 39e407644d
commit 03b569d708
289 changed files with 7325 additions and 27598 deletions
@@ -45,7 +45,7 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
const fetchItems = useCallback(async () => {
try {
const res = await fetch('/api/extensions/invoice-inbox/inbox')
const res = await fetch('/api/extensions/ext/invoice-inbox/inbox')
if (res.ok) {
const { data } = await res.json()
setItems(data ?? [])
@@ -59,7 +59,7 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
const fetchSettings = useCallback(async () => {
try {
const res = await fetch('/api/extensions/invoice-inbox/settings')
const res = await fetch('/api/extensions/ext/invoice-inbox/settings')
if (res.ok) {
const { data } = await res.json()
if (data) setSettings(data)
@@ -99,7 +99,7 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
for (let i = 0; i < 20; i++) {
await new Promise((r) => setTimeout(r, 3000))
try {
const res = await fetch(`/api/extensions/invoice-inbox/inbox/${itemId}`)
const res = await fetch(`/api/extensions/ext/invoice-inbox/inbox/${itemId}`)
if (!res.ok) continue
const { data } = await res.json()
if (data && data.status !== 'processing') {
@@ -122,7 +122,7 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
const body: Record<string, string> = {}
if (supplierId) body.supplier_id = supplierId
const res = await fetch(`/api/extensions/invoice-inbox/inbox/${itemId}/confirm`, {
const res = await fetch(`/api/extensions/ext/invoice-inbox/inbox/${itemId}/confirm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
@@ -138,7 +138,7 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
}
async function handleReject(itemId: string) {
const res = await fetch(`/api/extensions/invoice-inbox/inbox/${itemId}`, {
const res = await fetch(`/api/extensions/ext/invoice-inbox/inbox/${itemId}`, {
method: 'DELETE',
})
@@ -156,7 +156,7 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
)
setSelectedItem(null)
const res = await fetch(`/api/extensions/invoice-inbox/inbox/${itemId}/process`, {
const res = await fetch(`/api/extensions/ext/invoice-inbox/inbox/${itemId}/process`, {
method: 'POST',
})
@@ -172,7 +172,7 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
}
async function handleSaveSettings(updated: InvoiceInboxSettings) {
const res = await fetch('/api/extensions/invoice-inbox/settings', {
const res = await fetch('/api/extensions/ext/invoice-inbox/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updated),