Files
accounted/lib/reports/supplier-ledger.ts
T
Jakob WennbergandClaude Opus 4.6 fe137346df 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>
2026-04-16 15:46:27 +02:00

110 lines
3.1 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
export interface SupplierLedgerEntry {
supplier_id: string
supplier_name: string
current: number
days_1_30: number
days_31_60: number
days_61_90: number
days_90_plus: number
total_outstanding: number
}
export interface SupplierLedgerReport {
entries: SupplierLedgerEntry[]
total_outstanding: number
total_current: number
total_overdue: number
unpaid_count: number
}
/**
* Generate supplier ledger (leverantörsreskontra) with aging analysis
*/
export async function generateSupplierLedger(
supabase: SupabaseClient,
companyId: string,
asOfDate?: string
): Promise<SupplierLedgerReport> {
const refDate = asOfDate ? new Date(asOfDate) : new Date()
// Fetch all unpaid/partially_paid supplier invoices
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let invoices: any[]
try {
invoices = await fetchAllRows(({ from, to }) =>
supabase
.from('supplier_invoices')
.select('*, supplier:suppliers(id, name)')
.eq('company_id', companyId)
.in('status', ['registered', 'approved', 'partially_paid', 'overdue'])
.range(from, to)
)
} catch {
return {
entries: [],
total_outstanding: 0,
total_current: 0,
total_overdue: 0,
unpaid_count: 0,
}
}
// Group by supplier and calculate aging
const bySupplier = new Map<string, SupplierLedgerEntry>()
for (const inv of invoices) {
const supplierId = inv.supplier_id
const supplierName = inv.supplier?.name || 'Okänd leverantör'
if (!bySupplier.has(supplierId)) {
bySupplier.set(supplierId, {
supplier_id: supplierId,
supplier_name: supplierName,
current: 0,
days_1_30: 0,
days_31_60: 0,
days_61_90: 0,
days_90_plus: 0,
total_outstanding: 0,
})
}
const entry = bySupplier.get(supplierId)!
const dueDate = new Date(inv.due_date)
const daysOverdue = Math.floor((refDate.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24))
const amount = inv.remaining_amount || 0
if (daysOverdue <= 0) {
entry.current += amount
} else if (daysOverdue <= 30) {
entry.days_1_30 += amount
} else if (daysOverdue <= 60) {
entry.days_31_60 += amount
} else if (daysOverdue <= 90) {
entry.days_61_90 += amount
} else {
entry.days_90_plus += amount
}
entry.total_outstanding += amount
}
const entries = Array.from(bySupplier.values())
.sort((a, b) => b.total_outstanding - a.total_outstanding)
const total_outstanding = entries.reduce((sum, e) => sum + e.total_outstanding, 0)
const total_current = entries.reduce((sum, e) => sum + e.current, 0)
const total_overdue = total_outstanding - total_current
return {
entries,
total_outstanding: Math.round(total_outstanding * 100) / 100,
total_current: Math.round(total_current * 100) / 100,
total_overdue: Math.round(total_overdue * 100) / 100,
unpaid_count: invoices.length,
}
}