diff --git a/alembic/versions/2025_11_18_1441-b33fd7a2da6c_added_birth_date_storing_revenue_as_.py b/alembic/versions/2025_11_18_1441-b33fd7a2da6c_added_birth_date_storing_revenue_as_.py new file mode 100644 index 0000000..dc1d558 --- /dev/null +++ b/alembic/versions/2025_11_18_1441-b33fd7a2da6c_added_birth_date_storing_revenue_as_.py @@ -0,0 +1,66 @@ +"""Added birth_date, storing revenue as number + +Revision ID: b33fd7a2da6c +Revises: 630b0c367dcb +Create Date: 2025-11-18 14:41:17.567595 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b33fd7a2da6c' +down_revision: Union[str, Sequence[str], None] = '630b0c367dcb' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + + # Convert VARCHAR to Double with explicit CAST for PostgreSQL compatibility + # PostgreSQL requires USING clause for type conversion + connection = op.get_bind() + if connection.dialect.name == 'postgresql': + op.execute( + "ALTER TABLE conversion_rooms " + "ALTER COLUMN total_revenue TYPE DOUBLE PRECISION " + "USING total_revenue::DOUBLE PRECISION" + ) + else: + # For SQLite and other databases, use standard alter_column + op.alter_column('conversion_rooms', 'total_revenue', + existing_type=sa.VARCHAR(), + type_=sa.Double(), + existing_nullable=True) + + op.add_column('conversions', sa.Column('guest_birth_date', sa.Date(), nullable=True)) + op.add_column('conversions', sa.Column('guest_id', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('conversions', 'guest_id') + op.drop_column('conversions', 'guest_birth_date') + + # Convert Double back to VARCHAR with explicit CAST for PostgreSQL compatibility + connection = op.get_bind() + if connection.dialect.name == 'postgresql': + op.execute( + "ALTER TABLE conversion_rooms " + "ALTER COLUMN total_revenue TYPE VARCHAR " + "USING total_revenue::VARCHAR" + ) + else: + # For SQLite and other databases, use standard alter_column + op.alter_column('conversion_rooms', 'total_revenue', + existing_type=sa.Double(), + type_=sa.VARCHAR(), + existing_nullable=True) + # ### end Alembic commands ### diff --git a/config/alpinebits.log b/config/alpinebits.log index 04a5c39..9ccdb16 100644 --- a/config/alpinebits.log +++ b/config/alpinebits.log @@ -1454019,3 +1454019,107 @@ DETAIL: constraint conversion_rooms_conversion_id_fkey on table conversion_room HINT: Use DROP ... CASCADE to drop the dependent objects too. [SQL: DROP TABLE IF EXISTS conversions] (Background on this error at: https://sqlalche.me/e/20/dbapi) +2025-11-18 14:46:04 - root - INFO - Logging to file: config/alpinebits.log +2025-11-18 14:46:04 - root - INFO - Logging configured at INFO level +2025-11-18 14:46:04 - alpine_bits_python.notification_service - INFO - Registered notification backend: pushover +2025-11-18 14:46:04 - alpine_bits_python.notification_manager - INFO - Registered pushover backend with priority 0 +2025-11-18 14:46:04 - alpine_bits_python.notification_manager - INFO - Notification service configured with backends: ['pushover'] +2025-11-18 14:46:04 - alpine_bits_python.api - INFO - Application startup initiated (primary_worker=True) +2025-11-18 14:46:04 - alpine_bits_python.db - INFO - Configured database schema: alpinebits +2025-11-18 14:46:04 - alpine_bits_python.db - INFO - Setting PostgreSQL search_path to: alpinebits,public +2025-11-18 14:46:04 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_NOTIF_REPORT +2025-11-18 14:46:04 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_PING +2025-11-18 14:46:04 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS +2025-11-18 14:46:04 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_READ +2025-11-18 14:46:04 - alpine_bits_python.api - INFO - Hotel 39054_001 has no push_endpoint configured +2025-11-18 14:46:04 - alpine_bits_python.api - INFO - Hotel 135 has no push_endpoint configured +2025-11-18 14:46:04 - alpine_bits_python.api - INFO - Hotel 39052_001 has no push_endpoint configured +2025-11-18 14:46:04 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured +2025-11-18 14:46:04 - alpine_bits_python.api - INFO - Running startup tasks (primary worker)... +2025-11-18 14:46:05 - alpine_bits_python.db_setup - INFO - All existing customers already have hashed data +2025-11-18 14:46:05 - alpine_bits_python.api - INFO - Startup tasks completed +2025-11-18 14:46:05 - alpine_bits_python.api - INFO - Application startup complete +2025-11-18 14:46:37 - alpine_bits_python.api - INFO - Application shutdown initiated +2025-11-18 14:46:37 - alpine_bits_python.email_service - INFO - Shutting down email service thread pool +2025-11-18 14:46:37 - alpine_bits_python.email_service - INFO - Email service thread pool shut down complete +2025-11-18 14:46:37 - alpine_bits_python.api - INFO - Email service shut down +2025-11-18 14:46:37 - alpine_bits_python.api - INFO - Application shutdown complete +2025-11-18 14:46:37 - alpine_bits_python.worker_coordination - INFO - Released primary worker lock (pid=7588) +2025-11-18 14:46:40 - root - INFO - Logging to file: config/alpinebits.log +2025-11-18 14:46:40 - root - INFO - Logging configured at INFO level +2025-11-18 14:46:40 - alpine_bits_python.notification_service - INFO - Registered notification backend: pushover +2025-11-18 14:46:40 - alpine_bits_python.notification_manager - INFO - Registered pushover backend with priority 0 +2025-11-18 14:46:40 - alpine_bits_python.notification_manager - INFO - Notification service configured with backends: ['pushover'] +2025-11-18 14:46:40 - alpine_bits_python.api - INFO - Application startup initiated (primary_worker=True) +2025-11-18 14:46:40 - alpine_bits_python.db - INFO - Configured database schema: alpinebits +2025-11-18 14:46:40 - alpine_bits_python.db - INFO - Setting PostgreSQL search_path to: alpinebits,public +2025-11-18 14:46:40 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_NOTIF_REPORT +2025-11-18 14:46:40 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_PING +2025-11-18 14:46:40 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS +2025-11-18 14:46:40 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_READ +2025-11-18 14:46:40 - alpine_bits_python.api - INFO - Hotel 39054_001 has no push_endpoint configured +2025-11-18 14:46:40 - alpine_bits_python.api - INFO - Hotel 135 has no push_endpoint configured +2025-11-18 14:46:40 - alpine_bits_python.api - INFO - Hotel 39052_001 has no push_endpoint configured +2025-11-18 14:46:40 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured +2025-11-18 14:46:40 - alpine_bits_python.api - INFO - Running startup tasks (primary worker)... +2025-11-18 14:46:48 - alpine_bits_python.db_setup - INFO - All existing customers already have hashed data +2025-11-18 14:46:48 - alpine_bits_python.api - INFO - Startup tasks completed +2025-11-18 14:46:48 - alpine_bits_python.api - INFO - Application startup complete +2025-11-18 14:47:09 - alpine_bits_python.api - INFO - Application shutdown initiated +2025-11-18 14:47:09 - alpine_bits_python.email_service - INFO - Shutting down email service thread pool +2025-11-18 14:47:09 - alpine_bits_python.email_service - INFO - Email service thread pool shut down complete +2025-11-18 14:47:09 - alpine_bits_python.api - INFO - Email service shut down +2025-11-18 14:47:09 - alpine_bits_python.api - INFO - Application shutdown complete +2025-11-18 14:47:09 - alpine_bits_python.worker_coordination - INFO - Released primary worker lock (pid=7744) +2025-11-18 14:47:18 - root - INFO - Logging to file: config/alpinebits.log +2025-11-18 14:47:18 - root - INFO - Logging configured at INFO level +2025-11-18 14:47:18 - alpine_bits_python.notification_service - INFO - Registered notification backend: pushover +2025-11-18 14:47:18 - alpine_bits_python.notification_manager - INFO - Registered pushover backend with priority 0 +2025-11-18 14:47:18 - alpine_bits_python.notification_manager - INFO - Notification service configured with backends: ['pushover'] +2025-11-18 14:47:18 - alpine_bits_python.api - INFO - Application startup initiated (primary_worker=True) +2025-11-18 14:47:18 - alpine_bits_python.db - INFO - Configured database schema: alpinebits +2025-11-18 14:47:18 - alpine_bits_python.db - INFO - Setting PostgreSQL search_path to: alpinebits,public +2025-11-18 14:47:19 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_NOTIF_REPORT +2025-11-18 14:47:19 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_PING +2025-11-18 14:47:19 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS +2025-11-18 14:47:19 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_READ +2025-11-18 14:47:19 - alpine_bits_python.api - INFO - Hotel 39054_001 has no push_endpoint configured +2025-11-18 14:47:19 - alpine_bits_python.api - INFO - Hotel 135 has no push_endpoint configured +2025-11-18 14:47:19 - alpine_bits_python.api - INFO - Hotel 39052_001 has no push_endpoint configured +2025-11-18 14:47:19 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured +2025-11-18 14:47:19 - alpine_bits_python.api - INFO - Running startup tasks (primary worker)... +2025-11-18 14:47:26 - alpine_bits_python.db_setup - INFO - Backfilling advertising account IDs for existing reservations... +2025-11-18 14:47:26 - alpine_bits_python.db_setup - INFO - Found 4 hotel(s) with account configurations +2025-11-18 14:47:26 - alpine_bits_python.db_setup - INFO - Updated 7 reservations with meta_account_id for hotel 39054_001 +2025-11-18 14:47:26 - alpine_bits_python.db_setup - INFO - Updated 6 reservations with google_account_id for hotel 39054_001 +2025-11-18 14:47:26 - alpine_bits_python.db_setup - INFO - Backfill complete: 7 reservations updated with meta_account_id, 6 with google_account_id +2025-11-18 14:47:26 - alpine_bits_python.db_setup - INFO - Backfilling usernames for existing acked_requests... +2025-11-18 14:47:26 - alpine_bits_python.db_setup - INFO - Found 4 hotel(s) with usernames in config +2025-11-18 14:47:26 - alpine_bits_python.api - INFO - Startup tasks completed +2025-11-18 14:47:26 - alpine_bits_python.api - INFO - Application startup complete +2025-11-18 14:48:07 - alpine_bits_python.api - INFO - Application shutdown initiated +2025-11-18 14:48:07 - alpine_bits_python.email_service - INFO - Shutting down email service thread pool +2025-11-18 14:48:07 - alpine_bits_python.email_service - INFO - Email service thread pool shut down complete +2025-11-18 14:48:07 - alpine_bits_python.api - INFO - Email service shut down +2025-11-18 14:48:07 - alpine_bits_python.api - INFO - Application shutdown complete +2025-11-18 14:48:07 - alpine_bits_python.worker_coordination - INFO - Released primary worker lock (pid=7963) +2025-11-18 14:48:14 - root - INFO - Logging to file: config/alpinebits.log +2025-11-18 14:48:14 - root - INFO - Logging configured at INFO level +2025-11-18 14:48:14 - alpine_bits_python.notification_service - INFO - Registered notification backend: pushover +2025-11-18 14:48:14 - alpine_bits_python.notification_manager - INFO - Registered pushover backend with priority 0 +2025-11-18 14:48:14 - alpine_bits_python.notification_manager - INFO - Notification service configured with backends: ['pushover'] +2025-11-18 14:48:14 - alpine_bits_python.api - INFO - Application startup initiated (primary_worker=True) +2025-11-18 14:48:14 - alpine_bits_python.db - INFO - Configured database schema: alpinebits +2025-11-18 14:48:14 - alpine_bits_python.db - INFO - Setting PostgreSQL search_path to: alpinebits,public +2025-11-18 14:48:14 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_NOTIF_REPORT +2025-11-18 14:48:14 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_PING +2025-11-18 14:48:14 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS +2025-11-18 14:48:14 - alpine_bits_python.alpinebits_server - INFO - Initializing action instance for AlpineBitsActionName.OTA_READ +2025-11-18 14:48:14 - alpine_bits_python.api - INFO - Hotel 39054_001 has no push_endpoint configured +2025-11-18 14:48:14 - alpine_bits_python.api - INFO - Hotel 135 has no push_endpoint configured +2025-11-18 14:48:14 - alpine_bits_python.api - INFO - Hotel 39052_001 has no push_endpoint configured +2025-11-18 14:48:14 - alpine_bits_python.api - INFO - Hotel 39040_001 has no push_endpoint configured +2025-11-18 14:48:14 - alpine_bits_python.api - INFO - Running startup tasks (primary worker)... +2025-11-18 14:48:22 - alpine_bits_python.db_setup - WARNING - No engine provided to run_startup_tasks, skipping config-based backfill tasks +2025-11-18 14:48:22 - alpine_bits_python.api - INFO - Startup tasks completed +2025-11-18 14:48:22 - alpine_bits_python.api - INFO - Application startup complete diff --git a/config/config.yaml b/config/config.yaml index 77098f8..ac0455d 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -8,8 +8,8 @@ database: # Use annotatedyaml for secrets and environment-specific overrides logger: - level: "WARNING" # Set to DEBUG for more verbose output - file: "config/alpinebits.log" # Log file path, or null for console only + level: "INFO" # Set to DEBUG for more verbose output + file: "config/alpinebits.log" # Log file path, or null for console only server: codecontext: "ADVERTISING" @@ -23,22 +23,19 @@ alpine_bits_auth: username: "bemelman" password: !secret BEMELMANS_PASSWORD meta_account: "238334370765317" - google_account: "7581209925" # Optional: Meta advertising account ID + google_account: "7581209925" # Optional: Meta advertising account ID - - hotel_id: "135" hotel_name: "Testhotel" username: "sebastian" password: !secret BOB_PASSWORD - - hotel_id: "39052_001" hotel_name: "Jagthof Kaltern" username: "jagthof" password: !secret JAGTHOF_PASSWORD meta_account: "948363300784757" - google_account: "1951919786" # Optional: Meta advertising account ID - + google_account: "1951919786" # Optional: Meta advertising account ID - hotel_id: "39040_001" hotel_name: "Residence Erika" @@ -46,11 +43,9 @@ alpine_bits_auth: password: !secret ERIKA_PASSWORD google_account: "6604634947" - api_tokens: - tLTI8wXF1OVEvUX7kdZRhSW3Qr5feBCz0mHo-kbnEp0 - # Email configuration (SMTP service config - kept for when port is unblocked) email: # SMTP server configuration @@ -69,8 +64,8 @@ email: # Pushover configuration (push notification service config) pushover: # Pushover API credentials (get from https://pushover.net) - user_key: !secret PUSHOVER_USER_KEY # Your user/group key - api_token: !secret PUSHOVER_API_TOKEN # Your application API token + user_key: !secret PUSHOVER_USER_KEY # Your user/group key + api_token: !secret PUSHOVER_API_TOKEN # Your application API token # Unified notification system - recipient-based routing notifications: @@ -82,7 +77,7 @@ notifications: #- type: "email" # address: "jonas@vaius.ai" - type: "pushover" - priority: 0 # Pushover priority: -2=lowest, -1=low, 0=normal, 1=high, 2=emergency + priority: 0 # Pushover priority: -2=lowest, -1=low, 0=normal, 1=high, 2=emergency # Daily report configuration (applies to all recipients) daily_report: @@ -104,5 +99,3 @@ notifications: log_levels: - "ERROR" - "CRITICAL" - - diff --git a/src/alpine_bits_python/api.py b/src/alpine_bits_python/api.py index 702b832..16ede27 100644 --- a/src/alpine_bits_python/api.py +++ b/src/alpine_bits_python/api.py @@ -336,7 +336,7 @@ async def lifespan(app: FastAPI): # via run_migrations.py or `uv run alembic upgrade head` if is_primary: _LOGGER.info("Running startup tasks (primary worker)...") - await run_startup_tasks(AsyncSessionLocal, config) + await run_startup_tasks(AsyncSessionLocal, config, engine) _LOGGER.info("Startup tasks completed") else: _LOGGER.info("Skipping startup tasks (non-primary worker)") diff --git a/src/alpine_bits_python/conversion_service.py b/src/alpine_bits_python/conversion_service.py index 6058e86..8dc944c 100644 --- a/src/alpine_bits_python/conversion_service.py +++ b/src/alpine_bits_python/conversion_service.py @@ -658,7 +658,7 @@ class ConversionService: rate_plan_code=rate_plan_code, connected_room_type=connected_room_type, daily_sales=daily_sales_list if daily_sales_list else None, - total_revenue=str(total_revenue) if total_revenue > 0 else None, + total_revenue=total_revenue if total_revenue > 0 else None, created_at=datetime.now(), updated_at=datetime.now(), ) diff --git a/src/alpine_bits_python/db.py b/src/alpine_bits_python/db.py index 4348e52..bbfcef7 100644 --- a/src/alpine_bits_python/db.py +++ b/src/alpine_bits_python/db.py @@ -10,6 +10,7 @@ from sqlalchemy import ( Column, Date, DateTime, + Double, ForeignKey, Integer, String, @@ -459,6 +460,8 @@ class Conversion(Base): guest_last_name = Column(String, index=True) # lastName from guest element guest_email = Column(String, index=True) # email from guest element guest_country_code = Column(String) # countryCode from guest element + guest_birth_date = Column(Date) # birthDate from guest element + guest_id = Column(String) # id from guest element # Advertising/tracking data - used for matching to existing reservations advertising_medium = Column( @@ -527,7 +530,7 @@ class ConversionRoom(Base): # Extracted total revenue for efficient querying (sum of all revenue_total in daily_sales) # Kept as string to preserve decimal precision - total_revenue = Column(String, nullable=True) + total_revenue = Column(Double, nullable=True) # Metadata created_at = Column(DateTime(timezone=True)) # When this record was imported diff --git a/src/alpine_bits_python/db_setup.py b/src/alpine_bits_python/db_setup.py index 2f6b076..08e0a80 100644 --- a/src/alpine_bits_python/db_setup.py +++ b/src/alpine_bits_python/db_setup.py @@ -9,8 +9,10 @@ before the application starts accepting requests. It includes: import asyncio from typing import Any +from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker +from .const import CONF_GOOGLE_ACCOUNT, CONF_HOTEL_ID, CONF_META_ACCOUNT from .customer_service import CustomerService from .db import create_database_engine from .logging_config import get_logger @@ -62,8 +64,182 @@ async def setup_database(config: dict[str, Any] | None = None) -> tuple[AsyncEng raise +async def backfill_advertising_account_ids( + engine: AsyncEngine, config: dict[str, Any] +) -> None: + """Backfill advertising account IDs for existing reservations. + + Updates existing reservations to populate meta_account_id and google_account_id + based on the conditional logic: + - If fbclid is present, set meta_account_id from hotel config + - If gclid is present, set google_account_id from hotel config + + This is a startup task that runs after schema migrations to ensure + existing data is consistent with config. + + Args: + engine: SQLAlchemy async engine + config: Application configuration dict + """ + _LOGGER.info("Backfilling advertising account IDs for existing reservations...") + + # Build a mapping of hotel_id -> account IDs from config + hotel_accounts = {} + alpine_bits_auth = config.get("alpine_bits_auth", []) + + for hotel in alpine_bits_auth: + hotel_id = hotel.get(CONF_HOTEL_ID) + meta_account = hotel.get(CONF_META_ACCOUNT) + google_account = hotel.get(CONF_GOOGLE_ACCOUNT) + + if hotel_id: + hotel_accounts[hotel_id] = { + "meta_account": meta_account, + "google_account": google_account, + } + + if not hotel_accounts: + _LOGGER.debug("No hotel accounts found in config, skipping backfill") + return + + _LOGGER.info("Found %d hotel(s) with account configurations", len(hotel_accounts)) + + # Update reservations with meta_account_id where fbclid is present + meta_updated = 0 + for hotel_id, accounts in hotel_accounts.items(): + if accounts["meta_account"]: + async with engine.begin() as conn: + sql = text( + "UPDATE reservations " + "SET meta_account_id = :meta_account " + "WHERE hotel_code = :hotel_id " + "AND fbclid IS NOT NULL " + "AND fbclid != '' " + "AND (meta_account_id IS NULL OR meta_account_id = '')" + ) + result = await conn.execute( + sql, + {"meta_account": accounts["meta_account"], "hotel_id": hotel_id}, + ) + count = result.rowcount + if count > 0: + _LOGGER.info( + "Updated %d reservations with meta_account_id for hotel %s", + count, + hotel_id, + ) + meta_updated += count + + # Update reservations with google_account_id where gclid is present + google_updated = 0 + for hotel_id, accounts in hotel_accounts.items(): + if accounts["google_account"]: + async with engine.begin() as conn: + sql = text( + "UPDATE reservations " + "SET google_account_id = :google_account " + "WHERE hotel_code = :hotel_id " + "AND gclid IS NOT NULL " + "AND gclid != '' " + "AND (google_account_id IS NULL OR google_account_id = '')" + ) + result = await conn.execute( + sql, + { + "google_account": accounts["google_account"], + "hotel_id": hotel_id, + }, + ) + count = result.rowcount + if count > 0: + _LOGGER.info( + "Updated %d reservations with google_account_id for hotel %s", + count, + hotel_id, + ) + google_updated += count + + if meta_updated > 0 or google_updated > 0: + _LOGGER.info( + "Backfill complete: %d reservations updated with meta_account_id, " + "%d with google_account_id", + meta_updated, + google_updated, + ) + + +async def backfill_acked_requests_username( + engine: AsyncEngine, config: dict[str, Any] +) -> None: + """Backfill username for existing acked_requests records. + + For each acknowledgement, find the corresponding reservation to determine + its hotel_code, then look up the username for that hotel in the config + and update the acked_request record. + + This is a startup task that runs after schema migrations to ensure + existing data is consistent with config. + + Args: + engine: SQLAlchemy async engine + config: Application configuration dict + """ + _LOGGER.info("Backfilling usernames for existing acked_requests...") + + # Build a mapping of hotel_id -> username from config + hotel_usernames = {} + alpine_bits_auth = config.get("alpine_bits_auth", []) + + for hotel in alpine_bits_auth: + hotel_id = hotel.get(CONF_HOTEL_ID) + username = hotel.get("username") + + if hotel_id and username: + hotel_usernames[hotel_id] = username + + if not hotel_usernames: + _LOGGER.debug("No hotel usernames found in config, skipping backfill") + return + + _LOGGER.info("Found %d hotel(s) with usernames in config", len(hotel_usernames)) + + # Update acked_requests with usernames by matching to reservations + total_updated = 0 + async with engine.begin() as conn: + for hotel_id, username in hotel_usernames.items(): + sql = text( + """ + UPDATE acked_requests + SET username = :username + WHERE unique_id IN ( + SELECT md5_unique_id FROM reservations WHERE hotel_code = :hotel_id + ) + AND username IS NULL + """ + ) + result = await conn.execute( + sql, {"username": username, "hotel_id": hotel_id} + ) + count = result.rowcount + if count > 0: + _LOGGER.info( + "Updated %d acknowledgements with username for hotel %s", + count, + hotel_id, + ) + total_updated += count + + if total_updated > 0: + _LOGGER.info( + "Backfill complete: %d acknowledgements updated with username", + total_updated, + ) + + async def run_startup_tasks( - sessionmaker: async_sessionmaker, config: dict[str, Any] | None = None + sessionmaker: async_sessionmaker, + config: dict[str, Any] | None = None, + engine: AsyncEngine | None = None, ) -> None: """Run one-time startup tasks. @@ -73,6 +249,7 @@ async def run_startup_tasks( Args: sessionmaker: SQLAlchemy async sessionmaker config: Application configuration dictionary + engine: SQLAlchemy async engine (optional, for backfill tasks) """ # Hash any existing customers that don't have hashed data async with sessionmaker() as session: @@ -83,4 +260,15 @@ async def run_startup_tasks( "Backfilled hashed data for %d existing customers", hashed_count ) else: - _LOGGER.info("All existing customers already have hashed data") + _LOGGER.debug("All existing customers already have hashed data") + + # Backfill advertising account IDs and usernames based on config + # This ensures existing data is consistent with current configuration + if config and engine: + await backfill_advertising_account_ids(engine, config) + await backfill_acked_requests_username(engine, config) + elif config and not engine: + _LOGGER.warning( + "No engine provided to run_startup_tasks, " + "skipping config-based backfill tasks" + ) diff --git a/src/alpine_bits_python/migrations.py b/src/alpine_bits_python/migrations.py index 48f2405..8d3f0c7 100644 --- a/src/alpine_bits_python/migrations.py +++ b/src/alpine_bits_python/migrations.py @@ -1,7 +1,24 @@ -"""Database migrations for AlpineBits. +"""DEPRECATED: Legacy database migrations for AlpineBits. -This module contains migration functions that are automatically run at app startup -to update existing database schemas without losing data. +⚠️ WARNING: This module is deprecated and no longer used. ⚠️ + +SCHEMA MIGRATIONS are now handled by Alembic (see alembic/versions/). +STARTUP TASKS (data backfills) are now in db_setup.py. + +Migration History: +- migrate_add_room_types: Schema migration (should be in Alembic) +- migrate_add_advertising_account_ids: Schema + backfill (split into Alembic + db_setup.py) +- migrate_add_username_to_acked_requests: Schema + backfill (split into Alembic + db_setup.py) +- migrate_normalize_conversions: Schema migration (should be in Alembic) + +Current Status: +- All schema changes are now managed via Alembic migrations +- All data backfills are now in db_setup.py as startup tasks +- This file is kept for reference but is no longer executed + +Do not add new migrations here. Instead: +1. For schema changes: Create Alembic migration with `uv run alembic revision --autogenerate -m "description"` +2. For data backfills: Add to db_setup.py as a startup task """ from typing import Any