03b569d708
- 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>
171 lines
4.8 KiB
TypeScript
171 lines
4.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// ============================================================
|
|
// Mock — sequential result queue
|
|
// ============================================================
|
|
|
|
let resultIdx: number
|
|
let results: Array<{ data?: unknown; error?: unknown }>
|
|
|
|
function makeBuilder() {
|
|
const b: Record<string, unknown> = {}
|
|
for (const m of ['select', 'eq', 'in']) {
|
|
b[m] = vi.fn().mockReturnValue(b)
|
|
}
|
|
b.single = vi.fn().mockImplementation(async () => results[resultIdx++] ?? { data: null, error: null })
|
|
b.then = (resolve: (v: unknown) => void) => resolve(results[resultIdx++] ?? { data: null, error: null })
|
|
return b
|
|
}
|
|
|
|
function makeClient() {
|
|
return {
|
|
from: vi.fn().mockImplementation(() => makeBuilder()),
|
|
} as any
|
|
}
|
|
|
|
import { generateARReconciliation } from '../ar-reconciliation'
|
|
|
|
let supabase: ReturnType<typeof makeClient>
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
resultIdx = 0
|
|
results = []
|
|
supabase = makeClient()
|
|
})
|
|
|
|
describe('generateARReconciliation', () => {
|
|
it('returns reconciled when AR ledger matches account 1510', async () => {
|
|
results = [
|
|
// 0: invoices
|
|
{
|
|
data: [
|
|
{ total: 5000, paid_amount: 2000 },
|
|
{ total: 3000, paid_amount: 0 },
|
|
],
|
|
error: null,
|
|
},
|
|
// 1: journal_entry_lines for account 1510
|
|
{
|
|
data: [
|
|
{ debit_amount: 8000, credit_amount: 0, journal_entry_id: 'e1' },
|
|
{ debit_amount: 0, credit_amount: 2000, journal_entry_id: 'e2' },
|
|
],
|
|
error: null,
|
|
},
|
|
]
|
|
|
|
const result = await generateARReconciliation(supabase, 'user-1', 'period-1')
|
|
|
|
// AR: (5000-2000) + (3000-0) = 6000
|
|
expect(result.ar_ledger_total).toBe(6000)
|
|
// 1510: 8000 - 2000 = 6000
|
|
expect(result.account_1510_balance).toBe(6000)
|
|
expect(result.difference).toBe(0)
|
|
expect(result.is_reconciled).toBe(true)
|
|
})
|
|
|
|
it('detects difference when AR ledger does not match account 1510', async () => {
|
|
results = [
|
|
// 0: invoices
|
|
{
|
|
data: [
|
|
{ total: 5000, paid_amount: 0 },
|
|
],
|
|
error: null,
|
|
},
|
|
// 1: journal_entry_lines — manual debit on 1510 creates mismatch
|
|
{
|
|
data: [
|
|
{ debit_amount: 5000, credit_amount: 0, journal_entry_id: 'e1' },
|
|
{ debit_amount: 1000, credit_amount: 0, journal_entry_id: 'e2' }, // manual entry
|
|
],
|
|
error: null,
|
|
},
|
|
]
|
|
|
|
const result = await generateARReconciliation(supabase, 'user-1', 'period-1')
|
|
|
|
expect(result.ar_ledger_total).toBe(5000)
|
|
expect(result.account_1510_balance).toBe(6000)
|
|
expect(result.difference).toBe(-1000)
|
|
expect(result.is_reconciled).toBe(false)
|
|
})
|
|
|
|
it('returns zero balances when no data exists', async () => {
|
|
results = [
|
|
{ data: [], error: null },
|
|
{ data: [], error: null },
|
|
]
|
|
|
|
const result = await generateARReconciliation(supabase, 'user-1', 'period-1')
|
|
|
|
expect(result.ar_ledger_total).toBe(0)
|
|
expect(result.account_1510_balance).toBe(0)
|
|
expect(result.difference).toBe(0)
|
|
expect(result.is_reconciled).toBe(true)
|
|
})
|
|
|
|
it('handles null invoice data gracefully', async () => {
|
|
results = [
|
|
{ data: null, error: null },
|
|
{
|
|
data: [
|
|
{ debit_amount: 3000, credit_amount: 0, journal_entry_id: 'e1' },
|
|
],
|
|
error: null,
|
|
},
|
|
]
|
|
|
|
const result = await generateARReconciliation(supabase, 'user-1', 'period-1')
|
|
|
|
expect(result.ar_ledger_total).toBe(0)
|
|
expect(result.account_1510_balance).toBe(3000)
|
|
expect(result.difference).toBe(-3000)
|
|
expect(result.is_reconciled).toBe(false)
|
|
})
|
|
|
|
it('uses correct debit-normal balance for account 1510 (asset)', async () => {
|
|
results = [
|
|
{ data: [], error: null },
|
|
{
|
|
data: [
|
|
{ debit_amount: 10000, credit_amount: 0, journal_entry_id: 'e1' },
|
|
{ debit_amount: 0, credit_amount: 4000, journal_entry_id: 'e2' },
|
|
{ debit_amount: 0, credit_amount: 3000, journal_entry_id: 'e3' },
|
|
],
|
|
error: null,
|
|
},
|
|
]
|
|
|
|
const result = await generateARReconciliation(supabase, 'user-1', 'period-1')
|
|
|
|
// Balance = debits - credits = 10000 - 4000 - 3000 = 3000
|
|
expect(result.account_1510_balance).toBe(3000)
|
|
})
|
|
|
|
it('uses Math.round for monetary precision', async () => {
|
|
results = [
|
|
{
|
|
data: [
|
|
{ total: 100.1, paid_amount: 33.33 },
|
|
],
|
|
error: null,
|
|
},
|
|
{
|
|
data: [
|
|
{ debit_amount: 66.77, credit_amount: 0, journal_entry_id: 'e1' },
|
|
],
|
|
error: null,
|
|
},
|
|
]
|
|
|
|
const result = await generateARReconciliation(supabase, 'user-1', 'period-1')
|
|
|
|
expect(result.ar_ledger_total).toBe(66.77)
|
|
expect(result.account_1510_balance).toBe(66.77)
|
|
expect(result.difference).toBe(0)
|
|
expect(result.is_reconciled).toBe(true)
|
|
})
|
|
})
|