Add endpoint comparison diagnostic for cost figure discrepancies
GET /wages/api/sync/compare?from=YYYY-MM-DD&to=YYYY-MM-DD returns cost totals from four Workforce endpoint variants side-by-side: shifts (all statuses), shifts (APPROVED only), timesheets (all statuses), timesheets (APPROVED only). Useful for identifying which endpoint matches the expected payroll figure. Also adds data.timesheets to wfFetchPaged response unwrap chain. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3a068ece23
commit
c15147b298
2 changed files with 84 additions and 2 deletions
|
|
@ -40,7 +40,7 @@ async function wfFetchPaged(path) {
|
|||
const data = await wfFetch(`${path}${sep}page=${page}&page_size=100`)
|
||||
const items = Array.isArray(data)
|
||||
? data
|
||||
: (data.users ?? data.departments ?? data.schedules ?? data.shifts ?? [])
|
||||
: (data.users ?? data.departments ?? data.schedules ?? data.shifts ?? data.timesheets ?? [])
|
||||
results.push(...items)
|
||||
if (items.length < 100) break
|
||||
page++
|
||||
|
|
@ -258,6 +258,75 @@ export async function runRollingSync() {
|
|||
return { actualRows, scheduledRows }
|
||||
}
|
||||
|
||||
// Fetch a raw item list from a path and return a cost summary — used for endpoint comparison
|
||||
async function fetchAndSummarise(path) {
|
||||
try {
|
||||
const items = await wfFetchPaged(path)
|
||||
if (!Array.isArray(items) || items.length === 0) return { count: 0, baseCost: 0, totalCost: 0, byDept: {} }
|
||||
let baseCost = 0, totalCost = 0
|
||||
const byDept = {}
|
||||
for (const s of items) {
|
||||
// timesheets may use start (unix ts) as the date source; shifts have date directly
|
||||
const base = parseFloat(s.cost ?? 0)
|
||||
const total = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
|
||||
baseCost += base
|
||||
totalCost += total
|
||||
const deptId = String(s.department_id ?? 'unknown')
|
||||
const deptName = s.department_name || deptId
|
||||
if (!byDept[deptId]) byDept[deptId] = { name: deptName, baseCost: 0, totalCost: 0, count: 0 }
|
||||
byDept[deptId].baseCost += base
|
||||
byDept[deptId].totalCost += total
|
||||
byDept[deptId].count++
|
||||
}
|
||||
const deptList = Object.entries(byDept)
|
||||
.map(([id, v]) => ({ id, name: v.name, baseCost: +v.baseCost.toFixed(2), totalCost: +v.totalCost.toFixed(2), count: v.count }))
|
||||
.sort((a, b) => b.totalCost - a.totalCost)
|
||||
return { count: items.length, baseCost: +baseCost.toFixed(2), totalCost: +totalCost.toFixed(2), topDepts: deptList.slice(0, 15) }
|
||||
} catch (err) {
|
||||
return { error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
// Compare cost totals from all available endpoints for a given date range.
|
||||
// Call GET /api/sync/compare?from=YYYY-MM-DD&to=YYYY-MM-DD to diagnose figure discrepancies.
|
||||
export async function compareEndpoints(from, to) {
|
||||
const creds = await getWorkforceCreds()
|
||||
const locationId = creds.location_id
|
||||
|
||||
// Shifts — all statuses (current sync method)
|
||||
let shiftsAll = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
||||
if (locationId) shiftsAll += `&report_location_id=${locationId}`
|
||||
|
||||
// Shifts — APPROVED only
|
||||
let shiftsApproved = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true&status=APPROVED`
|
||||
if (locationId) shiftsApproved += `&report_location_id=${locationId}`
|
||||
|
||||
// Timesheets — range endpoint (closest to payroll actuals)
|
||||
let timesheets = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
|
||||
if (locationId) timesheets += `&location_id=${locationId}`
|
||||
|
||||
// Timesheets — APPROVED only
|
||||
let timesheetsApproved = `/api/v2/timesheets?from=${from}&to=${to}&show_costs=true&include_oncosts=true&status=APPROVED`
|
||||
if (locationId) timesheetsApproved += `&location_id=${locationId}`
|
||||
|
||||
const [s_all, s_approved, ts_all, ts_approved] = await Promise.all([
|
||||
fetchAndSummarise(shiftsAll),
|
||||
fetchAndSummarise(shiftsApproved),
|
||||
fetchAndSummarise(timesheets),
|
||||
fetchAndSummarise(timesheetsApproved),
|
||||
])
|
||||
|
||||
return {
|
||||
from, to,
|
||||
current_method: 'shifts_all_statuses',
|
||||
shifts_all_statuses: s_all,
|
||||
shifts_approved_only: s_approved,
|
||||
timesheets_all_statuses: ts_all,
|
||||
timesheets_approved_only: ts_approved,
|
||||
note: 'baseCost = wage only; totalCost = with estimated employer on-costs. Switch current_method to whichever matches your expected payroll figure.',
|
||||
}
|
||||
}
|
||||
|
||||
export async function runBackfill(onProgress, signal) {
|
||||
const today = new Date()
|
||||
const endDate = new Date(today)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { requireAuth, requireCap } from '../auth.js'
|
||||
import { getConfig, setConfig } from '../db.js'
|
||||
import { runRollingSync, runBackfill } from '../lib/workforce.js'
|
||||
import { runRollingSync, runBackfill, compareEndpoints } from '../lib/workforce.js'
|
||||
import { fetchAllDepartments } from '../lib/workforce.js'
|
||||
|
||||
let _backfillAbort = null
|
||||
|
|
@ -66,6 +66,19 @@ export async function syncRoutes(fastify) {
|
|||
return { ok: true }
|
||||
})
|
||||
|
||||
// Compare cost totals from all available Workforce endpoints for a date range.
|
||||
// Useful for diagnosing figure discrepancies between shifts vs timesheets.
|
||||
// Usage: GET /wages/api/sync/compare?from=2025-06-01&to=2025-06-30
|
||||
fastify.get('/api/sync/compare', { preHandler: requireCap('sync') }, async (req, reply) => {
|
||||
const { from, to } = req.query
|
||||
if (!from || !to) return reply.status(400).send({ error: 'from and to are required (YYYY-MM-DD)' })
|
||||
try {
|
||||
return await compareEndpoints(from, to)
|
||||
} catch (err) {
|
||||
return reply.status(500).send({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch departments from Workforce (for the settings filter)
|
||||
fastify.get('/api/departments', { preHandler: requireCap('settings') }, async (_, reply) => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue