From 7f25fb2b0224b1454291fe26b05feebfdaf006f0 Mon Sep 17 00:00:00 2001 From: Jonas Linter Date: Mon, 29 Sep 2025 16:09:42 +0200 Subject: [PATCH] Works but form_id is not unique. Need to find something else --- logs/wix_test_data_20250929_160843.json | 170 ++++++++++++++++++ src/alpine_bits_python/alpine_bits_helpers.py | 150 +++++++++++++++- src/alpine_bits_python/alpinebits_server.py | 18 ++ 3 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 logs/wix_test_data_20250929_160843.json diff --git a/logs/wix_test_data_20250929_160843.json b/logs/wix_test_data_20250929_160843.json new file mode 100644 index 0000000..4873a18 --- /dev/null +++ b/logs/wix_test_data_20250929_160843.json @@ -0,0 +1,170 @@ +{ + "timestamp": "2025-09-29T16:08:43.177480", + "client_ip": "127.0.0.1", + "headers": { + "host": "localhost:8080", + "content-type": "application/json", + "user-agent": "insomnia/2023.5.8", + "accept": "*/*", + "content-length": "4518" + }, + "data": { + "data": { + "formName": "Contact us", + "submissions": [ + { + "label": "Anreisedatum", + "value": "2026-01-17" + }, + { + "label": "Abreisedatum", + "value": "2026-01-24" + }, + { + "label": "Anzahl Erwachsene", + "value": "2" + }, + { + "label": "Anzahl Kinder", + "value": "0" + }, + { + "label": "Anrede", + "value": "Herr" + }, + { + "label": "Vorname", + "value": "Weislinger " + }, + { + "label": "Nachname", + "value": "Alain " + }, + { + "label": "Email", + "value": "alain-et-evelyne@hotmail.fr" + }, + { + "label": "Phone", + "value": "+33 6 41 77 99 09" + }, + { + "label": "Einwilligung Marketing", + "value": "Cochée" + } + ], + "field:date_picker_7e65": "2026-01-24", + "field:number_7cf5": "2", + "submissionTime": "2025-09-27T19:36:39.137Z", + "field:form_field_5a7b": "Cochée", + "context": { + "metaSiteId": "7b28c2ce-1e20-4d07-9e86-73d822007e18", + "activationId": "d59c463c-96e0-4742-b4f7-70b8f0431168" + }, + "field:email_5139": "alain-et-evelyne@hotmail.fr", + "field:phone_4c77": "+33 6 41 77 99 09", + "_context": { + "activation": { + "id": "d59c463c-96e0-4742-b4f7-70b8f0431168" + }, + "configuration": { + "id": "483806f6-24ba-413f-9431-6b1ad9379f5c" + }, + "app": { + "id": "225dd912-7dea-4738-8688-4b8c6955ffc2" + }, + "action": { + "id": "a85d9873-f8ed-426a-90b0-fb64a8e50406" + }, + "trigger": { + "key": "wix_form_app-form_submitted" + } + }, + "formFieldMask": [ + "field:angebot_auswaehlen", + "field:date_picker_a7c8", + "field:date_picker_7e65", + "field:number_7cf5", + "field:anzahl_kinder", + "field:alter_kind_3", + "field:alter_kind_25", + "field:alter_kind_4", + "field:alter_kind_5", + "field:alter_kind_6", + "field:alter_kind_7", + "field:alter_kind_8", + "field:alter_kind_9", + "field:alter_kind_10", + "field:alter_kind_11", + "field:anrede", + "field:first_name_abae", + "field:last_name_d97c", + "field:email_5139", + "field:phone_4c77", + "field:long_answer_3524", + "field:form_field_5a7b", + "field:utm_source", + "field:utm_medium", + "field:utm_campaign", + "field:utm_term", + "field:utm_content", + "field:utm_term_id", + "field:utm_content_id", + "field:gad_source", + "field:gad_campaignid", + "field:gbraid", + "field:gclid", + "field:fbclid", + "metaSiteId" + ], + "contact": { + "name": { + "first": "Weislinger", + "last": "Alain" + }, + "email": "alain-et-evelyne@hotmail.fr", + "locale": "de-de", + "phones": [ + { + "tag": "UNTAGGED", + "formattedPhone": "+33 6 41 77 99 09", + "id": "90ffc824-1fd7-4167-b29f-24a4b62a0773", + "countryCode": "FR", + "e164Phone": "+33641779909", + "primary": true, + "phone": "6 41 77 99 09" + } + ], + "contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7", + "emails": [ + { + "id": "2c071108-2410-4db8-99fa-b50b75a02493", + "tag": "UNTAGGED", + "email": "alain-et-evelyne@hotmail.fr", + "primary": true + } + ], + "updatedDate": "2025-09-27T19:36:41.908Z", + "phone": "+33641779909", + "createdDate": "2025-09-27T19:36:41.054Z" + }, + "submissionId": "6cfee967-69a8-454a-a10e-0aa03868ba6d", + "field:anzahl_kinder": "0", + "field:first_name_abae": "Weislinger ", + "contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7", + "field:date_picker_a7c8": "2026-01-17", + "field:last_name_d97c": "Alain ", + "submissionsLink": "https://manage.wix.app/forms/submissions/7b28c2ce-1e20-4d07-9e86-73d822007e18/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F7b28c2ce-1e20-4d07-9e86-73d822007e18%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true", + "field:anrede": "Herr", + "formId": "e084006b-ae83-4e4d-b2f5-074118cdb3b1" + } + }, + "origin_header": null, + "all_headers": { + "host": "localhost:8080", + "content-type": "application/json", + "user-agent": "insomnia/2023.5.8", + "accept": "*/*", + "content-length": "4518" + } +} \ No newline at end of file diff --git a/src/alpine_bits_python/alpine_bits_helpers.py b/src/alpine_bits_python/alpine_bits_helpers.py index cce6ac1..ec14089 100644 --- a/src/alpine_bits_python/alpine_bits_helpers.py +++ b/src/alpine_bits_python/alpine_bits_helpers.py @@ -1,10 +1,19 @@ +from datetime import datetime, timezone from typing import Union, Optional, Any, TypeVar from pydantic import BaseModel, ConfigDict, Field from dataclasses import dataclass from enum import Enum +from typing import Tuple + +from alpine_bits_python.db import Customer, Reservation + # Import the generated classes -from .generated.alpinebits import OtaHotelResNotifRq, OtaResRetrieveRs, CommentName2 +from .generated.alpinebits import HotelReservationResStatus, OtaHotelResNotifRq, OtaResRetrieveRs, CommentName2, UniqueIdType2 +import logging + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.INFO) # Define type aliases for the two Customer types NotifCustomer = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGuests.ResGuest.Profiles.ProfileInfo.Profile.Customer @@ -650,7 +659,146 @@ class AlpineBitsFactory: raise ValueError(f"Unsupported object type: {type(obj)}") +def create_xml_from_db(list: list[Tuple[Reservation, Customer]]): + """ Create RetrievedReservation XML from database entries. + list of pairs (Reservation, Customer) + """ + + reservations_list = [] + + for reservation, customer in list: + _LOGGER.info(f"Creating XML for reservation {reservation.form_id} and customer {customer.given_name}") + + phone_numbers = [(customer.phone, PhoneTechType.MOBILE)] if customer.phone is not None else [] + 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=customer.email_address, + 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, OtaMessageType.RETRIEVE + ) + + # Guest counts + children_ages = [int(a) for a in reservation.children_ages.split(",") if a] + guest_counts = GuestCountsFactory.create_retrieve_guest_counts( + reservation.num_adults, children_ages + ) + + # UniqueID + unique_id = OtaResRetrieveRs.ReservationsList.HotelReservation.UniqueId( + type_value=UniqueIdType2.VALUE_14, id=reservation.form_id + ) + + # TimeSpan + time_span = OtaResRetrieveRs.ReservationsList.HotelReservation.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, + ) + room_stay = ( + OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay( + time_span=time_span, + guest_counts=guest_counts, + ) + ) + room_stays = OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays( + room_stay=[room_stay], + ) + hotel_res_id_data = HotelReservationIdData( + res_id_type="13", + res_id_value=reservation.fbclid or reservation.gclid, + res_id_source=None, + res_id_source_context="99tales", + ) + + hotel_res_id = alpine_bits_factory.create( + hotel_res_id_data, OtaMessageType.RETRIEVE + ) + hotel_res_ids = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.HotelReservationIds( + hotel_reservation_id=[hotel_res_id] + ) + basic_property_info = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.BasicPropertyInfo( + hotel_code=reservation.hotel_code, + hotel_name=reservation.hotel_name, + ) + # Comments + offer_comment = CommentData( + name=CommentName2.ADDITIONAL_INFO, + text="Angebot/Offerta", + 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] if comment else [offer_comment] + comments_data = CommentsData(comments=comments) + comments_xml = alpine_bits_factory.create(comments_data, OtaMessageType.RETRIEVE) + + res_global_info = ( + OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo( + hotel_reservation_ids=hotel_res_ids, + basic_property_info=basic_property_info, + comments=comments_xml, + ) + ) + + hotel_reservation = OtaResRetrieveRs.ReservationsList.HotelReservation( + create_date_time=datetime.now(timezone.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, + ) + + reservations_list.append(hotel_reservation) + + retrieved_reservations = OtaResRetrieveRs.ReservationsList( + hotel_reservation=reservations_list + ) + + ota_res_retrieve_rs = OtaResRetrieveRs( + version="7.000", success=None, reservations_list=retrieved_reservations + ) + + try: + ota_res_retrieve_rs.model_validate(ota_res_retrieve_rs.model_dump()) + except Exception as e: + _LOGGER.error(f"Validation error: {e}") + raise + + return ota_res_retrieve_rs diff --git a/src/alpine_bits_python/alpinebits_server.py b/src/alpine_bits_python/alpinebits_server.py index 9e0ef73..b7bb990 100644 --- a/src/alpine_bits_python/alpinebits_server.py +++ b/src/alpine_bits_python/alpinebits_server.py @@ -17,6 +17,10 @@ from xml.etree import ElementTree as ET from dataclasses import dataclass from enum import Enum, IntEnum +from alpine_bits_python.alpine_bits_helpers import PhoneTechType, create_xml_from_db + + + from .generated.alpinebits import OtaPingRq, OtaPingRs, WarningStatus, OtaReadRq from xsdata_pydantic.bindings import XmlSerializer from xsdata.formats.dataclass.serializers.config import SerializerConfig @@ -407,6 +411,8 @@ def validate_hotel_authentication(username: str, password: str, hotelid: str, co + + class ReadAction(AlpineBitsAction): """Implementation for OTA_Read action.""" @@ -478,7 +484,19 @@ class ReadAction(AlpineBitsAction): for reservation, customer in reservation_customer_pairs: _LOGGER.info(f"Reservation: {reservation.id}, Customer: {customer.given_name}") + res_retrive_rs = create_xml_from_db(reservation_customer_pairs) + config = SerializerConfig( + pretty_print=True, xml_declaration=True, encoding="UTF-8" + ) + serializer = XmlSerializer(config=config) + response_xml = serializer.render( + res_retrive_rs, ns_map={None: "http://www.opentravel.org/OTA/2003/05"} + ) + + return AlpineBitsResponse(response_xml, HttpStatusCode.OK) + +