/** * pg-real test for get_unlinked_1930_lines (PR 3 of erp-mafia/gnubok#443). * * Verifies the RPC excludes opening_balance vouchers from the unmatched-1930 * set, while preserving the existing behavior for posted bank-import vouchers, * date-range filtering, and company scoping. * * Migration: 20260514132534_unlinked_1930_lines_exclude_opening_balance.sql */ import { describe, it, expect } from 'vitest' import { randomUUID } from 'node:crypto' import { getPool } from './setup' import { insertAuthUser, insertCompany, insertFiscalPeriod } from './fixtures' async function insertPostedJournalEntry(params: { userId: string companyId: string fiscalPeriodId: string entryDate: string sourceType: 'opening_balance' | 'manual' | 'bank_transaction' | 'import' voucherNumber: number amount?: number }): Promise { const id = randomUUID() const amount = params.amount ?? 1000 // Insert as posted directly. This bypasses commit_journal_entry's voucher // sequencing; that's fine for testing the read-side RPC, which only cares // about (account_number, status, source_type, date_range, link presence). await getPool().query( `INSERT INTO public.journal_entries (id, user_id, company_id, fiscal_period_id, voucher_number, voucher_series, entry_date, description, source_type, status) VALUES ($1, $2, $3, $4, $5, 'A', $6, $7, $8, 'posted')`, [ id, params.userId, params.companyId, params.fiscalPeriodId, params.voucherNumber, params.entryDate, `Test ${params.sourceType}`, params.sourceType, ], ) // Balanced pair on 1930 + 2091 (balanserad vinst/förlust — the realistic // carried-forward counterpart for an IB on a bank account; harmless for the // other source_types where the test only cares about the 1930 side). await getPool().query( `INSERT INTO public.journal_entry_lines (journal_entry_id, account_number, debit_amount, credit_amount) VALUES ($1, '1930', $2, 0), ($1, '2091', 0, $2)`, [id, amount], ) return id } describe('get_unlinked_1930_lines RPC — opening_balance exclusion', () => { it('excludes opening_balance vouchers from the unmatched-1930 set', async () => { const userId = await insertAuthUser() const companyId = await insertCompany({ createdBy: userId }) const fiscalPeriodId = await insertFiscalPeriod({ userId, companyId, periodStart: '2026-01-01', periodEnd: '2026-12-31', }) // Three vouchers on 1930, all posted, none linked to a transaction: // IB voucher (source_type='opening_balance') — should be EXCLUDED // Bank import voucher — should be RETURNED // Manual voucher — should be RETURNED await insertPostedJournalEntry({ userId, companyId, fiscalPeriodId, entryDate: '2026-01-01', sourceType: 'opening_balance', voucherNumber: 1, amount: 50000, }) const bankEntryId = await insertPostedJournalEntry({ userId, companyId, fiscalPeriodId, entryDate: '2026-03-15', sourceType: 'bank_transaction', voucherNumber: 2, amount: 1500, }) const manualEntryId = await insertPostedJournalEntry({ userId, companyId, fiscalPeriodId, entryDate: '2026-04-20', sourceType: 'manual', voucherNumber: 3, amount: 200, }) const { rows } = await getPool().query( `SELECT journal_entry_id, source_type FROM public.get_unlinked_1930_lines($1)`, [companyId], ) const returnedIds = new Set(rows.map((r) => r.journal_entry_id)) expect(returnedIds.has(bankEntryId)).toBe(true) expect(returnedIds.has(manualEntryId)).toBe(true) // IB voucher should NOT be returned regardless of company/date scope. expect(rows.find((r) => r.source_type === 'opening_balance')).toBeUndefined() }) it('still applies date_from / date_to filtering', async () => { const userId = await insertAuthUser() const companyId = await insertCompany({ createdBy: userId }) const fiscalPeriodId = await insertFiscalPeriod({ userId, companyId, periodStart: '2026-01-01', periodEnd: '2026-12-31', }) await insertPostedJournalEntry({ userId, companyId, fiscalPeriodId, entryDate: '2026-02-01', sourceType: 'bank_transaction', voucherNumber: 10, }) await insertPostedJournalEntry({ userId, companyId, fiscalPeriodId, entryDate: '2026-08-01', sourceType: 'bank_transaction', voucherNumber: 11, }) // Window covers only the second voucher. const { rows } = await getPool().query( `SELECT entry_date FROM public.get_unlinked_1930_lines($1, $2, $3) ORDER BY entry_date`, [companyId, '2026-07-01', '2026-12-31'], ) expect(rows).toHaveLength(1) expect(rows[0].entry_date.toISOString().slice(0, 10)).toBe('2026-08-01') }) it('scopes to the requested company only', async () => { const userA = await insertAuthUser() const userB = await insertAuthUser() const companyA = await insertCompany({ createdBy: userA, name: 'A' }) const companyB = await insertCompany({ createdBy: userB, name: 'B' }) const fpA = await insertFiscalPeriod({ userId: userA, companyId: companyA }) const fpB = await insertFiscalPeriod({ userId: userB, companyId: companyB }) await insertPostedJournalEntry({ userId: userA, companyId: companyA, fiscalPeriodId: fpA, entryDate: '2026-03-01', sourceType: 'bank_transaction', voucherNumber: 1, }) await insertPostedJournalEntry({ userId: userB, companyId: companyB, fiscalPeriodId: fpB, entryDate: '2026-03-01', sourceType: 'bank_transaction', voucherNumber: 1, }) const { rows: rowsA } = await getPool().query( `SELECT 1 FROM public.get_unlinked_1930_lines($1)`, [companyA], ) const { rows: rowsB } = await getPool().query( `SELECT 1 FROM public.get_unlinked_1930_lines($1)`, [companyB], ) expect(rowsA).toHaveLength(1) expect(rowsB).toHaveLength(1) }) })