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:
parent
69ecf1012e
commit
38b0c36923
6 changed files with 132 additions and 46 deletions
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue