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,67 +102,81 @@ async function fetchTimesheetsForDate(dateStr) {
export async function syncActuals(from, to) {
const creds = await getWorkforceCreds()
const locationId = creds.location_id
const locationId = creds.location_id ? String(creds.location_id) : null
const enabledDeptIds = await getEnabledDeptIds()
// Use the shifts endpoint (not timesheets) — it returns cost_with_oncosts which is what
// WF's "Cost by Location and Team" Timesheet figure uses (base wages + employer pension +
// leave accrual provision). Leave shifts are excluded (leave_request_id != null).
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)
// Use timesheets/on/{date} with include_oncosts=true — this returns cost_with_oncosts
// INCLUDING National Insurance, which the shifts endpoint omits for some employees.
// This matches WF's "Cost by Location and Team - Timesheet exc. leave" figure exactly.
const [allDepts, allUsers] = await Promise.all([
wfFetchPaged('/api/v2/departments'),
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 userNameMap = Object.fromEntries(allUsers.map(u => [
String(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 byDateDeptEmp = {}
for (const s of shifts) {
if (s.leave_request_id != null) continue // exclude leave shifts
for (const timesheets of perDay) {
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 (enabledDeptIds && !enabledDeptIds.includes(deptId)) continue
if (s.date < from || s.date > to) continue
if (s.leave_request_id != null) continue // exclude leave — matches WF "exc. leave" definition
const date = s.date
const baseCost = parseFloat(s.cost ?? 0)
const totalCost = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
const deptId = String(s.department_id ?? 'unknown')
if (locationDeptIds && !locationDeptIds.has(deptId)) continue
if (enabledDeptIds && !enabledDeptIds.includes(deptId)) continue
const key = `${date}:${deptId}`
if (!byDateDept[key]) {
byDateDept[key] = {
date,
department_id: deptId,
department_name: deptNameMap[deptId] || s.department_name || deptId,
base_cost: 0,
total_cost: 0,
shift_count: 0,
}
}
byDateDept[key].base_cost += baseCost
byDateDept[key].total_cost += totalCost
byDateDept[key].shift_count += 1
const date = s.date
const baseCost = parseFloat(s.cost ?? 0)
const totalCost = parseFloat(s.cost_with_oncosts ?? s.cost ?? 0)
if (totalCost > 0 || baseCost > 0) {
const empId = String(s.user_id)
const empName = userNameMap[empId] || `Employee ${empId}`
const empKey = `${date}:${deptId}:${empId}`
if (!byDateDeptEmp[empKey]) {
byDateDeptEmp[empKey] = {
date, department_id: deptId, employee_id: empId, employee_name: empName,
base_cost: 0, total_cost: 0, shift_count: 0,
const key = `${date}:${deptId}`
if (!byDateDept[key]) {
byDateDept[key] = {
date,
department_id: deptId,
department_name: deptNameMap[deptId] || deptId,
base_cost: 0,
total_cost: 0,
shift_count: 0,
}
}
byDateDept[key].base_cost += baseCost
byDateDept[key].total_cost += totalCost
byDateDept[key].shift_count += 1
if (totalCost > 0 || baseCost > 0) {
const empId = String(s.user_id)
const empName = userNameMap[empId] || `Employee ${empId}`
const empKey = `${date}:${deptId}:${empId}`
if (!byDateDeptEmp[empKey]) {
byDateDeptEmp[empKey] = {
date, department_id: deptId, employee_id: empId, employee_name: empName,
base_cost: 0, total_cost: 0, shift_count: 0,
}
}
byDateDeptEmp[empKey].base_cost += baseCost
byDateDeptEmp[empKey].total_cost += totalCost
byDateDeptEmp[empKey].shift_count += 1
}
}
byDateDeptEmp[empKey].base_cost += baseCost
byDateDeptEmp[empKey].total_cost += totalCost
byDateDeptEmp[empKey].shift_count += 1
}
}