Files
accounted/lib/transactions/__tests__/migration-coverage.test.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

109 lines
4.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fetchMigrationCoverageEnd } from '@/lib/transactions/migration-coverage'
import type { SupabaseClient } from '@supabase/supabase-js'
/**
* Recording chain mock: remembers every chained call so the test can assert
* the exact filters. The generic createMockSupabase proxy swallows arguments.
* Results are dequeued per from() call: first for sie_imports (arm gate),
* second for journal_entries.
*/
function createRecordingSupabase(results: Array<{ data: unknown; error: unknown }>) {
const calls: Array<{ method: string; args: unknown[] }> = []
const queue = [...results]
const buildChain = () => {
const result = queue.shift() ?? { data: null, error: null }
const chain: Record<string, unknown> = {}
const record =
(method: string) =>
(...args: unknown[]) => {
calls.push({ method, args })
return chain
}
for (const method of ['select', 'eq', 'neq', 'not', 'order', 'limit']) {
chain[method] = record(method)
}
chain.maybeSingle = vi.fn().mockImplementation(() => {
calls.push({ method: 'maybeSingle', args: [] })
return Promise.resolve(result)
})
return chain
}
const from = vi.fn().mockImplementation((...args: unknown[]) => {
calls.push({ method: 'from', args })
return buildChain()
})
return { supabase: { from } as unknown as SupabaseClient, calls }
}
const armed = { data: { id: 'import-1' }, error: null }
describe('fetchMigrationCoverageEnd', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns the latest imported entry date for a migrated company', async () => {
const { supabase } = createRecordingSupabase([
armed,
{ data: { entry_date: '2026-06-30' }, error: null },
])
await expect(fetchMigrationCoverageEnd(supabase, 'company-1')).resolves.toBe('2026-06-30')
})
it('returns null without a completed SIE import, even if import-typed entries exist', async () => {
// Arm gate: source_type='import' is accepted from API clients, so a
// company that never migrated must never get a cutoff from such entries.
const { supabase, calls } = createRecordingSupabase([
{ data: null, error: null },
{ data: { entry_date: '2026-12-31' }, error: null },
])
await expect(fetchMigrationCoverageEnd(supabase, 'company-1')).resolves.toBeNull()
expect(calls.filter((c) => c.method === 'from').map((c) => c.args[0])).toEqual(['sie_imports'])
})
it('returns null when a migrated company has no imported entries', async () => {
const { supabase } = createRecordingSupabase([armed, { data: null, error: null }])
await expect(fetchMigrationCoverageEnd(supabase, 'company-1')).resolves.toBeNull()
})
it('arms on completed imports and takes the max over posted import entries excluding the omföringsverifikation', async () => {
const { supabase, calls } = createRecordingSupabase([armed, { data: null, error: null }])
await fetchMigrationCoverageEnd(supabase, 'company-1')
expect(calls.filter((c) => c.method === 'from').map((c) => c.args[0])).toEqual([
'sie_imports',
'journal_entries',
])
expect(calls).toContainEqual({ method: 'eq', args: ['status', 'completed'] })
expect(calls).toContainEqual({ method: 'eq', args: ['company_id', 'company-1'] })
expect(calls).toContainEqual({ method: 'eq', args: ['status', 'posted'] })
expect(calls).toContainEqual({ method: 'eq', args: ['source_type', 'import'] })
// The importer's omföringsverifikation is dated at fiscal year end; without
// this exclusion a mid-year migration with skipped vouchers would get a
// future cutoff again (the bug this module exists to fix). Excluded by its
// hardcoded description prefix, NOT by voucher series: imported vouchers
// keep the source file's series, and real files use series M.
expect(calls).toContainEqual({
method: 'not',
args: ['description', 'like', 'Omföringsverifikation:%'],
})
expect(calls.filter((c) => c.method === 'neq')).toEqual([])
expect(calls).toContainEqual({
method: 'order',
args: ['entry_date', { ascending: false }],
})
expect(calls).toContainEqual({ method: 'limit', args: [1] })
})
it('never reads sie_imports.fiscal_year_end as the cutoff', async () => {
// Regression guard for the original bug: the cutoff value must come from
// actual imported data, never the #RAR-declared fiscal year. sie_imports
// is consulted only as the arm gate (select id).
const { supabase, calls } = createRecordingSupabase([armed, { data: null, error: null }])
await fetchMigrationCoverageEnd(supabase, 'company-1')
const selects = calls.filter((c) => c.method === 'select').map((c) => c.args[0])
expect(selects).toEqual(['id', 'entry_date'])
})
})