24107338fa
* feat: implement fiscal period date fields component and validation logic * feat: update fiscal period validation and naming logic * feat: implement RPC for computing prior opening balances - Added `compute_prior_opening_balances` RPC to aggregate opening balances for balance-sheet accounts when no opening balance entry is set. - Updated tests across various reports to utilize the new RPC for fetching prior balances. - Refactored `getOpeningBalances` to call the RPC when necessary, improving performance and reliability. - Introduced a script to repair fiscal period chains for companies with broken periods, ensuring proper linking and continuity. - Enhanced error handling and validation in the repair script to ensure data integrity during the process. * feat: implement duplicate opening-balance repair for multi-year SIE imports * feat: enhance SIE entry listing and deduplication logic for opening balances * fix: refine companyHasPriorActivity logic to exclude storno entries and improve balance counting
182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
vi.mock('@/lib/supabase/fetch-all', () => ({
|
|
fetchAllRows: vi.fn(),
|
|
}))
|
|
|
|
import { getOpeningBalances } from '../opening-balances'
|
|
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
|
|
|
const mockFetchAllRows = vi.mocked(fetchAllRows)
|
|
|
|
function createSupabaseWithRpc(
|
|
rpcImpl: (fn: string, args: Record<string, unknown>) => Promise<{ data: unknown; error: unknown }>
|
|
) {
|
|
const rpc = vi.fn(rpcImpl)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return { rpc } as any
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('getOpeningBalances', () => {
|
|
it('returns empty map and null obEntryId when period is null', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const supabase = {} as any
|
|
const { balances, obEntryId } = await getOpeningBalances(supabase, 'company-1', null)
|
|
|
|
expect(balances.size).toBe(0)
|
|
expect(obEntryId).toBeNull()
|
|
})
|
|
|
|
describe('with opening_balance_entry_id (OB entry path)', () => {
|
|
const period = {
|
|
period_start: '2025-01-01',
|
|
opening_balance_entry_id: 'ob-entry-123',
|
|
}
|
|
|
|
it('returns balances from the OB entry lines', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const supabase = {} as any
|
|
mockFetchAllRows.mockResolvedValue([
|
|
{ account_number: '1930', debit_amount: 50000, credit_amount: 0 },
|
|
{ account_number: '2440', debit_amount: 0, credit_amount: 10000 },
|
|
])
|
|
|
|
const { balances, obEntryId } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(balances.get('1930')).toEqual({ debit: 50000, credit: 0 })
|
|
expect(balances.get('2440')).toEqual({ debit: 0, credit: 10000 })
|
|
expect(obEntryId).toBe('ob-entry-123')
|
|
})
|
|
|
|
it('aggregates multiple lines for the same account', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const supabase = {} as any
|
|
mockFetchAllRows.mockResolvedValue([
|
|
{ account_number: '1930', debit_amount: 30000, credit_amount: 0 },
|
|
{ account_number: '1930', debit_amount: 20000, credit_amount: 0 },
|
|
])
|
|
|
|
const { balances } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(balances.get('1930')).toEqual({ debit: 50000, credit: 0 })
|
|
})
|
|
|
|
it('returns the obEntryId string', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const supabase = {} as any
|
|
mockFetchAllRows.mockResolvedValue([])
|
|
|
|
const { obEntryId } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(obEntryId).toBe('ob-entry-123')
|
|
})
|
|
})
|
|
|
|
describe('without opening_balance_entry_id (fallback path via RPC)', () => {
|
|
const period = {
|
|
period_start: '2025-01-01',
|
|
opening_balance_entry_id: null,
|
|
}
|
|
|
|
it('calls compute_prior_opening_balances RPC with the right args', async () => {
|
|
const supabase = createSupabaseWithRpc(async () => ({ data: [], error: null }))
|
|
|
|
await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(supabase.rpc).toHaveBeenCalledTimes(1)
|
|
expect(supabase.rpc).toHaveBeenCalledWith('compute_prior_opening_balances', {
|
|
p_company_id: 'company-1',
|
|
p_period_start: '2025-01-01',
|
|
})
|
|
})
|
|
|
|
it('does NOT call the RPC when an OB entry is present', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const supabase = { rpc: vi.fn() } as any
|
|
mockFetchAllRows.mockResolvedValue([])
|
|
|
|
await getOpeningBalances(supabase, 'company-1', {
|
|
period_start: '2025-01-01',
|
|
opening_balance_entry_id: 'ob-entry-999',
|
|
})
|
|
|
|
expect(supabase.rpc).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('maps RPC rows to the balances map', async () => {
|
|
const supabase = createSupabaseWithRpc(async () => ({
|
|
data: [
|
|
{ account_number: '1930', debit: 100000, credit: 5000 },
|
|
{ account_number: '2440', debit: 0, credit: 25000 },
|
|
{ account_number: '1510', debit: 8000, credit: 1000 },
|
|
],
|
|
error: null,
|
|
}))
|
|
|
|
const { balances, obEntryId } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(balances.get('1930')).toEqual({ debit: 100000, credit: 5000 })
|
|
expect(balances.get('2440')).toEqual({ debit: 0, credit: 25000 })
|
|
expect(balances.get('1510')).toEqual({ debit: 8000, credit: 1000 })
|
|
expect(obEntryId).toBeNull()
|
|
})
|
|
|
|
it('returns empty map when RPC returns no rows', async () => {
|
|
const supabase = createSupabaseWithRpc(async () => ({ data: [], error: null }))
|
|
|
|
const { balances } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(balances.size).toBe(0)
|
|
})
|
|
|
|
it('handles RPC returning null data', async () => {
|
|
const supabase = createSupabaseWithRpc(async () => ({ data: null, error: null }))
|
|
|
|
const { balances } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(balances.size).toBe(0)
|
|
})
|
|
|
|
it('coerces string-typed numerics (Postgres numeric) to numbers', async () => {
|
|
const supabase = createSupabaseWithRpc(async () => ({
|
|
data: [{ account_number: '1930', debit: '12345.67', credit: '0' }],
|
|
error: null,
|
|
}))
|
|
|
|
const { balances } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(balances.get('1930')).toEqual({ debit: 12345.67, credit: 0 })
|
|
})
|
|
|
|
it('throws when the RPC returns an error', async () => {
|
|
const supabase = createSupabaseWithRpc(async () => ({
|
|
data: null,
|
|
error: { message: 'boom' },
|
|
}))
|
|
|
|
await expect(getOpeningBalances(supabase, 'company-1', period)).rejects.toThrow('boom')
|
|
})
|
|
})
|
|
|
|
it('coerces null/undefined debit/credit to 0 on the OB entry path', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const supabase = {} as any
|
|
const period = {
|
|
period_start: '2025-01-01',
|
|
opening_balance_entry_id: 'ob-entry-1',
|
|
}
|
|
|
|
mockFetchAllRows.mockResolvedValue([
|
|
{ account_number: '1930', debit_amount: null, credit_amount: undefined },
|
|
])
|
|
|
|
const { balances } = await getOpeningBalances(supabase, 'company-1', period)
|
|
|
|
expect(balances.get('1930')).toEqual({ debit: 0, credit: 0 })
|
|
})
|
|
})
|