#820 fixed the primary #793 truncation (the nested embedded-resource select hit PostgREST's row ceiling, exporting ~30 vouchers) by splitting entries and lines into two fetchAllRows calls. But both queries paged without a stable TOTAL order: the entries query ordered by voucher_number alone (not unique across voucher series) and the lines query had no .order() at all. Per the fetch-all.ts invariant, .range() paging without a unique total order can duplicate or skip rows across the 1000-row boundary — so a year with >1000 entries/lines and multiple series could still drop or double a voucher in the SIE file (BFL completeness). - Entries: order by voucher_series + voucher_number (unique per company+period). - Lines: order by the line PK id. - Both carry dedupeBy: r => r.id as defense-in-depth, mirroring the general ledger / trial balance fix in #811. - Give the large-period test's line fixtures unique ids so the >1-page dedupe path is exercised realistically (all 5000 lines survive). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0e8698f538
commit
4872c0f242
@@ -374,9 +374,9 @@ describe('generateSIEExport', () => {
|
||||
status: 'posted',
|
||||
}))
|
||||
|
||||
const lines = entries.flatMap((e) => [
|
||||
{ journal_entry_id: e.id, account_number: '1510', debit_amount: 100, credit_amount: 0, line_description: null, cost_center: null, project: null },
|
||||
{ journal_entry_id: e.id, account_number: '3001', debit_amount: 0, credit_amount: 100, line_description: null, cost_center: null, project: null },
|
||||
const lines = entries.flatMap((e, i) => [
|
||||
{ id: `l${i * 2 + 1}`, journal_entry_id: e.id, account_number: '1510', debit_amount: 100, credit_amount: 0, line_description: null, cost_center: null, project: null },
|
||||
{ id: `l${i * 2 + 2}`, journal_entry_id: e.id, account_number: '3001', debit_amount: 0, credit_amount: 100, line_description: null, cost_center: null, project: null },
|
||||
])
|
||||
|
||||
// fetchAllRows paginates at PAGE_SIZE = 1000; chunk the mock data so the
|
||||
|
||||
@@ -74,8 +74,15 @@ export async function generateSIEExport(
|
||||
q = q.neq('source_type', 'year_end')
|
||||
}
|
||||
|
||||
return q.order('voucher_number').range(from, to)
|
||||
})
|
||||
// Stable TOTAL order: voucher_series + voucher_number is unique per
|
||||
// company+period, so fetchAllRows paging can't duplicate or skip a voucher
|
||||
// across the 1000-row boundary on large years (voucher_number alone is not
|
||||
// unique across series). dedupeBy is defense-in-depth — see fetch-all.ts.
|
||||
return q
|
||||
.order('voucher_series', { ascending: true })
|
||||
.order('voucher_number', { ascending: true })
|
||||
.range(from, to)
|
||||
}, { dedupeBy: (r) => r.id })
|
||||
|
||||
// Fetch all lines for those entries, filtered server-side via an inner join
|
||||
// so the same company/period/status (and year-end exclusion) constraints
|
||||
@@ -92,9 +99,11 @@ export async function generateSIEExport(
|
||||
q = q.neq('journal_entries.source_type', 'year_end')
|
||||
}
|
||||
|
||||
// Stable total order on the line PK so paging can't duplicate/skip a line
|
||||
// across the 1000-row boundary; dedupeBy is the defense-in-depth net.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return q.range(from, to) as any
|
||||
})
|
||||
return q.order('id', { ascending: true }).range(from, to) as any
|
||||
}, { dedupeBy: (r) => r.id })
|
||||
|
||||
const linesByEntryId = new Map<string, JournalEntryLine[]>()
|
||||
for (const line of allLines) {
|
||||
|
||||
Reference in New Issue
Block a user