From e9cf648f8c56dcbbf60ea15f08728dcd5cfc2b66 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Mon, 20 Jul 2026 09:23:30 +0000 Subject: [PATCH] Add daily audit summary cross-checks to multiday report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fetch reports_daily_audit_summary per day. Use cash_income total as the reconciliation audit column (independent payment cross-check vs transaction flow). Add a second Audit (DAS) column on the sales breakdown using accrual_income totals alongside the earned revenue Audit (ER) column — both highlight in amber if they diverge from the configured columns gross. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/reports.js | 13 +++++- frontend/src/pages/MultiDayReport.tsx | 63 ++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/backend/src/routes/reports.js b/backend/src/routes/reports.js index 30b9fe7..a523053 100644 --- a/backend/src/routes/reports.js +++ b/backend/src/routes/reports.js @@ -147,13 +147,24 @@ export async function reportRoutes(app) { .filter(r => r.period === date) .reduce((s, r) => s + (r.earned_revenue || 0), 0) + // Daily audit summary — independent cross-check from a separate Newbook endpoint + const auditRows = await fetchDailyAuditSummary(date).catch(() => []) + // cash_income amounts are negative in the API (like transaction_flow) — negate to get positive + const audit_cash_total = auditRows + .filter(r => r.report_type === 'cash_income') + .reduce((s, r) => s - parseFloat(r.amount || 0), 0) + // accrual_income amounts are positive + const audit_accrual_total = auditRows + .filter(r => r.report_type === 'accrual_income') + .reduce((s, r) => s + parseFloat(r.amount || 0), 0) + // Daily stats from payments const grossSales = freshPayments.reduce((s, p) => s + parseFloat(p.amount), 0) const dailyStats = grossSales > 0 ? { business_date: date, gross_sales: grossSales, transaction_count: freshPayments.length } : null - return { date, cash_up: cashUp, reconciliation, daily_stats: dailyStats, sales_breakdown: salesBreakdown, daily_gross_sales } + return { date, cash_up: cashUp, reconciliation, daily_stats: dailyStats, sales_breakdown: salesBreakdown, daily_gross_sales, audit_cash_total, audit_accrual_total } })) return { diff --git a/frontend/src/pages/MultiDayReport.tsx b/frontend/src/pages/MultiDayReport.tsx index 5c50908..c24d4fe 100644 --- a/frontend/src/pages/MultiDayReport.tsx +++ b/frontend/src/pages/MultiDayReport.tsx @@ -14,6 +14,8 @@ interface DayData { daily_stats: { gross_sales: number; transaction_count: number } | null sales_breakdown: SalesCol[] daily_gross_sales?: number + audit_cash_total?: number + audit_accrual_total?: number } interface OccupancyCategoryRaw { category_id?: string | number; category_name?: string @@ -488,9 +490,13 @@ export function MultiDayReport() { {days.map((day, ri) => { const vals = BANKED_COLS.map(c => recon(day, c.key)) const reportedTotal = vals.reduce((s, v) => s + v.reported_amount, 0) - const audit = dayGrossSales(day) - const auditVar = audit - reportedTotal - const auditStyle: React.CSSProperties = Math.abs(auditVar) < 0.005 ? { background: '#d4edda', fontWeight: 700 } : auditVar < 0 ? { background: '#f8d7da', fontWeight: 700 } : { background: '#fff3cd', fontWeight: 700 } + const audit = day.audit_cash_total ?? null + const auditVar = audit != null ? audit - reportedTotal : null + const auditStyle: React.CSSProperties = audit == null + ? { background: '#f5f5f5', color: '#999', fontWeight: 700 } + : auditVar != null && Math.abs(auditVar) < 0.005 ? { background: '#d4edda', fontWeight: 700 } + : auditVar != null && auditVar < 0 ? { background: '#f8d7da', fontWeight: 700 } + : { background: '#fff3cd', fontWeight: 700 } return ( {fmtDate(day.date)} @@ -499,7 +505,14 @@ export function MultiDayReport() { return {v.reported_amount ? fmtGBP(v.reported_amount) : '£0.00'} })} {fmtGBP(reportedTotal)} - {fmtGBP(audit)} + + {audit != null ? fmtGBP(audit) : '—'} + {auditVar != null && Math.abs(auditVar) >= 0.005 && ( + + {auditVar < 0 ? '▼ ' : '▲ +'}{fmtGBP(auditVar)} + + )} + ) })} @@ -513,9 +526,12 @@ export function MultiDayReport() { })} {fmtGBP(days.reduce((s, d) => s + BANKED_COLS.reduce((ss, c) => ss + recon(d, c.key).reported_amount, 0), 0))} {(() => { - const tot = days.reduce((s, d) => s + dayGrossSales(d), 0) + const hasAudit = days.some(d => d.audit_cash_total != null) + if (!hasAudit) return — + const tot = days.reduce((s, d) => s + (d.audit_cash_total ?? 0), 0) const rep = days.reduce((s, d) => s + BANKED_COLS.reduce((ss, c) => ss + recon(d, c.key).reported_amount, 0), 0) - const vstyle: React.CSSProperties = Math.abs(tot - rep) < 0.005 ? { background: '#d4edda' } : {} + const v = tot - rep + const vstyle: React.CSSProperties = Math.abs(v) < 0.005 ? { background: '#d4edda' } : v < 0 ? { background: '#f8d7da' } : { background: '#fff3cd' } return {fmtGBP(tot)} })()} @@ -570,7 +586,8 @@ export function MultiDayReport() { Total Net VAT Gross Total - Audit + Audit (ER) + Audit (DAS) @@ -602,6 +619,24 @@ export function MultiDayReport() { )} + {(() => { + const das = day.audit_accrual_total ?? null + if (das == null) return — + const dasVar = das - totalGross + const dasStyle: React.CSSProperties = Math.abs(dasVar) < 0.005 + ? { background: '#d4edda', fontWeight: 700 } + : { background: '#fff3cd', fontWeight: 700, color: '#856404' } + return ( + + {fmtGBP(das)} + {Math.abs(dasVar) >= 0.005 && ( + + {dasVar > 0 ? '▲ +' : '▼ '}{fmtGBP(dasVar)} + + )} + + ) + })()} ) })} @@ -628,6 +663,20 @@ export function MultiDayReport() { ) })()} + {(() => { + const hasDas = days.some(d => d.audit_accrual_total != null) + if (!hasDas) return — + const totGross = days.reduce((s, d) => s + d.sales_breakdown.reduce((ss, sb) => ss + (sb.gross_amount ?? 0), 0), 0) + const totDas = days.reduce((s, d) => s + (d.audit_accrual_total ?? 0), 0) + const v = totDas - totGross + const vstyle: React.CSSProperties = Math.abs(v) < 0.005 ? { background: '#d4edda' } : { background: '#fff3cd', color: '#856404' } + return ( + + {fmtGBP(totDas)} + {Math.abs(v) >= 0.005 && {v > 0 ? '▲ +' : '▼ '}{fmtGBP(v)}} + + ) + })()}