431 lines
14 KiB
Python
431 lines
14 KiB
Python
"""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
|
|
|
|
|
|
def validate_and_convert_id(field_name: str, value: str | int) -> str:
|
|
"""Validate that an ID field is convertible to integer and return as string.
|
|
|
|
This helper ensures ID fields (like reservation_id, guest_id) are valid integers,
|
|
which is important since the Pydantic models will convert them from strings to ints.
|
|
|
|
Args:
|
|
field_name: Name of the field for error messages
|
|
value: The ID value (can be string or int)
|
|
|
|
Returns:
|
|
String representation of the validated integer ID
|
|
|
|
Raises:
|
|
ValueError: If value cannot be converted to a valid positive integer
|
|
|
|
"""
|
|
def _raise_invalid_type_error():
|
|
"""Raise error for invalid ID type."""
|
|
msg = (
|
|
f"{field_name} must be convertible to a positive integer, "
|
|
f"got: {value!r} (type: {type(value).__name__})"
|
|
)
|
|
raise ValueError(msg)
|
|
|
|
try:
|
|
# Convert to int first to validate it's a valid integer
|
|
int_value = int(value)
|
|
if int_value <= 0:
|
|
msg = f"{field_name} must be a positive integer, got: {value}"
|
|
raise ValueError(msg)
|
|
# Return as string for XML attributes
|
|
return str(int_value)
|
|
except (ValueError, TypeError):
|
|
_raise_invalid_type_error()
|
|
|
|
|
|
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 | int,
|
|
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 (must be convertible to positive integer)
|
|
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 = validate_and_convert_id("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 | int,
|
|
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 (must be convertible to positive integer)
|
|
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
|
|
"""
|
|
validated_guest_id = validate_and_convert_id("guest_id", guest_id)
|
|
self.guest_data = {
|
|
"id": validated_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
|