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
+4 -4
View File
@@ -1,4 +1,4 @@
import { createClient } from '@/lib/supabase/server'
import type { SupabaseClient } from '@supabase/supabase-js'
import type { AuditLogEntry, AuditAction } from '@/types'
/**
@@ -22,10 +22,10 @@ export interface AuditLogFilters {
* Get paginated audit log entries for a user
*/
export async function getAuditLog(
supabase: SupabaseClient,
userId: string,
filters: AuditLogFilters = {}
): Promise<{ data: AuditLogEntry[]; count: number }> {
const supabase = await createClient()
const page = filters.page ?? 1
const pageSize = filters.pageSize ?? 50
const offset = (page - 1) * pageSize
@@ -69,11 +69,11 @@ export async function getAuditLog(
* Get full history of a single record (all mutations)
*/
export async function getEntityHistory(
supabase: SupabaseClient,
userId: string,
tableName: string,
recordId: string
): Promise<AuditLogEntry[]> {
const supabase = await createClient()
const { data, error } = await supabase
.from('audit_log')
@@ -95,10 +95,10 @@ export async function getEntityHistory(
* original → storno (reversal) → corrected entry
*/
export async function getCorrectionChain(
supabase: SupabaseClient,
userId: string,
journalEntryId: string
): Promise<AuditLogEntry[]> {
const supabase = await createClient()
// First, find the entry and its linked entries
const { data: entry, error: entryError } = await supabase
@@ -29,10 +29,6 @@ function makeClient() {
}
}
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => makeClient()),
}))
import { lockPeriod, closePeriod, createNextPeriod } from '../period-service'
beforeEach(() => {
@@ -55,7 +51,8 @@ describe('lockPeriod', () => {
const handler = vi.fn()
eventBus.on('period.locked', handler)
const result = await lockPeriod('user-1', 'fp-1')
const supabase = makeClient()
const result = await lockPeriod(supabase as never, 'user-1', 'fp-1')
expect(result.locked_at).toBeTruthy()
expect(handler).toHaveBeenCalledOnce()
@@ -70,7 +67,8 @@ describe('lockPeriod', () => {
results = [{ data: period, error: null }]
await expect(lockPeriod('user-1', 'fp-1')).rejects.toThrow('already locked')
const supabase = makeClient()
await expect(lockPeriod(supabase as never, 'user-1', 'fp-1')).rejects.toThrow('already locked')
})
})
@@ -89,7 +87,8 @@ describe('closePeriod', () => {
{ data: closedPeriod, error: null },
]
const result = await closePeriod('user-1', 'fp-1')
const supabase = makeClient()
const result = await closePeriod(supabase as never, 'user-1', 'fp-1')
expect(result.is_closed).toBe(true)
})
@@ -103,7 +102,8 @@ describe('closePeriod', () => {
results = [{ data: period, error: null }]
await expect(closePeriod('user-1', 'fp-1')).rejects.toThrow('must be locked')
const supabase = makeClient()
await expect(closePeriod(supabase as never, 'user-1', 'fp-1')).rejects.toThrow('must be locked')
})
it('rejects if no closing_entry_id', async () => {
@@ -116,7 +116,8 @@ describe('closePeriod', () => {
results = [{ data: period, error: null }]
await expect(closePeriod('user-1', 'fp-1')).rejects.toThrow(
const supabase = makeClient()
await expect(closePeriod(supabase as never, 'user-1', 'fp-1')).rejects.toThrow(
'Year-end closing must be executed'
)
})
@@ -144,7 +145,8 @@ describe('createNextPeriod', () => {
{ data: nextPeriod, error: null }, // insert
]
const result = await createNextPeriod('user-1', 'fp-2024')
const supabase = makeClient()
const result = await createNextPeriod(supabase as never, 'user-1', 'fp-2024')
expect(result.period_start).toBe('2025-01-01')
expect(result.period_end).toBe('2025-12-31')
expect(result.previous_period_id).toBe('fp-2024')
@@ -171,7 +173,8 @@ describe('createNextPeriod', () => {
{ data: nextPeriod, error: null },
]
const result = await createNextPeriod('user-1', 'fp-2024')
const supabase = makeClient()
const result = await createNextPeriod(supabase as never, 'user-1', 'fp-2024')
expect(result.period_start).toBe('2024-07-01')
expect(result.period_end).toBe('2025-06-30')
})
@@ -26,10 +26,6 @@ function makeClient() {
}
}
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => makeClient()),
}))
vi.mock('@/lib/bookkeeping/engine', () => ({
validateBalance: vi.fn().mockReturnValue({ valid: true, totalDebit: 1000, totalCredit: 1000 }),
getNextVoucherNumber: vi.fn(async () => ++resultIdx), // just increment
@@ -100,14 +96,16 @@ describe('correctEntry', () => {
it('creates reversal with swapped debit/credit lines', async () => {
setupResults()
const result = await correctEntry('user-1', 'orig-1', correctedLines)
const supabase = makeClient()
const result = await correctEntry(supabase as never, 'user-1', 'orig-1', correctedLines)
expect(result.reversal).toBeDefined()
expect(result.reversal.reverses_id).toBe('orig-1')
})
it('links original ↔ reversal ↔ corrected via IDs', async () => {
setupResults()
const result = await correctEntry('user-1', 'orig-1', correctedLines)
const supabase = makeClient()
const result = await correctEntry(supabase as never, 'user-1', 'orig-1', correctedLines)
expect(result.reversal.id).toBe('reversal-1')
expect(result.corrected.id).toBe('corrected-1')
expect(result.corrected.correction_of_id).toBe('orig-1')
@@ -120,8 +118,9 @@ describe('correctEntry', () => {
totalCredit: 1000,
})
const supabase = makeClient()
await expect(
correctEntry('user-1', 'orig-1', [
correctEntry(supabase as never, 'user-1', 'orig-1', [
{ account_number: '5420', debit_amount: 1200, credit_amount: 0 },
{ account_number: '1930', debit_amount: 0, credit_amount: 1000 },
])
@@ -134,7 +133,8 @@ describe('correctEntry', () => {
const handler = vi.fn()
eventBus.on('journal_entry.corrected', handler)
await correctEntry('user-1', 'orig-1', correctedLines)
const supabase = makeClient()
await correctEntry(supabase as never, 'user-1', 'orig-1', correctedLines)
expect(handler).toHaveBeenCalledOnce()
expect(handler).toHaveBeenCalledWith(
@@ -27,10 +27,6 @@ function makeClient() {
}
}
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => makeClient()),
}))
vi.mock('@/lib/reports/trial-balance', () => ({
generateTrialBalance: vi.fn(),
}))
@@ -80,7 +76,8 @@ describe('validateYearEndReadiness', () => {
totalCredit: 0,
} as never)
const result = await validateYearEndReadiness('user-1', 'fp-1')
const supabase = makeClient()
const result = await validateYearEndReadiness(supabase as never, 'user-1', 'fp-1')
expect(result.ready).toBe(false)
expect(result.errors.some((e: string) => e.includes('draft'))).toBe(true)
})
@@ -101,7 +98,8 @@ describe('validateYearEndReadiness', () => {
totalCredit: 9500,
} as never)
const result = await validateYearEndReadiness('user-1', 'fp-1')
const supabase = makeClient()
const result = await validateYearEndReadiness(supabase as never, 'user-1', 'fp-1')
expect(result.ready).toBe(false)
expect(result.trialBalanceBalanced).toBe(false)
expect(result.errors.some((e: string) => e.includes('Trial balance'))).toBe(true)
@@ -110,17 +108,15 @@ describe('validateYearEndReadiness', () => {
it('warns on voucher gaps', async () => {
const period = makeFiscalPeriod({ id: 'fp-1', is_closed: false, closing_entry_id: null })
// Override makeClient to return gaps from rpc
const { createClient } = await import('@/lib/supabase/server')
// Create a client with custom rpc that returns gap data
const builder = makeBuilder()
const client = {
const supabase = {
from: vi.fn().mockImplementation(() => builder),
rpc: vi.fn().mockResolvedValue({
data: [{ gap_start: 5, gap_end: 7 }],
error: null,
}),
}
vi.mocked(createClient).mockResolvedValue(client as never)
resultIdx = 0
results = [
@@ -136,7 +132,7 @@ describe('validateYearEndReadiness', () => {
totalCredit: 10000,
} as never)
const result = await validateYearEndReadiness('user-1', 'fp-1')
const result = await validateYearEndReadiness(supabase as never, 'user-1', 'fp-1')
expect(result.warnings.some((w: string) => w.includes('gap'))).toBe(true)
expect(result.voucherGaps).toHaveLength(1)
})
@@ -164,7 +160,8 @@ describe('previewYearEndClosing', () => {
totalCredit: 500000,
} as never)
const preview = await previewYearEndClosing('user-1', 'fp-1')
const supabase = makeClient()
const preview = await previewYearEndClosing(supabase as never, 'user-1', 'fp-1')
expect(preview.netResult).toBe(150000)
expect(preview.closingAccount).toBe('2099')
@@ -189,7 +186,8 @@ describe('previewYearEndClosing', () => {
totalCredit: 100000,
} as never)
const preview = await previewYearEndClosing('user-1', 'fp-1')
const supabase = makeClient()
const preview = await previewYearEndClosing(supabase as never, 'user-1', 'fp-1')
expect(preview.closingAccount).toBe('2010')
expect(preview.closingAccountName).toBe('Eget kapital')
+5 -5
View File
@@ -1,4 +1,4 @@
import { createClient } from '@/lib/supabase/server'
import type { SupabaseClient } from '@supabase/supabase-js'
import { eventBus } from '@/lib/events'
import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration'
import type { FiscalPeriod, PeriodStatus } from '@/types'
@@ -8,10 +8,10 @@ import type { FiscalPeriod, PeriodStatus } from '@/types'
* Requires: period exists, belongs to user, not already locked/closed.
*/
export async function lockPeriod(
supabase: SupabaseClient,
userId: string,
fiscalPeriodId: string
): Promise<FiscalPeriod> {
const supabase = await createClient()
// Fetch period
const { data: period, error: fetchError } = await supabase
@@ -60,10 +60,10 @@ export async function lockPeriod(
* Requires: period is locked AND closing_entry_id is set (year-end must run first).
*/
export async function closePeriod(
supabase: SupabaseClient,
userId: string,
fiscalPeriodId: string
): Promise<FiscalPeriod> {
const supabase = await createClient()
const { data: period, error: fetchError } = await supabase
.from('fiscal_periods')
@@ -112,10 +112,10 @@ export async function closePeriod(
* Sets previous_period_id for chain validation.
*/
export async function createNextPeriod(
supabase: SupabaseClient,
userId: string,
currentPeriodId: string
): Promise<FiscalPeriod> {
const supabase = await createClient()
const { data: current, error: fetchError } = await supabase
.from('fiscal_periods')
@@ -198,10 +198,10 @@ export async function createNextPeriod(
* Get status summary for a fiscal period.
*/
export async function getPeriodStatus(
supabase: SupabaseClient,
userId: string,
fiscalPeriodId: string
): Promise<PeriodStatus> {
const supabase = await createClient()
const { data: period, error: fetchError } = await supabase
.from('fiscal_periods')
+4 -3
View File
@@ -1,4 +1,4 @@
import { createClient } from '@/lib/supabase/server'
import type { SupabaseClient } from '@supabase/supabase-js'
import { eventBus } from '@/lib/events'
import type {
CreateJournalEntryLineInput,
@@ -23,6 +23,7 @@ import { validateBalance, getNextVoucherNumber } from '@/lib/bookkeeping/engine'
* Returns: { reversal, corrected } - the two new entries created
*/
export async function correctEntry(
supabase: SupabaseClient,
userId: string,
originalEntryId: string,
correctedLines: CreateJournalEntryLineInput[]
@@ -35,8 +36,6 @@ export async function correctEntry(
)
}
const supabase = await createClient()
// Fetch original entry with lines
const { data: original, error: fetchError } = await supabase
.from('journal_entries')
@@ -57,6 +56,7 @@ export async function correctEntry(
// ===== Step 1: Create storno (reversal) entry =====
const reversalVoucherNumber = await getNextVoucherNumber(
supabase,
userId,
original.fiscal_period_id,
original.voucher_series || 'A'
@@ -146,6 +146,7 @@ export async function correctEntry(
try {
const correctedVoucherNumber = await getNextVoucherNumber(
supabase,
userId,
original.fiscal_period_id,
original.voucher_series || 'A'
+17 -17
View File
@@ -1,4 +1,4 @@
import { createClient } from '@/lib/supabase/server'
import type { SupabaseClient } from '@supabase/supabase-js'
import { eventBus } from '@/lib/events'
import { createJournalEntry } from '@/lib/bookkeeping/engine'
import { generateTrialBalance } from '@/lib/reports/trial-balance'
@@ -19,10 +19,10 @@ import type {
* Returns blocking errors and informational warnings.
*/
export async function validateYearEndReadiness(
supabase: SupabaseClient,
userId: string,
fiscalPeriodId: string
): Promise<YearEndValidation> {
const supabase = await createClient()
const errors: string[] = []
const warnings: string[] = []
@@ -84,7 +84,7 @@ export async function validateYearEndReadiness(
}
// Check: trial balance is balanced
const trialBalance = await generateTrialBalance(userId, fiscalPeriodId)
const trialBalance = await generateTrialBalance(supabase, userId, fiscalPeriodId)
const trialBalanceBalanced = trialBalance.isBalanced
if (!trialBalanceBalanced) {
@@ -120,10 +120,10 @@ export async function validateYearEndReadiness(
* Shows the net result, closing account, and the journal entry lines that would be created.
*/
export async function previewYearEndClosing(
supabase: SupabaseClient,
userId: string,
fiscalPeriodId: string
): Promise<YearEndPreview> {
const supabase = await createClient()
// Get entity type to determine closing account
const { data: settings } = await supabase
@@ -140,11 +140,11 @@ export async function previewYearEndClosing(
: 'Årets resultat'
// Get income statement for net result
const incomeStatement = await generateIncomeStatement(userId, fiscalPeriodId)
const incomeStatement = await generateIncomeStatement(supabase, userId, fiscalPeriodId)
const netResult = incomeStatement.net_result
// Get trial balance for individual account balances in class 3-8
const { rows } = await generateTrialBalance(userId, fiscalPeriodId)
const { rows } = await generateTrialBalance(supabase, userId, fiscalPeriodId)
const resultAccounts = rows.filter(
(r) => r.account_class >= 3 && r.account_class <= 8
)
@@ -233,17 +233,16 @@ export async function previewYearEndClosing(
* 7. Generate opening balances in next period
*/
export async function executeYearEndClosing(
supabase: SupabaseClient,
userId: string,
fiscalPeriodId: string
): Promise<YearEndResult> {
// 1. Validate readiness
const validation = await validateYearEndReadiness(userId, fiscalPeriodId)
const validation = await validateYearEndReadiness(supabase, userId, fiscalPeriodId)
if (!validation.ready) {
throw new Error(`Year-end closing not ready: ${validation.errors.join('; ')}`)
}
const supabase = await createClient()
// Fetch the period for dates
const { data: period } = await supabase
.from('fiscal_periods')
@@ -257,14 +256,14 @@ export async function executeYearEndClosing(
}
// 2. Get closing preview
const preview = await previewYearEndClosing(userId, fiscalPeriodId)
const preview = await previewYearEndClosing(supabase, userId, fiscalPeriodId)
if (preview.closingLines.length === 0) {
throw new Error('No result accounts to close — period has no activity')
}
// 3. Create closing entry via the journal engine
const closingEntry = await createJournalEntry(userId, {
const closingEntry = await createJournalEntry(supabase, userId, {
fiscal_period_id: fiscalPeriodId,
entry_date: period.period_end,
description: `Årsbokslut ${period.name}`,
@@ -285,16 +284,17 @@ export async function executeYearEndClosing(
}
// 5. Lock the period
await lockPeriod(userId, fiscalPeriodId)
await lockPeriod(supabase, userId, fiscalPeriodId)
// 6. Close the period
await closePeriod(userId, fiscalPeriodId)
await closePeriod(supabase, userId, fiscalPeriodId)
// 7. Create next period
const nextPeriod = await createNextPeriod(userId, fiscalPeriodId)
const nextPeriod = await createNextPeriod(supabase, userId, fiscalPeriodId)
// 8. Generate opening balances in next period
const openingBalanceEntry = await generateOpeningBalances(
supabase,
userId,
fiscalPeriodId,
nextPeriod.id
@@ -330,11 +330,11 @@ export async function executeYearEndClosing(
* The entry must be balanced (total debit openings = total credit openings).
*/
export async function generateOpeningBalances(
supabase: SupabaseClient,
userId: string,
closedPeriodId: string,
nextPeriodId: string
): Promise<JournalEntry> {
const supabase = await createClient()
// Get next period for the entry date
const { data: nextPeriod } = await supabase
@@ -349,7 +349,7 @@ export async function generateOpeningBalances(
}
// Get trial balance of closed period (includes the closing entry)
const { rows } = await generateTrialBalance(userId, closedPeriodId)
const { rows } = await generateTrialBalance(supabase, userId, closedPeriodId)
// Filter to balance sheet accounts (class 1-2) with non-zero closing balance
const balanceSheetAccounts = rows.filter(
@@ -397,7 +397,7 @@ export async function generateOpeningBalances(
}
// Create opening balance entry in next period
const openingEntry = await createJournalEntry(userId, {
const openingEntry = await createJournalEntry(supabase, userId, {
fiscal_period_id: nextPeriodId,
entry_date: nextPeriod.period_start,
description: `Ingående balans ${nextPeriod.name}`,
@@ -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
@@ -26,10 +26,6 @@ function makeClient() {
}
}
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => makeClient()),
}))
import { getTaxCodeByCode, calculateMomsFromTaxCodes } from '../tax-code-service'
beforeEach(() => {
@@ -50,7 +46,8 @@ describe('getTaxCodeByCode', () => {
results = [{ data: userCode, error: null }]
const result = await getTaxCodeByCode('user-1', 'MP1')
const supabase = makeClient()
const result = await getTaxCodeByCode(supabase as never, 'user-1', 'MP1')
expect(result).not.toBeNull()
expect(result!.user_id).toBe('user-1')
expect(result!.description).toBe('Custom 25% moms')
@@ -59,7 +56,8 @@ describe('getTaxCodeByCode', () => {
it('returns null when code does not exist', async () => {
results = [{ data: null, error: { code: 'PGRST116' } }]
const result = await getTaxCodeByCode('user-1', 'NONEXISTENT')
const supabase = makeClient()
const result = await getTaxCodeByCode(supabase as never, 'user-1', 'NONEXISTENT')
expect(result).toBeNull()
})
})
@@ -96,7 +94,8 @@ describe('calculateMomsFromTaxCodes', () => {
{ data: [mp1, ip1], error: null },
]
const result = await calculateMomsFromTaxCodes('user-1', '2024-01-01', '2024-12-31')
const supabase = makeClient()
const result = await calculateMomsFromTaxCodes(supabase as never, 'user-1', '2024-01-01', '2024-12-31')
expect(result.length).toBeGreaterThan(0)
// Results should be sorted by box
+6 -8
View File
@@ -1,4 +1,4 @@
import { createClient } from '@/lib/supabase/server'
import type { SupabaseClient } from '@supabase/supabase-js'
import type { TaxCode } from '@/types'
/**
@@ -12,8 +12,7 @@ import type { TaxCode } from '@/types'
/**
* Get all active tax codes for a user (including system codes)
*/
export async function getTaxCodes(userId: string): Promise<TaxCode[]> {
const supabase = await createClient()
export async function getTaxCodes(supabase: SupabaseClient, userId: string): Promise<TaxCode[]> {
const { data, error } = await supabase
.from('tax_codes')
@@ -32,10 +31,10 @@ export async function getTaxCodes(userId: string): Promise<TaxCode[]> {
* Get a single tax code by code string
*/
export async function getTaxCodeByCode(
supabase: SupabaseClient,
userId: string,
code: string
): Promise<TaxCode | null> {
const supabase = await createClient()
// Prefer user-specific code over system code
const { data, error } = await supabase
@@ -72,11 +71,11 @@ export interface MomsBoxResult {
* category-based VAT calculation.
*/
export async function calculateMomsFromTaxCodes(
supabase: SupabaseClient,
userId: string,
periodStart: string,
periodEnd: string
): Promise<MomsBoxResult[]> {
const supabase = await createClient()
// Fetch journal entry lines with tax_code in the period
const { data: lines, error: linesError } = await supabase
@@ -104,7 +103,7 @@ export async function calculateMomsFromTaxCodes(
}
// Fetch all tax codes for lookup
const taxCodes = await getTaxCodes(userId)
const taxCodes = await getTaxCodes(supabase, userId)
const taxCodeMap = new Map<string, TaxCode>()
for (const tc of taxCodes) {
// User codes take precedence over system codes
@@ -154,8 +153,7 @@ export async function calculateMomsFromTaxCodes(
/**
* Seed tax codes for a user by calling the database function
*/
export async function seedTaxCodes(userId: string): Promise<void> {
const supabase = await createClient()
export async function seedTaxCodes(supabase: SupabaseClient, userId: string): Promise<void> {
const { error } = await supabase.rpc('seed_tax_codes_for_user', {
p_user_id: userId,