Add daily audit summary cross-checks to multiday report

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 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-20 09:23:30 +00:00
parent fa62c7c44a
commit e9cf648f8c
2 changed files with 68 additions and 8 deletions

View file

@ -147,13 +147,24 @@ export async function reportRoutes(app) {
.filter(r => r.period === date) .filter(r => r.period === date)
.reduce((s, r) => s + (r.earned_revenue || 0), 0) .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 // Daily stats from payments
const grossSales = freshPayments.reduce((s, p) => s + parseFloat(p.amount), 0) const grossSales = freshPayments.reduce((s, p) => s + parseFloat(p.amount), 0)
const dailyStats = grossSales > 0 const dailyStats = grossSales > 0
? { business_date: date, gross_sales: grossSales, transaction_count: freshPayments.length } ? { business_date: date, gross_sales: grossSales, transaction_count: freshPayments.length }
: null : 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 { return {

View file

@ -14,6 +14,8 @@ interface DayData {
daily_stats: { gross_sales: number; transaction_count: number } | null daily_stats: { gross_sales: number; transaction_count: number } | null
sales_breakdown: SalesCol[] sales_breakdown: SalesCol[]
daily_gross_sales?: number daily_gross_sales?: number
audit_cash_total?: number
audit_accrual_total?: number
} }
interface OccupancyCategoryRaw { interface OccupancyCategoryRaw {
category_id?: string | number; category_name?: string category_id?: string | number; category_name?: string
@ -488,9 +490,13 @@ export function MultiDayReport() {
{days.map((day, ri) => { {days.map((day, ri) => {
const vals = BANKED_COLS.map(c => recon(day, c.key)) const vals = BANKED_COLS.map(c => recon(day, c.key))
const reportedTotal = vals.reduce((s, v) => s + v.reported_amount, 0) const reportedTotal = vals.reduce((s, v) => s + v.reported_amount, 0)
const audit = dayGrossSales(day) const audit = day.audit_cash_total ?? null
const auditVar = audit - reportedTotal const auditVar = audit != null ? audit - reportedTotal : null
const auditStyle: React.CSSProperties = Math.abs(auditVar) < 0.005 ? { background: '#d4edda', fontWeight: 700 } : auditVar < 0 ? { background: '#f8d7da', fontWeight: 700 } : { background: '#fff3cd', fontWeight: 700 } 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 ( return (
<tr key={day.date} style={{ borderBottom: '1px solid #e0e0e0' }}> <tr key={day.date} style={{ borderBottom: '1px solid #e0e0e0' }}>
<td style={{ ...td, ...cs('tbl-reported', ri + 2, 0) }}>{fmtDate(day.date)}</td> <td style={{ ...td, ...cs('tbl-reported', ri + 2, 0) }}>{fmtDate(day.date)}</td>
@ -499,7 +505,14 @@ export function MultiDayReport() {
return <td key={c.key} style={{ ...td, background: '#fffbf5', ...cs('tbl-reported', ri + 2, ci + 1) }}>{v.reported_amount ? fmtGBP(v.reported_amount) : '£0.00'}</td> return <td key={c.key} style={{ ...td, background: '#fffbf5', ...cs('tbl-reported', ri + 2, ci + 1) }}>{v.reported_amount ? fmtGBP(v.reported_amount) : '£0.00'}</td>
})} })}
<td style={{ ...td, background: '#fffbf5', fontWeight: 700, ...cs('tbl-reported', ri + 2, 7) }}>{fmtGBP(reportedTotal)}</td> <td style={{ ...td, background: '#fffbf5', fontWeight: 700, ...cs('tbl-reported', ri + 2, 7) }}>{fmtGBP(reportedTotal)}</td>
<td style={{ ...td, ...auditStyle, ...cs('tbl-reported', ri + 2, 8) }}>{fmtGBP(audit)}</td> <td style={{ ...td, ...auditStyle, ...cs('tbl-reported', ri + 2, 8) }}>
{audit != null ? fmtGBP(audit) : '—'}
{auditVar != null && Math.abs(auditVar) >= 0.005 && (
<span style={{ display: 'block', fontSize: '0.7rem', marginTop: '1px' }}>
{auditVar < 0 ? '▼ ' : '▲ +'}{fmtGBP(auditVar)}
</span>
)}
</td>
</tr> </tr>
) )
})} })}
@ -513,9 +526,12 @@ export function MultiDayReport() {
})} })}
<th style={{ ...th, ...cs('tbl-reported', days.length + 2, 7) }}>{fmtGBP(days.reduce((s, d) => s + BANKED_COLS.reduce((ss, c) => ss + recon(d, c.key).reported_amount, 0), 0))}</th> <th style={{ ...th, ...cs('tbl-reported', days.length + 2, 7) }}>{fmtGBP(days.reduce((s, d) => s + BANKED_COLS.reduce((ss, c) => ss + recon(d, c.key).reported_amount, 0), 0))}</th>
{(() => { {(() => {
const tot = days.reduce((s, d) => s + dayGrossSales(d), 0) const hasAudit = days.some(d => d.audit_cash_total != null)
if (!hasAudit) return <th style={{ ...th, ...cs('tbl-reported', days.length + 2, 8) }}></th>
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 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 <th style={{ ...th, ...vstyle, ...cs('tbl-reported', days.length + 2, 8) }}>{fmtGBP(tot)}</th> return <th style={{ ...th, ...vstyle, ...cs('tbl-reported', days.length + 2, 8) }}>{fmtGBP(tot)}</th>
})()} })()}
</tr> </tr>
@ -570,7 +586,8 @@ export function MultiDayReport() {
<th style={th}>Total Net</th> <th style={th}>Total Net</th>
<th style={th}>VAT</th> <th style={th}>VAT</th>
<th style={th}>Gross Total</th> <th style={th}>Gross Total</th>
<th style={th}>Audit</th> <th style={th}>Audit (ER)</th>
<th style={th}>Audit (DAS)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -602,6 +619,24 @@ export function MultiDayReport() {
</span> </span>
)} )}
</td> </td>
{(() => {
const das = day.audit_accrual_total ?? null
if (das == null) return <td style={{ ...td, textAlign: 'right', color: '#999', ...cs('tbl-sales', ri + 1, salesCols.length + 5) }}></td>
const dasVar = das - totalGross
const dasStyle: React.CSSProperties = Math.abs(dasVar) < 0.005
? { background: '#d4edda', fontWeight: 700 }
: { background: '#fff3cd', fontWeight: 700, color: '#856404' }
return (
<td style={{ ...td, textAlign: 'right', ...dasStyle, ...cs('tbl-sales', ri + 1, salesCols.length + 5) }}>
{fmtGBP(das)}
{Math.abs(dasVar) >= 0.005 && (
<span style={{ display: 'block', fontSize: '0.7rem', marginTop: '1px' }}>
{dasVar > 0 ? '▲ +' : '▼ '}{fmtGBP(dasVar)}
</span>
)}
</td>
)
})()}
</tr> </tr>
) )
})} })}
@ -628,6 +663,20 @@ export function MultiDayReport() {
</th> </th>
) )
})()} })()}
{(() => {
const hasDas = days.some(d => d.audit_accrual_total != null)
if (!hasDas) return <th style={{ ...th, color: '#999', ...cs('tbl-sales', days.length + 1, salesCols.length + 5) }}></th>
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 (
<th style={{ ...th, ...vstyle, ...cs('tbl-sales', days.length + 1, salesCols.length + 5) }}>
{fmtGBP(totDas)}
{Math.abs(v) >= 0.005 && <span style={{ display: 'block', fontSize: '0.7rem', fontWeight: 400 }}>{v > 0 ? '▲ +' : '▼ '}{fmtGBP(v)}</span>}
</th>
)
})()}
</tr> </tr>
</tfoot> </tfoot>
</table> </table>