Fix same-day turnover: pick the most-active booking per site, not just the first
pollOnce() previously collapsed multiple bookings overlapping today for one site down to a single candidate before computing room_state, with a weak tie-break (prefer 'arrived' status, else whichever the API happened to return first). On a same-day turnover — departing guest + incoming guest both returned by NewBook's 'staying' list for the same site — if neither was marked 'arrived' yet, this could arbitrarily lock onto the incoming guest's booking (still in 'booked', prep not started) instead of the departing guest's (which should be 'cooling_down'), even though the departing guest was the one actually still checked into the room. Now every candidate booking for a site is evaluated (each zone applies its own offsets, so this only resolves once we have the zone), and the state machine picks whichever produces the most active result: occupied > heating_up > cooling_down > booked > vacant. Zero behaviour change for the common single-booking-per-site case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d10e4884b8
commit
99ec8b9747
1 changed files with 25 additions and 12 deletions
|
|
@ -16,6 +16,14 @@ const HEAT_STATES = ['heating_up', 'occupied']
|
|||
const COOL_STATES = ['vacant', 'cooling_down', 'booked']
|
||||
const AC_DEVICE_TYPES = ['mhi_modbus', 'mhi_mqtt'] // matches routes/override.js's mhi-control scope
|
||||
|
||||
// On a same-day turnover NewBook's 'staying' list returns BOTH the departing guest's
|
||||
// booking and the incoming guest's booking for one site — there's no single "the"
|
||||
// booking to pick upfront. Rank each candidate's resulting room_state and take the
|
||||
// most active one: someone actually in-house beats any prep state; actively prepping
|
||||
// for an arrival beats a static post-checkout "cooling down" state; a future stay
|
||||
// that hasn't started its prep window yet is least urgent of the non-vacant states.
|
||||
const STATE_PRIORITY = { occupied: 4, heating_up: 3, cooling_down: 2, booked: 1, vacant: 0 }
|
||||
|
||||
let timerHandle = null
|
||||
let running = false
|
||||
|
||||
|
|
@ -230,25 +238,32 @@ export async function pollOnce() {
|
|||
return
|
||||
}
|
||||
|
||||
// Group every booking overlapping today by site — may be more than one on a
|
||||
// same-day turnover, resolved per-zone below (each zone applies its own offsets,
|
||||
// so the "most active" candidate can only be determined once we have the zone).
|
||||
const bookingsBySite = new Map()
|
||||
for (const b of bookings) {
|
||||
const siteId = String(b.site_id ?? b.room_id ?? '')
|
||||
if (!siteId) continue
|
||||
// If multiple bookings overlap today for a site, prefer the one that's
|
||||
// actually arrived, then the one arriving soonest.
|
||||
const existing = bookingsBySite.get(siteId)
|
||||
if (!existing) { bookingsBySite.set(siteId, b); continue }
|
||||
const existingStatus = (existing.booking_status || '').toLowerCase()
|
||||
const candidateStatus = (b.booking_status || '').toLowerCase()
|
||||
if (candidateStatus === 'arrived' && existingStatus !== 'arrived') bookingsBySite.set(siteId, b)
|
||||
if (!bookingsBySite.has(siteId)) bookingsBySite.set(siteId, [])
|
||||
bookingsBySite.get(siteId).push(b)
|
||||
}
|
||||
|
||||
for (const zone of zones) {
|
||||
try {
|
||||
const booking = bookingsBySite.get(String(zone.newbook_site_id)) || null
|
||||
const candidates = bookingsBySite.get(String(zone.newbook_site_id)) || []
|
||||
|
||||
// Cache the booking on every successful fetch so a future failed fetch has
|
||||
// something to fall back to (booking_cache is per-zone, TTL is implicit —
|
||||
let booking = null, newState = 'vacant'
|
||||
for (const b of candidates) {
|
||||
const schedule = calculateHeatingSchedule(zone, b, defaults)
|
||||
const st = determineRoomState(b, schedule, now)
|
||||
if (!booking || STATE_PRIORITY[st] > STATE_PRIORITY[newState]) {
|
||||
booking = b; newState = st
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the winning booking on every successful fetch so a future failed fetch
|
||||
// has something to fall back to (booking_cache is per-zone, TTL is implicit —
|
||||
// the fail-safe path above never even reaches this loop on a failed fetch).
|
||||
await pool.query(
|
||||
`INSERT INTO booking_cache (zone_id, booking_data, fetched_at) VALUES ($1, $2, NOW())
|
||||
|
|
@ -256,8 +271,6 @@ export async function pollOnce() {
|
|||
[zone.id, booking ? JSON.stringify(booking) : null]
|
||||
)
|
||||
|
||||
const schedule = booking ? calculateHeatingSchedule(zone, booking, defaults) : null
|
||||
const newState = determineRoomState(booking, schedule, now)
|
||||
const prev = await getZoneState(zone.id)
|
||||
const oldState = prev?.room_state || 'vacant'
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue