Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
172 lines
4.9 KiB
TypeScript
172 lines
4.9 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()),
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} 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)
|
|
})
|
|
})
|