4 Commits

Author SHA1 Message Date
Jonas Linter
fce2dbc8de Fixed incorrect overlap detection 2025-12-09 15:29:35 +01:00
f6929ca7cc Small logging improvement 2025-12-09 14:13:58 +00:00
Jonas Linter
c73747e02d Update free_rooms is_closing season detection. Should also accept 1 as True 2025-12-09 14:45:22 +01:00
Jonas Linter
13e404d07c Only update updated_at timestamps if something actually changes. 2025-12-09 14:06:00 +01:00
5 changed files with 663 additions and 30 deletions

View File

@@ -842,4 +842,4 @@ class AlpineBitsServer:
# Ensure FreeRoomsAction is registered with ServerCapabilities discovery
#from .free_rooms_action import FreeRoomsAction
from .free_rooms_action import FreeRoomsAction

View File

@@ -119,6 +119,28 @@ class ConversionService:
f"session must be AsyncSession or SessionMaker, got {type(session)}"
)
@staticmethod
def _update_timestamp_if_modified(
obj: Conversion | ConversionRoom, session: AsyncSession
) -> bool:
"""Update the updated_at timestamp only if the object has been modified.
Uses SQLAlchemy's change tracking to determine if any scalar attributes
have changed. Only updates the timestamp if actual changes were detected.
Args:
obj: The ORM object to check and potentially update
session: The session managing this object
Returns:
True if the object was modified and timestamp was updated, False otherwise
"""
if session.is_modified(obj, include_collections=False):
obj.updated_at = datetime.now()
return True
return False
@staticmethod
def _parse_required_int(value: str | None, field_name: str) -> int:
"""Parse an integer attribute that must be present."""
@@ -897,26 +919,39 @@ class ConversionService:
existing_conversion = existing_result.scalar_one_or_none()
if existing_conversion:
# Update existing conversion - only update reservation metadata and advertising data
# Update existing conversion using Pydantic validation
# Guest info is stored in ConversionGuest table, not here
# Don't clear reservation/customer links (matching logic will update if needed)
existing_conversion.reservation_number = (
parsed_reservation.reservation_number
# Preserve reservation/customer links (matching logic will update if needed)
conversion_data = ConversionData(
hotel_id=hotel_id,
pms_reservation_id=pms_reservation_id,
guest_id=parsed_reservation.guest_id,
reservation_number=parsed_reservation.reservation_number,
reservation_date=parsed_reservation.reservation_date,
creation_time=parsed_reservation.creation_time,
reservation_type=parsed_reservation.reservation_type,
booking_channel=parsed_reservation.booking_channel,
advertising_medium=parsed_reservation.advertising_medium,
advertising_partner=parsed_reservation.advertising_partner,
advertising_campagne=parsed_reservation.advertising_campagne,
# Preserve existing values (managed separately)
created_at=existing_conversion.created_at,
reservation_id=existing_conversion.reservation_id,
customer_id=existing_conversion.customer_id,
directly_attributable=existing_conversion.directly_attributable,
guest_matched=existing_conversion.guest_matched,
)
existing_conversion.reservation_date = parsed_reservation.reservation_date
existing_conversion.creation_time = parsed_reservation.creation_time
existing_conversion.reservation_type = parsed_reservation.reservation_type
existing_conversion.booking_channel = parsed_reservation.booking_channel
existing_conversion.advertising_medium = (
parsed_reservation.advertising_medium
# Apply validated data, excluding managed fields
validated_dict = conversion_data.model_dump(
exclude={'created_at', 'updated_at', 'reservation_id', 'customer_id',
'directly_attributable', 'guest_matched'}
)
existing_conversion.advertising_partner = (
parsed_reservation.advertising_partner
)
existing_conversion.advertising_campagne = (
parsed_reservation.advertising_campagne
)
existing_conversion.updated_at = datetime.now()
for key, value in validated_dict.items():
setattr(existing_conversion, key, value)
# Only update timestamp if something actually changed
self._update_timestamp_if_modified(existing_conversion, session)
conversion = existing_conversion
_LOGGER.debug(
"Updated conversion %s (pms_id=%s)",
@@ -998,7 +1033,8 @@ class ConversionService:
else None
)
existing_room_reservation.total_revenue = room_reservation.total_revenue
existing_room_reservation.updated_at = datetime.now()
# Only update timestamp if something actually changed
self._update_timestamp_if_modified(existing_room_reservation, session)
_LOGGER.debug(
"Updated room reservation %s (pms_id=%s, room=%s)",
existing_room_reservation.id,
@@ -1358,8 +1394,8 @@ class ConversionService:
# ID-based matches are always directly attributable
conversion.directly_attributable = True
conversion.guest_matched = False
conversion.updated_at = datetime.now()
# Only update timestamp if something actually changed
self._update_timestamp_if_modified(conversion, session)
# Update stats if provided
if stats is not None:
@@ -1506,7 +1542,8 @@ class ConversionService:
elif conversion.reservation_id is None:
conversion.directly_attributable = False
conversion.updated_at = datetime.now()
# Only update timestamp if something actually changed
self._update_timestamp_if_modified(conversion, session)
return matched_reservation, matched_customer

View File

@@ -290,21 +290,32 @@ class FreeRoomsAction(AlpineBitsAction):
# Validate standard inventory entries
inv_type_code = (sac.inv_type_code or "").strip()
if not inv_type_code:
error_message = "InvTypeCode is required unless AllInvCode=\"true\" or similar truthy values"
_LOGGER.info(error_message)
raise FreeRoomsProcessingError(
"InvTypeCode is required unless AllInvCode=\"true\"",
error_message,
HttpStatusCode.BAD_REQUEST,
)
# Validate date range
start_date, end_date = self._parse_date_range(sac.start, sac.end)
# Check if this inventory entry has any counts (available rooms)
# Entries without counts represent unavailable rooms
has_availability = inventory.inv_counts is not None and inventory.inv_counts.inv_count
# Check for overlap with closing seasons
for closing_start, closing_end in closing_season_ranges:
if self._date_ranges_overlap(start_date, end_date, closing_start, closing_end):
raise FreeRoomsProcessingError(
f"Inventory entry ({start_date} to {end_date}) overlaps with closing season ({closing_start} to {closing_end})",
HttpStatusCode.BAD_REQUEST,
)
# Only entries with availability (counts) cannot overlap with closing seasons
# Entries without counts (unavailable rooms) can overlap with closing seasons
if has_availability:
for closing_start, closing_end in closing_season_ranges:
if self._date_ranges_overlap(start_date, end_date, closing_start, closing_end):
error_message = f"Inventory entry ({start_date} to {end_date}) overlaps with closing season ({closing_start} to {closing_end})"
_LOGGER.info(error_message)
raise FreeRoomsProcessingError(
error_message,
HttpStatusCode.BAD_REQUEST,
)
# Check for overlap with other inventory entries for the same room/category
inv_code = sac.inv_code.strip() if sac.inv_code else None
@@ -586,7 +597,12 @@ class FreeRoomsAction(AlpineBitsAction):
self,
sac: OtaHotelInvCountNotifRq.Inventories.Inventory.StatusApplicationControl,
) -> bool:
return (sac.all_inv_code or "").strip().lower() == "true"
"""Check if AllInvCode is a truthy boolean value.
Accepts: "true", "True", "TRUE", "1", "yes", "Yes", "YES", etc.
"""
value = (sac.all_inv_code or "").strip().lower()
return value in ("true", "1", "yes")
def _extract_counts(
self,

View File

@@ -0,0 +1,549 @@
<?xml version="1.0" ?>
<!--
Bespielfile von Sebastian zum testen der Closing Seasons Funktionalität
-->
<OTA_HotelInvCountNotifRQ xmlns='http://www.opentravel.org/OTA/2003/05' Version='3.000' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://www.opentravel.org/OTA/2003/05 OTA_HotelInvCountNotifRQ.xsd'>
<UniqueID Type='16' ID='1' Instance='CompleteSet'/>
<Inventories HotelCode='TESTHOTEL'>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' AllInvCode='1'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='106' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='106' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='106' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-09' InvCode='107' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-10' End='2025-12-19' InvCode='107' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='107' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2025-12-28' InvCode='107' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-29' End='2026-01-04' InvCode='107' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2026-01-05' End='2026-01-31' InvCode='107' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='108' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='108' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='108' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='206' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='206' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='206' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='207' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='207' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='207' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='208' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='208' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='208' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='306' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='306' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='306' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='307' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='307' InvTypeCode='EZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='307' InvTypeCode='EZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='101' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='101' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='101' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2026-01-31' InvCode='102' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='103' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='103' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='103' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='104' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='104' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-04' InvCode='104' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2026-01-05' End='2026-01-05' InvCode='104' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2026-01-06' End='2026-01-31' InvCode='104' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='105' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='105' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='105' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='201' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='201' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='201' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='202' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='202' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='202' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='203' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='203' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='203' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='204' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='204' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='204' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='205' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2026-01-05' InvCode='205' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2026-01-06' End='2026-01-31' InvCode='205' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='301' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='301' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='301' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='302' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='302' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='302' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='303' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='303' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='303' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='304' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-25' InvCode='304' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2026-01-31' InvCode='304' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='305' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='305' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='305' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='501' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='501' InvTypeCode='DZ'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='501' InvTypeCode='DZ'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-11' InvCode='109' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-12' End='2025-12-24' InvCode='109' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-25' End='2025-12-25' InvCode='109' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-26' End='2025-12-26' InvCode='109' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-27' End='2026-01-13' InvCode='109' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2026-01-14' End='2026-01-14' InvCode='109' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2026-01-15' End='2026-01-31' InvCode='109' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-16' InvCode='110' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-17' End='2025-12-23' InvCode='110' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='110' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-17' InvCode='209' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-18' End='2025-12-23' InvCode='209' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='209' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='210' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='210' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='210' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='309' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='309' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='309' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='310' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='310' InvTypeCode='SUI'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='310' InvTypeCode='SUI'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='401' InvTypeCode='FW'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='401' InvTypeCode='FW'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='401' InvTypeCode='FW'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='402' InvTypeCode='FW'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='402' InvTypeCode='FW'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='402' InvTypeCode='FW'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='403' InvTypeCode='FW'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='403' InvTypeCode='FW'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='403' InvTypeCode='FW'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-09' End='2025-12-19' InvCode='308' InvTypeCode='COD'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-20' End='2025-12-23' InvCode='308' InvTypeCode='COD'/>
</Inventory>
<Inventory>
<StatusApplicationControl Start='2025-12-24' End='2026-01-31' InvCode='308' InvTypeCode='COD'/>
<InvCounts>
<InvCount CountType='2' Count='1'/>
</InvCounts>
</Inventory>
</Inventories>
</OTA_HotelInvCountNotifRQ>

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
import pytest
import pytest_asyncio
@@ -231,6 +232,36 @@ async def test_closing_season_entries_marked_correctly(db_session: AsyncSession)
assert len(closing_rows) == 2
assert all(row.bookable_type_2 is None for row in closing_rows)
@pytest.mark.asyncio
async def test_closing_seasons_test_file(db_session: AsyncSession):
await insert_test_hotel(db_session)
action = make_action()
Path(__file__).parent / "test_data" / "ClosingSeasons.xml"
xml = (Path(__file__).parent / "test_data" / "ClosingSeasons.xml").read_text()
response = await action.handle(
"OTA_HotelInvCountNotif:FreeRooms",
xml,
Version.V2024_10,
make_client_info(),
db_session,
)
assert response.status_code == HttpStatusCode.OK, f"Response was not OK {response.xml_content}"
inventories = (await db_session.execute(select(HotelInventory))).scalars().all()
closing_inventory = next(inv for inv in inventories if inv.inv_type_code == "__CLOSE")
assert closing_inventory.inv_code is None
rows = (
await db_session.execute(select(RoomAvailability).order_by(RoomAvailability.date))
).scalars().all()
closing_rows = [row for row in rows if row.is_closing_season]
# Closing season from 2025-12-20 to 2025-12-23 = 4 days
assert len(closing_rows) == 4
assert all(row.bookable_type_2 is None for row in closing_rows)
@pytest.mark.asyncio
async def test_closing_season_not_allowed_in_delta(db_session: AsyncSession):