Files
accounted/lib/reports/__tests__/latest-vouchers.test.ts
T

195 lines
6.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { getLatestPostedVouchers } from '../latest-vouchers'
import { formatLatestVouchers, LATEST_VOUCHERS_LABEL } from '../latest-vouchers-format'
import { createQueuedMockSupabase } from '@/tests/helpers'
import enMessages from '../../../messages/en.json'
import svMessages from '../../../messages/sv.json'
beforeEach(() => {
vi.clearAllMocks()
})
interface Row {
id: string
voucher_series: string | null
voucher_number: number
}
function row(id: string, series: string | null, number: number): Row {
return { id, voucher_series: series, voucher_number: number }
}
describe('getLatestPostedVouchers', () => {
it('returns the highest number per series, sorted by series', async () => {
const q = createQueuedMockSupabase()
q.enqueue({
data: [
row('e1', 'B', 12),
row('e2', 'A', 214),
row('e3', 'A', 7),
row('e4', 'B', 37),
row('e5', 'A', 100),
],
error: null,
})
const result = await getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ toDate: '2026-12-31' }
)
expect(result).toEqual([
{ series: 'A', last_number: 214 },
{ series: 'B', last_number: 37 },
])
})
it('returns an empty array when the window holds no posted vouchers', async () => {
const q = createQueuedMockSupabase()
q.enqueue({ data: [], error: null })
const result = await getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ toDate: '2026-12-31' }
)
expect(result).toEqual([])
})
it('excludes drafts and cancelled entries, which is what voucher_number > 0 buys', async () => {
const q = createQueuedMockSupabase()
q.enqueue({ data: [row('e1', 'A', 5)], error: null })
await getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ toDate: '2026-12-31' }
)
// Drafts and cancelled entries both carry voucher_number = 0.
expect(q.findCall('journal_entries', 'gt')).toEqual(['voucher_number', 0])
// Reversed entries keep their number and their slot in the series, so they
// count: excluding them would report a gap that is not there.
const statusFilter = q.findCall('journal_entries', 'in')
expect(statusFilter?.[0]).toBe('status')
expect(statusFilter?.[1]).toEqual(['posted', 'reversed'])
})
it('bounds the window by toDate only when no fromDate is given (balansrapport)', async () => {
const q = createQueuedMockSupabase()
q.enqueue({ data: [row('e1', 'A', 5)], error: null })
await getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ toDate: '2026-03-31' }
)
expect(q.findCall('journal_entries', 'lte')).toEqual(['entry_date', '2026-03-31'])
// The fiscal period filter is the lower bound; no explicit gte.
expect(q.findCall('journal_entries', 'gte')).toBeUndefined()
expect(q.findCall('journal_entries', 'eq')).toEqual(['company_id', 'company-1'])
})
it('bounds both ends when fromDate is given (resultatrapport)', async () => {
const q = createQueuedMockSupabase()
q.enqueue({ data: [row('e1', 'A', 5)], error: null })
await getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ fromDate: '2026-01-01', toDate: '2026-03-31' }
)
expect(q.findCall('journal_entries', 'gte')).toEqual(['entry_date', '2026-01-01'])
expect(q.findCall('journal_entries', 'lte')).toEqual(['entry_date', '2026-03-31'])
})
it('orders on the primary key so paging cannot skip or duplicate rows', async () => {
const q = createQueuedMockSupabase()
q.enqueue({ data: [row('e1', 'A', 5)], error: null })
await getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ toDate: '2026-12-31' }
)
expect(q.findCall('journal_entries', 'order')).toEqual(['id', { ascending: true }])
})
it('falls back to series A for entries with a null voucher_series', async () => {
const q = createQueuedMockSupabase()
q.enqueue({ data: [row('e1', null, 9), row('e2', 'A', 4)], error: null })
const result = await getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ toDate: '2026-12-31' }
)
expect(result).toEqual([{ series: 'A', last_number: 9 }])
})
it('propagates a query error so the caller can decide (engines swallow it)', async () => {
const q = createQueuedMockSupabase()
q.enqueue({ data: null, error: { message: 'boom' } })
await expect(
getLatestPostedVouchers(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
q.supabase as any,
'company-1',
'period-1',
{ toDate: '2026-12-31' }
)
).rejects.toThrow('boom')
})
})
describe('formatLatestVouchers', () => {
it('joins series and number with a comma', () => {
expect(
formatLatestVouchers([
{ series: 'A', last_number: 214 },
{ series: 'B', last_number: 37 },
])
).toBe('A 214, B 37')
})
it('returns null for an empty list so surfaces drop the line entirely', () => {
expect(formatLatestVouchers([])).toBeNull()
})
it('returns null for undefined', () => {
expect(formatLatestVouchers(undefined)).toBeNull()
})
it('names the number as posted, not allocated', () => {
// The distinction is the whole point: an allocated number can sit ahead of
// the books, and a reconciler told the wrong one chases a phantom gap.
expect(LATEST_VOUCHERS_LABEL).toBe('Senaste bokförda verifikat')
})
it('keeps the web label explicit in both supported locales', () => {
expect(svMessages.reports.latest_posted_vouchers).toBe(LATEST_VOUCHERS_LABEL)
expect(enMessages.reports.latest_posted_vouchers).toBe('Latest posted vouchers')
})
})