Remove old Resos sync path (resos_bookings table)

Consolidates to single sync path: resos_bookings_sync.py →
resos_bookings_data table. Removes sync_resos_data(), duplicate
load_resos_custom_field_mappings(), resos_api_key and
sync_resos_enabled seed rows, and the dead resos_sync scheduler job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-13 21:07:47 +00:00
parent 1a0b18f06e
commit fc15bc5d9a
3 changed files with 14 additions and 273 deletions

View file

@ -1,5 +1,5 @@
""" """
Data sync job - pulls data from Newbook and Resos APIs Data sync job - pulls data from Newbook API
""" """
import json import json
import logging import logging
@ -9,7 +9,6 @@ from typing import Optional, Dict, Set
from sqlalchemy import text from sqlalchemy import text
from database import SyncSessionLocal from database import SyncSessionLocal
from services.newbook_client import NewbookClient from services.newbook_client import NewbookClient
from services.resos_client import ResosClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -75,13 +74,6 @@ def load_newbook_credentials(db) -> dict:
} }
def load_resos_credentials(db) -> dict:
"""Load Resos API credentials from central Settings service."""
from services.central_settings import get_resos_credentials_sync
creds = get_resos_credentials_sync()
return creds or {'api_key': None}
def load_gl_config(db) -> tuple: def load_gl_config(db) -> tuple:
""" """
Load GL code configuration for identifying breakfast/dinner items. Load GL code configuration for identifying breakfast/dinner items.
@ -167,7 +159,7 @@ async def run_data_sync(
triggered_by: str = "scheduler" triggered_by: str = "scheduler"
): ):
""" """
Main data sync job - runs Newbook bookings, Newbook occupancy report, and Resos sync. Main data sync job - runs Newbook bookings and Newbook occupancy report.
Args: Args:
full_sync: If True, pulls all bookings. If False, only pulls changes since last sync. full_sync: If True, pulls all bookings. If False, only pulls changes since last sync.
@ -186,11 +178,6 @@ async def run_data_sync(
occ_to_date = date.today() + timedelta(days=365) occ_to_date = date.today() + timedelta(days=365)
await sync_newbook_occupancy_report(occ_from_date, occ_to_date, triggered_by) await sync_newbook_occupancy_report(occ_from_date, occ_to_date, triggered_by)
# Sync Resos data (still uses date range for now)
from_date = date.today() - timedelta(days=7)
to_date = date.today() + timedelta(days=365)
await sync_resos_data(from_date, to_date, triggered_by)
logger.info("Data sync completed successfully") logger.info("Data sync completed successfully")
except Exception as e: except Exception as e:
logger.error(f"Data sync failed: {e}") logger.error(f"Data sync failed: {e}")
@ -546,236 +533,6 @@ async def sync_newbook_data(
db.close() db.close()
def load_resos_custom_field_mappings(db) -> Dict[str, dict]:
"""
Load custom field mappings from resos_custom_field_mapping table.
Returns dict: {field_id: {"maps_to": "hotel_guest", "value_for_true": "Yes"}}
"""
result = db.execute(text("""
SELECT field_id, maps_to, value_for_true
FROM resos_custom_field_mapping
WHERE maps_to != 'ignore'
"""))
mappings = {}
for row in result.fetchall():
mappings[row.field_id] = {
"maps_to": row.maps_to,
"value_for_true": row.value_for_true
}
return mappings
async def sync_resos_data(
from_date: date,
to_date: date,
triggered_by: str = "scheduler"
):
"""
Sync restaurant bookings from Resos.
"""
logger.info(f"Starting Resos sync from {from_date} to {to_date}")
db = next(iter([SyncSessionLocal()]))
try:
# Log sync start
db.execute(
text("""
INSERT INTO sync_log (sync_type, source, started_at, status, date_from, date_to, triggered_by)
VALUES ('bookings', 'resos', NOW(), 'running', :from_date, :to_date, :triggered_by)
"""),
{"from_date": from_date, "to_date": to_date, "triggered_by": triggered_by}
)
db.commit()
# Load Resos credentials from database
resos_creds = load_resos_credentials(db)
if not resos_creds.get('api_key'):
raise Exception("Resos API key not configured in database")
# Load custom field mappings from database
cf_mappings = load_resos_custom_field_mappings(db)
logger.info(f"Loaded {len(cf_mappings)} Resos custom field mappings")
async with ResosClient(api_key=resos_creds['api_key']) as client:
# Test connection
if not await client.test_connection():
raise Exception("Resos connection failed")
# Fetch bookings
bookings = await client.get_bookings(from_date, to_date)
logger.info(f"Fetched {len(bookings)} bookings from Resos")
records_created = 0
for booking in bookings:
# Parse guest info
guest = booking.get("guest", {})
resos_id = booking.get("_id")
booking_date = booking.get("date")
status = booking.get("status")
# Extract custom fields using configured mappings
custom_fields = booking.get("customFields", [])
is_hotel_guest = None
is_dbb = None
is_package = None
hotel_booking_number = None
allergies = None
for cf in custom_fields:
# Get field ID - Resos may use 'id' or '_id'
field_id = cf.get("id") or cf.get("_id") or cf.get("fieldId")
# For radio/checkbox fields, use multipleChoiceValueName (human-readable label)
# Fall back to value field for text fields
field_value_label = cf.get("multipleChoiceValueName") or cf.get("value")
field_value = cf.get("value")
# Check if this field has a configured mapping
if field_id and field_id in cf_mappings:
mapping = cf_mappings[field_id]
maps_to = mapping["maps_to"]
value_for_true = mapping.get("value_for_true")
# Debug logging for matched mappings (first 5 records only)
if records_created < 5:
logger.info(f"Matched mapping: field_id={field_id}, maps_to={maps_to}, label={field_value_label}, value_for_true={value_for_true}")
if maps_to == "hotel_guest":
# For boolean fields, check label against value_for_true
if value_for_true:
is_hotel_guest = str(field_value_label) == str(value_for_true)
else:
is_hotel_guest = str(field_value_label).lower() in ("yes", "true", "1")
elif maps_to == "dbb":
if value_for_true:
is_dbb = str(field_value_label) == str(value_for_true)
else:
is_dbb = str(field_value_label).lower() in ("yes", "true", "1")
elif maps_to == "package":
if value_for_true:
is_package = str(field_value) == str(value_for_true)
else:
is_package = str(field_value).lower() in ("yes", "true", "1")
elif maps_to == "booking_number":
hotel_booking_number = str(field_value) if field_value else None
elif maps_to == "allergies":
allergies = str(field_value) if field_value else None
# Fallback to keyword matching if no mapping configured
elif not cf_mappings:
field_name = cf.get("name", "").lower()
if "hotel" in field_name and "guest" in field_name:
is_hotel_guest = str(field_value).lower() in ("yes", "true", "1")
elif "dbb" in field_name or "dinner bed breakfast" in field_name:
is_dbb = str(field_value).lower() in ("yes", "true", "1")
elif "package" in field_name:
is_package = str(field_value).lower() in ("yes", "true", "1")
elif "booking" in field_name and "number" in field_name:
hotel_booking_number = str(field_value) if field_value else None
elif "allerg" in field_name:
allergies = str(field_value) if field_value else None
# Upsert booking with full data
db.execute(
text("""
INSERT INTO resos_bookings (
resos_id, booking_date, booking_time, covers,
status, source, opening_hour_id, table_name, table_area,
is_hotel_guest, is_dbb, is_package, hotel_booking_number, allergies,
notes, fetched_at
) VALUES (
:resos_id, :booking_date, :booking_time, :covers,
:status, :source, :opening_hour_id, :table_name, :table_area,
:is_hotel_guest, :is_dbb, :is_package, :hotel_booking_number, :allergies,
:notes, NOW()
)
ON CONFLICT (resos_id) DO UPDATE SET
status = :status,
covers = :covers,
is_hotel_guest = COALESCE(:is_hotel_guest, resos_bookings.is_hotel_guest),
is_dbb = COALESCE(:is_dbb, resos_bookings.is_dbb),
is_package = COALESCE(:is_package, resos_bookings.is_package),
fetched_at = NOW()
"""),
{
"resos_id": resos_id,
"booking_date": booking_date,
"booking_time": booking.get("time"),
"covers": booking.get("people"),
"status": status,
"source": booking.get("source"),
"opening_hour_id": booking.get("openingHourId"),
"table_name": booking.get("tables", [{}])[0].get("name") if booking.get("tables") else None,
"table_area": booking.get("tables", [{}])[0].get("area", {}).get("name") if booking.get("tables") else None,
"is_hotel_guest": is_hotel_guest,
"is_dbb": is_dbb,
"is_package": is_package,
"hotel_booking_number": hotel_booking_number,
"allergies": allergies,
"notes": str(booking.get("restaurantNotes", []))
}
)
records_created += 1
# Queue date for aggregation
if booking_date:
db.execute(
text("""
INSERT INTO aggregation_queue (date, source, reason, booking_id)
VALUES (:date, 'resos', :reason, :booking_id)
ON CONFLICT (date, source, booking_id) DO UPDATE SET
queued_at = NOW(),
aggregated_at = NULL
"""),
{
"date": booking_date,
"reason": f"booking_{status.lower() if status else 'modified'}",
"booking_id": str(resos_id)
}
)
db.commit()
# Update sync log
db.execute(
text("""
UPDATE sync_log
SET completed_at = NOW(), status = 'success',
records_fetched = :fetched, records_created = :created
WHERE id = (
SELECT id FROM sync_log
WHERE source = 'resos' AND status = 'running'
ORDER BY started_at DESC LIMIT 1
)
"""),
{"fetched": len(bookings), "created": records_created}
)
db.commit()
logger.info(f"Resos sync completed: {records_created} records processed")
except Exception as e:
logger.error(f"Resos sync failed: {e}")
db.execute(
text("""
UPDATE sync_log
SET completed_at = NOW(), status = 'failed', error_message = :error
WHERE id = (
SELECT id FROM sync_log
WHERE source = 'resos' AND status = 'running'
ORDER BY started_at DESC LIMIT 1
)
"""),
{"error": str(e)}
)
db.commit()
raise
finally:
db.close()
async def sync_newbook_occupancy_report( async def sync_newbook_occupancy_report(
from_date: date, from_date: date,
to_date: date, to_date: date,

View file

@ -8,7 +8,6 @@ from sqlalchemy import text
from jobs.data_sync import ( from jobs.data_sync import (
sync_newbook_data, sync_newbook_data,
sync_resos_data,
sync_newbook_occupancy_report, sync_newbook_occupancy_report,
sync_newbook_earned_revenue sync_newbook_earned_revenue
) )
@ -23,6 +22,7 @@ from jobs.weekly_forecast_snapshot import run_weekly_forecast_snapshot
from jobs.fetch_current_rates import run_fetch_current_rates from jobs.fetch_current_rates import run_fetch_current_rates
from jobs.scrape_booking_rates import run_scheduled_booking_scrape_async from jobs.scrape_booking_rates import run_scheduled_booking_scrape_async
from jobs.ai_insights import run_ai_insights_generation from jobs.ai_insights import run_ai_insights_generation
from jobs.weather_sync import run_weather_sync
from database import SyncSessionLocal from database import SyncSessionLocal
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -108,15 +108,6 @@ async def run_scheduled_resos_bookings_sync():
logger.info("Scheduled Resos bookings sync skipped (disabled in settings)") logger.info("Scheduled Resos bookings sync skipped (disabled in settings)")
async def run_scheduled_resos_sync():
"""Wrapper to check if Resos sync is enabled before running"""
if is_sync_enabled("resos"):
logger.info("Running scheduled Resos sync")
await sync_resos_data(triggered_by="scheduler")
else:
logger.info("Scheduled Resos sync skipped (disabled in settings)")
async def run_scheduled_occupancy_report_sync(): async def run_scheduled_occupancy_report_sync():
"""Wrapper to run occupancy report sync (uses dedicated occupancy enabled flag)""" """Wrapper to run occupancy report sync (uses dedicated occupancy enabled flag)"""
from datetime import date, timedelta from datetime import date, timedelta
@ -181,21 +172,6 @@ def reschedule_sync_jobs():
) )
logger.info(f" Resos bookings sync scheduled for {rsb_hour:02d}:{rsb_min:02d}") logger.info(f" Resos bookings sync scheduled for {rsb_hour:02d}:{rsb_min:02d}")
# Resos sync - uses general sync_schedule_time for now
rs_time = get_config_value("sync_schedule_time", "05:05")
try:
rs_hour, rs_min = int(rs_time.split(':')[0]), int(rs_time.split(':')[1])
except:
rs_hour, rs_min = 5, 5
scheduler.add_job(
run_scheduled_resos_sync,
CronTrigger(hour=rs_hour, minute=rs_min),
id="resos_sync",
name=f"Daily Resos Sync ({rs_hour:02d}:{rs_min:02d})",
replace_existing=True
)
logger.info(f" Resos sync scheduled for {rs_hour:02d}:{rs_min:02d}")
# Newbook occupancy report # Newbook occupancy report
occ_hour, occ_min = get_sync_time("newbook_occupancy", 5, 8) occ_hour, occ_min = get_sync_time("newbook_occupancy", 5, 8)
scheduler.add_job( scheduler.add_job(
@ -356,6 +332,17 @@ def start_scheduler():
) )
logger.info(f" AI Insights scheduled for {ai_hour:02d}:{ai_min:02d}") logger.info(f" AI Insights scheduled for {ai_hour:02d}:{ai_min:02d}")
# Weather sync - Daily at 05:15 (last 7 days, catches ERA5 corrections)
wx_hour, wx_min = get_sync_time("weather", 5, 15)
scheduler.add_job(
run_weather_sync,
CronTrigger(hour=wx_hour, minute=wx_min),
id="weather_sync",
name=f"Daily Weather Sync ({wx_hour:02d}:{wx_min:02d})",
replace_existing=True
)
logger.info(f" Weather sync scheduled for {wx_hour:02d}:{wx_min:02d}")
scheduler.start() scheduler.start()
logger.info("Scheduler started successfully") logger.info("Scheduler started successfully")

View file

@ -59,13 +59,11 @@ INSERT INTO system_config (config_key, description) VALUES
('newbook_username', 'Newbook Username'), ('newbook_username', 'Newbook Username'),
('newbook_password', 'Newbook Password'), ('newbook_password', 'Newbook Password'),
('newbook_region', 'Newbook Region Code'), ('newbook_region', 'Newbook Region Code'),
('resos_api_key', 'Resos API Key'),
('total_rooms', 'Total number of hotel rooms'), ('total_rooms', 'Total number of hotel rooms'),
('hotel_name', 'Hotel/Property Name'), ('hotel_name', 'Hotel/Property Name'),
('timezone', 'Local timezone (e.g., Europe/London)'), ('timezone', 'Local timezone (e.g., Europe/London)'),
('accommodation_vat_rate', 'VAT rate for accommodation (e.g., 0.20 for 20%)'), ('accommodation_vat_rate', 'VAT rate for accommodation (e.g., 0.20 for 20%)'),
('sync_newbook_enabled', 'Enable automatic Newbook sync (true/false)'), ('sync_newbook_enabled', 'Enable automatic Newbook sync (true/false)'),
('sync_resos_enabled', 'Enable automatic Resos sync (true/false)'),
('sync_schedule_time', 'Time for daily sync (HH:MM format)'), ('sync_schedule_time', 'Time for daily sync (HH:MM format)'),
('sync_newbook_bookings_enabled', 'Enable automatic Newbook bookings data sync (true/false)'), ('sync_newbook_bookings_enabled', 'Enable automatic Newbook bookings data sync (true/false)'),
('sync_newbook_bookings_type', 'Newbook bookings sync type (incremental/full)'), ('sync_newbook_bookings_type', 'Newbook bookings sync type (incremental/full)'),
@ -85,7 +83,6 @@ UPDATE system_config SET config_value = '80' WHERE config_key = 'total_rooms' AN
UPDATE system_config SET config_value = 'Europe/London' WHERE config_key = 'timezone' AND config_value IS NULL; UPDATE system_config SET config_value = 'Europe/London' WHERE config_key = 'timezone' AND config_value IS NULL;
UPDATE system_config SET config_value = '0.20' WHERE config_key = 'accommodation_vat_rate' AND config_value IS NULL; UPDATE system_config SET config_value = '0.20' WHERE config_key = 'accommodation_vat_rate' AND config_value IS NULL;
UPDATE system_config SET config_value = 'false' WHERE config_key = 'sync_newbook_enabled' AND config_value IS NULL; UPDATE system_config SET config_value = 'false' WHERE config_key = 'sync_newbook_enabled' AND config_value IS NULL;
UPDATE system_config SET config_value = 'false' WHERE config_key = 'sync_resos_enabled' AND config_value IS NULL;
UPDATE system_config SET config_value = '05:00' WHERE config_key = 'sync_schedule_time' AND config_value IS NULL; UPDATE system_config SET config_value = '05:00' WHERE config_key = 'sync_schedule_time' AND config_value IS NULL;
UPDATE system_config SET config_value = 'false' WHERE config_key = 'sync_newbook_bookings_enabled' AND config_value IS NULL; UPDATE system_config SET config_value = 'false' WHERE config_key = 'sync_newbook_bookings_enabled' AND config_value IS NULL;
UPDATE system_config SET config_value = 'incremental' WHERE config_key = 'sync_newbook_bookings_type' AND config_value IS NULL; UPDATE system_config SET config_value = 'incremental' WHERE config_key = 'sync_newbook_bookings_type' AND config_value IS NULL;