Files
alpinebits_python/src/alpine_bits_python/alpine_bits_helpers.py
2025-12-03 10:41:34 +01:00

936 lines
33 KiB
Python

import re
import traceback
from dataclasses import dataclass
from datetime import UTC
from enum import Enum
from typing import Any
from email_validator import EmailNotValidError, validate_email
from alpine_bits_python.db import Customer, Reservation
from alpine_bits_python.logging_config import get_logger
from alpine_bits_python.schemas import (
CommentData,
CommentListItemData,
CommentsData,
CustomerData,
HotelReservationIdData,
PhoneTechType,
)
# Import the generated classes
from .generated.alpinebits import (
CommentName2,
HotelReservationResStatus,
OtaHotelResNotifRq,
OtaResRetrieveRs,
ProfileProfileType,
RoomTypeRoomType,
UniqueIdType2,
)
_LOGGER = get_logger(__name__)
# Define type aliases for the two Customer types
NotifCustomer = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGuests.ResGuest.Profiles.ProfileInfo.Profile.Customer # noqa: E501
RetrieveCustomer = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGuests.ResGuest.Profiles.ProfileInfo.Profile.Customer # noqa: E501
# Define type aliases for HotelReservationId types
NotifHotelReservationId = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.HotelReservationIds.HotelReservationId # noqa: E501
RetrieveHotelReservationId = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.HotelReservationIds.HotelReservationId # noqa: E501
# Define type aliases for Comments types
NotifComments = (
OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments
)
RetrieveComments = (
OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments
)
NotifComment = (
OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments.Comment
)
RetrieveComment = (
OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments.Comment
)
# type aliases for GuestCounts
NotifGuestCounts = (
OtaHotelResNotifRq.HotelReservations.HotelReservation.RoomStays.RoomStay.GuestCounts
)
RetrieveGuestCounts = (
OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.GuestCounts
)
NotifUniqueId = OtaHotelResNotifRq.HotelReservations.HotelReservation.UniqueId
RetrieveUniqueId = OtaResRetrieveRs.ReservationsList.HotelReservation.UniqueId
NotifTimeSpan = (
OtaHotelResNotifRq.HotelReservations.HotelReservation.RoomStays.RoomStay.TimeSpan
)
RetrieveTimeSpan = (
OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.TimeSpan
)
NotifRoomStays = OtaHotelResNotifRq.HotelReservations.HotelReservation.RoomStays
RetrieveRoomStays = OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays
NotifHotelReservation = OtaHotelResNotifRq.HotelReservations.HotelReservation
RetrieveHotelReservation = OtaResRetrieveRs.ReservationsList.HotelReservation
NotifRoomTypes = (
OtaHotelResNotifRq.HotelReservations.HotelReservation.RoomStays.RoomStay.RoomTypes
)
RetrieveRoomTypes = (
OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.RoomTypes
)
from .const import RESERVATION_ID_TYPE
# Enum to specify which OTA message type to use
class OtaMessageType(Enum):
NOTIF = "notification" # For OtaHotelResNotifRq
RETRIEVE = "retrieve" # For OtaResRetrieveRs
@dataclass
class KidsAgeData:
"""Data class to hold information about children's ages."""
ages: list[int]
class GuestCountsFactory:
"""Factory class to create GuestCounts instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
@staticmethod
def create_guest_counts(
adults: int,
kids: list[int] | None = None,
message_type: OtaMessageType = OtaMessageType.RETRIEVE,
) -> NotifGuestCounts:
"""Create a GuestCounts object for OtaHotelResNotifRq or OtaResRetrieveRs.
:param adults: Number of adults
:param kids: List of ages for each kid (optional)
:return: GuestCounts instance
"""
if message_type == OtaMessageType.RETRIEVE:
return GuestCountsFactory._create_guest_counts(
adults, kids, RetrieveGuestCounts
)
if message_type == OtaMessageType.NOTIF:
return GuestCountsFactory._create_guest_counts(
adults, kids, NotifGuestCounts
)
raise ValueError(f"Unsupported message type: {message_type}")
@staticmethod
def _create_guest_counts(
adults: int, kids: list[int] | None, guest_counts_class: type
) -> Any:
"""Create a GuestCounts object of the specified type.
:param adults: Number of adults
:param kids: List of ages for each kid (optional)
:param guest_counts_class: The GuestCounts class to instantiate
:return: GuestCounts instance
"""
GuestCount = guest_counts_class.GuestCount
guest_count_list = []
if adults > 0:
guest_count_list.append(GuestCount(count=str(adults)))
if kids:
# create a dict with amount of kids for each age
age_count = {}
for age in kids:
if age in age_count:
age_count[age] += 1
else:
age_count[age] = 1
for age, count in age_count.items():
guest_count_list.append(GuestCount(count=str(count), age=str(age)))
return guest_counts_class(guest_count=guest_count_list)
class CustomerFactory:
"""Factory class to create Customer instances for both Retrieve and Notif."""
@staticmethod
def create_notif_customer(data: CustomerData) -> NotifCustomer:
"""Create a Customer for OtaHotelResNotifRq."""
return CustomerFactory._create_customer(NotifCustomer, data)
@staticmethod
def create_retrieve_customer(data: CustomerData) -> RetrieveCustomer:
"""Create a Customer for OtaResRetrieveRs."""
return CustomerFactory._create_customer(RetrieveCustomer, data)
@staticmethod
def _create_customer(
customer_class: type[RetrieveCustomer | NotifCustomer], data: CustomerData
) -> Any:
"""Create a customer of the specified type."""
# Create PersonName
person_name = customer_class.PersonName(
given_name=data.given_name,
surname=data.surname,
name_prefix=data.name_prefix,
name_title=data.name_title,
)
# Create telephone list
telephones = []
for phone_number, phone_tech_type in data.phone_numbers:
telephone = customer_class.Telephone(
phone_number=phone_number,
phone_tech_type=phone_tech_type.value if phone_tech_type else None,
)
telephones.append(telephone)
# Create email if provided
email = None
if data.email_address:
remark = None
if data.email_newsletter is not None:
remark = f"newsletter:{'yes' if data.email_newsletter else 'no'}"
email = customer_class.Email(value=data.email_address, remark=remark)
# Create address if any address fields are provided
address = None
if any(
[data.address_line, data.city_name, data.postal_code, data.country_code]
):
country_name = None
if data.country_code:
country_name = customer_class.Address.CountryName(
code=data.country_code
)
address_remark = None
if data.address_catalog is not None:
address_remark = f"catalog:{'yes' if data.address_catalog else 'no'}"
address = customer_class.Address(
address_line=data.address_line,
city_name=data.city_name,
postal_code=data.postal_code,
country_name=country_name,
remark=address_remark,
)
# Create the customer
return customer_class(
person_name=person_name,
telephone=telephones,
email=email,
address=address,
gender=data.gender,
birth_date=data.birth_date,
language=data.language,
)
@staticmethod
def from_notif_customer(customer: NotifCustomer) -> CustomerData:
"""Convert a NotifCustomer back to CustomerData."""
return CustomerFactory._customer_to_data(customer)
@staticmethod
def from_retrieve_customer(customer: RetrieveCustomer) -> CustomerData:
"""Convert a RetrieveCustomer back to CustomerData."""
return CustomerFactory._customer_to_data(customer)
@staticmethod
def _customer_to_data(customer: Any) -> CustomerData:
"""Convert any customer type to CustomerData."""
# Extract phone numbers
phone_numbers = []
if customer.telephone:
phone_numbers.extend(
[
(
tel.phone_number,
PhoneTechType(tel.phone_tech_type)
if tel.phone_tech_type
else None,
)
for tel in customer.telephone
]
)
# Extract email info
email_address = None
email_newsletter = None
if customer.email:
email_address = customer.email.value
if customer.email.remark:
if "newsletter:yes" in customer.email.remark:
email_newsletter = True
elif "newsletter:no" in customer.email.remark:
email_newsletter = False
# Extract address info
address_line = None
city_name = None
postal_code = None
country_code = None
address_catalog = None
if customer.address:
address_line = customer.address.address_line
city_name = customer.address.city_name
postal_code = customer.address.postal_code
if customer.address.country_name:
country_code = customer.address.country_name.code
if customer.address.remark:
if "catalog:yes" in customer.address.remark:
address_catalog = True
elif "catalog:no" in customer.address.remark:
address_catalog = False
return CustomerData(
given_name=customer.person_name.given_name,
surname=customer.person_name.surname,
name_prefix=customer.person_name.name_prefix,
name_title=customer.person_name.name_title,
phone_numbers=phone_numbers,
email_address=email_address,
email_newsletter=email_newsletter,
address_line=address_line,
city_name=city_name,
postal_code=postal_code,
country_code=country_code,
address_catalog=address_catalog,
gender=customer.gender,
birth_date=customer.birth_date,
language=customer.language,
)
class HotelReservationIdFactory:
"""Factory class to create HotelReservationId instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
@staticmethod
def create_notif_hotel_reservation_id(
data: HotelReservationIdData,
) -> NotifHotelReservationId:
"""Create a HotelReservationId for OtaHotelResNotifRq."""
return HotelReservationIdFactory._create_hotel_reservation_id(
NotifHotelReservationId, data
)
@staticmethod
def create_retrieve_hotel_reservation_id(
data: HotelReservationIdData,
) -> RetrieveHotelReservationId:
"""Create a HotelReservationId for OtaResRetrieveRs."""
return HotelReservationIdFactory._create_hotel_reservation_id(
RetrieveHotelReservationId, data
)
@staticmethod
def _create_hotel_reservation_id(
hotel_reservation_id_class: type, data: HotelReservationIdData
) -> Any:
"""Create a hotel reservation id of the specified type."""
return hotel_reservation_id_class(
res_id_type=data.res_id_type,
res_id_value=data.res_id_value,
res_id_source=data.res_id_source,
res_id_source_context=data.res_id_source_context,
)
@staticmethod
def from_notif_hotel_reservation_id(
hotel_reservation_id: NotifHotelReservationId,
) -> HotelReservationIdData:
"""Convert a NotifHotelReservationId back to HotelReservationIdData."""
return HotelReservationIdFactory._hotel_reservation_id_to_data(
hotel_reservation_id
)
@staticmethod
def from_retrieve_hotel_reservation_id(
hotel_reservation_id: RetrieveHotelReservationId,
) -> HotelReservationIdData:
"""Convert a RetrieveHotelReservationId back to HotelReservationIdData."""
return HotelReservationIdFactory._hotel_reservation_id_to_data(
hotel_reservation_id
)
@staticmethod
def _hotel_reservation_id_to_data(
hotel_reservation_id: Any,
) -> HotelReservationIdData:
"""Internal method to convert any hotel reservation id type to HotelReservationIdData."""
return HotelReservationIdData(
res_id_type=hotel_reservation_id.res_id_type,
res_id_value=hotel_reservation_id.res_id_value,
res_id_source=hotel_reservation_id.res_id_source,
res_id_source_context=hotel_reservation_id.res_id_source_context,
)
class CommentFactory:
"""Factory class to create Comment instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
@staticmethod
def create_notif_comments(data: CommentsData) -> NotifComments:
"""Create Comments for OtaHotelResNotifRq."""
return CommentFactory._create_comments(NotifComments, NotifComment, data)
@staticmethod
def create_retrieve_comments(data: CommentsData) -> RetrieveComments:
"""Create Comments for OtaResRetrieveRs."""
return CommentFactory._create_comments(RetrieveComments, RetrieveComment, data)
@staticmethod
def _create_comments(
comments_class: type[RetrieveComments] | type[NotifComments],
comment_class: type[RetrieveComment] | type[NotifComment],
data: CommentsData,
) -> Any:
"""Internal method to create comments of the specified type."""
comments_list = []
for comment_data in data.comments:
# Create list items
list_items = []
for item_data in comment_data.list_items:
_LOGGER.debug(
"Creating list item: value=%s, list_item=%s, language=%s",
item_data.value,
item_data.list_item,
item_data.language,
)
list_item = comment_class.ListItem(
value=item_data.value,
list_item=item_data.list_item,
language=item_data.language,
)
list_items.append(list_item)
# Create comment
comment = comment_class(
name=comment_data.name, text=comment_data.text, list_item=list_items
)
comments_list.append(comment)
# Create comments container
return comments_class(comment=comments_list)
@staticmethod
def from_notif_comments(comments: NotifComments) -> CommentsData:
"""Convert NotifComments back to CommentsData."""
return CommentFactory._comments_to_data(comments)
@staticmethod
def from_retrieve_comments(comments: RetrieveComments) -> CommentsData:
"""Convert RetrieveComments back to CommentsData."""
return CommentFactory._comments_to_data(comments)
@staticmethod
def _comments_to_data(comments: Any) -> CommentsData:
"""Internal method to convert any comments type to CommentsData."""
comments_data_list = []
for comment in comments.comment:
# Extract list items
list_items_data = []
if comment.list_item:
for list_item in comment.list_item:
list_items_data.append(
CommentListItemData(
value=list_item.value,
list_item=list_item.list_item,
language=list_item.language,
)
)
comments_data_list.append(comment)
return CommentsData(comments=comments_data_list)
# Define type aliases for ResGuests types
NotifResGuests = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGuests
RetrieveResGuests = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGuests
class ResGuestFactory:
"""Factory class to create complete ResGuests structures with a primary customer."""
@staticmethod
def create_notif_res_guests(customer_data: CustomerData) -> NotifResGuests:
"""Create a complete ResGuests structure for OtaHotelResNotifRq with primary customer."""
return ResGuestFactory._create_res_guests(
NotifResGuests, NotifCustomer, customer_data
)
@staticmethod
def create_retrieve_res_guests(customer_data: CustomerData) -> RetrieveResGuests:
"""Create a complete ResGuests structure for OtaResRetrieveRs with primary customer."""
return ResGuestFactory._create_res_guests(
RetrieveResGuests, RetrieveCustomer, customer_data
)
@staticmethod
def _create_res_guests(
res_guests_class: type[RetrieveResGuests] | type[NotifResGuests],
customer_class: type[NotifCustomer | RetrieveCustomer],
customer_data: CustomerData,
) -> Any:
"""Create the complete ResGuests structure."""
# Create the customer using the existing CustomerFactory
customer = CustomerFactory._create_customer(customer_class, customer_data)
# Create Profile with the customer
profile = res_guests_class.ResGuest.Profiles.ProfileInfo.Profile(
customer=customer
)
# Create ProfileInfo with the profile
profile_info = res_guests_class.ResGuest.Profiles.ProfileInfo(profile=profile)
# Create Profiles with the profile_info
profiles = res_guests_class.ResGuest.Profiles(profile_info=profile_info)
# Create ResGuest with the profiles
res_guest = res_guests_class.ResGuest(profiles=profiles)
# Create ResGuests with the res_guest
return res_guests_class(res_guest=res_guest)
@staticmethod
def extract_primary_customer(
res_guests: NotifResGuests | RetrieveResGuests,
) -> CustomerData:
"""Extract the primary customer data from a ResGuests structure."""
# Navigate down the nested structure to get the customer
customer = res_guests.res_guest.profiles.profile_info.profile.customer
# Use the existing CustomerFactory conversion method
if isinstance(res_guests, NotifResGuests):
return CustomerFactory.from_notif_customer(customer)
return CustomerFactory.from_retrieve_customer(customer)
class AlpineBitsFactory:
"""Unified factory class for creating AlpineBits objects with a simple interface."""
@staticmethod
def create(
data: CustomerData | HotelReservationIdData | CommentsData,
message_type: OtaMessageType,
) -> Any:
"""Create an AlpineBits object based on the data type and message type.
Args:
data: The data object (CustomerData, HotelReservationIdData, CommentsData, etc.)
message_type: Whether to create for NOTIF or RETRIEVE message types
Returns:
The appropriate AlpineBits object based on the data type and message type
"""
if isinstance(data, CustomerData):
if message_type == OtaMessageType.NOTIF:
return CustomerFactory.create_notif_customer(data)
return CustomerFactory.create_retrieve_customer(data)
if isinstance(data, HotelReservationIdData):
if message_type == OtaMessageType.NOTIF:
return HotelReservationIdFactory.create_notif_hotel_reservation_id(data)
return HotelReservationIdFactory.create_retrieve_hotel_reservation_id(data)
if isinstance(data, CommentsData):
if message_type == OtaMessageType.NOTIF:
return CommentFactory.create_notif_comments(data)
return CommentFactory.create_retrieve_comments(data)
raise ValueError(f"Unsupported data type: {type(data)}")
@staticmethod
def create_res_guests(
customer_data: CustomerData, message_type: OtaMessageType
) -> NotifResGuests | RetrieveResGuests:
"""Create a complete ResGuests structure with a primary customer.
Args:
customer_data: The customer data
message_type: Whether to create for NOTIF or RETRIEVE message types
Returns:
The appropriate ResGuests object
"""
if message_type == OtaMessageType.NOTIF:
return ResGuestFactory.create_notif_res_guests(customer_data)
return ResGuestFactory.create_retrieve_res_guests(customer_data)
@staticmethod
def extract_data(
obj: Any,
) -> CustomerData | HotelReservationIdData | CommentsData:
"""Extract data from an AlpineBits object back to a simple data class.
Args:
obj: The AlpineBits object to extract data from
Returns:
The appropriate data object
"""
# Check if it's a Customer object
if hasattr(obj, "person_name") and hasattr(obj.person_name, "given_name"):
if isinstance(obj, NotifCustomer):
return CustomerFactory.from_notif_customer(obj)
if isinstance(obj, RetrieveCustomer):
return CustomerFactory.from_retrieve_customer(obj)
# Check if it's a HotelReservationId object
elif hasattr(obj, "res_id_type"):
if isinstance(obj, NotifHotelReservationId):
return HotelReservationIdFactory.from_notif_hotel_reservation_id(obj)
if isinstance(obj, RetrieveHotelReservationId):
return HotelReservationIdFactory.from_retrieve_hotel_reservation_id(obj)
# Check if it's a Comments object
elif hasattr(obj, "comment"):
if isinstance(obj, NotifComments):
return CommentFactory.from_notif_comments(obj)
if isinstance(obj, RetrieveComments):
return CommentFactory.from_retrieve_comments(obj)
# Check if it's a ResGuests object
elif hasattr(obj, "res_guest"):
return ResGuestFactory.extract_primary_customer(obj)
else:
raise ValueError(f"Unsupported object type: {type(obj)}")
return None
def create_res_retrieve_response(
list: list[tuple[Reservation, Customer]], config: dict[str, Any]
) -> OtaResRetrieveRs:
"""Create RetrievedReservation XML from database entries."""
return _create_xml_from_db(list, OtaMessageType.RETRIEVE, config)
def create_res_notif_push_message(
list: tuple[Reservation, Customer], config: dict[str, Any]
):
"""Create Reservation Notification XML from database entries."""
return _create_xml_from_db(list, OtaMessageType.NOTIF, config)
def _validate_and_repair_email(email: str | None) -> str | None:
if email is None:
return None
try:
# remove numbers from top-level domain (TLD) if any
#email = re.sub(r"(\.\d+)(@|$)", r"\2", email)
email_info = validate_email(email)
except EmailNotValidError as e:
_LOGGER.warning("invalid email address: %s -> %s", email, e)
return None
return email_info.normalized
def _process_single_reservation(
reservation: Reservation,
customer: Customer,
message_type: OtaMessageType,
config: dict[str, Any],
):
phone_numbers = (
[(customer.phone, PhoneTechType.MOBILE)] if customer.phone is not None else []
)
# Validate and repair email address
email = _validate_and_repair_email(customer.email_address)
customer_data = CustomerData(
given_name=customer.given_name,
surname=customer.surname,
name_prefix=customer.name_prefix,
name_title=customer.name_title,
phone_numbers=phone_numbers,
email_address=email,
email_newsletter=customer.email_newsletter,
address_line=customer.address_line,
city_name=customer.city_name,
postal_code=customer.postal_code,
country_code=customer.country_code,
address_catalog=customer.address_catalog,
gender=customer.gender,
birth_date=customer.birth_date,
language=customer.language,
)
alpine_bits_factory = AlpineBitsFactory()
res_guests = alpine_bits_factory.create_res_guests(customer_data, message_type)
# Guest counts
children_ages = [int(a) for a in reservation.children_ages.split(",") if a]
guest_counts = GuestCountsFactory.create_guest_counts(
reservation.num_adults, children_ages, message_type
)
if message_type == OtaMessageType.NOTIF:
UniqueId = NotifUniqueId
RoomStays = NotifRoomStays
HotelReservation = NotifHotelReservation
Profile = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Profiles.ProfileInfo.Profile
elif message_type == OtaMessageType.RETRIEVE:
UniqueId = RetrieveUniqueId
RoomStays = RetrieveRoomStays
HotelReservation = RetrieveHotelReservation
Profile = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Profiles.ProfileInfo.Profile
else:
raise ValueError("Unsupported message type: %s", message_type.value)
unique_id_str = reservation.md5_unique_id
# UniqueID
unique_id = UniqueId(type_value=UniqueIdType2.VALUE_14, id=unique_id_str)
# TimeSpan
time_span = RoomStays.RoomStay.TimeSpan(
start=reservation.start_date.isoformat() if reservation.start_date else None,
end=reservation.end_date.isoformat() if reservation.end_date else None,
)
# RoomTypes (optional) - only create if at least one field is present
room_types = None
if any([reservation.room_type_code, reservation.room_classification_code, reservation.room_type]):
# Convert room_type string to enum if present
room_type_enum = None
if reservation.room_type:
room_type_enum = RoomTypeRoomType(reservation.room_type)
# Create RoomType instance
room_type_obj = RoomStays.RoomStay.RoomTypes.RoomType(
room_type_code=reservation.room_type_code,
room_classification_code=reservation.room_classification_code,
room_type=room_type_enum,
)
# Create RoomTypes container
room_types = RoomStays.RoomStay.RoomTypes(room_type=room_type_obj)
room_stay = RoomStays.RoomStay(
time_span=time_span,
guest_counts=guest_counts,
room_types=room_types,
)
room_stays = RoomStays(
room_stay=[room_stay],
)
# Always send md5_unique_id as the primary tracking ID
# This is guaranteed to fit in 64 chars and has low collision risk
res_id_value = reservation.md5_unique_id
res_id_source = "website"
# Determine the source based on available click tracking data (for informational purposes)
if reservation.fbclid != "":
res_id_source = "meta"
elif reservation.gclid != "":
res_id_source = "google"
# Get utm_medium if available, otherwise use source
if reservation.utm_medium is not None and str(reservation.utm_medium) != "":
res_id_source = str(reservation.utm_medium)
# Use Pydantic model for automatic validation and truncation
# It will automatically:
# - Trim whitespace
# - Truncate to 64 characters if needed
# - Convert empty strings to None
res_id_source_context = config["server"]["res_id_source_context"]
hotel_res_id_data = HotelReservationIdData(
res_id_type=RESERVATION_ID_TYPE,
res_id_value=res_id_value,
res_id_source=res_id_source,
res_id_source_context=res_id_source_context,
)
hotel_res_id = alpine_bits_factory.create(hotel_res_id_data, message_type)
hotel_res_ids = HotelReservation.ResGlobalInfo.HotelReservationIds(
hotel_reservation_id=[hotel_res_id]
)
if reservation.hotel_id is None:
raise ValueError("Reservation hotel_code is None")
hotel_code = str(reservation.hotel_id)
hotel_name = None if reservation.hotel_name is None else str(reservation.hotel_name)
basic_property_info = HotelReservation.ResGlobalInfo.BasicPropertyInfo(
hotel_code=hotel_code,
hotel_name=hotel_name,
)
# Comments
offer_comment = None
if reservation.offer is not None:
offer_comment = CommentData(
name=CommentName2.ADDITIONAL_INFO,
text="Angebot/Offerta: " + reservation.offer,
# list_items=[
# CommentListItemData(
# value=reservation.offer,
# language=customer.language,
# list_item="1",
# )
# ],
)
comment = None
if reservation.user_comment:
comment = CommentData(
name=CommentName2.CUSTOMER_COMMENT,
text=reservation.user_comment,
# list_items=[
# CommentListItemData(
# value="Landing page comment",
# language=customer.language,
# list_item="1",
# )
# ],
)
comments = [offer_comment, comment]
# filter out None comments
comments = [c for c in comments if c is not None]
comments_xml = None
if comments:
for c in comments:
_LOGGER.debug(
"Creating comment: name=%s, text=%s, list_items=%s",
c.name,
c.text,
len(c.list_items),
)
comments_data = CommentsData(comments=comments)
comments_xml = alpine_bits_factory.create(comments_data, message_type)
company_name_value = config["server"]["companyname"]
company_code = config["server"]["code"]
codecontext = config["server"]["codecontext"]
company_name = Profile.CompanyInfo.CompanyName(
value=company_name_value, code=company_code, code_context=codecontext
)
company_info = Profile.CompanyInfo(company_name=company_name)
profile = Profile(
company_info=company_info, profile_type=ProfileProfileType.VALUE_4
)
profile_info = HotelReservation.ResGlobalInfo.Profiles.ProfileInfo(profile=profile)
_LOGGER.info("Type of profile_info: %s", type(profile_info))
profiles = HotelReservation.ResGlobalInfo.Profiles(profile_info=profile_info)
res_global_info = HotelReservation.ResGlobalInfo(
hotel_reservation_ids=hotel_res_ids,
basic_property_info=basic_property_info,
comments=comments_xml,
profiles=profiles,
)
return HotelReservation(
create_date_time=reservation.created_at.replace(tzinfo=UTC).isoformat(),
res_status=HotelReservationResStatus.REQUESTED,
room_stay_reservation="true",
unique_id=unique_id,
room_stays=room_stays,
res_guests=res_guests,
res_global_info=res_global_info,
)
def _create_xml_from_db(
entries: list[tuple[Reservation, Customer]] | tuple[Reservation, Customer],
type: OtaMessageType,
config: dict[str, Any],
):
"""Create RetrievedReservation XML from database entries.
list of pairs (Reservation, Customer)
"""
reservations_list = []
# if entries isn't a list wrap the element in a list
if not isinstance(entries, list):
entries = [entries]
for reservation, customer in entries:
_LOGGER.info(
"Creating XML for reservation %s and customer %s",
reservation.id,
customer.id,
)
try:
hotel_reservation = _process_single_reservation(
reservation, customer, type, config
)
reservations_list.append(hotel_reservation)
except Exception:
_LOGGER.exception(
"Error creating XML for reservation %s and customer %s",
reservation.unique_id,
customer.given_name,
)
_LOGGER.debug(traceback.format_exc())
if type == OtaMessageType.NOTIF:
res_list_obj = OtaHotelResNotifRq.HotelReservations(
hotel_reservation=reservations_list
)
ota_hotel_res_notif_rq = OtaHotelResNotifRq(
version="7.000", hotel_reservations=res_list_obj
)
try:
ota_hotel_res_notif_rq.model_validate(ota_hotel_res_notif_rq.model_dump())
except Exception:
_LOGGER.exception("Validation error: ")
raise
return ota_hotel_res_notif_rq
if type == OtaMessageType.RETRIEVE:
res_list_obj = OtaResRetrieveRs.ReservationsList(
hotel_reservation=reservations_list
)
ota_res_retrieve_rs = OtaResRetrieveRs(
version="7.000", success="", reservations_list=res_list_obj
)
try:
ota_res_retrieve_rs.model_validate(ota_res_retrieve_rs.model_dump())
except Exception as e:
_LOGGER.exception(f"Validation error: {e}")
raise
return ota_res_retrieve_rs
raise ValueError(f"Unsupported message type: {type}")