Fix/balance inconsitency (#306)

* 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
This commit is contained in:
Mattsson
2026-04-21 21:39:57 +02:00
committed by GitHub
parent 28df5d851e
commit 24107338fa
15 changed files with 1317 additions and 137 deletions
@@ -234,4 +234,114 @@ describe('POST /api/bookkeeping/fiscal-periods', () => {
const res = await POST(req)
expect(res.status).toBe(400)
})
it('sets previous_period_id when chaining forward', async () => {
// Build a mock that captures the insert payload.
const insertSpy = vi.fn().mockReturnValue({
select: vi.fn().mockReturnValue({
single: vi.fn().mockResolvedValue({
data: { id: 'new-period', name: 'FY 2026' },
error: null,
}),
}),
})
let fpCallIndex = 0
const supabase = {
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: { id: 'user-1' } } }) },
from: vi.fn().mockImplementation(() => {
fpCallIndex++
const callNum = fpCallIndex
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
select: vi.fn().mockImplementation((_sel: string, opts?: any) => {
if (opts?.count === 'exact') {
return { eq: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ count: 0 }) }) }
}
if (callNum === 1) {
return {
eq: vi.fn().mockReturnValue({
order: vi.fn().mockResolvedValue({
data: [{ id: 'prior-period-id', period_start: '2025-01-01', period_end: '2025-12-31', is_closed: true, closing_entry_id: null }],
error: null,
}),
}),
}
}
return {
eq: vi.fn().mockReturnValue({
lte: vi.fn().mockReturnValue({
gte: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue({ data: [], error: null }) }),
}),
}),
}
}),
insert: insertSpy,
update: vi.fn().mockReturnValue({ eq: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }) }),
}
}),
}
;(createClient as ReturnType<typeof vi.fn>).mockResolvedValue(supabase)
const req = createMockRequest({ name: 'FY 2026', period_start: '2026-01-01', period_end: '2026-12-31' })
const res = await POST(req)
expect(res.status).toBe(200)
expect(insertSpy).toHaveBeenCalledTimes(1)
const insertArg = insertSpy.mock.calls[0][0]
expect(insertArg.previous_period_id).toBe('prior-period-id')
expect(insertArg.period_start).toBe('2026-01-01')
})
it('does not set previous_period_id for the first period', async () => {
const insertSpy = vi.fn().mockReturnValue({
select: vi.fn().mockReturnValue({
single: vi.fn().mockResolvedValue({
data: { id: 'new-period', name: 'FY 2025' },
error: null,
}),
}),
})
let fpCallIndex = 0
const supabase = {
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: { id: 'user-1' } } }) },
from: vi.fn().mockImplementation(() => {
fpCallIndex++
const callNum = fpCallIndex
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
select: vi.fn().mockImplementation((_sel: string, opts?: any) => {
if (opts?.count === 'exact') {
return { eq: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ count: 0 }) }) }
}
if (callNum === 1) {
return {
eq: vi.fn().mockReturnValue({
order: vi.fn().mockResolvedValue({ data: [], error: null }),
}),
}
}
return {
eq: vi.fn().mockReturnValue({
lte: vi.fn().mockReturnValue({
gte: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue({ data: [], error: null }) }),
}),
}),
}
}),
insert: insertSpy,
update: vi.fn().mockReturnValue({ eq: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }) }),
}
}),
}
;(createClient as ReturnType<typeof vi.fn>).mockResolvedValue(supabase)
const req = createMockRequest({ name: 'FY 2025', period_start: '2025-01-01', period_end: '2025-12-31' })
const res = await POST(req)
expect(res.status).toBe(200)
expect(insertSpy).toHaveBeenCalledTimes(1)
expect(insertSpy.mock.calls[0][0].previous_period_id).toBeNull()
})
})
@@ -132,6 +132,19 @@ export async function POST(request: Request) {
)
}
// Resolve previous_period_id for forward chaining so the new period is
// linked to the period it follows. Without this, balance-sheet/trial-balance
// reports fall back to scanning every prior journal line (BFNAR 2013:2
// continuity chain is broken). Backward chaining sets previous_period_id
// on the old earliest period instead (see below), not on the new one.
let previousPeriodId: string | null = null
if (allPeriods && allPeriods.length > 0) {
const latest = allPeriods[allPeriods.length - 1]
if (body.period_start > latest.period_end) {
previousPeriodId = latest.id
}
}
const { data, error } = await supabase
.from('fiscal_periods')
.insert({
@@ -140,6 +153,7 @@ export async function POST(request: Request) {
name: body.name,
period_start: body.period_start,
period_end: body.period_end,
previous_period_id: previousPeriodId,
})
.select()
.single()