concurrency-fix #15
@@ -731,17 +731,10 @@ class ConversionService:
|
|||||||
# Flush to ensure conversion has an ID before creating room reservations
|
# Flush to ensure conversion has an ID before creating room reservations
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
# Batch-load existing room reservations to avoid N+1 queries
|
# Fetch ALL existing rooms for this conversion (not just the ones in current XML)
|
||||||
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
|
|
||||||
]
|
|
||||||
|
|
||||||
existing_rooms_result = await session.execute(
|
existing_rooms_result = await session.execute(
|
||||||
select(ConversionRoom).where(
|
select(ConversionRoom).where(
|
||||||
ConversionRoom.pms_hotel_reservation_id.in_(pms_hotel_reservation_ids)
|
ConversionRoom.conversion_id == conversion.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
existing_rooms = {
|
existing_rooms = {
|
||||||
@@ -749,6 +742,9 @@ class ConversionService:
|
|||||||
for room in existing_rooms_result.scalars().all()
|
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
|
# Process room reservations
|
||||||
for room_reservation in room_reservations.findall("roomReservation"):
|
for room_reservation in room_reservations.findall("roomReservation"):
|
||||||
# Extract room reservation details
|
# Extract room reservation details
|
||||||
@@ -786,6 +782,9 @@ class ConversionService:
|
|||||||
# This allows updating the same room reservation if it appears again
|
# This allows updating the same room reservation if it appears again
|
||||||
pms_hotel_reservation_id = f"{pms_reservation_id}_{room_number}"
|
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
|
# Process daily sales and extract total revenue
|
||||||
daily_sales_elem = room_reservation.find("dailySales")
|
daily_sales_elem = room_reservation.find("dailySales")
|
||||||
daily_sales_list = []
|
daily_sales_list = []
|
||||||
@@ -880,6 +879,24 @@ class ConversionService:
|
|||||||
num_adults,
|
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
|
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["total_daily_sales"] == 0
|
||||||
assert stats["errors"] == 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:
|
class TestHashedMatchingLogic:
|
||||||
"""Test the hashed matching logic used in ConversionService."""
|
"""Test the hashed matching logic used in ConversionService."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_conversion_guest_hashed_fields_are_populated(
|
async def test_conversion_guest_hashed_fields_are_populated(
|
||||||
self, test_db_session
|
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