195 lines
6.3 KiB
TypeScript
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')
|
|
})
|
|
})
|