- Guestline: read‑only ingestion (no write‑back), near real‑time via polling.
- Modeling: per‑room‑type recommendations for a single Dublin hotel.
- Add dual engines: Mock vs Actual for fast backend progress without PMS access. (DEPENDENCY INJECTION)
- Incorporate external signals (events/news/social/flights/weather/comp rates) to react after‑hours (eg: bon Jovi announced Dublin tour, hotel booked out literally less than an hour later) and emit block/max‑rate advisories.
Add or keep these tables (extend backend/app/models/schemas.sql): -- Aside: this file isint actually being ran
hotels(id, name, timezone)room_types(id, hotel_id, code, name, capacity)inventory_daily(date, hotel_id, room_type_id, rooms_total, rooms_out)bookings_daily(date, hotel_id, room_type_id, rooms_sold, adr, pickup_24h)rates_daily(date, hotel_id, room_type_id, published_rate)events_daily(date, hotel_id, impact_score, source_json)predictions(created_at, date, hotel_id, room_type_id, price_rec, price_min, price_max, drivers, input_json)model_registry(id serial, created_at, artifact_uri text, is_active boolean)- NEW
signals_raw(id, ts, source, payload_json)andsignals_events(id, hotel_id, start_date, end_date, kind, score, source_ids jsonb, summary) - NEW
advisories(id, created_at, hotel_id, room_type_id, date, type, rationale, confidence, exported bool default false)
- Define a common interface
PricingEngineused by controllers. - Engines:
MockPricingEngine(backend/app/services/mock_pricing_engine.py): deterministic seeded curves per(room_type_code, date)with realistic trends (DOW/seasonality), random noise; returnsPricingItem[]+model_version="mock:v1". No DB writes.ActualMLService(currentml_service.py): loads trained artifact frommodel_registry, builds features from Neon, predicts with guardrails, logs batch topredictions.
- DI provider (in
ml_service.pyorservices/__init__.py):- Env
PRICING_ENGINE_IMPL=mock|actualcontrols which engineget_pricing_engine()returns. - Health endpoint shows active engine and artifact URI.
- Env
Sketch:
class PricingEngine(Protocol):
def quote(self, *, hotel_id: int, room_type_code: str, dates: list[str], as_of: str|None=None, persist: bool=True) -> tuple[list[PricingItem], str]: ...
def get_pricing_engine() -> PricingEngine:
return MockPricingEngine() if os.getenv("PRICING_ENGINE_IMPL","mock")=="mock" else MLService()- Goal: detect demand shocks independent of PMS (e.g., Croke Park concert announced after hours) and convert to date‑window impact scores.
backend/app/services/signals_service.pywith plug‑in connectors (HTTP + light scraping where allowed):- News/events (RSS/Google News queries: Dublin, Croke Park, concerts, sports, conferences)
- Social (X/Reddit/Facebook pages via APIs or RSS; rate‑limited)
- Flights (flash sales/route volume proxies)
- Weather (heatwave indices; med‑term forecasts)
- Comp rates (small comp set public OTA pages; polite cadence and caching)
- Pipeline:
-
Fetch → store raw in
signals_raw. -
Normalize + dedupe →
signals_events(kind, start_date..end_date, score ∈ [0,1]). -
Nightly/hourly scorer writes
events_daily.impact_scoreaggregates per date/hotel.
- Cadence: news/social 10–15 min; weather/flights hourly; comp rates 2–4×/day.
- Layer 0 — Data ingestion: Guestline (read), External Signals, Manual events (UI).
- Layer 1 — Demand model per room‑type (LightGBM/XGBoost): features = DOW/seasonality, lead_time buckets, occupancy & pickup deltas, event_impact, comp‑price delta, weather indices, price_yesterday, etc.
- Layer 2 — Optimizer/guardrails: price bands per room‑type; elasticity‑informed uplift/discount; pacing rules by lead‑time.
- Layer 3 — Shock handler: if
signals_events.scorehigh and pickup spikes after hours, apply immediate uplifts and emit advisories. - Layer 4 — LLM assist (optional, feature‑flag): classify raw text into event types, estimate coarse uplift prior when history is absent, and generate human‑readable explanations. LLM never sets numeric price; it augments features/explanations.
- Output: recommendations + explanations + advisories.
- Rules emit
advisories:- High impact signal AND pickup spike →
type=max_ratefor affected dates/room‑types. - Occupancy > threshold with long lead time →
type=max_rate. - Extreme shock (like instant sell‑out) →
type=suggest_stop_sell(manager action). No automation to “invalidate cards”.
- High impact signal AND pickup spike →
- Dashboard lists advisories with CSV export for quick action in PMS.
ml/pipelines/hourly_refresh.py: hourly feature build → predictions per room‑type → upsertpredictionsand cache for API.ml/pipelines/daily_retrain.py: 03:00 Europe/Dublin retrain → if better, publish artifact and switchmodel_registry.is_active.ml/pipelines/score_events.py: 4×/day recomputeevents_daily.impact_scorefromsignals_events.backend/app/services/scheduler_service.py: runs Guestline polling (15–60 min), signals connectors, and triggers recompute when thresholds exceeded.
pricing_controller.pyPOST /api/pricing/quote(engine‑agnostic) body{hotel_id, room_type_code, dates[]}→ items +model_version.GET /api/pricing/calendar?hotel_id&start&end→ aggregate predictions across room‑types.GET /api/pricing/export.csv.
events_controller.py— manual events CRUD →events_daily.signals_controller.py— recent signals & impacts; for debugging.health_controller.py— engine kind, artifact uri, last PMS/Signals sync, last model run.- DTOs mirrored in
frontend/src/types/.
src/api/pricing.ts→getQuote,getCalendar,exportCSV.- Pages:
recommendations(table + per room‑type filters + explanations);events(manual add); lightweightsignalspanel. - Status chips: “Last PMS sync”, “Last signals sync”, “Model vX” (engine aware).
-
Dashboard
/dashboard- UI: KPIs tiles (Occ 7d, ADR vs Rec ADR, Active Advisories), status chips, latest advisories list.
- Initial data fetch (SSR or client):
GET /api/health- Response:
{ engine: "mock"|"actual", model_uri: string, last_pms_sync: ts, last_signals_sync: ts, last_model_run: ts }
- Response:
GET /api/pricing/calendar?hotel_id={id}&start={today}&end={today+7}- Response:
{ items: [{ date, room_type_code, price_rec, price_min, price_max }...] }
- Response:
GET /api/advisories?hotel_id={id}&start={today}&end={today+30}(when implemented)- Response:
{ advisories: [{ date, room_type_code, type, rationale, confidence }] }
- Response:
- Polling:
/api/healthevery 30–60s for status chips.
-
Recommendations
/recommendations- UI: Date range picker (default 30 days), room‑type filter, table/grid view with Explain popover.
- Data flow:
GET /api/pricing/calendar?hotel_id={id}&start={s}&end={e}[&room_type_code=RT]- Fast path from cache; used for rendering the table.
- On manual refresh for selected rows (or if date range is unusual):
POST /api/pricing/quotewith body: {"hotel_id": 1, "room_type_code": "DLX-QUEEN", "dates": ["2025-11-01","2025-11-02"]}- Response: {"items": [{"date":"2025-11-01","room_type_code":"DLX-QUEEN","price_rec":155.0,"price_min":135.0,"price_max":195.0,"drivers":["event","pace"]}], "model_version": "mock:v1"}
- Export:
GET /api/pricing/export.csv?hotel_id={id}&start={s}&end={e}[&room_type_code=RT][&ids=...]→text/csv
- Empty/error states: if
engine=mock, display badge “demo data”. If/calendarstale (last_model_run > 60m), show warning.
-
Events
/events- UI: calendar with badges, form: {label, date range, expected impact: low|med|high}.
- API:
GET /api/events?hotel_id={id}&start={s}&end={e}→ list existing manual events, and derivedimpact_scorefor context.POST /api/eventsbody{hotel_id, start_date, end_date, label, impact_level}→ create/update.DELETE /api/events/{id}
- Side effects: creating/updating triggers recompute (feature flag/queued task) and may produce advisories.
-
Signals panel
/signals(optional)- UI: list of recent signals with filters (kind: news/social/flights/weather/otas), link to the source.
- API:
GET /api/signals/recent?hotel_id={id}&limit=50→ fromsignals_eventsjoined tosignals_rawids.
- Purpose: transparency; helps trust the recommendations and advisories.
-
Global pieces
src/api/pricing.tswrapsutils/apiClient.tsxand exposes:getCalendar(hotelId, start, end, roomTypeCode?)getQuote(hotelId, roomTypeCode, dates)exportCSV(hotelId, start, end, roomTypeCode?, ids?)
src/api/system.tsforgetHealth()and latergetAdvisories().- Types mirrored under
src/types/to match backend DTOs (PricingItem,PricingResponse,Advisory).
ml/features/bootstrap_from_csv.pyto ingestinfra/foresight-data/2025 PMS.csvand historic CSVs as hotel‑level backfill. We’ll request room‑type‑level export from Guestline; until then, allow an optional allocation mapping file to split to room‑types (pilot‑only fallback; clearly documented).
- Processes on Render:
- API service (FastAPI) — env
PRICING_ENGINE_IMPLcontrols Mock vs Actual. - Signals worker — connectors + scoring (APScheduler/Celery beat).
- ML worker — hourly refresh + daily retrain + artifact publish to S3/R2.
- API service (FastAPI) — env
- Neon for Postgres; S3/R2 for artifacts; Vercel for frontend.
- GitHub Actions: backend lint/pytest + frontend typecheck/build; separate staging/prod.
- Feature flags:
LLM_EXPLAINERS,ENABLE_SIGNALS,PRICING_ENGINE_IMPL.
- They confirm after‑hours event announcements causing instant booking spikes and manager desire to temporarily max rate or suggest stop‑sell. The plan’s Signals Service + Advisories + Shock handler directly address this.
- Single role: Revenue Manager (RM) for the pilot hotel. Authenticated users land in the app; others are blocked.
- Dashboard (default)
- Status chips: Last PMS sync, Last Signals sync, Model version/engine (mock or actual).
- At‑a‑glance: next 7‑day occupancy trend, current ADR vs recommended ADR, count of active advisories.
- Advisory feed: “Max rate suggested on 2026‑08‑30 (High event impact + pickup spike)” with link to details and CSV export.
- Recommendations page
- Calendar/table for 14–60 days ahead filtered by room type.
- Columns: Date, Room Type, Recommended Price, Min/Max band, Explanation (popover showing top drivers), Last updated.
- Actions: Select rows → Export CSV (for PMS import). No write‑back in pilot.
- Events page
- Add/edit manual events (date range, label, expected impact low/med/high) to nudge model/guardrails.
- Shows computed Impact Score per day, merged with external signals.
- Signals panel (lite, optional for pilot)
- Recent detected signals (news/social/flights/weather/OTAs) with source links and mapped date window.
- RM logs in → sees Dashboard with status and any new advisories.
- RM opens Recommendations → reviews price curve for room types → exports selected dates as CSV to apply in PMS.
- If RM knows a special event, they add it on Events page to pre‑empt demand.
- During shocks (e.g., gig announced after hours), Advisories appear quickly (max‑rate/stop‑sell suggestion). RM exports and acts in PMS.
- PMS ingestion (read‑only): Guestline poller runs every 15–60 min.
- Pulls reservations/availability/rates per room type for the next 60 days.
- Normalizes into Neon:
inventory_daily,bookings_daily,rates_dailyand computespickup_24hdeltas. - Updates “Last PMS sync” status.
- External Signals connectors (independent of PMS)
- Poll news/RSS/Google News, social, flights, weather, and comp set OTAs on their cadences.
- Raw payloads →
signals_raw; dedup/normalize →signals_events (kind, start_date..end_date, score). - Scorer updates
events_daily.impact_scoreper date/hotel; updates “Last Signals sync”.
- Feature build and caching (hourly and on triggers)
- Joins daily tables +
events_dailyto create per‑room‑type feature rows for the next 60 days (lead‑time buckets, pickup deltas, seasonality, event impact, comp deltas, weather indices, price_yesterday, etc.). - Keeps a small cache for the Pricing API.
- Joins daily tables +
- Pricing engine selection (DI)
- Env
PRICING_ENGINE_IMPL=mock|actual. - Mock:
MockPricingEngineproduces deterministic seeded curves for stable testing. - Actual:
MLServiceloads the active model artifact from S3/R2 viamodel_registry, predicts, applies guardrails/explanations.
- Env
- Recompute triggers
- Scheduled hourly; or immediately when (a) pickup delta crosses threshold, (b) new high‑impact signal arrives, (c) RM adds a manual event.
- Writes results to
predictionsand updates cache.
- Advisory engine (read‑only guidance)
- Rules examine
signals_events.score, pickup spikes, occupancy/lead‑time, and recent predictions. - Emits
advisoriesrows:max_rateorsuggest_stop_sell, with rationale and confidence.
- Rules examine
- API layer (FastAPI)
/api/pricing/quote→ engine‑agnostic on‑demand quote for a hotel/room‑type/dates./api/pricing/calendar→ aggregates cached predictions for range./api/pricing/export.csv→ CSV stream of selected dates./api/events/*→ manual event CRUD;/api/health→ engine kind, model uri, last PMS/Signals sync, last run.
- Frontend consumption
- Recommendations table/calendars read from
/calendar(fast), fall back to/quotefor ad‑hoc ranges. - CSV export calls
/export.csvwith selected row IDs/dates. - Dashboard polls
/api/healthfor status chips.
- Recommendations table/calendars read from
-
Guestline ingestion worker
- Reads: Guestline API.
- Writes: staging temp tables (optional) → upserts to
inventory_daily,bookings_daily,rates_daily(composite PKdate, hotel_id, room_type_id). - Computes:
pickup_24hby diffing rolling snapshots. - Indexes:
(hotel_id, room_type_id, date)on all daily tables.
-
Signals connectors
- Reads: external sources (HTTP/RSS/APIs); each payload stored in
signals_rawwith source, ts, and hash for dedupe. - Normalizer scorer:
- Groups by event; maps to
start_date..end_date; assignskindand ascore ∈ [0,1]. - Writes to
signals_events; aggregates per day intoevents_daily.impact_score.
- Groups by event; maps to
- Reads: external sources (HTTP/RSS/APIs); each payload stored in
-
Feature builder
- Query joins daily tables by
(hotel_id, room_type_id, date)for horizon[today, +60d]. - Adds engineered columns: lead_time_bucket, pace indices, moving averages, event_impact, comp_delta, weather flags.
- Materialization options: on‑the‑fly DataFrame vs temporary materialized view (pilot keeps it simple with on‑the‑fly build + cache).
- Query joins daily tables by
-
Pricing engines
- Mock: computes deterministic
price_recwith DOW/seasonality multipliers + noise; returns bands. - Actual: loads artifact once (hot‑reloaded if
model_registry.is_activechanges), predicts row‑by‑row; post‑process applies guardrails (min/max, uplift caps, pacing).
- Mock: computes deterministic
-
Caching strategy
- After each hourly refresh, latest predictions for
[today, +60d]are cached in memory (process cache) keyed by(hotel_id, room_type_code). /calendarserves from cache;/quotebypasses cache for ad‑hoc dates.- Cache invalidated on successful recompute or model switch.
- After each hourly refresh, latest predictions for
-
Advisories
- Rule engine scans fresh predictions and recent signals to detect shock conditions.
- Writes human‑readable
rationaleandconfidencetoadvisories. - Export endpoint filters by date/room‑type for RM action in PMS.
- News connector detects “Croke Park concert announced”, classifies as high‑impact; maps to date window; score escalates.
- Signals scorer updates
events_daily.impact_scorefor that date; trigger fires. - Pricing engine recomputes immediately; Advisory engine emits
max_rate(and possiblysuggest_stop_sell). - Dashboard shows new advisory within minutes; Recommendations reflect higher prices. RM exports CSV and applies in PMS.
- ML worker: hourly refresh, daily 03:00 retrain → publish artifact to
model_registryif improved. - API service: serves requests with current engine; hot‑reloads to new artifact on next call.
- Signals worker: runs connectors/scorer on cadence.
- PMS unreachable: show stale badge; continue serving last predictions; keep Signals/advisories active.
- Signals API rate‑limited: degrade to manual events; keep base recommendations.
- Engine = mock: UI/flows remain identical; health shows
engine=mockand no DB writes for predictions.
- Inputs:
inventory_daily,bookings_daily,rates_daily,events_daily,signals_events. - ML outputs:
predictions(per date/room‑type price),advisories(human actions), cached responses for UI. - Registry:
model_registrycontrols active artifact.
- Add MockPricingEngine and DI switch PRICING_ENGINE_IMPL
- Implement signals connectors + scoring into events_daily
- Add advisories table and emit rules + UI export
- Optional LLM classifiers/explainers behind a flag
- Guestline read-only adapter + polling scheduler
- Bootstrap Neon from foresight-data CSVs
- Per-room-type feature build + training scripts
- Model loading + predict with guardrails
- Hourly refresh, daily retrain, 4× event scoring jobs
- Finish pricing/events/signals/health controllers + DTOs
- Recommendations/events pages and status chips
- Render/Vercel deploy and GitHub Actions