fix: prevent silent data truncation in report generators (#257)

* fix: prevent silent data truncation in report generators

Supabase PostgREST silently truncates queries at 1000 rows. Several
report generators used bare .select() without fetchAllRows(), causing
incomplete financial data — a BFL compliance violation.

Wrapped 10 queries across 7 files with fetchAllRows():
- monthly-breakdown: journal_entry_lines (easily >1000/period)
- ar-ledger: unpaid invoices
- supplier-ledger: unpaid supplier invoices
- salary-journal: salary_run_employees (+ optimized with !inner join)
- vacation-liability: employees + salary_run_employees
- full-archive-export: document_attachments + journal_entry IDs
- ingest.ts: supplier invoices for auto-matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Greptile review — try/catch and client-side safety checks

- Wrap full-archive-export document fetch in try/catch to match the
  VAT section pattern — a failed document query should not prevent
  the rest of the archive from being generated.
- Restore client-side year/status safety checks in salary-journal and
  vacation-liability as defense-in-depth against PostgREST !inner
  filter regressions, per BFL lönejournal and BFNAR 2016:10 compliance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-04-16 15:46:27 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 4fbfadb2b7
commit fe137346df
10 changed files with 217 additions and 167 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ let results: Array<{ data?: unknown; error?: unknown }>
function makeBuilder() {
const b: Record<string, unknown> = {}
for (const m of ['select', 'eq', 'in']) {
for (const m of ['select', 'eq', 'in', 'range']) {
b[m] = vi.fn().mockReturnValue(b)
}
b.single = vi.fn().mockImplementation(async () => results[resultIdx++] ?? { data: null, error: null })
+71 -65
View File
@@ -54,11 +54,13 @@ describe('generateMonthlyBreakdown', () => {
select: () => ({
eq: () => ({
eq: () => ({
eq: () =>
Promise.resolve({
data: [],
error: null,
}),
eq: () => ({
range: () =>
Promise.resolve({
data: [],
error: null,
}),
}),
}),
}),
}),
@@ -96,36 +98,38 @@ describe('generateMonthlyBreakdown', () => {
select: () => ({
eq: () => ({
eq: () => ({
eq: () =>
Promise.resolve({
data: [
{
account_number: '3001',
debit_amount: 0,
credit_amount: 10000,
journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '5010',
debit_amount: 3000,
credit_amount: 0,
journal_entry: { entry_date: '2024-01-20', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '3001',
debit_amount: 0,
credit_amount: 5000,
journal_entry: { entry_date: '2024-02-10', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '6200',
debit_amount: 1500,
credit_amount: 0,
journal_entry: { entry_date: '2024-02-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
],
error: null,
}),
eq: () => ({
range: () =>
Promise.resolve({
data: [
{
account_number: '3001',
debit_amount: 0,
credit_amount: 10000,
journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '5010',
debit_amount: 3000,
credit_amount: 0,
journal_entry: { entry_date: '2024-01-20', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '3001',
debit_amount: 0,
credit_amount: 5000,
journal_entry: { entry_date: '2024-02-10', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '6200',
debit_amount: 1500,
credit_amount: 0,
journal_entry: { entry_date: '2024-02-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
],
error: null,
}),
}),
}),
}),
}),
@@ -175,36 +179,38 @@ describe('generateMonthlyBreakdown', () => {
select: () => ({
eq: () => ({
eq: () => ({
eq: () =>
Promise.resolve({
data: [
{
account_number: '1930',
debit_amount: 10000,
credit_amount: 0,
journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '2611',
debit_amount: 0,
credit_amount: 2500,
journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '8400',
debit_amount: 500,
credit_amount: 0,
journal_entry: { entry_date: '2024-01-20', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '8300',
debit_amount: 0,
credit_amount: 200,
journal_entry: { entry_date: '2024-01-25', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
],
error: null,
}),
eq: () => ({
range: () =>
Promise.resolve({
data: [
{
account_number: '1930',
debit_amount: 10000,
credit_amount: 0,
journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '2611',
debit_amount: 0,
credit_amount: 2500,
journal_entry: { entry_date: '2024-01-15', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '8400',
debit_amount: 500,
credit_amount: 0,
journal_entry: { entry_date: '2024-01-20', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
{
account_number: '8300',
debit_amount: 0,
credit_amount: 200,
journal_entry: { entry_date: '2024-01-25', status: 'posted', user_id: 'user-1', fiscal_period_id: 'period-1' },
},
],
error: null,
}),
}),
}),
}),
}),
@@ -9,7 +9,7 @@ let results: Array<{ data?: unknown; error?: unknown }>
function makeBuilder() {
const b: Record<string, unknown> = {}
for (const m of ['select', 'eq', 'in']) {
for (const m of ['select', 'eq', 'in', 'range']) {
b[m] = vi.fn().mockReturnValue(b)
}
b.single = vi.fn().mockImplementation(async () => results[resultIdx++] ?? { data: null, error: null })