feat: implement fiscal period date fields component and validation logic (#301)

* feat: implement fiscal period date fields component and validation logic

* feat: update fiscal period validation and naming logic
This commit is contained in:
Mattsson
2026-04-21 17:08:38 +02:00
committed by GitHub
parent 08991218ee
commit 885dd8a2e4
8 changed files with 504 additions and 257 deletions
+79
View File
@@ -6,6 +6,7 @@ import {
ensureFiscalPeriod,
importVouchers,
computeVoucherNumberRanges,
linkOpeningBalanceEntryToPeriod,
} from '../sie-import'
import { createQueuedMockSupabase } from '@/tests/helpers'
import type { ParsedSIEFile, AccountMapping } from '../types'
@@ -421,6 +422,84 @@ describe('ensureFiscalPeriod validation', () => {
})
})
describe('linkOpeningBalanceEntryToPeriod', () => {
// Regression: SIE import created the opening-balance entry but never wrote
// its ID back to fiscal_periods. Without the link, getOpeningBalances falls
// through to summing all prior journal lines, which inflates balance-sheet
// accounts across multi-year imports (each year's IB double-counted against
// the prior year's UB).
type Supabase = Parameters<typeof linkOpeningBalanceEntryToPeriod>[0]
it('writes opening_balance_entry_id and opening_balances_set to the fiscal period', async () => {
const updates: Array<{ payload: Record<string, unknown>; filters: Record<string, unknown> }> = []
const supabase = {
from: (table: string) => {
if (table !== 'fiscal_periods') {
throw new Error(`Unexpected table: ${table}`)
}
let pendingPayload: Record<string, unknown> = {}
const filters: Record<string, unknown> = {}
const chain = {
update: (payload: Record<string, unknown>) => {
pendingPayload = payload
return chain
},
eq: (col: string, val: unknown) => {
filters[col] = val
return chain
},
then: (resolve: (v: unknown) => void) => {
updates.push({ payload: pendingPayload, filters: { ...filters } })
resolve({ data: null, error: null })
},
}
return chain
},
}
await linkOpeningBalanceEntryToPeriod(
supabase as unknown as Supabase,
'company-1',
'period-1',
'ob-entry-1',
)
expect(updates).toHaveLength(1)
expect(updates[0].payload).toEqual({
opening_balance_entry_id: 'ob-entry-1',
opening_balances_set: true,
})
expect(updates[0].filters).toEqual({
id: 'period-1',
company_id: 'company-1',
})
})
it('throws a descriptive error when the update fails', async () => {
const supabase = {
from: () => {
const chain = {
update: () => chain,
eq: () => chain,
then: (resolve: (v: unknown) => void) =>
resolve({ data: null, error: { message: 'permission denied' } }),
}
return chain
},
}
await expect(
linkOpeningBalanceEntryToPeriod(
supabase as unknown as Supabase,
'company-1',
'period-1',
'ob-entry-1',
),
).rejects.toThrow(/Failed to link opening balance entry.*permission denied/)
})
})
describe('isBalanceSheetAccount', () => {
it('returns true for class 1 (assets)', () => {
expect(isBalanceSheetAccount('1510')).toBe(true)
+38
View File
@@ -207,6 +207,7 @@ async function cleanupStaleImportRecords(
.eq('company_id', companyId)
.eq('file_hash', fileHash)
.in('status', ['pending', 'failed'])
.lt('created_at', oneHourAgo)
}
/**
@@ -458,6 +459,36 @@ async function createOpeningBalanceEntry(
return entry.id
}
/**
* Link an opening-balance journal entry to its fiscal period so balance-sheet
* reports use the explicit IB path in getOpeningBalances() (reads only that
* entry's lines for IB) instead of falling through to summing all prior
* journal lines — which inflates multi-year imports, because each year's IB
* is double-counted against the prior year's UB.
*
* Mirrors the pattern used by the Excel-based OB import at
* app/api/import/opening-balance/execute/route.ts:224-231.
*/
export async function linkOpeningBalanceEntryToPeriod(
supabase: SupabaseClient,
companyId: string,
fiscalPeriodId: string,
openingBalanceEntryId: string
): Promise<void> {
const { error } = await supabase
.from('fiscal_periods')
.update({
opening_balance_entry_id: openingBalanceEntryId,
opening_balances_set: true,
})
.eq('id', fiscalPeriodId)
.eq('company_id', companyId)
if (error) {
throw new Error(`Failed to link opening balance entry to fiscal period: ${error.message}`)
}
}
/**
* Create journal entries from vouchers using batch insert for performance.
*
@@ -1636,6 +1667,13 @@ export async function executeSIEImport(
if (result.openingBalanceEntryId) {
result.journalEntriesCreated++
result.journalEntryIds.push(result.openingBalanceEntryId)
await linkOpeningBalanceEntryToPeriod(
supabase,
companyId,
result.fiscalPeriodId,
result.openingBalanceEntryId
)
}
}
}