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 COOL_STATES = ['vacant', 'cooling_down', 'booked']
|
||||||
const AC_DEVICE_TYPES = ['mhi_modbus', 'mhi_mqtt'] // matches routes/override.js's mhi-control scope
|
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 timerHandle = null
|
||||||
let running = false
|
let running = false
|
||||||
|
|
||||||
|
|
@ -230,25 +238,32 @@ export async function pollOnce() {
|
||||||
return
|
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()
|
const bookingsBySite = new Map()
|
||||||
for (const b of bookings) {
|
for (const b of bookings) {
|
||||||
const siteId = String(b.site_id ?? b.room_id ?? '')
|
const siteId = String(b.site_id ?? b.room_id ?? '')
|
||||||
if (!siteId) continue
|
if (!siteId) continue
|
||||||
// If multiple bookings overlap today for a site, prefer the one that's
|
if (!bookingsBySite.has(siteId)) bookingsBySite.set(siteId, [])
|
||||||
// actually arrived, then the one arriving soonest.
|
bookingsBySite.get(siteId).push(b)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const zone of zones) {
|
for (const zone of zones) {
|
||||||
try {
|
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
|
let booking = null, newState = 'vacant'
|
||||||
// something to fall back to (booking_cache is per-zone, TTL is implicit —
|
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).
|
// the fail-safe path above never even reaches this loop on a failed fetch).
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO booking_cache (zone_id, booking_data, fetched_at) VALUES ($1, $2, NOW())
|
`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]
|
[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 prev = await getZoneState(zone.id)
|
||||||
const oldState = prev?.room_state || 'vacant'
|
const oldState = prev?.room_state || 'vacant'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue