Use text-search approach for rate conditions (breakfast/cancel/payment)

Original scrapy approach: extract <li> condition lines from each rate plan row
and text-search for known keywords. No match = null (unknown), not assumed false.

Adds breakfast_text, cancel_text, payment_text columns to booking_com_rates.
Booleans now nullable (null = not mentioned, true/false = explicit signal).

JS extraction searches all <li> items in the row (falls back to newline-split
innerText if none). Breakfast: 'breakfast' keyword. Cancel: 'free cancellation',
'non-refundable', 'total cost to cancel', 'fully chargeable'. Payment: 'no
prepayment', 'pay at the property', 'pay online'. data-fltrs used as fallback.

API snapshot endpoint now returns breakfast/cancel/payment text strings. Old
boolean-only rows degrade gracefully to derived labels.

Modal plan rows replace the fixed meal|cancel|price column layout with a single
stacked conditions cell: 0-3 lines depending on what the page actually shows.
Breakfast green when 'included', cancel green when 'Free cancellation…'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-14 18:10:00 +00:00
parent 69ecf1012e
commit 38b0c36923
6 changed files with 132 additions and 46 deletions

View file

@ -1368,10 +1368,14 @@ async def get_hotel_rate_snapshot(
r.room_type,
r.breakfast_included,
r.free_cancellation,
r.no_prepayment,
r.rate_gross::float AS price,
r.rooms_left,
r.max_persons,
r.availability_status
r.availability_status,
r.breakfast_text,
r.cancel_text,
r.payment_text
FROM booking_com_rates r
WHERE r.hotel_id = :hotel_id
AND r.rate_date = :stay_date
@ -1383,22 +1387,41 @@ async def get_hotel_rate_snapshot(
ORDER BY scraped_at DESC
LIMIT 1
)
ORDER BY r.room_type, r.breakfast_included, r.free_cancellation DESC, r.rate_gross
ORDER BY r.room_type, r.breakfast_included NULLS LAST, r.free_cancellation DESC NULLS LAST, r.rate_gross
"""),
{"hotel_id": hotel_id, "stay_date": stay_date},
)
rows = result.mappings().all()
# If no rate_plan data (old scraper rows), return a simple summary
if not rows:
return {"stay_date": str(stay_date), "rooms": [], "legacy": True}
# Check if this is old single-row data (no rate_plan_id breakdown)
has_breakdown = any(r["room_type"] for r in rows)
def _plan(r) -> dict:
# Use stored text when available; fall back to deriving from booleans for old rows.
breakfast = r["breakfast_text"] or (
"Breakfast included" if r["breakfast_included"] is True else
"Breakfast available as extra" if r["breakfast_included"] is False else None
)
cancel = r["cancel_text"] or (
"Free cancellation" if r["free_cancellation"] is True else
"Non-refundable" if r["free_cancellation"] is False else None
)
payment = r["payment_text"] or (
"No prepayment needed pay at the property" if r["no_prepayment"] is True else
"Pay online" if r["no_prepayment"] is False else None
)
return {
"breakfast": breakfast,
"cancel": cancel,
"payment": payment,
"price": r["price"],
"max_persons": r["max_persons"],
}
if not has_breakdown:
# Legacy: single row per hotel+date from search results scraper
row = rows[0]
return {
"stay_date": str(stay_date),
@ -1407,16 +1430,10 @@ async def get_hotel_rate_snapshot(
"room_type": "Best available",
"rooms_left": row["rooms_left"],
"availability_status": row["availability_status"],
"plans": [{
"meal": "B&B" if row["breakfast_included"] else "Room only",
"cancel": "Free cancellation" if row["free_cancellation"] else "Non-refundable",
"price": row["price"],
"max_persons": row["max_persons"],
}] if row["price"] else [],
"plans": [_plan(row)] if row["price"] else [],
}],
}
# Group by room_type, keeping rooms_left from the first occurrence (same per room)
rooms_map: dict = {}
for r in rows:
rt = (r["room_type"] or "Unknown").strip()
@ -1428,12 +1445,7 @@ async def get_hotel_rate_snapshot(
"plans": [],
}
if r["price"] and (r["max_persons"] is None or r["max_persons"] == 2):
rooms_map[rt]["plans"].append({
"meal": "B&B" if r["breakfast_included"] else "Room only",
"cancel": "Free cancellation" if r["free_cancellation"] else "Non-refundable",
"price": r["price"],
"max_persons": r["max_persons"],
})
rooms_map[rt]["plans"].append(_plan(r))
return {
"stay_date": str(stay_date),