Fixed room upsert logic

This commit is contained in:
Jonas Linter
2025-12-01 11:12:22 +01:00
parent 2be10ff899
commit 877b2909f2
6 changed files with 1293 additions and 12 deletions

197
tests/helpers/README.md Normal file
View 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
View File

@@ -0,0 +1,13 @@
"""Test helper utilities for creating test data."""
from .xml_builders import (
ReservationXMLBuilder,
MultiReservationXMLBuilder,
RoomReservationBuilder,
)
__all__ = [
"ReservationXMLBuilder",
"MultiReservationXMLBuilder",
"RoomReservationBuilder",
]

View 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