concurrency-fix #15
@@ -731,17 +731,10 @@ class ConversionService:
|
||||
# Flush to ensure conversion has an ID before creating room reservations
|
||||
await session.flush()
|
||||
|
||||
# Batch-load existing room reservations to avoid N+1 queries
|
||||
room_numbers = [
|
||||
rm.get("roomNumber") for rm in room_reservations.findall("roomReservation")
|
||||
]
|
||||
pms_hotel_reservation_ids = [
|
||||
f"{pms_reservation_id}_{room_num}" for room_num in room_numbers
|
||||
]
|
||||
|
||||
# Fetch ALL existing rooms for this conversion (not just the ones in current XML)
|
||||
existing_rooms_result = await session.execute(
|
||||
select(ConversionRoom).where(
|
||||
ConversionRoom.pms_hotel_reservation_id.in_(pms_hotel_reservation_ids)
|
||||
ConversionRoom.conversion_id == conversion.id
|
||||
)
|
||||
)
|
||||
existing_rooms = {
|
||||
@@ -749,6 +742,9 @@ class ConversionService:
|
||||
for room in existing_rooms_result.scalars().all()
|
||||
}
|
||||
|
||||
# Track which room IDs are present in the current XML
|
||||
current_pms_hotel_reservation_ids = set()
|
||||
|
||||
# Process room reservations
|
||||
for room_reservation in room_reservations.findall("roomReservation"):
|
||||
# Extract room reservation details
|
||||
@@ -786,6 +782,9 @@ class ConversionService:
|
||||
# This allows updating the same room reservation if it appears again
|
||||
pms_hotel_reservation_id = f"{pms_reservation_id}_{room_number}"
|
||||
|
||||
# Track this room as present in current XML
|
||||
current_pms_hotel_reservation_ids.add(pms_hotel_reservation_id)
|
||||
|
||||
# Process daily sales and extract total revenue
|
||||
daily_sales_elem = room_reservation.find("dailySales")
|
||||
daily_sales_list = []
|
||||
@@ -880,6 +879,24 @@ class ConversionService:
|
||||
num_adults,
|
||||
)
|
||||
|
||||
# Delete room entries that are no longer present in the current XML
|
||||
# This handles cases where a reservation is updated and room numbers change
|
||||
rooms_to_delete = [
|
||||
room
|
||||
for pms_id, room in existing_rooms.items()
|
||||
if pms_id not in current_pms_hotel_reservation_ids
|
||||
]
|
||||
|
||||
if rooms_to_delete:
|
||||
for room in rooms_to_delete:
|
||||
await session.delete(room)
|
||||
_LOGGER.debug(
|
||||
"Deleted room reservation %s (pms_id=%s, room=%s) - no longer in current XML",
|
||||
room.id,
|
||||
room.pms_hotel_reservation_id,
|
||||
room.room_number,
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
|
||||
197
tests/helpers/README.md
Normal file
197
tests/helpers/README.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Test Helpers
|
||||
|
||||
This directory contains helper utilities for creating test data.
|
||||
|
||||
## XML Builders
|
||||
|
||||
The `xml_builders` module provides convenient builder classes for creating reservation XML structures used in conversion service tests.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```python
|
||||
from tests.helpers import ReservationXMLBuilder
|
||||
|
||||
# Create a simple reservation
|
||||
xml = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
revenue_logis_per_day=150.0, # Fixed revenue per night
|
||||
)
|
||||
.build_xml()
|
||||
)
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
#### ReservationXMLBuilder
|
||||
|
||||
The main builder class for creating reservation XML structures.
|
||||
|
||||
**Key Features:**
|
||||
- Fluent API for method chaining
|
||||
- Automatic daily sales generation from arrival to departure
|
||||
- Convenient revenue-per-day specification (no need to manually create each dailySale)
|
||||
- Support for advertising campaign data
|
||||
- Guest information with optional fields
|
||||
|
||||
**Example - Multi-room reservation:**
|
||||
|
||||
```python
|
||||
xml = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="Jane",
|
||||
last_name="Smith",
|
||||
email="jane@example.com",
|
||||
country_code="US",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_number="101",
|
||||
room_type="DZV",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_number="102",
|
||||
room_type="DZM",
|
||||
revenue_logis_per_day=200.0,
|
||||
)
|
||||
.build_xml()
|
||||
)
|
||||
```
|
||||
|
||||
#### Daily Sales Generation
|
||||
|
||||
The builder automatically generates `<dailySale>` entries for each day from arrival to departure (inclusive).
|
||||
|
||||
- **Days before departure**: Include `revenueTotal` and `revenueLogis` attributes
|
||||
- **Departure day**: No revenue attributes (just the date)
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
# A 3-night stay (Dec 1-4)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-04",
|
||||
revenue_logis_per_day=160.0,
|
||||
)
|
||||
```
|
||||
|
||||
Generates:
|
||||
```xml
|
||||
<dailySales>
|
||||
<dailySale date="2025-12-01" revenueTotal="160.0" revenueLogis="160.0"/>
|
||||
<dailySale date="2025-12-02" revenueTotal="160.0" revenueLogis="160.0"/>
|
||||
<dailySale date="2025-12-03" revenueTotal="160.0" revenueLogis="160.0"/>
|
||||
<dailySale date="2025-12-04"/> <!-- No revenue on departure day -->
|
||||
</dailySales>
|
||||
```
|
||||
|
||||
#### MultiReservationXMLBuilder
|
||||
|
||||
For creating XML documents with multiple reservations:
|
||||
|
||||
```python
|
||||
from tests.helpers import ReservationXMLBuilder, MultiReservationXMLBuilder
|
||||
|
||||
multi_builder = MultiReservationXMLBuilder()
|
||||
|
||||
# Add first reservation
|
||||
res1 = (
|
||||
ReservationXMLBuilder(...)
|
||||
.set_guest(...)
|
||||
.add_room(...)
|
||||
)
|
||||
multi_builder.add_reservation(res1)
|
||||
|
||||
# Add second reservation
|
||||
res2 = (
|
||||
ReservationXMLBuilder(...)
|
||||
.set_guest(...)
|
||||
.add_room(...)
|
||||
)
|
||||
multi_builder.add_reservation(res2)
|
||||
|
||||
xml = multi_builder.build_xml()
|
||||
```
|
||||
|
||||
#### RoomReservationBuilder
|
||||
|
||||
Low-level builder for creating individual room reservations. Usually you'll use `ReservationXMLBuilder.add_room()` instead, but this is available for advanced use cases.
|
||||
|
||||
```python
|
||||
from tests.helpers import RoomReservationBuilder
|
||||
|
||||
room_builder = RoomReservationBuilder(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_type="DZV",
|
||||
room_number="101",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
|
||||
# Get the XML element (not a string)
|
||||
room_elem = room_builder.build()
|
||||
```
|
||||
|
||||
### Common Parameters
|
||||
|
||||
**ReservationXMLBuilder:**
|
||||
- `hotel_id` - Hotel ID (required)
|
||||
- `reservation_id` - Reservation ID (required)
|
||||
- `reservation_number` - Reservation number (required)
|
||||
- `reservation_date` - Reservation date YYYY-MM-DD (required)
|
||||
- `creation_time` - Creation timestamp (optional, defaults to reservation_date + T00:00:00)
|
||||
- `advertising_medium` - Advertising medium (optional)
|
||||
- `advertising_partner` - Advertising partner (optional)
|
||||
- `advertising_campagne` - Advertising campaign (optional)
|
||||
|
||||
**set_guest() parameters:**
|
||||
- `guest_id` - Guest ID (required)
|
||||
- `first_name` - First name (required)
|
||||
- `last_name` - Last name (required)
|
||||
- `email` - Email address (required)
|
||||
- `language` - Language code (default: "en")
|
||||
- `gender` - Gender (optional)
|
||||
- `country_code` - Country code (optional)
|
||||
- `country` - Country name (optional)
|
||||
|
||||
**add_room() parameters:**
|
||||
- `arrival` - Arrival date YYYY-MM-DD (required)
|
||||
- `departure` - Departure date YYYY-MM-DD (required)
|
||||
- `room_type` - Room type code (default: "DZV")
|
||||
- `room_number` - Room number (default: "101")
|
||||
- `status` - Reservation status (default: "reserved")
|
||||
- `adults` - Number of adults (default: 2)
|
||||
- `children` - Number of children (default: 0)
|
||||
- `infants` - Number of infants (default: 0)
|
||||
- `rate_plan_code` - Rate plan code (default: "STANDARD")
|
||||
- `revenue_logis_per_day` - Fixed revenue per night (optional, generates daily sales)
|
||||
- `revenue_total_per_day` - Total revenue per night (optional, defaults to revenue_logis_per_day)
|
||||
|
||||
### See Also
|
||||
|
||||
- [tests/test_xml_builders.py](../test_xml_builders.py) - Unit tests demonstrating all features
|
||||
- [tests/test_conversion_service.py](../test_conversion_service.py) - Integration examples (TestXMLBuilderUsage class)
|
||||
13
tests/helpers/__init__.py
Normal file
13
tests/helpers/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Test helper utilities for creating test data."""
|
||||
|
||||
from .xml_builders import (
|
||||
ReservationXMLBuilder,
|
||||
MultiReservationXMLBuilder,
|
||||
RoomReservationBuilder,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ReservationXMLBuilder",
|
||||
"MultiReservationXMLBuilder",
|
||||
"RoomReservationBuilder",
|
||||
]
|
||||
392
tests/helpers/xml_builders.py
Normal file
392
tests/helpers/xml_builders.py
Normal file
@@ -0,0 +1,392 @@
|
||||
"""XML builder helpers for creating test reservation data.
|
||||
|
||||
This module provides convenient builder classes for generating reservation XML
|
||||
structures used in conversion service tests.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
|
||||
class RoomReservationBuilder:
|
||||
"""Builder for creating roomReservation XML elements with daily sales."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
arrival: str,
|
||||
departure: str,
|
||||
room_type: str = "DZV",
|
||||
room_number: str = "101",
|
||||
status: str = "reserved",
|
||||
adults: int = 2,
|
||||
children: int = 0,
|
||||
infants: int = 0,
|
||||
rate_plan_code: str = "STANDARD",
|
||||
connected_room_type: str = "0",
|
||||
revenue_logis_per_day: Optional[float] = None,
|
||||
revenue_total_per_day: Optional[float] = None,
|
||||
):
|
||||
"""Initialize room reservation builder.
|
||||
|
||||
Args:
|
||||
arrival: Arrival date in YYYY-MM-DD format
|
||||
departure: Departure date in YYYY-MM-DD format
|
||||
room_type: Room type code
|
||||
room_number: Room number
|
||||
status: Reservation status (reserved, request, confirmed, etc.)
|
||||
adults: Number of adults
|
||||
children: Number of children
|
||||
infants: Number of infants
|
||||
rate_plan_code: Rate plan code
|
||||
connected_room_type: Connected room type code
|
||||
revenue_logis_per_day: Revenue per day (if None, no revenue attributes)
|
||||
revenue_total_per_day: Total revenue per day (defaults to revenue_logis_per_day)
|
||||
"""
|
||||
self.arrival = arrival
|
||||
self.departure = departure
|
||||
self.room_type = room_type
|
||||
self.room_number = room_number
|
||||
self.status = status
|
||||
self.adults = adults
|
||||
self.children = children
|
||||
self.infants = infants
|
||||
self.rate_plan_code = rate_plan_code
|
||||
self.connected_room_type = connected_room_type
|
||||
self.revenue_logis_per_day = revenue_logis_per_day
|
||||
self.revenue_total_per_day = revenue_total_per_day or revenue_logis_per_day
|
||||
|
||||
def build(self) -> ET.Element:
|
||||
"""Build the roomReservation XML element with daily sales.
|
||||
|
||||
Returns:
|
||||
XML Element for the room reservation
|
||||
"""
|
||||
room_attrs = {
|
||||
"arrival": self.arrival,
|
||||
"departure": self.departure,
|
||||
"status": self.status,
|
||||
"roomType": self.room_type,
|
||||
"roomNumber": self.room_number,
|
||||
"adults": str(self.adults),
|
||||
"ratePlanCode": self.rate_plan_code,
|
||||
"connectedRoomType": self.connected_room_type,
|
||||
}
|
||||
|
||||
if self.children > 0:
|
||||
room_attrs["children"] = str(self.children)
|
||||
if self.infants > 0:
|
||||
room_attrs["infants"] = str(self.infants)
|
||||
|
||||
room_elem = ET.Element("roomReservation", room_attrs)
|
||||
|
||||
# Create dailySales element
|
||||
daily_sales_elem = ET.SubElement(room_elem, "dailySales")
|
||||
|
||||
# Generate daily sale entries from arrival to departure (inclusive of departure for the no-revenue entry)
|
||||
arrival_date = datetime.strptime(self.arrival, "%Y-%m-%d")
|
||||
departure_date = datetime.strptime(self.departure, "%Y-%m-%d")
|
||||
|
||||
current_date = arrival_date
|
||||
while current_date <= departure_date:
|
||||
date_str = current_date.strftime("%Y-%m-%d")
|
||||
daily_sale_attrs = {"date": date_str}
|
||||
|
||||
# Add revenue attributes for all days except departure day
|
||||
if current_date < departure_date and self.revenue_logis_per_day is not None:
|
||||
daily_sale_attrs["revenueTotal"] = str(self.revenue_total_per_day)
|
||||
daily_sale_attrs["revenueLogis"] = str(self.revenue_logis_per_day)
|
||||
|
||||
ET.SubElement(daily_sales_elem, "dailySale", daily_sale_attrs)
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return room_elem
|
||||
|
||||
|
||||
class ReservationXMLBuilder:
|
||||
"""Builder for creating complete reservation XML structures for testing.
|
||||
|
||||
This builder provides a fluent interface for constructing reservation XML
|
||||
that matches the format expected by the ConversionService.
|
||||
|
||||
Example usage:
|
||||
builder = ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14"
|
||||
)
|
||||
builder.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com"
|
||||
)
|
||||
builder.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
revenue_logis_per_day=150.0
|
||||
)
|
||||
xml_string = builder.build_xml()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hotel_id: str,
|
||||
reservation_id: str,
|
||||
reservation_number: str,
|
||||
reservation_date: str,
|
||||
creation_time: Optional[str] = None,
|
||||
reservation_type: str = "reservation",
|
||||
advertising_medium: Optional[str] = None,
|
||||
advertising_partner: Optional[str] = None,
|
||||
advertising_campagne: Optional[str] = None,
|
||||
):
|
||||
"""Initialize reservation builder.
|
||||
|
||||
Args:
|
||||
hotel_id: Hotel ID
|
||||
reservation_id: Reservation ID
|
||||
reservation_number: Reservation number
|
||||
reservation_date: Reservation date in YYYY-MM-DD format
|
||||
creation_time: Creation timestamp (defaults to reservation_date + T00:00:00)
|
||||
reservation_type: Type of reservation (reservation, request, etc.)
|
||||
advertising_medium: Advertising medium
|
||||
advertising_partner: Advertising partner
|
||||
advertising_campagne: Advertising campaign
|
||||
"""
|
||||
self.hotel_id = hotel_id
|
||||
self.reservation_id = reservation_id
|
||||
self.reservation_number = reservation_number
|
||||
self.reservation_date = reservation_date
|
||||
self.creation_time = creation_time or f"{reservation_date}T00:00:00"
|
||||
self.reservation_type = reservation_type
|
||||
self.advertising_medium = advertising_medium
|
||||
self.advertising_partner = advertising_partner
|
||||
self.advertising_campagne = advertising_campagne
|
||||
|
||||
self.guest_data: Optional[dict] = None
|
||||
self.rooms: list[RoomReservationBuilder] = []
|
||||
|
||||
def set_guest(
|
||||
self,
|
||||
guest_id: str,
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str,
|
||||
language: str = "en",
|
||||
gender: Optional[str] = None,
|
||||
country_code: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
) -> "ReservationXMLBuilder":
|
||||
"""Set guest information for the reservation.
|
||||
|
||||
Args:
|
||||
guest_id: Guest ID
|
||||
first_name: Guest first name
|
||||
last_name: Guest last name
|
||||
email: Guest email
|
||||
language: Guest language code
|
||||
gender: Guest gender
|
||||
country_code: Guest country code
|
||||
country: Guest country name
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
self.guest_data = {
|
||||
"id": guest_id,
|
||||
"firstName": first_name,
|
||||
"lastName": last_name,
|
||||
"email": email,
|
||||
"language": language,
|
||||
}
|
||||
if gender:
|
||||
self.guest_data["gender"] = gender
|
||||
if country_code:
|
||||
self.guest_data["countryCode"] = country_code
|
||||
if country:
|
||||
self.guest_data["country"] = country
|
||||
|
||||
return self
|
||||
|
||||
def add_room(
|
||||
self,
|
||||
arrival: str,
|
||||
departure: str,
|
||||
room_type: str = "DZV",
|
||||
room_number: str = "101",
|
||||
status: str = "reserved",
|
||||
adults: int = 2,
|
||||
children: int = 0,
|
||||
infants: int = 0,
|
||||
rate_plan_code: str = "STANDARD",
|
||||
connected_room_type: str = "0",
|
||||
revenue_logis_per_day: Optional[float] = None,
|
||||
revenue_total_per_day: Optional[float] = None,
|
||||
) -> "ReservationXMLBuilder":
|
||||
"""Add a room reservation with convenient daily sales generation.
|
||||
|
||||
Args:
|
||||
arrival: Arrival date in YYYY-MM-DD format
|
||||
departure: Departure date in YYYY-MM-DD format
|
||||
room_type: Room type code
|
||||
room_number: Room number
|
||||
status: Reservation status
|
||||
adults: Number of adults
|
||||
children: Number of children
|
||||
infants: Number of infants
|
||||
rate_plan_code: Rate plan code
|
||||
connected_room_type: Connected room type
|
||||
revenue_logis_per_day: Fixed revenue per day (auto-generates dailySale entries)
|
||||
revenue_total_per_day: Total revenue per day (defaults to revenue_logis_per_day)
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
room_builder = RoomReservationBuilder(
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
room_type=room_type,
|
||||
room_number=room_number,
|
||||
status=status,
|
||||
adults=adults,
|
||||
children=children,
|
||||
infants=infants,
|
||||
rate_plan_code=rate_plan_code,
|
||||
connected_room_type=connected_room_type,
|
||||
revenue_logis_per_day=revenue_logis_per_day,
|
||||
revenue_total_per_day=revenue_total_per_day,
|
||||
)
|
||||
self.rooms.append(room_builder)
|
||||
return self
|
||||
|
||||
def add_room_builder(
|
||||
self, room_builder: RoomReservationBuilder
|
||||
) -> "ReservationXMLBuilder":
|
||||
"""Add a pre-configured room builder.
|
||||
|
||||
Args:
|
||||
room_builder: RoomReservationBuilder instance
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
self.rooms.append(room_builder)
|
||||
return self
|
||||
|
||||
def build(self) -> ET.Element:
|
||||
"""Build the reservation XML element.
|
||||
|
||||
Returns:
|
||||
XML Element for the reservation
|
||||
"""
|
||||
reservation_attrs = {
|
||||
"hotelID": self.hotel_id,
|
||||
"id": self.reservation_id,
|
||||
"number": self.reservation_number,
|
||||
"date": self.reservation_date,
|
||||
"creationTime": self.creation_time,
|
||||
"type": self.reservation_type,
|
||||
}
|
||||
|
||||
if self.advertising_medium:
|
||||
reservation_attrs["advertisingMedium"] = self.advertising_medium
|
||||
if self.advertising_partner:
|
||||
reservation_attrs["advertisingPartner"] = self.advertising_partner
|
||||
if self.advertising_campagne:
|
||||
reservation_attrs["advertisingCampagne"] = self.advertising_campagne
|
||||
|
||||
reservation_elem = ET.Element("reservation", reservation_attrs)
|
||||
|
||||
# Add guest element
|
||||
if self.guest_data:
|
||||
ET.SubElement(reservation_elem, "guest", self.guest_data)
|
||||
|
||||
# Add roomReservations
|
||||
if self.rooms:
|
||||
room_reservations_elem = ET.SubElement(
|
||||
reservation_elem, "roomReservations"
|
||||
)
|
||||
for room_builder in self.rooms:
|
||||
room_elem = room_builder.build()
|
||||
room_reservations_elem.append(room_elem)
|
||||
|
||||
return reservation_elem
|
||||
|
||||
def build_xml(self, include_xml_declaration: bool = True) -> str:
|
||||
"""Build the complete XML string for this reservation.
|
||||
|
||||
Args:
|
||||
include_xml_declaration: Whether to include <?xml version="1.0"?> declaration
|
||||
|
||||
Returns:
|
||||
XML string
|
||||
"""
|
||||
reservation_elem = self.build()
|
||||
|
||||
# Wrap in <reservations> root element
|
||||
root = ET.Element("reservations")
|
||||
root.append(reservation_elem)
|
||||
|
||||
xml_str = ET.tostring(root, encoding="unicode")
|
||||
|
||||
if include_xml_declaration:
|
||||
xml_str = '<?xml version="1.0" ?>\n' + xml_str
|
||||
|
||||
return xml_str
|
||||
|
||||
|
||||
class MultiReservationXMLBuilder:
|
||||
"""Builder for creating XML documents with multiple reservations.
|
||||
|
||||
Example:
|
||||
builder = MultiReservationXMLBuilder()
|
||||
builder.add_reservation(
|
||||
ReservationXMLBuilder(...).set_guest(...).add_room(...)
|
||||
)
|
||||
builder.add_reservation(
|
||||
ReservationXMLBuilder(...).set_guest(...).add_room(...)
|
||||
)
|
||||
xml_string = builder.build_xml()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize multi-reservation builder."""
|
||||
self.reservations: list[ReservationXMLBuilder] = []
|
||||
|
||||
def add_reservation(
|
||||
self, reservation_builder: ReservationXMLBuilder
|
||||
) -> "MultiReservationXMLBuilder":
|
||||
"""Add a reservation to the document.
|
||||
|
||||
Args:
|
||||
reservation_builder: ReservationXMLBuilder instance
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
self.reservations.append(reservation_builder)
|
||||
return self
|
||||
|
||||
def build_xml(self, include_xml_declaration: bool = True) -> str:
|
||||
"""Build the complete XML string with all reservations.
|
||||
|
||||
Args:
|
||||
include_xml_declaration: Whether to include <?xml version="1.0"?> declaration
|
||||
|
||||
Returns:
|
||||
XML string with multiple reservations
|
||||
"""
|
||||
root = ET.Element("reservations")
|
||||
|
||||
for reservation_builder in self.reservations:
|
||||
reservation_elem = reservation_builder.build()
|
||||
root.append(reservation_elem)
|
||||
|
||||
xml_str = ET.tostring(root, encoding="unicode")
|
||||
|
||||
if include_xml_declaration:
|
||||
xml_str = '<?xml version="1.0" ?>\n' + xml_str
|
||||
|
||||
return xml_str
|
||||
@@ -340,13 +340,348 @@ class TestConversionServiceWithImportedData:
|
||||
assert stats["total_daily_sales"] == 0
|
||||
assert stats["errors"] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_reservations(self, test_db_session):
|
||||
"""Test that room entries are correctly updated when reservation status changes.
|
||||
|
||||
This test detects a bug where ConversionRoom records are not properly upserted
|
||||
when the same reservation is processed multiple times with different room numbers.
|
||||
|
||||
Scenario:
|
||||
1. Process reservation with status='request', no revenue, room_number='101'
|
||||
2. Process reservation with status='reservation', with revenue, room_number='102'
|
||||
3. Swap: Process same reservations but reversed - first one now has status='reservation'
|
||||
with room_number='201', second has status='request' with room_number='202'
|
||||
4. The old room entries (101, 102) should no longer exist in the database
|
||||
"""
|
||||
from tests.helpers import ReservationXMLBuilder, MultiReservationXMLBuilder
|
||||
|
||||
# First batch: Process two reservations
|
||||
multi_builder1 = MultiReservationXMLBuilder()
|
||||
|
||||
# Reservation 1: Request status, no revenue, room 101
|
||||
res1_v1 = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="res_001",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
reservation_type="request",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="Alice",
|
||||
last_name="Johnson",
|
||||
email="alice@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-03",
|
||||
room_number="101",
|
||||
status="request",
|
||||
# No revenue
|
||||
)
|
||||
)
|
||||
multi_builder1.add_reservation(res1_v1)
|
||||
|
||||
# Reservation 2: Reservation status, with revenue, room 102
|
||||
res2_v1 = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="res_002",
|
||||
reservation_number="RES-002",
|
||||
reservation_date="2025-11-15",
|
||||
reservation_type="reservation",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_002",
|
||||
first_name="Bob",
|
||||
last_name="Smith",
|
||||
email="bob@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-10",
|
||||
departure="2025-12-12",
|
||||
room_number="102",
|
||||
status="reserved",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
)
|
||||
multi_builder1.add_reservation(res2_v1)
|
||||
|
||||
xml_content1 = multi_builder1.build_xml()
|
||||
|
||||
# Process first batch
|
||||
service = ConversionService(test_db_session)
|
||||
stats1 = await service.process_conversion_xml(xml_content1)
|
||||
|
||||
assert stats1["total_reservations"] == 2
|
||||
|
||||
# Verify rooms exist in database
|
||||
result = await test_db_session.execute(
|
||||
select(ConversionRoom).where(ConversionRoom.room_number == "101")
|
||||
)
|
||||
room_101 = result.scalar_one_or_none()
|
||||
assert room_101 is not None, "Room 101 should exist after first processing"
|
||||
|
||||
result = await test_db_session.execute(
|
||||
select(ConversionRoom).where(ConversionRoom.room_number == "102")
|
||||
)
|
||||
room_102 = result.scalar_one_or_none()
|
||||
assert room_102 is not None, "Room 102 should exist after first processing"
|
||||
|
||||
# Second batch: Swap the reservations and change room numbers
|
||||
multi_builder2 = MultiReservationXMLBuilder()
|
||||
|
||||
# Reservation 1: NOW has reservation status, with revenue, room 201 (changed from 101)
|
||||
res1_v2 = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="res_001", # Same ID
|
||||
reservation_number="RES-001", # Same number
|
||||
reservation_date="2025-11-14",
|
||||
reservation_type="reservation", # Changed from request
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="Alice",
|
||||
last_name="Johnson",
|
||||
email="alice@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-03",
|
||||
room_number="201", # Changed from 101
|
||||
status="reserved",
|
||||
revenue_logis_per_day=200.0, # Now has revenue
|
||||
)
|
||||
)
|
||||
multi_builder2.add_reservation(res1_v2)
|
||||
|
||||
# Reservation 2: NOW has request status, no revenue, room 202 (changed from 102)
|
||||
res2_v2 = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="res_002", # Same ID
|
||||
reservation_number="RES-002", # Same number
|
||||
reservation_date="2025-11-15",
|
||||
reservation_type="request", # Changed from reservation
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_002",
|
||||
first_name="Bob",
|
||||
last_name="Smith",
|
||||
email="bob@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-10",
|
||||
departure="2025-12-12",
|
||||
room_number="202", # Changed from 102
|
||||
status="request",
|
||||
# No revenue anymore
|
||||
)
|
||||
)
|
||||
multi_builder2.add_reservation(res2_v2)
|
||||
|
||||
xml_content2 = multi_builder2.build_xml()
|
||||
|
||||
# Process second batch
|
||||
stats2 = await service.process_conversion_xml(xml_content2)
|
||||
|
||||
assert stats2["total_reservations"] == 2
|
||||
|
||||
# BUG DETECTION: Old room entries (101, 102) should NOT exist anymore
|
||||
# They should have been replaced by new room entries (201, 202)
|
||||
|
||||
result = await test_db_session.execute(
|
||||
select(ConversionRoom).where(ConversionRoom.room_number == "101")
|
||||
)
|
||||
room_101_after = result.scalar_one_or_none()
|
||||
assert room_101_after is None, (
|
||||
"BUG: Room 101 should no longer exist after reprocessing with room 201. "
|
||||
"Old room entries are not being removed when reservation is updated."
|
||||
)
|
||||
|
||||
result = await test_db_session.execute(
|
||||
select(ConversionRoom).where(ConversionRoom.room_number == "102")
|
||||
)
|
||||
room_102_after = result.scalar_one_or_none()
|
||||
assert room_102_after is None, (
|
||||
"BUG: Room 102 should no longer exist after reprocessing with room 202. "
|
||||
"Old room entries are not being removed when reservation is updated."
|
||||
)
|
||||
|
||||
# New room entries should exist
|
||||
result = await test_db_session.execute(
|
||||
select(ConversionRoom).where(ConversionRoom.room_number == "201")
|
||||
)
|
||||
room_201 = result.scalar_one_or_none()
|
||||
assert room_201 is not None, "Room 201 should exist after second processing"
|
||||
|
||||
result = await test_db_session.execute(
|
||||
select(ConversionRoom).where(ConversionRoom.room_number == "202")
|
||||
)
|
||||
room_202 = result.scalar_one_or_none()
|
||||
assert room_202 is not None, "Room 202 should exist after second processing"
|
||||
|
||||
# Verify we only have 2 conversion room records total (not 4)
|
||||
result = await test_db_session.execute(select(ConversionRoom))
|
||||
all_rooms = result.scalars().all()
|
||||
assert len(all_rooms) == 2, (
|
||||
f"BUG: Expected 2 conversion rooms total, but found {len(all_rooms)}. "
|
||||
f"Old room entries are not being deleted. Room numbers: {[r.room_number for r in all_rooms]}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TestXMLBuilderUsage:
|
||||
"""Demonstrate usage of XML builder helpers for creating test data."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_using_xml_builder_for_simple_reservation(self, test_db_session):
|
||||
"""Example: Create a simple reservation using the XML builder helper."""
|
||||
from tests.helpers import ReservationXMLBuilder
|
||||
|
||||
# Build a reservation with convenient fluent API
|
||||
xml_content = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="test_123",
|
||||
reservation_number="RES-123",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
country_code="US",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_type="DZV",
|
||||
room_number="101",
|
||||
revenue_logis_per_day=150.0,
|
||||
adults=2
|
||||
)
|
||||
.build_xml()
|
||||
)
|
||||
|
||||
# Process the XML
|
||||
service = ConversionService(test_db_session)
|
||||
stats = await service.process_conversion_xml(xml_content)
|
||||
|
||||
assert stats["total_reservations"] == 1
|
||||
assert stats["total_daily_sales"] == 5 # 4 nights + departure day
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_using_xml_builder_for_multi_room_reservation(
|
||||
self, test_db_session
|
||||
):
|
||||
"""Example: Create a reservation with multiple rooms."""
|
||||
from tests.helpers import ReservationXMLBuilder
|
||||
|
||||
xml_content = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="test_456",
|
||||
reservation_number="RES-456",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_002",
|
||||
first_name="Jane",
|
||||
last_name="Smith",
|
||||
email="jane@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_number="101",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_number="102",
|
||||
revenue_logis_per_day=200.0,
|
||||
)
|
||||
.build_xml()
|
||||
)
|
||||
|
||||
service = ConversionService(test_db_session)
|
||||
stats = await service.process_conversion_xml(xml_content)
|
||||
|
||||
assert stats["total_reservations"] == 1
|
||||
# 2 rooms × 5 daily sales each = 10 total
|
||||
assert stats["total_daily_sales"] == 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_using_multi_reservation_builder(self, test_db_session):
|
||||
"""Example: Create multiple reservations in one XML document."""
|
||||
from tests.helpers import ReservationXMLBuilder, MultiReservationXMLBuilder
|
||||
|
||||
multi_builder = MultiReservationXMLBuilder()
|
||||
|
||||
# Add first reservation
|
||||
res1 = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="test_001",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="Alice",
|
||||
last_name="Johnson",
|
||||
email="alice@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-03",
|
||||
revenue_logis_per_day=100.0,
|
||||
)
|
||||
)
|
||||
multi_builder.add_reservation(res1)
|
||||
|
||||
# Add second reservation
|
||||
res2 = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="test_002",
|
||||
reservation_number="RES-002",
|
||||
reservation_date="2025-11-15",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_002",
|
||||
first_name="Bob",
|
||||
last_name="Williams",
|
||||
email="bob@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-10",
|
||||
departure="2025-12-12",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
)
|
||||
multi_builder.add_reservation(res2)
|
||||
|
||||
xml_content = multi_builder.build_xml()
|
||||
|
||||
# Process the XML
|
||||
service = ConversionService(test_db_session)
|
||||
stats = await service.process_conversion_xml(xml_content)
|
||||
|
||||
assert stats["total_reservations"] == 2
|
||||
# Res1: 3 days (2 nights), Res2: 3 days (2 nights) = 6 total
|
||||
assert stats["total_daily_sales"] == 6
|
||||
|
||||
|
||||
class TestHashedMatchingLogic:
|
||||
"""Test the hashed matching logic used in ConversionService."""
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conversion_guest_hashed_fields_are_populated(
|
||||
self, test_db_session
|
||||
|
||||
327
tests/test_xml_builders.py
Normal file
327
tests/test_xml_builders.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""Tests for XML builder helpers."""
|
||||
|
||||
import pytest
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from tests.helpers.xml_builders import (
|
||||
ReservationXMLBuilder,
|
||||
MultiReservationXMLBuilder,
|
||||
RoomReservationBuilder,
|
||||
)
|
||||
|
||||
|
||||
class TestRoomReservationBuilder:
|
||||
"""Test RoomReservationBuilder functionality."""
|
||||
|
||||
def test_basic_room_without_revenue(self):
|
||||
"""Test creating a basic room reservation without revenue."""
|
||||
builder = RoomReservationBuilder(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-03",
|
||||
room_type="DZV",
|
||||
room_number="101",
|
||||
)
|
||||
|
||||
elem = builder.build()
|
||||
|
||||
assert elem.tag == "roomReservation"
|
||||
assert elem.get("arrival") == "2025-12-01"
|
||||
assert elem.get("departure") == "2025-12-03"
|
||||
assert elem.get("roomType") == "DZV"
|
||||
assert elem.get("roomNumber") == "101"
|
||||
|
||||
# Check daily sales - should have 3 entries (12-01, 12-02, 12-03)
|
||||
daily_sales = elem.find("dailySales")
|
||||
assert daily_sales is not None
|
||||
daily_sale_elements = daily_sales.findall("dailySale")
|
||||
assert len(daily_sale_elements) == 3
|
||||
|
||||
# First two should have no revenue attributes
|
||||
assert daily_sale_elements[0].get("revenueTotal") is None
|
||||
assert daily_sale_elements[0].get("revenueLogis") is None
|
||||
|
||||
def test_room_with_revenue(self):
|
||||
"""Test creating a room with revenue per day."""
|
||||
builder = RoomReservationBuilder(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-03",
|
||||
room_type="DZV",
|
||||
room_number="101",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
|
||||
elem = builder.build()
|
||||
daily_sales = elem.find("dailySales")
|
||||
daily_sale_elements = daily_sales.findall("dailySale")
|
||||
|
||||
# Should have 3 entries total
|
||||
assert len(daily_sale_elements) == 3
|
||||
|
||||
# First two days should have revenue
|
||||
assert daily_sale_elements[0].get("revenueTotal") == "150.0"
|
||||
assert daily_sale_elements[0].get("revenueLogis") == "150.0"
|
||||
assert daily_sale_elements[1].get("revenueTotal") == "150.0"
|
||||
assert daily_sale_elements[1].get("revenueLogis") == "150.0"
|
||||
|
||||
# Departure day should have no revenue
|
||||
assert daily_sale_elements[2].get("revenueTotal") is None
|
||||
assert daily_sale_elements[2].get("revenueLogis") is None
|
||||
|
||||
def test_room_with_children_and_infants(self):
|
||||
"""Test room with children and infants attributes."""
|
||||
builder = RoomReservationBuilder(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-02",
|
||||
adults=2,
|
||||
children=1,
|
||||
infants=1,
|
||||
)
|
||||
|
||||
elem = builder.build()
|
||||
assert elem.get("adults") == "2"
|
||||
assert elem.get("children") == "1"
|
||||
assert elem.get("infants") == "1"
|
||||
|
||||
|
||||
class TestReservationXMLBuilder:
|
||||
"""Test ReservationXMLBuilder functionality."""
|
||||
|
||||
def test_basic_reservation(self):
|
||||
"""Test creating a basic reservation with one room."""
|
||||
builder = ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
builder.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
builder.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
|
||||
xml_string = builder.build_xml()
|
||||
|
||||
# Parse and verify structure
|
||||
root = ET.fromstring(xml_string)
|
||||
assert root.tag == "reservations"
|
||||
|
||||
reservation = root.find("reservation")
|
||||
assert reservation is not None
|
||||
assert reservation.get("hotelID") == "39054_001"
|
||||
assert reservation.get("id") == "12345"
|
||||
assert reservation.get("number") == "RES-001"
|
||||
|
||||
guest = reservation.find("guest")
|
||||
assert guest is not None
|
||||
assert guest.get("firstName") == "John"
|
||||
assert guest.get("lastName") == "Doe"
|
||||
assert guest.get("email") == "john@example.com"
|
||||
|
||||
room_reservations = reservation.find("roomReservations")
|
||||
assert room_reservations is not None
|
||||
rooms = room_reservations.findall("roomReservation")
|
||||
assert len(rooms) == 1
|
||||
|
||||
def test_reservation_with_multiple_rooms(self):
|
||||
"""Test reservation with multiple rooms."""
|
||||
builder = ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
builder.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
builder.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_number="101",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
builder.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
room_number="102",
|
||||
revenue_logis_per_day=200.0,
|
||||
)
|
||||
|
||||
xml_string = builder.build_xml()
|
||||
root = ET.fromstring(xml_string)
|
||||
|
||||
reservation = root.find("reservation")
|
||||
room_reservations = reservation.find("roomReservations")
|
||||
rooms = room_reservations.findall("roomReservation")
|
||||
|
||||
assert len(rooms) == 2
|
||||
assert rooms[0].get("roomNumber") == "101"
|
||||
assert rooms[1].get("roomNumber") == "102"
|
||||
|
||||
def test_reservation_with_advertising_data(self):
|
||||
"""Test reservation with advertising campaign data."""
|
||||
builder = ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
advertising_medium="99TALES",
|
||||
advertising_partner="google",
|
||||
advertising_campagne="EAIaIQobChMI...",
|
||||
)
|
||||
builder.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
builder.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
)
|
||||
|
||||
xml_string = builder.build_xml()
|
||||
root = ET.fromstring(xml_string)
|
||||
|
||||
reservation = root.find("reservation")
|
||||
assert reservation.get("advertisingMedium") == "99TALES"
|
||||
assert reservation.get("advertisingPartner") == "google"
|
||||
assert reservation.get("advertisingCampagne") == "EAIaIQobChMI..."
|
||||
|
||||
|
||||
class TestMultiReservationXMLBuilder:
|
||||
"""Test MultiReservationXMLBuilder functionality."""
|
||||
|
||||
def test_multiple_reservations(self):
|
||||
"""Test creating XML with multiple reservations."""
|
||||
multi_builder = MultiReservationXMLBuilder()
|
||||
|
||||
# Add first reservation
|
||||
res1 = ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
res1.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
res1.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-03",
|
||||
revenue_logis_per_day=150.0,
|
||||
)
|
||||
multi_builder.add_reservation(res1)
|
||||
|
||||
# Add second reservation
|
||||
res2 = ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12346",
|
||||
reservation_number="RES-002",
|
||||
reservation_date="2025-11-15",
|
||||
)
|
||||
res2.set_guest(
|
||||
guest_id="guest_002",
|
||||
first_name="Jane",
|
||||
last_name="Smith",
|
||||
email="jane@example.com",
|
||||
)
|
||||
res2.add_room(
|
||||
arrival="2025-12-10",
|
||||
departure="2025-12-12",
|
||||
revenue_logis_per_day=200.0,
|
||||
)
|
||||
multi_builder.add_reservation(res2)
|
||||
|
||||
xml_string = multi_builder.build_xml()
|
||||
root = ET.fromstring(xml_string)
|
||||
|
||||
assert root.tag == "reservations"
|
||||
reservations = root.findall("reservation")
|
||||
assert len(reservations) == 2
|
||||
assert reservations[0].get("id") == "12345"
|
||||
assert reservations[1].get("id") == "12346"
|
||||
|
||||
|
||||
class TestConvenienceFeatures:
|
||||
"""Test convenience features for common test scenarios."""
|
||||
|
||||
def test_simple_one_liner_reservation(self):
|
||||
"""Test creating a simple reservation in a fluent style."""
|
||||
xml = (
|
||||
ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
.add_room(
|
||||
arrival="2025-12-01",
|
||||
departure="2025-12-05",
|
||||
revenue_logis_per_day=160.0,
|
||||
)
|
||||
.build_xml()
|
||||
)
|
||||
|
||||
assert '<?xml version="1.0" ?>' in xml
|
||||
assert 'hotelID="39054_001"' in xml
|
||||
assert 'revenueLogis="160.0"' in xml
|
||||
|
||||
def test_revenue_calculation_for_multi_day_stay(self):
|
||||
"""Test that daily sales are correctly generated for multi-day stays."""
|
||||
builder = ReservationXMLBuilder(
|
||||
hotel_id="39054_001",
|
||||
reservation_id="12345",
|
||||
reservation_number="RES-001",
|
||||
reservation_date="2025-11-14",
|
||||
)
|
||||
builder.set_guest(
|
||||
guest_id="guest_001",
|
||||
first_name="John",
|
||||
last_name="Doe",
|
||||
email="john@example.com",
|
||||
)
|
||||
# 7-day stay (June 25 - July 2, 7 nights)
|
||||
builder.add_room(
|
||||
arrival="2026-06-25",
|
||||
departure="2026-07-02",
|
||||
revenue_logis_per_day=160.0,
|
||||
)
|
||||
|
||||
elem = builder.build()
|
||||
room_reservations = elem.find("roomReservations")
|
||||
room = room_reservations.find("roomReservation")
|
||||
daily_sales = room.find("dailySales")
|
||||
daily_sale_elements = daily_sales.findall("dailySale")
|
||||
|
||||
# Should have 8 daily sale entries (7 nights + departure day)
|
||||
assert len(daily_sale_elements) == 8
|
||||
|
||||
# First 7 should have revenue
|
||||
for i in range(7):
|
||||
assert daily_sale_elements[i].get("revenueLogis") == "160.0"
|
||||
|
||||
# Departure day should not have revenue
|
||||
assert daily_sale_elements[7].get("revenueLogis") is None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user