Fixed room upsert logic
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user