New system for acknowledgments
This commit is contained in:
@@ -512,11 +512,13 @@ class ReadAction(AlpineBitsAction):
|
||||
start_date=start_date, hotel_code=hotelid
|
||||
)
|
||||
)
|
||||
elif client_info.client_id:
|
||||
# Remove reservations that have been acknowledged via client_id
|
||||
elif client_info.username or client_info.client_id:
|
||||
# Remove reservations that have been acknowledged via username (preferred) or client_id
|
||||
reservation_customer_pairs = (
|
||||
await reservation_service.get_unacknowledged_reservations(
|
||||
client_id=client_info.client_id, hotel_code=hotelid
|
||||
username=client_info.username,
|
||||
client_id=client_info.client_id,
|
||||
hotel_code=hotelid
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -611,9 +613,9 @@ class NotifReportReadAction(AlpineBitsAction):
|
||||
for entry in (
|
||||
notif_report_details.hotel_notif_report.hotel_reservations.hotel_reservation
|
||||
): # type: ignore
|
||||
unique_id = entry.unique_id.id
|
||||
md5_unique_id = entry.unique_id.id
|
||||
await reservation_service.record_acknowledgement(
|
||||
client_id=client_info.client_id, unique_id=unique_id
|
||||
client_id=client_info.client_id, unique_id=md5_unique_id, username=client_info.username
|
||||
)
|
||||
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
|
||||
@@ -329,6 +329,7 @@ class AckedRequest(Base):
|
||||
__tablename__ = "acked_requests"
|
||||
id = Column(Integer, primary_key=True)
|
||||
client_id = Column(String, index=True)
|
||||
username = Column(String, index=True, nullable=True) # Username of the client making the request
|
||||
unique_id = Column(
|
||||
String, index=True
|
||||
) # Should match Reservation.form_id or another unique field
|
||||
|
||||
@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
from .const import CONF_GOOGLE_ACCOUNT, CONF_HOTEL_ID, CONF_META_ACCOUNT
|
||||
from .logging_config import get_logger
|
||||
from .db import Reservation
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
@@ -223,6 +224,90 @@ async def _backfill_advertising_account_ids(engine: AsyncEngine, config: dict[st
|
||||
)
|
||||
|
||||
|
||||
async def migrate_add_username_to_acked_requests(engine: AsyncEngine, config: dict[str, Any] | None = None) -> None:
|
||||
"""Migration: Add username column to acked_requests table and backfill with hotel usernames.
|
||||
|
||||
This migration adds a username column to acked_requests to track acknowledgements by username
|
||||
instead of just client_id. This improves consistency since client_ids can change but usernames are stable.
|
||||
|
||||
For existing acknowledgements, this migration queries reservations to determine the hotel_code,
|
||||
then looks up the corresponding username from the config and populates the new column.
|
||||
|
||||
Safe to run multiple times - will skip if column already exists.
|
||||
|
||||
Args:
|
||||
engine: SQLAlchemy async engine
|
||||
config: Application configuration dict containing hotel usernames
|
||||
"""
|
||||
_LOGGER.info("Running migration: add_username_to_acked_requests")
|
||||
|
||||
# Add the username column if it doesn't exist
|
||||
if await add_column_if_not_exists(engine, "acked_requests", "username", "VARCHAR"):
|
||||
_LOGGER.info("Added username column to acked_requests table")
|
||||
else:
|
||||
_LOGGER.info("Username column already exists in acked_requests, skipping")
|
||||
return
|
||||
|
||||
# Backfill existing acknowledgements with username from config
|
||||
if config:
|
||||
await _backfill_acked_requests_username(engine, config)
|
||||
else:
|
||||
_LOGGER.warning("No config provided, skipping backfill of acked_requests usernames")
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.info("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 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
|
||||
|
||||
_LOGGER.info("Backfill complete: %d acknowledgements updated with username", total_updated)
|
||||
|
||||
|
||||
async def run_all_migrations(engine: AsyncEngine, config: dict[str, Any] | None = None) -> None:
|
||||
"""Run all pending migrations.
|
||||
|
||||
@@ -239,6 +324,7 @@ async def run_all_migrations(engine: AsyncEngine, config: dict[str, Any] | None
|
||||
# Add new migrations here in chronological order
|
||||
await migrate_add_room_types(engine)
|
||||
await migrate_add_advertising_account_ids(engine, config)
|
||||
await migrate_add_username_to_acked_requests(engine, config)
|
||||
|
||||
_LOGGER.info("Database migrations completed successfully")
|
||||
|
||||
|
||||
@@ -172,28 +172,39 @@ class ReservationService:
|
||||
|
||||
async def get_unacknowledged_reservations(
|
||||
self,
|
||||
client_id: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
hotel_code: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
client_id: Optional[str] = None,
|
||||
) -> list[tuple[Reservation, Customer]]:
|
||||
"""Get reservations that haven't been acknowledged by a client.
|
||||
|
||||
Prioritizes checking by username if provided, falls back to client_id for backward compatibility.
|
||||
|
||||
Args:
|
||||
client_id: The client ID to check acknowledgements for
|
||||
start_date: Filter by start date >= this value
|
||||
end_date: Filter by end date <= this value
|
||||
hotel_code: Filter by hotel code
|
||||
username: The username of the client (preferred for lookup)
|
||||
client_id: The client ID (fallback for backward compatibility)
|
||||
|
||||
Returns:
|
||||
List of (Reservation, Customer) tuples that are unacknowledged
|
||||
"""
|
||||
# Get all acknowledged md5_unique_ids for this client
|
||||
acked_result = await self.session.execute(
|
||||
select(AckedRequest.unique_id).where(
|
||||
AckedRequest.client_id == client_id
|
||||
# Get all acknowledged unique_ids for this client/username
|
||||
if username:
|
||||
acked_result = await self.session.execute(
|
||||
select(AckedRequest.unique_id).where(
|
||||
AckedRequest.username == username
|
||||
)
|
||||
)
|
||||
else:
|
||||
acked_result = await self.session.execute(
|
||||
select(AckedRequest.unique_id).where(
|
||||
AckedRequest.client_id == client_id
|
||||
)
|
||||
)
|
||||
)
|
||||
acked_md5_ids = {row[0] for row in acked_result.all()}
|
||||
|
||||
# Get all reservations with filters
|
||||
@@ -209,19 +220,21 @@ class ReservationService:
|
||||
]
|
||||
|
||||
async def record_acknowledgement(
|
||||
self, client_id: str, unique_id: str
|
||||
self, client_id: str, unique_id: str, username: Optional[str] = None
|
||||
) -> AckedRequest:
|
||||
"""Record that a client has acknowledged a reservation.
|
||||
|
||||
Args:
|
||||
client_id: The client ID
|
||||
unique_id: The unique_id of the reservation
|
||||
unique_id: The unique_id of the reservation (md5_unique_id)
|
||||
username: The username of the client making the request (optional)
|
||||
|
||||
Returns:
|
||||
Created AckedRequest instance
|
||||
"""
|
||||
acked = AckedRequest(
|
||||
client_id=client_id,
|
||||
username=username,
|
||||
unique_id=unique_id,
|
||||
timestamp=datetime.now(UTC),
|
||||
)
|
||||
@@ -230,24 +243,37 @@ class ReservationService:
|
||||
await self.session.refresh(acked)
|
||||
return acked
|
||||
|
||||
async def is_acknowledged(self, client_id: str, unique_id: str) -> bool:
|
||||
async def is_acknowledged(self, unique_id: str, username: Optional[str] = None, client_id: Optional[str] = None) -> bool:
|
||||
"""Check if a reservation has been acknowledged by a client.
|
||||
|
||||
Prioritizes checking by username if provided, falls back to client_id for backward compatibility.
|
||||
|
||||
Args:
|
||||
client_id: The client ID
|
||||
unique_id: The reservation unique_id
|
||||
username: The username of the client (preferred for lookup)
|
||||
client_id: The client ID (fallback for backward compatibility)
|
||||
|
||||
Returns:
|
||||
True if acknowledged, False otherwise
|
||||
"""
|
||||
result = await self.session.execute(
|
||||
select(AckedRequest).where(
|
||||
and_(
|
||||
AckedRequest.client_id == client_id,
|
||||
AckedRequest.unique_id == unique_id,
|
||||
if username:
|
||||
result = await self.session.execute(
|
||||
select(AckedRequest).where(
|
||||
and_(
|
||||
AckedRequest.username == username,
|
||||
AckedRequest.unique_id == unique_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
result = await self.session.execute(
|
||||
select(AckedRequest).where(
|
||||
and_(
|
||||
AckedRequest.client_id == client_id,
|
||||
AckedRequest.unique_id == unique_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user