import { describe, it, expect, vi, beforeEach } from 'vitest' import { validateBalance, getSwedishLocalDate, createDraftEntry, reverseEntry } from '../engine' import type { CreateJournalEntryLineInput, JournalEntryStatus } from '@/types' // Mock Supabase client for createDraftEntry/reverseEntry tests function createMockChain(overrides: Record = {}) { const chain: Record = { select: vi.fn().mockReturnThis(), single: vi.fn().mockResolvedValue({ data: overrides.singleData ?? null, error: overrides.singleError ?? null }), eq: vi.fn().mockReturnThis(), insert: vi.fn().mockReturnThis(), update: vi.fn().mockReturnThis(), delete: vi.fn().mockReturnThis(), in: vi.fn().mockReturnThis(), lte: vi.fn().mockReturnThis(), gte: vi.fn().mockReturnThis(), order: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), } return chain } // Mock event bus vi.mock('@/lib/events', () => ({ eventBus: { emit: vi.fn().mockResolvedValue([]) }, })) describe('validateBalance', () => { it('balanced entry (debit == credit) → valid: true', () => { const lines: CreateJournalEntryLineInput[] = [ { account_number: '1930', debit_amount: 1000, credit_amount: 0 }, { account_number: '3001', debit_amount: 0, credit_amount: 1000 }, ] const result = validateBalance(lines) expect(result.valid).toBe(true) expect(result.totalDebit).toBe(1000) expect(result.totalCredit).toBe(1000) }) it('unbalanced entry → valid: false', () => { const lines: CreateJournalEntryLineInput[] = [ { account_number: '1930', debit_amount: 1000, credit_amount: 0 }, { account_number: '3001', debit_amount: 0, credit_amount: 500 }, ] const result = validateBalance(lines) expect(result.valid).toBe(false) expect(result.totalDebit).toBe(1000) expect(result.totalCredit).toBe(500) }) it('zero amounts → valid: false (roundedDebit must be > 0)', () => { const lines: CreateJournalEntryLineInput[] = [ { account_number: '1930', debit_amount: 0, credit_amount: 0 }, { account_number: '3001', debit_amount: 0, credit_amount: 0 }, ] const result = validateBalance(lines) expect(result.valid).toBe(false) expect(result.totalDebit).toBe(0) expect(result.totalCredit).toBe(0) }) it('floating point edge case (33.33 + 33.33 + 33.34) → valid: true', () => { const lines: CreateJournalEntryLineInput[] = [ { account_number: '1930', debit_amount: 33.33, credit_amount: 0 }, { account_number: '1930', debit_amount: 33.33, credit_amount: 0 }, { account_number: '1930', debit_amount: 33.34, credit_amount: 0 }, { account_number: '3001', debit_amount: 0, credit_amount: 100 }, ] const result = validateBalance(lines) expect(result.valid).toBe(true) expect(result.totalDebit).toBe(100) expect(result.totalCredit).toBe(100) }) it('single line (only debit, no credit) → valid: false', () => { const lines: CreateJournalEntryLineInput[] = [ { account_number: '1930', debit_amount: 500, credit_amount: 0 }, ] const result = validateBalance(lines) expect(result.valid).toBe(false) }) }) describe('getSwedishLocalDate', () => { it('returns a date string in YYYY-MM-DD format', () => { const date = getSwedishLocalDate() expect(date).toMatch(/^\d{4}-\d{2}-\d{2}$/) }) it('returns a valid date', () => { const date = getSwedishLocalDate() const parsed = new Date(date) expect(parsed.toString()).not.toBe('Invalid Date') }) }) describe('createDraftEntry — cancelled status on line-insert failure', () => { it('sets status to cancelled (not delete) when line insert fails', async () => { const updateMock = vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }) const supabase = { from: vi.fn().mockImplementation((table: string) => { if (table === 'journal_entries') { return { insert: vi.fn().mockReturnValue({ select: vi.fn().mockReturnValue({ single: vi.fn().mockResolvedValue({ data: { id: 'entry-1', user_id: 'user-1', status: 'draft' as JournalEntryStatus }, error: null, }), }), }), update: updateMock, delete: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }), } } if (table === 'journal_entry_lines') { return { insert: vi.fn().mockResolvedValue({ error: { message: 'Line insert failed' } }), } } if (table === 'chart_of_accounts') { return { select: vi.fn().mockReturnValue({ eq: vi.fn().mockReturnValue({ in: vi.fn().mockResolvedValue({ data: [{ account_number: '1930', id: 'acc-1' }, { account_number: '3001', id: 'acc-2' }], error: null, }), }), }), } } return createMockChain() }), } await expect( createDraftEntry(supabase as never, 'company-1', 'user-1', { fiscal_period_id: 'period-1', entry_date: '2024-01-01', description: 'Test', source_type: 'manual', lines: [ { account_number: '1930', debit_amount: 1000, credit_amount: 0 }, { account_number: '3001', debit_amount: 0, credit_amount: 1000 }, ], }) ).rejects.toThrow('Failed to create journal entry lines') // Should call update with cancelled status, NOT delete expect(updateMock).toHaveBeenCalledWith({ status: 'cancelled' }) }) }) describe('JournalEntryStatus type includes cancelled', () => { it('cancelled is a valid JournalEntryStatus value', () => { const status: JournalEntryStatus = 'cancelled' expect(['draft', 'posted', 'reversed', 'cancelled']).toContain(status) }) })