Looking good

This commit is contained in:
Jonas Linter
2025-11-18 14:49:44 +01:00
parent db0b0afd33
commit ccdc66fb9b
8 changed files with 393 additions and 22 deletions

View File

@@ -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 ###

View File

@@ -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

View File

@@ -8,7 +8,7 @@ database:
# Use annotatedyaml for secrets and environment-specific overrides
logger:
level: "WARNING" # Set to DEBUG for more verbose output
level: "INFO" # Set to DEBUG for more verbose output
file: "config/alpinebits.log" # Log file path, or null for console only
server:
@@ -25,13 +25,11 @@ alpine_bits_auth:
meta_account: "238334370765317"
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"
@@ -39,18 +37,15 @@ alpine_bits_auth:
meta_account: "948363300784757"
google_account: "1951919786" # Optional: Meta advertising account ID
- hotel_id: "39040_001"
hotel_name: "Residence Erika"
username: "erika"
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
@@ -104,5 +99,3 @@ notifications:
log_levels:
- "ERROR"
- "CRITICAL"

View File

@@ -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)")

View File

@@ -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(),
)

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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