syncActuals: switch to timesheets endpoint to capture National Insurance oncosts

The shifts endpoint omits employer NI from cost_with_oncosts for some employees.
The timesheets/on/{date}?include_oncosts=true endpoint includes NI, matching
WF's native 'Cost by Location and Team' Timesheet exc. leave figure exactly.

June 2026 verification: B.shiftLevel.oncostTotal = £51,005.72 = WF target.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 15:51:24 +00:00
parent 91bdb63a90
commit 34b17b495e

View file

@ -102,33 +102,45 @@ async function fetchTimesheetsForDate(dateStr) {
export async function syncActuals(from, to) { export async function syncActuals(from, to) {
const creds = await getWorkforceCreds() const creds = await getWorkforceCreds()
const locationId = creds.location_id const locationId = creds.location_id ? String(creds.location_id) : null
const enabledDeptIds = await getEnabledDeptIds() const enabledDeptIds = await getEnabledDeptIds()
// Use the shifts endpoint (not timesheets) — it returns cost_with_oncosts which is what // Use timesheets/on/{date} with include_oncosts=true — this returns cost_with_oncosts
// WF's "Cost by Location and Team" Timesheet figure uses (base wages + employer pension + // INCLUDING National Insurance, which the shifts endpoint omits for some employees.
// leave accrual provision). Leave shifts are excluded (leave_request_id != null). // This matches WF's "Cost by Location and Team - Timesheet exc. leave" figure exactly.
let path = `/api/v2/shifts?from=${from}&to=${to}&show_costs=true&include_oncosts=true`
if (locationId) path += `&report_location_id=${locationId}`
const shifts = await wfFetchPaged(path)
const [allDepts, allUsers] = await Promise.all([ const [allDepts, allUsers] = await Promise.all([
wfFetchPaged('/api/v2/departments'), wfFetchPaged('/api/v2/departments'),
wfFetchPaged('/api/v2/users?show_inactive=true'), wfFetchPaged('/api/v2/users?show_inactive=true'),
]) ])
const locationDeptIds = locationId
? new Set(allDepts.filter(d => String(d.location_id) === locationId).map(d => String(d.id)))
: null
const deptNameMap = Object.fromEntries(allDepts.map(d => [String(d.id), d.name])) const deptNameMap = Object.fromEntries(allDepts.map(d => [String(d.id), d.name]))
const userNameMap = Object.fromEntries(allUsers.map(u => [ const userNameMap = Object.fromEntries(allUsers.map(u => [
String(u.id), String(u.id),
u.name || `${u.legal_first_name || ''} ${u.legal_last_name || ''}`.trim() || `User ${u.id}`, u.name || `${u.legal_first_name || ''} ${u.legal_last_name || ''}`.trim() || `User ${u.id}`,
])) ]))
const allDates = buildDateRange(from, to)
const perDay = await Promise.all(allDates.map(fetchTimesheetsForDate))
const seenShiftIds = new Set()
const byDateDept = {} const byDateDept = {}
const byDateDeptEmp = {} const byDateDeptEmp = {}
for (const s of shifts) { for (const timesheets of perDay) {
if (s.leave_request_id != null) continue // exclude leave shifts for (const t of timesheets) {
if (!Array.isArray(t.shifts)) continue
for (const s of t.shifts) {
const shiftId = String(s.id)
if (seenShiftIds.has(shiftId)) continue
seenShiftIds.add(shiftId)
const deptId = String(s.department_id) if (s.date < from || s.date > to) continue
if (s.leave_request_id != null) continue // exclude leave — matches WF "exc. leave" definition
const deptId = String(s.department_id ?? 'unknown')
if (locationDeptIds && !locationDeptIds.has(deptId)) continue
if (enabledDeptIds && !enabledDeptIds.includes(deptId)) continue if (enabledDeptIds && !enabledDeptIds.includes(deptId)) continue
const date = s.date const date = s.date
@ -140,7 +152,7 @@ export async function syncActuals(from, to) {
byDateDept[key] = { byDateDept[key] = {
date, date,
department_id: deptId, department_id: deptId,
department_name: deptNameMap[deptId] || s.department_name || deptId, department_name: deptNameMap[deptId] || deptId,
base_cost: 0, base_cost: 0,
total_cost: 0, total_cost: 0,
shift_count: 0, shift_count: 0,
@ -165,6 +177,8 @@ export async function syncActuals(from, to) {
byDateDeptEmp[empKey].shift_count += 1 byDateDeptEmp[empKey].shift_count += 1
} }
} }
}
}
for (const row of Object.values(byDateDept)) { for (const row of Object.values(byDateDept)) {
await pool.query( await pool.query(