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
@@ -43,13 +43,12 @@ function makeClient(storageOverrides: Record<string, unknown> = {}) {
}
}
// Keep createServiceClient mock since ensureDocumentsBucket still uses it
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => makeClient()),
createServiceClient: vi.fn(async () => makeClient()),
}))
import { uploadDocument, createNewVersion, verifyIntegrity, _resetBucketVerified } from '../document-service'
import { createClient } from '@/lib/supabase/server'
beforeEach(() => {
vi.clearAllMocks()
@@ -57,8 +56,6 @@ beforeEach(() => {
_resetBucketVerified()
resultIdx = 0
results = []
// Reset the mock to use default makeClient
vi.mocked(createClient).mockImplementation(async () => makeClient() as never)
})
describe('uploadDocument', () => {
@@ -76,8 +73,9 @@ describe('uploadDocument', () => {
const handler = vi.fn()
eventBus.on('document.uploaded', handler)
const supabase = makeClient()
const buffer = new TextEncoder().encode('test content').buffer
const result = await uploadDocument('user-1', {
const result = await uploadDocument(supabase as never, 'user-1', {
name: 'test.pdf',
buffer: buffer as ArrayBuffer,
type: 'application/pdf',
@@ -115,8 +113,9 @@ describe('createNewVersion', () => {
{ data: newVersion, error: null }, // insert new version
]
const supabase = makeClient()
const buffer = new TextEncoder().encode('new content').buffer
const result = await createNewVersion('user-1', 'doc-1', {
const result = await createNewVersion(supabase as never, 'user-1', 'doc-1', {
name: 'test-v2.pdf',
buffer: buffer as ArrayBuffer,
type: 'application/pdf',
@@ -140,17 +139,15 @@ describe('verifyIntegrity', () => {
{ data: { storage_path: 'docs/test.pdf', sha256_hash: expectedHash }, error: null },
]
// Override createClient to provide matching download content
vi.mocked(createClient).mockImplementation(async () =>
makeClient({
download: vi.fn().mockResolvedValue({
data: new Blob([content]),
error: null,
}),
}) as never
)
// Create a client with matching download content
const supabase = makeClient({
download: vi.fn().mockResolvedValue({
data: new Blob([content]),
error: null,
}),
})
const result = await verifyIntegrity('user-1', 'doc-1')
const result = await verifyIntegrity(supabase as never, 'user-1', 'doc-1')
expect(result.valid).toBe(true)
expect(result.storedHash).toBe(expectedHash)
expect(result.computedHash).toBe(expectedHash)
@@ -161,16 +158,14 @@ describe('verifyIntegrity', () => {
{ data: { storage_path: 'docs/test.pdf', sha256_hash: 'stored-hash-abc' }, error: null },
]
vi.mocked(createClient).mockImplementation(async () =>
makeClient({
download: vi.fn().mockResolvedValue({
data: new Blob(['different content']),
error: null,
}),
}) as never
)
const supabase = makeClient({
download: vi.fn().mockResolvedValue({
data: new Blob(['different content']),
error: null,
}),
})
const result = await verifyIntegrity('user-1', 'doc-1')
const result = await verifyIntegrity(supabase as never, 'user-1', 'doc-1')
expect(result.valid).toBe(false)
expect(result.storedHash).toBe('stored-hash-abc')
expect(result.computedHash).not.toBe('stored-hash-abc')
+6 -5
View File
@@ -1,4 +1,5 @@
import { createClient, createServiceClient } from '@/lib/supabase/server'
import type { SupabaseClient } from '@supabase/supabase-js'
import { createServiceClient } from '@/lib/supabase/server'
import { eventBus } from '@/lib/events'
import type { DocumentAttachment, CreateDocumentAttachmentInput, DocumentUploadSource } from '@/types'
@@ -50,6 +51,7 @@ export async function computeSHA256(buffer: ArrayBuffer): Promise<string> {
* Upload a document and create a record with SHA-256 integrity hash
*/
export async function uploadDocument(
supabase: SupabaseClient,
userId: string,
file: { name: string; buffer: ArrayBuffer; type?: string },
metadata: {
@@ -59,7 +61,6 @@ export async function uploadDocument(
} = {}
): Promise<DocumentAttachment> {
await ensureDocumentsBucket()
const supabase = await createClient()
// Compute SHA-256 hash
const sha256Hash = await computeSHA256(file.buffer)
@@ -126,12 +127,12 @@ export async function uploadDocument(
* - Single transaction (insert new + mark old superseded)
*/
export async function createNewVersion(
supabase: SupabaseClient,
userId: string,
originalId: string,
file: { name: string; buffer: ArrayBuffer; type?: string }
): Promise<DocumentAttachment> {
await ensureDocumentsBucket()
const supabase = await createClient()
// Compute SHA-256 hash
const sha256Hash = await computeSHA256(file.buffer)
@@ -186,12 +187,12 @@ export async function createNewVersion(
* Link an existing document to a journal entry
*/
export async function linkToJournalEntry(
supabase: SupabaseClient,
userId: string,
documentId: string,
journalEntryId: string,
journalEntryLineId?: string
): Promise<DocumentAttachment> {
const supabase = await createClient()
const { data, error } = await supabase
.from('document_attachments')
@@ -215,10 +216,10 @@ export async function linkToJournalEntry(
* Verify document integrity by re-hashing and comparing
*/
export async function verifyIntegrity(
supabase: SupabaseClient,
userId: string,
documentId: string
): Promise<{ valid: boolean; storedHash: string; computedHash: string }> {
const supabase = await createClient()
// Fetch document record
const { data: doc, error: docError } = await supabase