From 932302ebe2937c98f25c07796f3f847d6d601572 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Mon, 27 Jul 2026 16:38:53 +0000 Subject: [PATCH] Fix directors forecast report counting stale pickup for past days The report endpoint's totalPickup/totalRooms summed every day's pickup override regardless of whether the day had already passed, even though a past day's actual accommodation revenue already supersedes it (fAccomm ignores pickup for those days). This overstated Probable Pickup and the month's rooms estimate. Also fixed pickup.accomm to sum each day's own rate instead of multiplying the total by a flat session rate. --- backend/src/routes/directors-forecast.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/routes/directors-forecast.js b/backend/src/routes/directors-forecast.js index 66c73e80..cf6b901e 100644 --- a/backend/src/routes/directors-forecast.js +++ b/backend/src/routes/directors-forecast.js @@ -104,8 +104,10 @@ export async function directorsForecastRoutes(fastify) { const actualDry = isPast ? (rev.dry?.otb ?? null) : null const actualWet = isPast ? (rev.wet?.otb ?? null) : null - // User overrides - const pickupRooms = ovr.pickup_rooms != null ? parseInt(ovr.pickup_rooms) : 0 + // User overrides — once a day is past and has an actual, a leftover + // pickup override from before the day arrived is stale and shouldn't count. + const pickupApplies = !(isPast && actualAccomm != null) + const pickupRooms = pickupApplies && ovr.pickup_rooms != null ? parseInt(ovr.pickup_rooms) : 0 const dayRate = ovr.pickup_avg_rate != null ? parseFloat(ovr.pickup_avg_rate) : rate // Forecast values @@ -209,7 +211,7 @@ export async function directorsForecastRoutes(fastify) { '29–end': { accomm: 0, dry: 0, wet: 0, rooms: 0 }, } - let totalAvailable = 0, totalOtbRooms = 0, totalOtbNetRev = 0, totalPickup = 0 + let totalAvailable = 0, totalOtbRooms = 0, totalOtbNetRev = 0, totalPickup = 0, totalPickupRevenue = 0 let totalAccomm = 0, totalDry = 0, totalWet = 0 let totalBudgetAccomm = 0, totalBudgetDry = 0, totalBudgetWet = 0 let totalLyAccomm = 0, totalLyDry = 0, totalLyWet = 0, totalLyRooms = 0 @@ -224,14 +226,19 @@ export async function directorsForecastRoutes(fastify) { const available = rm.available_rooms ?? 0 const otbRooms = rm.otb_rooms ?? 0 const otbNetRev = rev.accom?.otb ?? 0 - const pickupRooms = ovr.pickup_rooms != null ? parseInt(ovr.pickup_rooms) : 0 - const dayRate = ovr.pickup_avg_rate != null ? parseFloat(ovr.pickup_avg_rate) : sessionRate - const totalRooms = otbRooms + pickupRooms const actualAccomm = isPast ? (rev.accom?.otb ?? null) : null const actualDry = isPast ? (rev.dry?.otb ?? null) : null const actualWet = isPast ? (rev.wet?.otb ?? null) : null + // Once a day is past and has an actual, it's realised — any leftover + // pickup override from before the day arrived is stale and must not + // still count as "probable" pickup (it never contributed to fAccomm below). + const pickupApplies = !(isPast && actualAccomm != null) + const pickupRooms = pickupApplies && ovr.pickup_rooms != null ? parseInt(ovr.pickup_rooms) : 0 + const dayRate = ovr.pickup_avg_rate != null ? parseFloat(ovr.pickup_avg_rate) : sessionRate + const totalRooms = otbRooms + pickupRooms + const fAccomm = (isPast && actualAccomm != null) ? actualAccomm : otbNetRev + pickupRooms * dayRate const fDry = ovr.forecast_dry != null ? parseFloat(ovr.forecast_dry) : ovr.dry_override != null ? parseFloat(ovr.dry_override) @@ -244,6 +251,7 @@ export async function directorsForecastRoutes(fastify) { totalOtbRooms += otbRooms totalOtbNetRev += otbNetRev totalPickup += pickupRooms + totalPickupRevenue += pickupRooms * dayRate totalAccomm += fAccomm totalDry += fDry totalWet += fWet @@ -312,7 +320,7 @@ export async function directorsForecastRoutes(fastify) { arr: totalOtbRooms > 0 ? totalOtbNetRev / totalOtbRooms : null, revpar: totalAvailable > 0 ? totalOtbNetRev / totalAvailable : null, }, - pickup: { rooms: totalPickup, accomm: totalPickup * sessionRate, avg_rate: sessionRate }, + pickup: { rooms: totalPickup, accomm: totalPickupRevenue, avg_rate: totalPickup > 0 ? totalPickupRevenue / totalPickup : sessionRate }, weekly: Object.entries(weekBands).map(([label, v]) => ({ label, ...v })), snapshots: snapshotsRes.rows, utilities,