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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user