Looking good
This commit is contained in:
@@ -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 ###
|
||||||
@@ -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.
|
HINT: Use DROP ... CASCADE to drop the dependent objects too.
|
||||||
[SQL: DROP TABLE IF EXISTS conversions]
|
[SQL: DROP TABLE IF EXISTS conversions]
|
||||||
(Background on this error at: https://sqlalche.me/e/20/dbapi)
|
(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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ database:
|
|||||||
# Use annotatedyaml for secrets and environment-specific overrides
|
# Use annotatedyaml for secrets and environment-specific overrides
|
||||||
|
|
||||||
logger:
|
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
|
file: "config/alpinebits.log" # Log file path, or null for console only
|
||||||
|
|
||||||
server:
|
server:
|
||||||
@@ -25,13 +25,11 @@ alpine_bits_auth:
|
|||||||
meta_account: "238334370765317"
|
meta_account: "238334370765317"
|
||||||
google_account: "7581209925" # Optional: Meta advertising account ID
|
google_account: "7581209925" # Optional: Meta advertising account ID
|
||||||
|
|
||||||
|
|
||||||
- hotel_id: "135"
|
- hotel_id: "135"
|
||||||
hotel_name: "Testhotel"
|
hotel_name: "Testhotel"
|
||||||
username: "sebastian"
|
username: "sebastian"
|
||||||
password: !secret BOB_PASSWORD
|
password: !secret BOB_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
- hotel_id: "39052_001"
|
- hotel_id: "39052_001"
|
||||||
hotel_name: "Jagthof Kaltern"
|
hotel_name: "Jagthof Kaltern"
|
||||||
username: "jagthof"
|
username: "jagthof"
|
||||||
@@ -39,18 +37,15 @@ alpine_bits_auth:
|
|||||||
meta_account: "948363300784757"
|
meta_account: "948363300784757"
|
||||||
google_account: "1951919786" # Optional: Meta advertising account ID
|
google_account: "1951919786" # Optional: Meta advertising account ID
|
||||||
|
|
||||||
|
|
||||||
- hotel_id: "39040_001"
|
- hotel_id: "39040_001"
|
||||||
hotel_name: "Residence Erika"
|
hotel_name: "Residence Erika"
|
||||||
username: "erika"
|
username: "erika"
|
||||||
password: !secret ERIKA_PASSWORD
|
password: !secret ERIKA_PASSWORD
|
||||||
google_account: "6604634947"
|
google_account: "6604634947"
|
||||||
|
|
||||||
|
|
||||||
api_tokens:
|
api_tokens:
|
||||||
- tLTI8wXF1OVEvUX7kdZRhSW3Qr5feBCz0mHo-kbnEp0
|
- tLTI8wXF1OVEvUX7kdZRhSW3Qr5feBCz0mHo-kbnEp0
|
||||||
|
|
||||||
|
|
||||||
# Email configuration (SMTP service config - kept for when port is unblocked)
|
# Email configuration (SMTP service config - kept for when port is unblocked)
|
||||||
email:
|
email:
|
||||||
# SMTP server configuration
|
# SMTP server configuration
|
||||||
@@ -104,5 +99,3 @@ notifications:
|
|||||||
log_levels:
|
log_levels:
|
||||||
- "ERROR"
|
- "ERROR"
|
||||||
- "CRITICAL"
|
- "CRITICAL"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ async def lifespan(app: FastAPI):
|
|||||||
# via run_migrations.py or `uv run alembic upgrade head`
|
# via run_migrations.py or `uv run alembic upgrade head`
|
||||||
if is_primary:
|
if is_primary:
|
||||||
_LOGGER.info("Running startup tasks (primary worker)...")
|
_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")
|
_LOGGER.info("Startup tasks completed")
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("Skipping startup tasks (non-primary worker)")
|
_LOGGER.info("Skipping startup tasks (non-primary worker)")
|
||||||
|
|||||||
@@ -658,7 +658,7 @@ class ConversionService:
|
|||||||
rate_plan_code=rate_plan_code,
|
rate_plan_code=rate_plan_code,
|
||||||
connected_room_type=connected_room_type,
|
connected_room_type=connected_room_type,
|
||||||
daily_sales=daily_sales_list if daily_sales_list else None,
|
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(),
|
created_at=datetime.now(),
|
||||||
updated_at=datetime.now(),
|
updated_at=datetime.now(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from sqlalchemy import (
|
|||||||
Column,
|
Column,
|
||||||
Date,
|
Date,
|
||||||
DateTime,
|
DateTime,
|
||||||
|
Double,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
Integer,
|
Integer,
|
||||||
String,
|
String,
|
||||||
@@ -459,6 +460,8 @@ class Conversion(Base):
|
|||||||
guest_last_name = Column(String, index=True) # lastName from guest element
|
guest_last_name = Column(String, index=True) # lastName from guest element
|
||||||
guest_email = Column(String, index=True) # email from guest element
|
guest_email = Column(String, index=True) # email from guest element
|
||||||
guest_country_code = Column(String) # countryCode 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/tracking data - used for matching to existing reservations
|
||||||
advertising_medium = Column(
|
advertising_medium = Column(
|
||||||
@@ -527,7 +530,7 @@ class ConversionRoom(Base):
|
|||||||
|
|
||||||
# Extracted total revenue for efficient querying (sum of all revenue_total in daily_sales)
|
# Extracted total revenue for efficient querying (sum of all revenue_total in daily_sales)
|
||||||
# Kept as string to preserve decimal precision
|
# Kept as string to preserve decimal precision
|
||||||
total_revenue = Column(String, nullable=True)
|
total_revenue = Column(Double, nullable=True)
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
created_at = Column(DateTime(timezone=True)) # When this record was imported
|
created_at = Column(DateTime(timezone=True)) # When this record was imported
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ before the application starts accepting requests. It includes:
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker
|
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 .customer_service import CustomerService
|
||||||
from .db import create_database_engine
|
from .db import create_database_engine
|
||||||
from .logging_config import get_logger
|
from .logging_config import get_logger
|
||||||
@@ -62,8 +64,182 @@ async def setup_database(config: dict[str, Any] | None = None) -> tuple[AsyncEng
|
|||||||
raise
|
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(
|
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:
|
) -> None:
|
||||||
"""Run one-time startup tasks.
|
"""Run one-time startup tasks.
|
||||||
|
|
||||||
@@ -73,6 +249,7 @@ async def run_startup_tasks(
|
|||||||
Args:
|
Args:
|
||||||
sessionmaker: SQLAlchemy async sessionmaker
|
sessionmaker: SQLAlchemy async sessionmaker
|
||||||
config: Application configuration dictionary
|
config: Application configuration dictionary
|
||||||
|
engine: SQLAlchemy async engine (optional, for backfill tasks)
|
||||||
"""
|
"""
|
||||||
# Hash any existing customers that don't have hashed data
|
# Hash any existing customers that don't have hashed data
|
||||||
async with sessionmaker() as session:
|
async with sessionmaker() as session:
|
||||||
@@ -83,4 +260,15 @@ async def run_startup_tasks(
|
|||||||
"Backfilled hashed data for %d existing customers", hashed_count
|
"Backfilled hashed data for %d existing customers", hashed_count
|
||||||
)
|
)
|
||||||
else:
|
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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
⚠️ WARNING: This module is deprecated and no longer used. ⚠️
|
||||||
to update existing database schemas without losing data.
|
|
||||||
|
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
|
from typing import Any
|
||||||
|
|||||||
Reference in New Issue
Block a user