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 logging
@ -9,7 +9,6 @@ from typing import Optional, Dict, Set
from sqlalchemy import text
from database import SyncSessionLocal
from services.newbook_client import NewbookClient
from services.resos_client import ResosClient
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:
"""
Load GL code configuration for identifying breakfast/dinner items.
@ -167,7 +159,7 @@ async def run_data_sync(
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:
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)
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")
except Exception as e:
logger.error(f"Data sync failed: {e}")
@ -546,236 +533,6 @@ async def sync_newbook_data(
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(
from_date: date,
to_date: date,

View file

@ -8,7 +8,6 @@ from sqlalchemy import text
from jobs.data_sync import (
sync_newbook_data,
sync_resos_data,
sync_newbook_occupancy_report,
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.scrape_booking_rates import run_scheduled_booking_scrape_async
from jobs.ai_insights import run_ai_insights_generation
from jobs.weather_sync import run_weather_sync
from database import SyncSessionLocal
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)")
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():
"""Wrapper to run occupancy report sync (uses dedicated occupancy enabled flag)"""
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}")
# 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
occ_hour, occ_min = get_sync_time("newbook_occupancy", 5, 8)
scheduler.add_job(
@ -356,6 +332,17 @@ def start_scheduler():
)
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()
logger.info("Scheduler started successfully")

View file

@ -59,13 +59,11 @@ INSERT INTO system_config (config_key, description) VALUES
('newbook_username', 'Newbook Username'),
('newbook_password', 'Newbook Password'),
('newbook_region', 'Newbook Region Code'),
('resos_api_key', 'Resos API Key'),
('total_rooms', 'Total number of hotel rooms'),
('hotel_name', 'Hotel/Property Name'),
('timezone', 'Local timezone (e.g., Europe/London)'),
('accommodation_vat_rate', 'VAT rate for accommodation (e.g., 0.20 for 20%)'),
('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_newbook_bookings_enabled', 'Enable automatic Newbook bookings data sync (true/false)'),
('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 = '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_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 = '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;