Added reservation_service aswell

This commit is contained in:
Jonas Linter
2025-10-13 10:59:05 +02:00
parent b045c62cee
commit 592a9d7ce7
3 changed files with 293 additions and 50 deletions

View File

@@ -15,7 +15,6 @@ from enum import Enum, IntEnum
from typing import Any, Optional, override from typing import Any, Optional, override
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from sqlalchemy import select
from xsdata.formats.dataclass.serializers.config import SerializerConfig from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata_pydantic.bindings import XmlParser, XmlSerializer from xsdata_pydantic.bindings import XmlParser, XmlSerializer
@@ -26,6 +25,7 @@ from alpine_bits_python.alpine_bits_helpers import (
from alpine_bits_python.logging_config import get_logger from alpine_bits_python.logging_config import get_logger
from .db import AckedRequest, Customer, Reservation from .db import AckedRequest, Customer, Reservation
from .reservation_service import ReservationService
from .generated.alpinebits import ( from .generated.alpinebits import (
OtaNotifReportRq, OtaNotifReportRq,
OtaNotifReportRs, OtaNotifReportRs,
@@ -510,32 +510,29 @@ class ReadAction(AlpineBitsAction):
hotel_read_request.selection_criteria.start hotel_read_request.selection_criteria.start
) )
# query all reservations for this hotel from the database, where start_date is greater than or equal to the given start_date # Use ReservationService to query reservations
reservation_service = ReservationService(dbsession)
stmt = (
select(Reservation, Customer)
.join(Customer, Reservation.customer_id == Customer.id)
.filter(Reservation.hotel_code == hotelid)
)
if start_date: if start_date:
_LOGGER.info("Filtering reservations from start date %s", start_date) _LOGGER.info("Filtering reservations from start date %s", start_date)
stmt = stmt.filter(Reservation.created_at >= start_date) reservation_customer_pairs = (
# remove reservations that have been acknowledged via client_id await reservation_service.get_reservations_with_filters(
start_date=start_date, hotel_code=hotelid
)
)
elif client_info.client_id: elif client_info.client_id:
subquery = ( # Remove reservations that have been acknowledged via client_id
select(Reservation.id) reservation_customer_pairs = (
.join( await reservation_service.get_unacknowledged_reservations(
AckedRequest, client_id=client_info.client_id, hotel_code=hotelid
Reservation.md5_unique_id == AckedRequest.unique_id, )
)
else:
reservation_customer_pairs = (
await reservation_service.get_reservations_with_filters(
hotel_code=hotelid
) )
.filter(AckedRequest.client_id == client_info.client_id)
) )
stmt = stmt.filter(~Reservation.id.in_(subquery))
result = await dbsession.execute(stmt)
reservation_customer_pairs: list[tuple[Reservation, Customer]] = (
result.all()
) # List of (Reservation, Customer) tuples
_LOGGER.info( _LOGGER.info(
"Querying reservations and customers for hotel %s from database", "Querying reservations and customers for hotel %s from database",
@@ -616,19 +613,16 @@ class NotifReportReadAction(AlpineBitsAction):
"Error: Something went wrong", HttpStatusCode.INTERNAL_SERVER_ERROR "Error: Something went wrong", HttpStatusCode.INTERNAL_SERVER_ERROR
) )
timestamp = datetime.now(ZoneInfo("UTC")) # Use ReservationService to record acknowledgements
reservation_service = ReservationService(dbsession)
for entry in ( for entry in (
notif_report_details.hotel_notif_report.hotel_reservations.hotel_reservation notif_report_details.hotel_notif_report.hotel_reservations.hotel_reservation
): # type: ignore ): # type: ignore
unique_id = entry.unique_id.id unique_id = entry.unique_id.id
acked_request = AckedRequest( await reservation_service.record_acknowledgement(
unique_id=unique_id, client_id=client_info.client_id, unique_id=unique_id
client_id=client_info.client_id,
timestamp=timestamp,
) )
dbsession.add(acked_request)
await dbsession.commit()
return AlpineBitsResponse(response_xml, HttpStatusCode.OK) return AlpineBitsResponse(response_xml, HttpStatusCode.OK)

View File

@@ -32,6 +32,7 @@ from .db import Base, get_database_url
from .db import Customer as DBCustomer from .db import Customer as DBCustomer
from .db import Reservation as DBReservation from .db import Reservation as DBReservation
from .logging_config import get_logger, setup_logging from .logging_config import get_logger, setup_logging
from .reservation_service import ReservationService
from .rate_limit import ( from .rate_limit import (
BURST_RATE_LIMIT, BURST_RATE_LIMIT,
DEFAULT_RATE_LIMIT, DEFAULT_RATE_LIMIT,
@@ -305,22 +306,6 @@ async def health_check(request: Request):
} }
def create_db_reservation_from_data(
reservation_model: ReservationData, db_customer_id: int
) -> DBReservation:
"""Convert ReservationData to DBReservation, handling children_ages conversion."""
data = reservation_model.model_dump(exclude_none=True)
children_list = data.pop("children_ages", [])
children_csv = ",".join(str(int(a)) for a in children_list) if children_list else ""
data["children_ages"] = children_csv
# Inject FK
data["customer_id"] = db_customer_id
return DBReservation(**data)
# Extracted business logic for handling Wix form submissions # Extracted business logic for handling Wix form submissions
async def process_wix_form_submission(request: Request, data: dict[str, Any], db): async def process_wix_form_submission(request: Request, data: dict[str, Any], db):
"""Shared business logic for handling Wix form submissions (test and production).""" """Shared business logic for handling Wix form submissions (test and production)."""
@@ -458,10 +443,11 @@ async def process_wix_form_submission(request: Request, data: dict[str, Any], db
if reservation.md5_unique_id is None: if reservation.md5_unique_id is None:
raise HTTPException(status_code=400, detail="Failed to generate md5_unique_id") raise HTTPException(status_code=400, detail="Failed to generate md5_unique_id")
db_reservation = create_db_reservation_from_data(reservation, db_customer.id) # Use ReservationService to create reservation
db.add(db_reservation) reservation_service = ReservationService(db)
await db.commit() db_reservation = await reservation_service.create_reservation(
await db.refresh(db_reservation) reservation, db_customer.id
)
async def push_event(): async def push_event():
# Fire event for listeners (push, etc.) - hotel-specific dispatch # Fire event for listeners (push, etc.) - hotel-specific dispatch

View File

@@ -0,0 +1,263 @@
"""Reservation service layer for handling reservation database operations."""
import hashlib
from datetime import UTC, datetime
from typing import Optional
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from .db import AckedRequest, Customer, Reservation
from .schemas import ReservationData
class ReservationService:
"""Service for managing reservations and related operations.
Handles all database operations for reservations including creation,
retrieval, and acknowledgement tracking.
"""
def __init__(self, session: AsyncSession):
self.session = session
def _convert_reservation_data_to_db(
self, reservation_model: ReservationData, customer_id: int
) -> Reservation:
"""Convert ReservationData to Reservation model.
Args:
reservation_model: ReservationData instance
customer_id: Customer ID to link to
Returns:
Reservation instance ready for database insertion
"""
data = reservation_model.model_dump(exclude_none=True)
# Convert children_ages list to CSV string
children_list = data.pop("children_ages", [])
children_csv = (
",".join(str(int(a)) for a in children_list) if children_list else ""
)
data["children_ages"] = children_csv
# Inject foreign key
data["customer_id"] = customer_id
return Reservation(**data)
async def create_reservation(
self, reservation_data: ReservationData, customer_id: int
) -> Reservation:
"""Create a new reservation.
Args:
reservation_data: ReservationData containing reservation details
customer_id: ID of the customer making the reservation
Returns:
Created Reservation instance
"""
reservation = self._convert_reservation_data_to_db(
reservation_data, customer_id
)
self.session.add(reservation)
await self.session.commit()
await self.session.refresh(reservation)
return reservation
async def get_reservation_by_unique_id(
self, unique_id: str
) -> Optional[Reservation]:
"""Get a reservation by unique_id.
Args:
unique_id: The unique_id to search for
Returns:
Reservation instance if found, None otherwise
"""
result = await self.session.execute(
select(Reservation).where(Reservation.unique_id == unique_id)
)
return result.scalar_one_or_none()
async def get_reservation_by_md5_unique_id(
self, md5_unique_id: str
) -> Optional[Reservation]:
"""Get a reservation by md5_unique_id.
Args:
md5_unique_id: The MD5 hash of unique_id
Returns:
Reservation instance if found, None otherwise
"""
result = await self.session.execute(
select(Reservation).where(
Reservation.md5_unique_id == md5_unique_id
)
)
return result.scalar_one_or_none()
async def check_duplicate_reservation(
self, unique_id: str, md5_unique_id: str
) -> bool:
"""Check if a reservation already exists.
Args:
unique_id: The unique_id to check
md5_unique_id: The MD5 hash to check
Returns:
True if reservation exists, False otherwise
"""
existing = await self.get_reservation_by_unique_id(unique_id)
if existing:
return True
existing_md5 = await self.get_reservation_by_md5_unique_id(md5_unique_id)
return existing_md5 is not None
async def get_reservations_for_customer(
self, customer_id: int
) -> list[Reservation]:
"""Get all reservations for a customer.
Args:
customer_id: The customer ID
Returns:
List of Reservation instances
"""
result = await self.session.execute(
select(Reservation).where(Reservation.customer_id == customer_id)
)
return list(result.scalars().all())
async def get_reservations_with_filters(
self,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
hotel_code: Optional[str] = None,
) -> list[tuple[Reservation, Customer]]:
"""Get reservations with optional filters, joined with customers.
Args:
start_date: Filter by created_at >= this value
end_date: Filter by created_at <= this value
hotel_code: Filter by hotel code
Returns:
List of (Reservation, Customer) tuples
"""
query = select(Reservation, Customer).join(
Customer, Reservation.customer_id == Customer.id
)
filters = []
if start_date:
filters.append(Reservation.created_at >= start_date)
if end_date:
filters.append(Reservation.created_at <= end_date)
if hotel_code:
filters.append(Reservation.hotel_code == hotel_code)
if filters:
query = query.where(and_(*filters))
result = await self.session.execute(query)
return list(result.all())
async def get_unacknowledged_reservations(
self,
client_id: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
hotel_code: Optional[str] = None,
) -> list[tuple[Reservation, Customer]]:
"""Get reservations that haven't been acknowledged by a client.
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
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
)
)
acked_md5_ids = {row[0] for row in acked_result.all()}
# Get all reservations with filters
all_reservations = await self.get_reservations_with_filters(
start_date, end_date, hotel_code
)
# Filter out acknowledged ones (comparing md5_unique_id)
return [
(res, cust)
for res, cust in all_reservations
if res.md5_unique_id not in acked_md5_ids
]
async def record_acknowledgement(
self, client_id: str, unique_id: str
) -> AckedRequest:
"""Record that a client has acknowledged a reservation.
Args:
client_id: The client ID
unique_id: The unique_id of the reservation
Returns:
Created AckedRequest instance
"""
acked = AckedRequest(
client_id=client_id,
unique_id=unique_id,
timestamp=datetime.now(UTC),
)
self.session.add(acked)
await self.session.commit()
await self.session.refresh(acked)
return acked
async def is_acknowledged(self, client_id: str, unique_id: str) -> bool:
"""Check if a reservation has been acknowledged by a client.
Args:
client_id: The client ID
unique_id: The reservation unique_id
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,
)
)
)
return result.scalar_one_or_none() is not None
@staticmethod
def generate_md5_unique_id(unique_id: str) -> str:
"""Generate MD5 hash of unique_id.
Args:
unique_id: The unique_id to hash
Returns:
MD5 hash as hex string
"""
return hashlib.md5(unique_id.encode("utf-8")).hexdigest()