* fix: resolve BFL compliance violations in general ledger and trial balance Fix two compliance violations and a pre-existing double-counting bug: 1. .in(entryIds) truncation (BFL 5:2 completeness) — general-ledger.ts and journal-register.ts used .in() with dynamic ID arrays that silently truncate at ~1000 rows. Migrated to joined queries with fetchAllRows pagination, matching the pattern already used by trial-balance.ts. 2. Trial balance missing IB columns (BFNAR 2013:2) — opening_debit and opening_credit were hardcoded to 0. Now computed from the opening_balance_entry (set by year-end closing) or by summing prior- period entries as a fallback. 3. Double-counting after year-end closing — the opening_balance_entry's lines were counted as both IB and period activity. Now excluded from period queries via .neq() when the OB entry exists. Extracted shared getOpeningBalances() helper used by both trial balance and general ledger. Refactored test mocks from positional arrays to table-keyed queues for readability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add pagination and user_id filter to OB entry query Address review feedback: the obEntryId fast path in getOpeningBalances used a bare single-shot query without fetchAllRows (inconsistent with the PR's truncation fix) and lacked the user_id defense-in-depth filter required by CLAUDE.md guidelines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
|
|
|
/**
|
|
* Get opening balances (ingående balans) for a fiscal period.
|
|
*
|
|
* Uses the opening_balance_entry set by year-end closing when available
|
|
* (O(accounts) — typically ~50 rows). Falls back to summing all entries
|
|
* prior to the period start date via a joined query (O(all_prior_lines) —
|
|
* expensive for companies that haven't run year-end closing).
|
|
*
|
|
* Returns per-account debit/credit opening balances and the OB entry ID
|
|
* (if any) so the caller can exclude it from period queries to prevent
|
|
* double-counting.
|
|
*
|
|
* NOTE: The account range filter (accountFrom/accountTo in the GL) is
|
|
* applied post-hoc by the caller, not here. This is consistent with the
|
|
* existing behavior and avoids complicating the queries for the common
|
|
* unfiltered case.
|
|
*/
|
|
export async function getOpeningBalances(
|
|
supabase: SupabaseClient,
|
|
userId: string,
|
|
period: { period_start: string; opening_balance_entry_id: string | null } | null
|
|
): Promise<{
|
|
balances: Map<string, { debit: number; credit: number }>
|
|
obEntryId: string | null
|
|
}> {
|
|
const balances = new Map<string, { debit: number; credit: number }>()
|
|
|
|
if (!period) {
|
|
return { balances, obEntryId: null }
|
|
}
|
|
|
|
const obEntryId = period.opening_balance_entry_id
|
|
|
|
if (obEntryId) {
|
|
// Use the explicit opening balance entry (set by year-end closing).
|
|
// Typically ~50 rows — one per balance sheet account. Uses fetchAllRows
|
|
// for consistency (avoids silent truncation) and joins journal_entries
|
|
// to enforce user_id ownership (defense in depth alongside RLS).
|
|
const obLines = await fetchAllRows<{
|
|
account_number: string
|
|
debit_amount: number
|
|
credit_amount: number
|
|
}>(({ from, to }) =>
|
|
supabase
|
|
.from('journal_entry_lines')
|
|
.select('account_number, debit_amount, credit_amount, journal_entries!inner(user_id)')
|
|
.eq('journal_entry_id', obEntryId)
|
|
.eq('journal_entries.user_id', userId)
|
|
.range(from, to)
|
|
)
|
|
|
|
for (const line of obLines) {
|
|
const existing = balances.get(line.account_number) || { debit: 0, credit: 0 }
|
|
existing.debit += Number(line.debit_amount) || 0
|
|
existing.credit += Number(line.credit_amount) || 0
|
|
balances.set(line.account_number, existing)
|
|
}
|
|
} else {
|
|
// Fallback: compute from all entries dated before this period's start.
|
|
// This is expensive for multi-year companies that haven't run year-end
|
|
// closing — consider prompting the user to close prior periods.
|
|
const priorLines = await fetchAllRows<{
|
|
account_number: string
|
|
debit_amount: number
|
|
credit_amount: number
|
|
}>(({ from, to }) =>
|
|
supabase
|
|
.from('journal_entry_lines')
|
|
.select('account_number, debit_amount, credit_amount, journal_entries!inner(user_id, status, entry_date)')
|
|
.eq('journal_entries.user_id', userId)
|
|
.in('journal_entries.status', ['posted', 'reversed'])
|
|
.lt('journal_entries.entry_date', period.period_start)
|
|
.range(from, to)
|
|
)
|
|
|
|
for (const line of priorLines) {
|
|
const existing = balances.get(line.account_number) || { debit: 0, credit: 0 }
|
|
existing.debit += Number(line.debit_amount) || 0
|
|
existing.credit += Number(line.credit_amount) || 0
|
|
balances.set(line.account_number, existing)
|
|
}
|
|
}
|
|
|
|
return { balances, obEntryId }
|
|
}
|