/** * Unit tests for the executors added to bring every declared op type up to a * callable state through `commitPendingOperation`. Tests run through the * public dispatcher (executors are not exported individually) so the wiring * is exercised alongside executor logic. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { eventBus } from '@/lib/events/bus' import { createQueuedMockSupabase, makeInvoice, makeFiscalPeriod } from '@/tests/helpers' import type { PendingOperation } from '@/types' vi.mock('@/lib/core/bookkeeping/period-service', async () => { const actual = await vi.importActual( '@/lib/core/bookkeeping/period-service' ) return { ...actual, unlockPeriod: vi.fn(), } }) vi.mock('@/lib/import/sie-parser', () => ({ parseSIEFile: vi.fn(), calculateFileHash: vi.fn(async () => 'mock-hash'), })) vi.mock('@/lib/import/sie-import', () => ({ executeSIEImport: vi.fn(), })) vi.mock('@/lib/bookkeeping/invoice-entries', async () => { const actual = await vi.importActual( '@/lib/bookkeeping/invoice-entries' ) return { ...actual, createCreditNoteJournalEntry: vi.fn(), } }) import { commitPendingOperation } from '../commit' import { unlockPeriod } from '@/lib/core/bookkeeping/period-service' import { parseSIEFile } from '@/lib/import/sie-parser' import { executeSIEImport } from '@/lib/import/sie-import' import { createCreditNoteJournalEntry } from '@/lib/bookkeeping/invoice-entries' function makePendingOp(overrides: Partial): PendingOperation { return { id: 'op-1', user_id: 'user-1', company_id: 'company-1', operation_type: 'create_customer', status: 'pending', title: 'test', params: {}, preview_data: {}, result_data: null, actor_type: 'user', actor_id: null, actor_label: null, risk_level: 'high', auto_commit_eligible: false, auto_committed_at: null, created_at: '2026-05-03T00:00:00Z', resolved_at: null, updated_at: '2026-05-03T00:00:00Z', ...overrides, } as PendingOperation } beforeEach(() => { vi.clearAllMocks() eventBus.clear() }) // ─── unlock_period ────────────────────────────────────────────────── describe('commitPendingOperation: unlock_period', () => { it('happy path: clears locked_at and returns committed', async () => { const period = makeFiscalPeriod({ id: 'fp-1', locked_at: null }) vi.mocked(unlockPeriod).mockResolvedValueOnce(period) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: null, error: null }) // dispatcher's pending_operations update const op = makePendingOp({ operation_type: 'unlock_period', params: { fiscal_period_id: 'fp-1' }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('committed') expect(result.data).toMatchObject({ period_id: 'fp-1', locked_at: null }) expect(unlockPeriod).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', 'fp-1') }) it('rejects when fiscal_period_id is missing', async () => { const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: null, error: null }) // dispatcher's reject update const op = makePendingOp({ operation_type: 'unlock_period', params: {} }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('failed') expect(result.http_status).toBe(400) expect(unlockPeriod).not.toHaveBeenCalled() }) it('surfaces underlying service errors', async () => { vi.mocked(unlockPeriod).mockRejectedValueOnce(new Error('Period is not locked')) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: null, error: null }) // dispatcher's reject update on throw const op = makePendingOp({ operation_type: 'unlock_period', params: { fiscal_period_id: 'fp-1' }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('failed') expect(result.error).toMatch(/not locked/) }) }) // ─── import_sie ───────────────────────────────────────────────────── describe('commitPendingOperation: import_sie', () => { it('happy path: parses, imports, returns committed with summary', async () => { vi.mocked(parseSIEFile).mockReturnValueOnce({} as never) vi.mocked(executeSIEImport).mockResolvedValueOnce({ success: true, importId: 'imp-1', fiscalPeriodId: 'fp-1', openingBalanceEntryId: 'ob-1', journalEntriesCreated: 5, journalEntryIds: ['je-1', 'je-2', 'je-3', 'je-4', 'je-5'], errors: [], warnings: ['minor warning'], }) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: null, error: null }) // dispatcher's update const op = makePendingOp({ operation_type: 'import_sie', params: { file_content: '#FLAGGA 0\n', filename: 'test.sie', mappings: [], create_fiscal_period: true, import_opening_balances: true, import_transactions: true, }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('committed') expect(result.data).toMatchObject({ import_id: 'imp-1', journal_entries_created: 5, warnings: ['minor warning'], }) expect(parseSIEFile).toHaveBeenCalledWith('#FLAGGA 0\n') }) it('rejects when required params are missing', async () => { const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: null, error: null }) // dispatcher's reject update const op = makePendingOp({ operation_type: 'import_sie', params: { filename: 'x.sie' } }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('failed') expect(result.http_status).toBe(400) expect(parseSIEFile).not.toHaveBeenCalled() }) it('returns the executeSIEImport errors when success=false', async () => { vi.mocked(parseSIEFile).mockReturnValueOnce({} as never) vi.mocked(executeSIEImport).mockResolvedValueOnce({ success: false, importId: null, fiscalPeriodId: null, openingBalanceEntryId: null, journalEntriesCreated: 0, journalEntryIds: [], errors: ['duplicate import'], warnings: [], }) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: null, error: null }) // dispatcher's reject update const op = makePendingOp({ operation_type: 'import_sie', params: { file_content: '#FLAGGA 0\n', filename: 'test.sie', mappings: [], create_fiscal_period: true, import_opening_balances: false, import_transactions: true, }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('failed') expect(result.error).toMatch(/duplicate import/) }) }) // ─── credit_invoice ───────────────────────────────────────────────── describe('commitPendingOperation: credit_invoice', () => { it('happy path (accrual): inserts negated credit note and books JE', async () => { const original = makeInvoice({ id: 'inv-1', invoice_number: 'F-2024001', status: 'sent', document_type: 'invoice', subtotal: 1000, vat_amount: 250, total: 1250, }) const originalWithItems = { ...original, items: [ { sort_order: 0, description: 'Service', quantity: 1, unit: 'st', unit_price: 1000, line_total: 1000, vat_rate: 25, vat_amount: 250 }, ], } const creditNoteRow = { ...original, id: 'cn-1', invoice_number: 'KR-F-2024001' } const completeCreditNote = { ...creditNoteRow, customer: { name: 'Acme AB' }, items: [] } const { supabase, enqueue } = createQueuedMockSupabase() // 0: CAS claim enqueue({ data: { id: 'op-1' }, error: null }) // 1: fetch original with items enqueue({ data: originalWithItems, error: null }) // 2: insert credit note enqueue({ data: creditNoteRow, error: null }) // 3: insert items (await thenable) enqueue({ data: null, error: null }) // 4: update original status='credited' enqueue({ data: null, error: null }) // 5: re-fetch complete credit note with customer + items enqueue({ data: completeCreditNote, error: null }) // 6: company_settings enqueue({ data: { entity_type: 'aktiebolag', accounting_method: 'accrual' }, error: null }) // 7: update invoice with journal_entry_id enqueue({ data: null, error: null }) // 8: dispatcher's pending_operations update enqueue({ data: null, error: null }) vi.mocked(createCreditNoteJournalEntry).mockResolvedValueOnce({ id: 'je-1' } as never) const op = makePendingOp({ operation_type: 'credit_invoice', params: { invoice_id: 'inv-1', reason: 'Wrong amount' }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('committed') expect(result.data).toMatchObject({ credit_note_id: 'cn-1', journal_entry_id: 'je-1' }) expect(createCreditNoteJournalEntry).toHaveBeenCalled() }) it('skips JE on cash accounting', async () => { const original = makeInvoice({ id: 'inv-1', status: 'paid', document_type: 'invoice' }) const originalWithItems = { ...original, items: [] } const creditNoteRow = { ...original, id: 'cn-2', invoice_number: 'KR-F-2024001' } const completeCreditNote = { ...creditNoteRow, customer: null, items: [] } const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: originalWithItems, error: null }) enqueue({ data: creditNoteRow, error: null }) enqueue({ data: null, error: null }) enqueue({ data: null, error: null }) enqueue({ data: completeCreditNote, error: null }) enqueue({ data: { entity_type: 'enskild_firma', accounting_method: 'cash' }, error: null }) // no JE update; go straight to dispatcher update enqueue({ data: null, error: null }) const op = makePendingOp({ operation_type: 'credit_invoice', params: { invoice_id: 'inv-1' }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('committed') expect(result.data).toMatchObject({ credit_note_id: 'cn-2', journal_entry_id: null }) expect(createCreditNoteJournalEntry).not.toHaveBeenCalled() }) it('auto-rejects when invoice is already credited (409)', async () => { const original = makeInvoice({ id: 'inv-1', status: 'credited', document_type: 'invoice' }) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: { ...original, items: [] }, error: null }) // dispatcher auto-reject path also does an update enqueue({ data: null, error: null }) const op = makePendingOp({ operation_type: 'credit_invoice', params: { invoice_id: 'inv-1' }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('rejected') expect(result.auto_rejected).toBe(true) expect(result.http_status).toBe(409) }) it('rejects when invoice_id is missing', async () => { const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: null, error: null }) // dispatcher's reject update const op = makePendingOp({ operation_type: 'credit_invoice', params: {} }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('failed') expect(result.http_status).toBe(400) }) it('rejects invoices with status outside sent/paid/overdue', async () => { const original = makeInvoice({ id: 'inv-1', status: 'draft', document_type: 'invoice' }) const { supabase, enqueue } = createQueuedMockSupabase() enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim enqueue({ data: { ...original, items: [] }, error: null }) enqueue({ data: null, error: null }) // dispatcher's reject update const op = makePendingOp({ operation_type: 'credit_invoice', params: { invoice_id: 'inv-1' }, }) const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op) expect(result.status).toBe('failed') expect(result.http_status).toBe(400) }) })