Add hotel discovery scrape, manual hotel add, and competitor-only filtering

- Scraper: restrict hotel-page scraper to 'competitor' tier only (was own+competitor); own hotel rates come from Newbook API, not Booking.com
- Backend: POST /competitors/discover — runs search-results scrape for one date to find market hotels regardless of current backend setting
- Backend: POST /competitors/hotels — add hotel manually from Booking.com URL + name + tier; upserts on slug conflict
- Frontend (Settings tab): Discover Market Hotels button added below manual date-range scrape
- Frontend (Hotels tab): Add Hotel form at top — paste URL (auto-derives name from slug), choose tier, submit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-10 00:08:58 +00:00
parent 30583c59a4
commit c49985bec7
3 changed files with 287 additions and 5 deletions

View file

@ -295,16 +295,16 @@ def cleanup_stale_batches(db: Session, max_age_minutes: int = 60):
def get_active_hotels(db: Session) -> List[Dict[str, Any]]:
"""Return own + competitor hotels with a booking_com_url (for hotel-page scraping).
Market-tier hotels are excluded they were auto-discovered from search results and
are not hotels we specifically want to track at rate-plan level."""
"""Return competitor hotels with a booking_com_url (for hotel-page scraping).
Own and market hotels are excluded market were auto-discovered from search results;
own hotel rates come from the Newbook API, not Booking.com scraping."""
rows = db.execute(
text("""
SELECT id, booking_com_id, name, booking_com_url
FROM booking_com_hotels
WHERE is_active = TRUE
AND booking_com_url IS NOT NULL
AND tier IN ('own', 'competitor')
AND tier = 'competitor'
ORDER BY display_order, id
""")
).fetchall()
@ -884,6 +884,49 @@ async def _run_manual_scrape_locked(
}
async def run_discovery_scrape(db: Session) -> Dict[str, Any]:
"""
Run a search-results scrape for a single date to discover market hotels.
Always uses playwright_local (search-results) regardless of the configured
backend intended for hotel discovery, not rate tracking.
Returns new_hotels: count of hotels added to the DB this run.
"""
config = get_scrape_config(db)
if not config:
return {'success': False, 'error': 'No scrape location configured.'}
hotels_before = db.execute(
text("SELECT COUNT(*) FROM booking_com_hotels WHERE is_active = TRUE")
).scalar() or 0
batch_id = create_scrape_batch(db, 'discovery')
check_in = date.today() + timedelta(days=14)
jobs: List[Tuple[date, Optional[int]]] = [(check_in, None)]
try:
agg = await _scrape_dates_concurrent(jobs, config, concurrency=1, batch_id=batch_id)
update_scrape_batch(
db, batch_id,
status='completed' if agg['completed'] else 'failed',
hotels_found=agg['hotels'],
rates_scraped=agg['rates'],
)
hotels_after = db.execute(
text("SELECT COUNT(*) FROM booking_com_hotels WHERE is_active = TRUE")
).scalar() or 0
return {
'success': agg['completed'] > 0,
'hotels_found': agg['hotels'],
'new_hotels': max(0, hotels_after - hotels_before),
}
except Exception as e:
logger.error(f"Discovery scrape error: {e}")
update_scrape_batch(db, batch_id, status='failed', error_message=str(e))
return {'success': False, 'error': str(e)}
# ============================================
# QUEUE MANAGEMENT
# ============================================