* fix(bokslut): make dispositions storno-safe and derive fond math from opening balances A reversed year_end voucher kept its storno in the income statement while the original was excluded (source_type asymmetry), inflating resultat fore dispositioner by exactly the reversed amount, and the posted-only fond balance produced a phantom negative 212X that leaked a bogus aterforing proposal. Support case: a user double-booked periodiseringsfond, reversed both correctly, and the dispositions page still showed wrong numbers. - trial-balance excludeYearEndClosing now also excludes entries chained to reversed year_end entries via reverses_id/correction_of_id (grammar verified against staging PostgREST) - listExistingPeriodiseringsfonder counts posted+reversed so storno pairs cancel, and returns opening balances per fond - schablonintakt per IL 30 kap 6a: opening balance base, rate = SLR per closing year (1.96% FY2025, 2.55% FY2026), replacing the wrong SLR+1pp 0.0355 constant - avsattning 25% cap is year-total: already-provisioned current-cohort growth consumes headroom in both preview and commit, so re-running the flow can no longer double-book the fond - SLP posts before avsattning (deductible, shrinks the cap base) and is posted-aware: no double proposal or double count on resumed runs - sumPostedYearEndDispositions counts correction replacements of reversed year_end entries and exposes the SLP portion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(bokslut): use roundOre for new fond/disposition rounding Satisfies the naive-ore-round ratchet that tightened on main; identical arithmetic, pinned by the existing exact-value tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): address PR #1063 review findings - computeProposal receives the already-validated period row: a transient DB failure can no longer silently skip a requested disposition (and two redundant per-item period fetches are gone) - getSchablonintaktRate fails closed for unmapped years instead of falling back to the latest known rate: statutory rates are never guessed; POST rate override remains the escape hatch - listExistingPeriodiseringsfonder is opening-balance-entry aware: a fond carried via the OB entry booked by year-end closing was counted twice (once from history, once from the OB entry); balances now derive from OB + current-period activity when an OB entry exists - periodStart is validated as a real calendar date, not just a shape - reversed year_end correction targets resolve company-wide in sumPostedYearEndDispositions, matching the trial balance exclusion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
549 lines
18 KiB
TypeScript
549 lines
18 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||
import {
|
||
proposeAvsattning,
|
||
proposeAteforing,
|
||
listExistingPeriodiseringsfonder,
|
||
getPeriodiseringsfondCohortAccount,
|
||
getSchablonintaktRate,
|
||
PFOND_AB_RATE,
|
||
PFOND_MAX_HOLD_YEARS,
|
||
type ExistingFond,
|
||
} from '../reserves/periodiseringsfond-service'
|
||
import { fetchEntryLines } from '@/lib/bookkeeping/entry-lines'
|
||
|
||
vi.mock('@/lib/bookkeeping/entry-lines', () => ({
|
||
fetchEntryLines: vi.fn(),
|
||
}))
|
||
|
||
describe('getPeriodiseringsfondCohortAccount', () => {
|
||
it('maps fiscal year to BAS 212X account', () => {
|
||
expect(getPeriodiseringsfondCohortAccount(2020)).toBe('2120')
|
||
expect(getPeriodiseringsfondCohortAccount(2025)).toBe('2125')
|
||
expect(getPeriodiseringsfondCohortAccount(2026)).toBe('2126')
|
||
expect(getPeriodiseringsfondCohortAccount(2027)).toBe('2127')
|
||
})
|
||
|
||
it('returns 2129 for 2019 per BAS collision rule', () => {
|
||
expect(getPeriodiseringsfondCohortAccount(2019)).toBe('2129')
|
||
})
|
||
})
|
||
|
||
describe('getSchablonintaktRate', () => {
|
||
it('returns the SLR for known closing years (IL 30 kap 6a §: SLR itself, not SLR + 1 pp)', () => {
|
||
expect(getSchablonintaktRate(2025)).toBe(0.0196)
|
||
expect(getSchablonintaktRate(2026)).toBe(0.0255)
|
||
})
|
||
|
||
it('fails closed for unmapped years: a statutory rate is never guessed', () => {
|
||
expect(() => getSchablonintaktRate(2030)).toThrow(/not configured/)
|
||
})
|
||
})
|
||
|
||
describe('proposeAvsattning', () => {
|
||
it('caps avsättning at 25% of base', () => {
|
||
const result = proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
desiredAmount: 200_000, // user asks for 50%: should be capped
|
||
fiscalYear: 2025,
|
||
})
|
||
expect(result).not.toBeNull()
|
||
expect(result!.amount).toBe(100_000) // 25% of 400_000
|
||
expect(result!.warnings).toHaveLength(1)
|
||
expect(result!.warnings[0]).toContain('25 %')
|
||
expect(result!.lines[0].account_number).toBe('8811')
|
||
expect(result!.lines[1].account_number).toBe('2125')
|
||
})
|
||
|
||
it('defaults to maximum when desiredAmount is omitted', () => {
|
||
const result = proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
fiscalYear: 2025,
|
||
})
|
||
expect(result!.amount).toBe(100_000)
|
||
expect(result!.warnings).toHaveLength(0)
|
||
})
|
||
|
||
it('honors a smaller desiredAmount', () => {
|
||
const result = proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
desiredAmount: 30_000,
|
||
fiscalYear: 2025,
|
||
})
|
||
expect(result!.amount).toBe(30_000)
|
||
expect(result!.warnings).toHaveLength(0)
|
||
})
|
||
|
||
it('returns null when base is negative (loss year)', () => {
|
||
expect(
|
||
proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: -100_000,
|
||
fiscalYear: 2025,
|
||
}),
|
||
).toBeNull()
|
||
})
|
||
|
||
it('returns null when desired is zero', () => {
|
||
expect(
|
||
proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
desiredAmount: 0,
|
||
fiscalYear: 2025,
|
||
}),
|
||
).toBeNull()
|
||
})
|
||
|
||
it('emits balanced lines (debit 8811 = credit 21XX)', () => {
|
||
const result = proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
fiscalYear: 2026,
|
||
})
|
||
expect(result!.lines).toHaveLength(2)
|
||
const totalDebit = result!.lines.reduce((s, l) => s + l.debit_amount, 0)
|
||
const totalCredit = result!.lines.reduce((s, l) => s + l.credit_amount, 0)
|
||
expect(totalDebit).toBe(totalCredit)
|
||
expect(result!.lines[1].account_number).toBe('2126')
|
||
})
|
||
|
||
it('uses fiscal year for cohort account number', () => {
|
||
const result = proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 100_000,
|
||
fiscalYear: 2027,
|
||
})
|
||
expect(result!.lines[1].account_number).toBe('2127')
|
||
})
|
||
|
||
it('reduces headroom by an already-provisioned current-year fond', () => {
|
||
const result = proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
fiscalYear: 2025,
|
||
alreadyProvisioned: 60_000,
|
||
})
|
||
// 25% of 400_000 = 100_000 cap; 60_000 already booked -> 40_000 left
|
||
expect(result!.amount).toBe(40_000)
|
||
})
|
||
|
||
it('returns null when the year cap is already fully provisioned', () => {
|
||
expect(
|
||
proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
fiscalYear: 2025,
|
||
alreadyProvisioned: 100_000,
|
||
}),
|
||
).toBeNull()
|
||
// Over-provisioned (legacy double booking) must not propose more either
|
||
expect(
|
||
proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
fiscalYear: 2025,
|
||
alreadyProvisioned: 150_000,
|
||
}),
|
||
).toBeNull()
|
||
})
|
||
|
||
it('caps an explicit desiredAmount to the remaining headroom with a warning', () => {
|
||
const result = proposeAvsattning({
|
||
skattemassigtResultatBeforeAvsattning: 400_000,
|
||
desiredAmount: 80_000,
|
||
fiscalYear: 2025,
|
||
alreadyProvisioned: 60_000,
|
||
})
|
||
expect(result!.amount).toBe(40_000)
|
||
expect(result!.warnings).toHaveLength(1)
|
||
expect(result!.warnings[0]).toContain('redan avsatt')
|
||
})
|
||
})
|
||
|
||
describe('proposeAteforing', () => {
|
||
it('forces full reversal of 6+ year old fonder and marks them required', () => {
|
||
const fonder: ExistingFond[] = [
|
||
{
|
||
account_number: '2120',
|
||
cohort_year: 2020,
|
||
balance: 50_000,
|
||
opening_balance: 50_000,
|
||
must_return_this_year: true,
|
||
},
|
||
]
|
||
const result = proposeAteforing(fonder, { schablonintaktRate: 0.03 })
|
||
expect(result.proposals).toHaveLength(1)
|
||
expect(result.proposals[0].amount).toBe(50_000)
|
||
expect(result.proposals[0].required).toBe(true)
|
||
expect(result.proposals[0].warnings[0]).toContain('6-årsgränsen')
|
||
// 50_000 × 0.03 = 1500
|
||
expect(result.schablonintaktAmount).toBe(1_500)
|
||
})
|
||
|
||
it('skips non-mandatory fonder when no return amount requested', () => {
|
||
const fonder: ExistingFond[] = [
|
||
{
|
||
account_number: '2122',
|
||
cohort_year: 2022,
|
||
balance: 100_000,
|
||
opening_balance: 100_000,
|
||
must_return_this_year: false,
|
||
},
|
||
]
|
||
const result = proposeAteforing(fonder, { schablonintaktRate: 0.03 })
|
||
expect(result.proposals).toHaveLength(0)
|
||
// Schablonintäkt is computed regardless of return decision
|
||
expect(result.schablonintaktAmount).toBe(3_000)
|
||
})
|
||
|
||
it('returns the requested optional amount when user opts in', () => {
|
||
const fonder: ExistingFond[] = [
|
||
{
|
||
account_number: '2123',
|
||
cohort_year: 2023,
|
||
balance: 80_000,
|
||
opening_balance: 80_000,
|
||
must_return_this_year: false,
|
||
},
|
||
]
|
||
const result = proposeAteforing(fonder, {
|
||
returns: { '2123': 50_000 },
|
||
schablonintaktRate: 0.03,
|
||
})
|
||
expect(result.proposals).toHaveLength(1)
|
||
expect(result.proposals[0].amount).toBe(50_000)
|
||
expect(result.proposals[0].required).toBeFalsy()
|
||
})
|
||
|
||
it('caps optional returns to the actual balance', () => {
|
||
const fonder: ExistingFond[] = [
|
||
{
|
||
account_number: '2124',
|
||
cohort_year: 2024,
|
||
balance: 30_000,
|
||
opening_balance: 30_000,
|
||
must_return_this_year: false,
|
||
},
|
||
]
|
||
const result = proposeAteforing(fonder, {
|
||
returns: { '2124': 100_000 },
|
||
schablonintaktRate: 0.03,
|
||
})
|
||
expect(result.proposals[0].amount).toBe(30_000) // capped to balance
|
||
})
|
||
|
||
it('emits balanced lines (debit 21XX = credit 8819)', () => {
|
||
const fonder: ExistingFond[] = [
|
||
{
|
||
account_number: '2120',
|
||
cohort_year: 2020,
|
||
balance: 50_000,
|
||
opening_balance: 50_000,
|
||
must_return_this_year: true,
|
||
},
|
||
]
|
||
const result = proposeAteforing(fonder, { schablonintaktRate: 0.03 })
|
||
const lines = result.proposals[0].lines
|
||
expect(lines).toHaveLength(2)
|
||
expect(lines[0].account_number).toBe('2120')
|
||
expect(lines[0].debit_amount).toBe(50_000)
|
||
expect(lines[1].account_number).toBe('8819')
|
||
expect(lines[1].credit_amount).toBe(50_000)
|
||
})
|
||
|
||
it('never proposes a return for zero or negative closing balances', () => {
|
||
// A negative balance is a data anomaly (e.g. a debit-only view of a
|
||
// storno pair): proposing a negative återföring would produce an
|
||
// uncommittable entry.
|
||
const fonder: ExistingFond[] = [
|
||
{
|
||
account_number: '2125',
|
||
cohort_year: 2025,
|
||
balance: -396_459,
|
||
opening_balance: 0,
|
||
must_return_this_year: false,
|
||
},
|
||
{
|
||
account_number: '2120',
|
||
cohort_year: 2020,
|
||
balance: 0,
|
||
opening_balance: 0,
|
||
must_return_this_year: true,
|
||
},
|
||
]
|
||
const result = proposeAteforing(fonder, { schablonintaktRate: 0.03 })
|
||
expect(result.proposals).toHaveLength(0)
|
||
expect(result.schablonintaktAmount).toBe(0)
|
||
})
|
||
|
||
it('computes schablonintäkt on opening balances per IL 30 kap 6a §', () => {
|
||
const fonder: ExistingFond[] = [
|
||
// Avsatt in THIS bokslut: opening 0, no schablonintäkt
|
||
{
|
||
account_number: '2125',
|
||
cohort_year: 2025,
|
||
balance: 200_000,
|
||
opening_balance: 0,
|
||
must_return_this_year: false,
|
||
},
|
||
// Held all year: full schablonintäkt
|
||
{
|
||
account_number: '2123',
|
||
cohort_year: 2023,
|
||
balance: 100_000,
|
||
opening_balance: 100_000,
|
||
must_return_this_year: false,
|
||
},
|
||
// Fully återförd during the year: schablonintäkt still due
|
||
{
|
||
account_number: '2120',
|
||
cohort_year: 2020,
|
||
balance: 0,
|
||
opening_balance: 50_000,
|
||
must_return_this_year: true,
|
||
},
|
||
]
|
||
const result = proposeAteforing(fonder, { schablonintaktRate: 0.03 })
|
||
// 0 + 100_000 × 0.03 + 50_000 × 0.03 = 4_500
|
||
expect(result.schablonintaktAmount).toBe(4_500)
|
||
// The 2125/2120 fonder yield no return proposal (2125 not requested,
|
||
// 2120 balance already zero despite the mandatory flag)
|
||
expect(result.proposals).toHaveLength(0)
|
||
})
|
||
|
||
it('exposes constants used by callers', () => {
|
||
expect(PFOND_AB_RATE).toBe(0.25)
|
||
expect(PFOND_MAX_HOLD_YEARS).toBe(6)
|
||
})
|
||
})
|
||
|
||
describe('listExistingPeriodiseringsfonder', () => {
|
||
const supabase = {} as SupabaseClient
|
||
const mockFetchEntryLines = vi.mocked(fetchEntryLines)
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks()
|
||
})
|
||
|
||
/** Chainable query spy that records every filter call in order. */
|
||
function makeChainableQuery() {
|
||
const calls: Array<[string, ...unknown[]]> = []
|
||
const q: Record<string, unknown> = {}
|
||
for (const m of ['eq', 'in', 'gte', 'lte'] as const) {
|
||
q[m] = vi.fn((...args: unknown[]) => {
|
||
calls.push([m, ...args])
|
||
return q
|
||
})
|
||
}
|
||
return { q, calls }
|
||
}
|
||
|
||
it('calls fetchEntryLines with entry dates attached for the opening-balance split', async () => {
|
||
mockFetchEntryLines.mockResolvedValue([])
|
||
|
||
const result = await listExistingPeriodiseringsfonder(
|
||
supabase, 'company-1', '2025-12-31', '2025-01-01',
|
||
)
|
||
|
||
expect(result).toEqual([])
|
||
expect(mockFetchEntryLines).toHaveBeenCalledTimes(1)
|
||
expect(mockFetchEntryLines).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
supabase,
|
||
entryColumns: 'id, entry_date',
|
||
lineColumns: 'account_number, debit_amount, credit_amount',
|
||
}),
|
||
)
|
||
})
|
||
|
||
it('scopes entries to company/posted+reversed/closing date and lines to the 21xx range', async () => {
|
||
mockFetchEntryLines.mockResolvedValue([])
|
||
|
||
await listExistingPeriodiseringsfonder(supabase, 'company-1', '2025-12-31', '2025-01-01')
|
||
|
||
const options = mockFetchEntryLines.mock.calls[0][0]
|
||
|
||
const entries = makeChainableQuery()
|
||
options.filterEntries(entries.q)
|
||
// Both statuses: a reversed original and its storno must cancel; a
|
||
// posted-only view would show the storno's debit as a phantom negative
|
||
// fond balance.
|
||
expect(entries.calls).toEqual([
|
||
['eq', 'company_id', 'company-1'],
|
||
['in', 'status', ['posted', 'reversed']],
|
||
['lte', 'entry_date', '2025-12-31'],
|
||
])
|
||
|
||
expect(options.filterLines).toBeDefined()
|
||
const lines = makeChainableQuery()
|
||
options.filterLines?.(lines.q)
|
||
expect(lines.calls).toEqual([
|
||
['gte', 'account_number', '2110'],
|
||
['lte', 'account_number', '2199'],
|
||
])
|
||
})
|
||
|
||
it('sums per-account balances (credit minus debit), splits opening vs closing, and maps cohort years', async () => {
|
||
mockFetchEntryLines.mockResolvedValue([
|
||
// 2125 avsatt during the period: closing 80_000, opening 0
|
||
{
|
||
account_number: '2125', debit_amount: 0, credit_amount: 100_000,
|
||
journal_entries: { entry_date: '2026-12-31' },
|
||
},
|
||
{
|
||
account_number: '2125', debit_amount: 20_000, credit_amount: 0,
|
||
journal_entries: { entry_date: '2026-12-31' },
|
||
},
|
||
// Numeric strings and nulls coerce like the embed rows did; booked in
|
||
// a prior period so it is part of the opening balance
|
||
{
|
||
account_number: '2120', debit_amount: null, credit_amount: '50000',
|
||
journal_entries: { entry_date: '2020-12-31' },
|
||
},
|
||
// 2129 is the 2019 collision account per the BAS 2020 seed
|
||
{
|
||
account_number: '2129', debit_amount: 0, credit_amount: 10_000,
|
||
journal_entries: { entry_date: '2019-12-31' },
|
||
},
|
||
// 2110 is a grouping account with no cohort: skipped
|
||
{
|
||
account_number: '2110', debit_amount: 0, credit_amount: 5_000,
|
||
journal_entries: { entry_date: '2020-12-31' },
|
||
},
|
||
])
|
||
|
||
const result = await listExistingPeriodiseringsfonder(
|
||
supabase, 'company-1', '2026-12-31', '2026-01-01',
|
||
)
|
||
|
||
// Sorted by cohort_year ascending
|
||
expect(result).toEqual([
|
||
{
|
||
account_number: '2129',
|
||
cohort_year: 2019,
|
||
balance: 10_000,
|
||
opening_balance: 10_000,
|
||
must_return_this_year: true, // 2019 + 6 = 2025 <= 2026
|
||
},
|
||
{
|
||
account_number: '2120',
|
||
cohort_year: 2020,
|
||
balance: 50_000,
|
||
opening_balance: 50_000,
|
||
must_return_this_year: true, // 2020 + 6 = 2026 <= 2026
|
||
},
|
||
{
|
||
account_number: '2125',
|
||
cohort_year: 2025,
|
||
balance: 80_000,
|
||
opening_balance: 0,
|
||
must_return_this_year: false, // 2025 + 6 = 2031 > 2026
|
||
},
|
||
])
|
||
})
|
||
|
||
it('does not double-count a fond carried via an opening-balance entry', async () => {
|
||
// Year-end closing of the previous period books an OB entry (dated at
|
||
// period_start) that carries the accumulated 21xx balances. Summing the
|
||
// OB entry together with the pre-period history would double the fond.
|
||
mockFetchEntryLines.mockResolvedValue([
|
||
// Prior-period avsättning: already carried inside the OB entry
|
||
{
|
||
account_number: '2124', debit_amount: 0, credit_amount: 60_000,
|
||
journal_entry_id: 'prior-1',
|
||
journal_entries: { entry_date: '2024-12-31' },
|
||
},
|
||
// The OB entry itself, dated exactly at period_start
|
||
{
|
||
account_number: '2124', debit_amount: 0, credit_amount: 60_000,
|
||
journal_entry_id: 'ob-1',
|
||
journal_entries: { entry_date: '2025-01-01' },
|
||
},
|
||
// Current-period partial återföring
|
||
{
|
||
account_number: '2124', debit_amount: 10_000, credit_amount: 0,
|
||
journal_entry_id: 'cur-1',
|
||
journal_entries: { entry_date: '2025-12-31' },
|
||
},
|
||
])
|
||
|
||
const result = await listExistingPeriodiseringsfonder(
|
||
supabase, 'company-1', '2025-12-31', '2025-01-01', 'ob-1',
|
||
)
|
||
|
||
expect(result).toEqual([
|
||
{
|
||
account_number: '2124',
|
||
cohort_year: 2024,
|
||
balance: 50_000, // 60_000 carried - 10_000 returned, NOT 110_000
|
||
opening_balance: 60_000, // the OB entry, despite being dated at period_start
|
||
must_return_this_year: false,
|
||
},
|
||
])
|
||
})
|
||
|
||
it('rejects a well-shaped but impossible period start date', async () => {
|
||
await expect(
|
||
listExistingPeriodiseringsfonder(supabase, 'company-1', '2025-12-31', '2025-02-31'),
|
||
).rejects.toThrow('Invalid period start: 2025-02-31')
|
||
expect(mockFetchEntryLines).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('keeps a fond fully återförd during the period (closing 0, opening > 0)', async () => {
|
||
mockFetchEntryLines.mockResolvedValue([
|
||
{
|
||
account_number: '2121', debit_amount: 0, credit_amount: 40_000,
|
||
journal_entries: { entry_date: '2021-12-31' },
|
||
},
|
||
{
|
||
account_number: '2121', debit_amount: 40_000, credit_amount: 0,
|
||
journal_entries: { entry_date: '2026-12-31' },
|
||
},
|
||
])
|
||
|
||
const result = await listExistingPeriodiseringsfonder(
|
||
supabase, 'company-1', '2026-12-31', '2026-01-01',
|
||
)
|
||
|
||
expect(result).toEqual([
|
||
{
|
||
account_number: '2121',
|
||
cohort_year: 2021,
|
||
balance: 0,
|
||
opening_balance: 40_000,
|
||
must_return_this_year: false, // 2021 + 6 = 2027 > 2026
|
||
},
|
||
])
|
||
})
|
||
|
||
it('drops near-zero balances below the 0.005 threshold', async () => {
|
||
mockFetchEntryLines.mockResolvedValue([
|
||
{ account_number: '2123', debit_amount: 1_000, credit_amount: 1_000.004 },
|
||
{ account_number: '2124', debit_amount: 0, credit_amount: 30_000 },
|
||
])
|
||
|
||
const result = await listExistingPeriodiseringsfonder(
|
||
supabase, 'company-1', '2026-12-31', '2026-01-01',
|
||
)
|
||
|
||
expect(result).toHaveLength(1)
|
||
expect(result[0].account_number).toBe('2124')
|
||
})
|
||
|
||
it('wraps helper failures in the periodiseringsfond error contract', async () => {
|
||
mockFetchEntryLines.mockRejectedValue(new Error('boom'))
|
||
|
||
await expect(
|
||
listExistingPeriodiseringsfonder(supabase, 'company-1', '2025-12-31', '2025-01-01'),
|
||
).rejects.toThrow('Failed to fetch periodiseringsfond balances: boom')
|
||
})
|
||
|
||
it('rejects an unparseable closing date before querying', async () => {
|
||
await expect(
|
||
listExistingPeriodiseringsfonder(supabase, 'company-1', 'not-a-date', '2025-01-01'),
|
||
).rejects.toThrow('Invalid closing date: not-a-date')
|
||
expect(mockFetchEntryLines).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('rejects an unparseable period start before querying', async () => {
|
||
await expect(
|
||
listExistingPeriodiseringsfonder(supabase, 'company-1', '2025-12-31', 'not-a-date'),
|
||
).rejects.toThrow('Invalid period start: not-a-date')
|
||
expect(mockFetchEntryLines).not.toHaveBeenCalled()
|
||
})
|
||
})
|