Migrations used try/except around ADD COLUMN inside a single engine.begin() block. When a 'column already exists' error was caught, asyncpg left the transaction in aborted state, causing all subsequent DDL in the block to fail with InFailedSQLTransactionError. Replace with IF NOT EXISTS to prevent the error entirely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
218 lines
9.1 KiB
Python
218 lines
9.1 KiB
Python
"""
|
|
Migration script to add Resos restaurant booking integration:
|
|
- Resos credential and configuration fields on kitchen_settings
|
|
- resos_bookings table
|
|
- resos_daily_stats table
|
|
- resos_opening_hours table
|
|
- resos_sync_log table
|
|
|
|
Run this script once after deploying the new code.
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
from sqlalchemy import text
|
|
from database import engine
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def run_migration():
|
|
"""Add Resos integration tables and settings columns."""
|
|
logger.info("Running Resos integration migrations...")
|
|
|
|
# Add Resos columns to kitchen_settings table
|
|
settings_columns = [
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_api_key VARCHAR(500)",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_last_sync TIMESTAMP",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_auto_sync_enabled BOOLEAN DEFAULT FALSE",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_large_group_threshold INTEGER DEFAULT 8",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_note_keywords TEXT",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_allergy_keywords TEXT",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_custom_field_mapping JSONB",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_opening_hours_mapping JSONB",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_restaurant_table_entities TEXT",
|
|
"ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS resos_flag_icon_mapping JSONB",
|
|
]
|
|
|
|
for sql in settings_columns:
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text(sql))
|
|
col_name = sql.split("ADD COLUMN")[1].split()[0]
|
|
logger.info(f"Added column: {col_name}")
|
|
except Exception as e:
|
|
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
|
|
logger.info(f"Column already exists, skipping")
|
|
else:
|
|
logger.warning(f"Column migration warning: {e}")
|
|
|
|
# Create resos_bookings table
|
|
create_resos_bookings = """
|
|
CREATE TABLE IF NOT EXISTS resos_bookings (
|
|
id SERIAL PRIMARY KEY,
|
|
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
|
|
resos_booking_id VARCHAR(255) NOT NULL,
|
|
booking_date DATE NOT NULL,
|
|
booking_time TIME NOT NULL,
|
|
people INTEGER NOT NULL,
|
|
status VARCHAR(50) NOT NULL,
|
|
seating_area VARCHAR(255),
|
|
hotel_booking_number VARCHAR(100),
|
|
is_hotel_guest BOOLEAN,
|
|
is_dbb BOOLEAN,
|
|
is_package BOOLEAN,
|
|
exclude_flag VARCHAR(500),
|
|
allergies TEXT,
|
|
notes TEXT,
|
|
booked_at TIMESTAMP,
|
|
opening_hour_id VARCHAR(255),
|
|
opening_hour_name VARCHAR(255),
|
|
is_flagged BOOLEAN DEFAULT FALSE,
|
|
flag_reasons TEXT,
|
|
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
is_forecast BOOLEAN DEFAULT FALSE,
|
|
CONSTRAINT uq_resos_booking UNIQUE(kitchen_id, resos_booking_id)
|
|
)
|
|
"""
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text(create_resos_bookings))
|
|
logger.info("Created resos_bookings table")
|
|
except Exception as e:
|
|
logger.warning(f"resos_bookings table: {e}")
|
|
|
|
# Create resos_daily_stats table
|
|
create_resos_daily_stats = """
|
|
CREATE TABLE IF NOT EXISTS resos_daily_stats (
|
|
id SERIAL PRIMARY KEY,
|
|
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
|
|
date DATE NOT NULL,
|
|
total_bookings INTEGER NOT NULL DEFAULT 0,
|
|
total_covers INTEGER NOT NULL DEFAULT 0,
|
|
service_breakdown JSONB,
|
|
flagged_booking_count INTEGER DEFAULT 0,
|
|
unique_flag_types JSONB,
|
|
bookings_summary JSONB,
|
|
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
is_forecast BOOLEAN DEFAULT FALSE,
|
|
CONSTRAINT uq_resos_daily_stat UNIQUE(kitchen_id, date)
|
|
)
|
|
"""
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text(create_resos_daily_stats))
|
|
logger.info("Created resos_daily_stats table")
|
|
except Exception as e:
|
|
logger.warning(f"resos_daily_stats table: {e}")
|
|
|
|
# Create resos_opening_hours table
|
|
create_resos_opening_hours = """
|
|
CREATE TABLE IF NOT EXISTS resos_opening_hours (
|
|
id SERIAL PRIMARY KEY,
|
|
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
|
|
resos_opening_hour_id VARCHAR(255) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
start_time TIME,
|
|
end_time TIME,
|
|
days_of_week VARCHAR(100),
|
|
is_special BOOLEAN DEFAULT FALSE,
|
|
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT uq_resos_opening_hour UNIQUE(kitchen_id, resos_opening_hour_id)
|
|
)
|
|
"""
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text(create_resos_opening_hours))
|
|
logger.info("Created resos_opening_hours table")
|
|
except Exception as e:
|
|
logger.warning(f"resos_opening_hours table: {e}")
|
|
|
|
# Create resos_sync_log table
|
|
create_resos_sync_log = """
|
|
CREATE TABLE IF NOT EXISTS resos_sync_log (
|
|
id SERIAL PRIMARY KEY,
|
|
kitchen_id INTEGER NOT NULL REFERENCES kitchens(id),
|
|
sync_type VARCHAR(50) NOT NULL,
|
|
status VARCHAR(20) NOT NULL,
|
|
date_from DATE,
|
|
date_to DATE,
|
|
bookings_fetched INTEGER DEFAULT 0,
|
|
bookings_flagged INTEGER DEFAULT 0,
|
|
error_message TEXT,
|
|
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
completed_at TIMESTAMP
|
|
)
|
|
"""
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text(create_resos_sync_log))
|
|
logger.info("Created resos_sync_log table")
|
|
except Exception as e:
|
|
logger.warning(f"resos_sync_log table: {e}")
|
|
|
|
# Add unique_flag_types column to resos_daily_stats if it doesn't exist
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("ALTER TABLE resos_daily_stats ADD COLUMN IF NOT EXISTS unique_flag_types JSONB"))
|
|
logger.info("Added unique_flag_types column to resos_daily_stats")
|
|
except Exception as e:
|
|
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
|
|
logger.info("unique_flag_types column already exists")
|
|
else:
|
|
logger.warning(f"unique_flag_types column migration: {e}")
|
|
|
|
# Add table_name column to resos_bookings (Phase 8.1)
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("ALTER TABLE resos_bookings ADD COLUMN IF NOT EXISTS table_name VARCHAR(100)"))
|
|
logger.info("Added table_name column to resos_bookings")
|
|
except Exception as e:
|
|
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
|
|
logger.info("table_name column already exists")
|
|
else:
|
|
logger.warning(f"table_name column migration: {e}")
|
|
|
|
# Add GL code columns for food/beverage split (Phase 8.1)
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS sambapos_food_gl_codes TEXT"))
|
|
logger.info("Added sambapos_food_gl_codes column to kitchen_settings")
|
|
except Exception as e:
|
|
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
|
|
logger.info("sambapos_food_gl_codes column already exists")
|
|
else:
|
|
logger.warning(f"sambapos_food_gl_codes column migration: {e}")
|
|
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text("ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS sambapos_beverage_gl_codes TEXT"))
|
|
logger.info("Added sambapos_beverage_gl_codes column to kitchen_settings")
|
|
except Exception as e:
|
|
if "already exists" in str(e).lower() or "duplicate column" in str(e).lower():
|
|
logger.info("sambapos_beverage_gl_codes column already exists")
|
|
else:
|
|
logger.warning(f"sambapos_beverage_gl_codes column migration: {e}")
|
|
|
|
# Create indexes
|
|
indexes = [
|
|
"CREATE INDEX IF NOT EXISTS ix_resos_bookings_date ON resos_bookings(kitchen_id, booking_date)",
|
|
"CREATE INDEX IF NOT EXISTS ix_resos_bookings_flags ON resos_bookings(kitchen_id, is_flagged)",
|
|
"CREATE INDEX IF NOT EXISTS ix_resos_daily_stats_date ON resos_daily_stats(kitchen_id, date)",
|
|
"CREATE INDEX IF NOT EXISTS ix_resos_daily_stats_kitchen_id ON resos_daily_stats(kitchen_id)",
|
|
"CREATE INDEX IF NOT EXISTS ix_resos_opening_hours_kitchen_id ON resos_opening_hours(kitchen_id)",
|
|
"CREATE INDEX IF NOT EXISTS ix_resos_sync_log_kitchen_id ON resos_sync_log(kitchen_id, started_at DESC)",
|
|
]
|
|
|
|
for sql in indexes:
|
|
try:
|
|
async with engine.begin() as conn:
|
|
await conn.execute(text(sql))
|
|
except Exception as e:
|
|
logger.warning(f"Index: {e}")
|
|
|
|
logger.info("Resos migration completed successfully!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(run_migration())
|