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:
co-authored by
Claude Opus 4.6
parent
39e407644d
commit
03b569d708
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user