3 Commits

Author SHA1 Message Date
Jonas Linter
87668e6dc0 Unhappy with push_listener 2025-10-06 11:09:08 +02:00
Jonas Linter
68e49aab34 Made helper methods more userfriendly. Guest requests still works as expected 2025-10-06 10:58:05 +02:00
Jonas Linter
2944b52d43 Super simple email newsletter parsing. Better safe then sorry 2025-10-06 10:21:41 +02:00
6 changed files with 826 additions and 223 deletions

View File

@@ -0,0 +1,262 @@
{
"timestamp": "2025-10-06T10:46:42.527300",
"client_ip": "127.0.0.1",
"headers": {
"host": "localhost:8080",
"content-type": "application/json",
"user-agent": "insomnia/2023.5.8",
"accept": "*/*",
"content-length": "7499"
},
"data": {
"data": {
"formName": "Contact us",
"submissions": [
{
"label": "Angebot auswählen",
"value": "Zimmer: Doppelzimmer"
},
{
"label": "Anreisedatum",
"value": "2025-12-21"
},
{
"label": "Abreisedatum",
"value": "2025-10-28"
},
{
"label": "Anzahl Erwachsene",
"value": "2"
},
{
"label": "Anzahl Kinder",
"value": "0"
},
{
"label": "Anrede",
"value": "Herr"
},
{
"label": "Vorname",
"value": "Ernst-Dieter"
},
{
"label": "Nachname",
"value": "Koepper"
},
{
"label": "Email",
"value": "koepper-ed@t-online.de"
},
{
"label": "Phone",
"value": "+49 175 8555456"
},
{
"label": "Message",
"value": "Guten Morgen,\nwir sind nicht gebau an die Reisedaten gebunden: Anreise ist möglich ab 20. Dezember, Aufenthalt mindestens eine Woche, gern auch 8 oder 9 Tage. Natürlich mit Halbpension. Mit freundlichem Gruß D. Köpper"
},
{
"label": "Einwilligung Marketing",
"value": "Angekreuzt"
},
{
"label": "utm_Source",
"value": ""
},
{
"label": "utm_Medium",
"value": ""
},
{
"label": "utm_Campaign",
"value": ""
},
{
"label": "utm_Term",
"value": ""
},
{
"label": "utm_Content",
"value": ""
},
{
"label": "utm_term_id",
"value": ""
},
{
"label": "utm_content_id",
"value": ""
},
{
"label": "gad_source",
"value": "5"
},
{
"label": "gad_campaignid",
"value": "23065043477"
},
{
"label": "gbraid",
"value": ""
},
{
"label": "gclid",
"value": "EAIaIQobChMI-d7Bn_-OkAMVuZJQBh09uD0vEAAYASAAEgKR8_D_BwE"
},
{
"label": "fbclid",
"value": ""
},
{
"label": "hotelid",
"value": "12345"
},
{
"label": "hotelname",
"value": "Bemelmans Post"
}
],
"field:date_picker_7e65": "2025-10-28",
"field:number_7cf5": "2",
"field:utm_source": "",
"submissionTime": "2025-10-06T07:05:34.001Z",
"field:gad_source": "5",
"field:form_field_5a7b": "Angekreuzt",
"field:gad_campaignid": "23065043477",
"field:utm_medium": "",
"field:utm_term_id": "",
"context": {
"metaSiteId": "1dea821c-8168-4736-96e4-4b92e8b364cf",
"activationId": "fd8e9c90-0335-4fd2-976d-985f065f3f80"
},
"field:email_5139": "koepper-ed@t-online.de",
"field:phone_4c77": "+49 175 8555456",
"_context": {
"activation": {
"id": "fd8e9c90-0335-4fd2-976d-985f065f3f80"
},
"configuration": {
"id": "a976f18c-fa86-495d-be1e-676df188eeae"
},
"app": {
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
},
"action": {
"id": "152db4d7-5263-40c4-be2b-1c81476318b7"
},
"trigger": {
"key": "wix_form_app-form_submitted"
}
},
"field:gclid": "EAIaIQobChMI-d7Bn_-OkAMVuZJQBh09uD0vEAAYASAAEgKR8_D_BwE",
"formFieldMask": [
"field:",
"field:",
"field:angebot_auswaehlen",
"field:date_picker_a7c8",
"field:date_picker_7e65",
"field:",
"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:",
"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:",
"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",
"field:hotelid",
"field:hotelname",
"field:",
"metaSiteId"
],
"contact": {
"name": {
"first": "Ernst-Dieter",
"last": "Koepper"
},
"email": "koepper-ed@t-online.de",
"locale": "de-de",
"phones": [
{
"tag": "UNTAGGED",
"formattedPhone": "+49 175 8555456",
"id": "530a3bf4-6dbe-4611-8963-a50df805785d",
"countryCode": "DE",
"e164Phone": "+491758555456",
"primary": true,
"phone": "175 8555456"
}
],
"contactId": "13659da8-4035-47fe-a66b-6ce461ad290f",
"emails": [
{
"id": "e1d2168e-ca3c-4844-8f93-f2e1b0ae70e3",
"tag": "UNTAGGED",
"email": "koepper-ed@t-online.de",
"primary": true
}
],
"updatedDate": "2025-10-06T07:05:35.675Z",
"phone": "+491758555456",
"createdDate": "2025-10-06T07:05:35.675Z"
},
"submissionId": "86d247dc-9d5a-4eb7-87a7-677bf64645ad",
"field:anzahl_kinder": "0",
"field:first_name_abae": "Ernst-Dieter",
"field:utm_content_id": "",
"field:utm_campaign": "",
"field:utm_term": "",
"contactId": "13659da8-4035-47fe-a66b-6ce461ad290f",
"field:date_picker_a7c8": "2025-12-21",
"field:hotelname": "Bemelmans Post",
"field:angebot_auswaehlen": "Zimmer: Doppelzimmer",
"field:utm_content": "",
"field:last_name_d97c": "Koepper",
"field:hotelid": "12345",
"submissionsLink": "https://manage.wix.app/forms/submissions/1dea821c-8168-4736-96e4-4b92e8b364cf/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F1dea821c-8168-4736-96e4-4b92e8b364cf%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
"field:gbraid": "",
"field:fbclid": "",
"submissionPdf": {
"fileName": "86d247dc-9d5a-4eb7-87a7-677bf64645ad.pdf",
"downloadUrl": "https://manage.wix.com/_api/form-submission-service/v4/submissions/86d247dc-9d5a-4eb7-87a7-677bf64645ad/download?accessToken=JWS.eyJraWQiOiJWLVNuLWhwZSIsImFsZyI6IkhTMjU2In0.eyJkYXRhIjoie1wibWV0YVNpdGVJZFwiOlwiMWRlYTgyMWMtODE2OC00NzM2LTk2ZTQtNGI5MmU4YjM2NGNmXCJ9IiwiaWF0IjoxNzU5NzM0MzM1LCJleHAiOjE3NTk3MzQ5MzV9.9koy-O_ptm0dRspjh01Yefkt2rCHiUlRCFtE_S3auYw"
},
"field:anrede": "Herr",
"field:long_answer_3524": "Guten Morgen,\nwir sind nicht gebau an die Reisedaten gebunden: Anreise ist möglich ab 20. Dezember, Aufenthalt mindestens eine Woche, gern auch 8 oder 9 Tage. Natürlich mit Halbpension. Mit freundlichem Gruß D. Köpper",
"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": "7499"
}
}

View File

@@ -0,0 +1,262 @@
{
"timestamp": "2025-10-06T10:57:32.973217",
"client_ip": "127.0.0.1",
"headers": {
"host": "localhost:8080",
"content-type": "application/json",
"user-agent": "insomnia/2023.5.8",
"accept": "*/*",
"content-length": "7499"
},
"data": {
"data": {
"formName": "Contact us",
"submissions": [
{
"label": "Angebot auswählen",
"value": "Zimmer: Doppelzimmer"
},
{
"label": "Anreisedatum",
"value": "2025-12-21"
},
{
"label": "Abreisedatum",
"value": "2025-10-28"
},
{
"label": "Anzahl Erwachsene",
"value": "2"
},
{
"label": "Anzahl Kinder",
"value": "0"
},
{
"label": "Anrede",
"value": "Herr"
},
{
"label": "Vorname",
"value": "Ernst-Dieter"
},
{
"label": "Nachname",
"value": "Koepper"
},
{
"label": "Email",
"value": "koepper-ed@t-online.de"
},
{
"label": "Phone",
"value": "+49 175 8555456"
},
{
"label": "Message",
"value": "Guten Morgen,\nwir sind nicht gebau an die Reisedaten gebunden: Anreise ist möglich ab 20. Dezember, Aufenthalt mindestens eine Woche, gern auch 8 oder 9 Tage. Natürlich mit Halbpension. Mit freundlichem Gruß D. Köpper"
},
{
"label": "Einwilligung Marketing",
"value": "Angekreuzt"
},
{
"label": "utm_Source",
"value": ""
},
{
"label": "utm_Medium",
"value": ""
},
{
"label": "utm_Campaign",
"value": ""
},
{
"label": "utm_Term",
"value": ""
},
{
"label": "utm_Content",
"value": ""
},
{
"label": "utm_term_id",
"value": ""
},
{
"label": "utm_content_id",
"value": ""
},
{
"label": "gad_source",
"value": "5"
},
{
"label": "gad_campaignid",
"value": "23065043477"
},
{
"label": "gbraid",
"value": ""
},
{
"label": "gclid",
"value": "EAIaIQobChMI-d7Bn_-OkAMVuZJQBh09uD0vEAAYASAAEgKR8_D_BwE"
},
{
"label": "fbclid",
"value": ""
},
{
"label": "hotelid",
"value": "12345"
},
{
"label": "hotelname",
"value": "Bemelmans Post"
}
],
"field:date_picker_7e65": "2025-10-28",
"field:number_7cf5": "2",
"field:utm_source": "",
"submissionTime": "2025-10-06T07:05:34.001Z",
"field:gad_source": "5",
"field:form_field_5a7b": "Angekreuzt",
"field:gad_campaignid": "23065043477",
"field:utm_medium": "",
"field:utm_term_id": "",
"context": {
"metaSiteId": "1dea821c-8168-4736-96e4-4b92e8b364cf",
"activationId": "fd8e9c90-0335-4fd2-976d-985f065f3f80"
},
"field:email_5139": "koepper-ed@t-online.de",
"field:phone_4c77": "+49 175 8555456",
"_context": {
"activation": {
"id": "fd8e9c90-0335-4fd2-976d-985f065f3f80"
},
"configuration": {
"id": "a976f18c-fa86-495d-be1e-676df188eeae"
},
"app": {
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
},
"action": {
"id": "152db4d7-5263-40c4-be2b-1c81476318b7"
},
"trigger": {
"key": "wix_form_app-form_submitted"
}
},
"field:gclid": "EAIaIQobChMI-d7Bn_-OkAMVuZJQBh09uD0vEAAYASAAEgKR8_D_BwE",
"formFieldMask": [
"field:",
"field:",
"field:angebot_auswaehlen",
"field:date_picker_a7c8",
"field:date_picker_7e65",
"field:",
"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:",
"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:",
"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",
"field:hotelid",
"field:hotelname",
"field:",
"metaSiteId"
],
"contact": {
"name": {
"first": "Ernst-Dieter",
"last": "Koepper"
},
"email": "koepper-ed@t-online.de",
"locale": "de-de",
"phones": [
{
"tag": "UNTAGGED",
"formattedPhone": "+49 175 8555456",
"id": "530a3bf4-6dbe-4611-8963-a50df805785d",
"countryCode": "DE",
"e164Phone": "+491758555456",
"primary": true,
"phone": "175 8555456"
}
],
"contactId": "13659da8-4035-47fe-a66b-6ce461ad290f",
"emails": [
{
"id": "e1d2168e-ca3c-4844-8f93-f2e1b0ae70e3",
"tag": "UNTAGGED",
"email": "koepper-ed@t-online.de",
"primary": true
}
],
"updatedDate": "2025-10-06T07:05:35.675Z",
"phone": "+491758555456",
"createdDate": "2025-10-06T07:05:35.675Z"
},
"submissionId": "86d247dc-9d5a-4eb7-87a7-677bf64645ad",
"field:anzahl_kinder": "0",
"field:first_name_abae": "Ernst-Dieter",
"field:utm_content_id": "",
"field:utm_campaign": "",
"field:utm_term": "",
"contactId": "13659da8-4035-47fe-a66b-6ce461ad290f",
"field:date_picker_a7c8": "2025-12-21",
"field:hotelname": "Bemelmans Post",
"field:angebot_auswaehlen": "Zimmer: Doppelzimmer",
"field:utm_content": "",
"field:last_name_d97c": "Koepper",
"field:hotelid": "12345",
"submissionsLink": "https://manage.wix.app/forms/submissions/1dea821c-8168-4736-96e4-4b92e8b364cf/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F1dea821c-8168-4736-96e4-4b92e8b364cf%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
"field:gbraid": "",
"field:fbclid": "",
"submissionPdf": {
"fileName": "86d247dc-9d5a-4eb7-87a7-677bf64645ad.pdf",
"downloadUrl": "https://manage.wix.com/_api/form-submission-service/v4/submissions/86d247dc-9d5a-4eb7-87a7-677bf64645ad/download?accessToken=JWS.eyJraWQiOiJWLVNuLWhwZSIsImFsZyI6IkhTMjU2In0.eyJkYXRhIjoie1wibWV0YVNpdGVJZFwiOlwiMWRlYTgyMWMtODE2OC00NzM2LTk2ZTQtNGI5MmU4YjM2NGNmXCJ9IiwiaWF0IjoxNzU5NzM0MzM1LCJleHAiOjE3NTk3MzQ5MzV9.9koy-O_ptm0dRspjh01Yefkt2rCHiUlRCFtE_S3auYw"
},
"field:anrede": "Herr",
"field:long_answer_3524": "Guten Morgen,\nwir sind nicht gebau an die Reisedaten gebunden: Anreise ist möglich ab 20. Dezember, Aufenthalt mindestens eine Woche, gern auch 8 oder 9 Tage. Natürlich mit Halbpension. Mit freundlichem Gruß D. Köpper",
"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": "7499"
}
}

View File

@@ -51,6 +51,18 @@ RetrieveGuestCounts = (
OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.GuestCounts 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)
# phonetechtype enum 1,3,5 voice, fax, mobile # phonetechtype enum 1,3,5 voice, fax, mobile
class PhoneTechType(Enum): class PhoneTechType(Enum):
@@ -104,31 +116,25 @@ class CustomerData:
class GuestCountsFactory: class GuestCountsFactory:
"""Factory class to create GuestCounts instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
@staticmethod @staticmethod
def create_notif_guest_counts( def create_guest_counts(
adults: int, kids: Optional[list[int]] = None adults: int, kids: Optional[list[int]] = None
) -> NotifGuestCounts: , message_type: OtaMessageType = OtaMessageType.RETRIEVE) -> NotifGuestCounts:
""" """
Create a GuestCounts object for OtaHotelResNotifRq. Create a GuestCounts object for OtaHotelResNotifRq or OtaResRetrieveRs.
:param adults: Number of adults :param adults: Number of adults
:param kids: List of ages for each kid (optional) :param kids: List of ages for each kid (optional)
:return: GuestCounts instance :return: GuestCounts instance
""" """
return GuestCountsFactory._create_guest_counts(adults, kids, NotifGuestCounts) if message_type == OtaMessageType.RETRIEVE:
return GuestCountsFactory._create_guest_counts(adults, kids, RetrieveGuestCounts)
elif message_type == OtaMessageType.NOTIF:
return GuestCountsFactory._create_guest_counts(adults, kids, NotifGuestCounts)
else:
raise ValueError(f"Unsupported message type: {message_type}")
@staticmethod
def create_retrieve_guest_counts(
adults: int, kids: Optional[list[int]] = None
) -> RetrieveGuestCounts:
"""
Create a GuestCounts object for OtaResRetrieveRs.
:param adults: Number of adults
:param kids: List of ages for each kid (optional)
:return: GuestCounts instance
"""
return GuestCountsFactory._create_guest_counts(
adults, kids, RetrieveGuestCounts
)
@staticmethod @staticmethod
def _create_guest_counts( def _create_guest_counts(
@@ -567,6 +573,9 @@ class ResGuestFactory:
return CustomerFactory.from_notif_customer(customer) return CustomerFactory.from_notif_customer(customer)
else: else:
return CustomerFactory.from_retrieve_customer(customer) return CustomerFactory.from_retrieve_customer(customer)
class AlpineBitsFactory: class AlpineBitsFactory:
@@ -669,9 +678,217 @@ class AlpineBitsFactory:
else: else:
raise ValueError(f"Unsupported object type: {type(obj)}") raise ValueError(f"Unsupported object type: {type(obj)}")
def create_res_retrieve_response(list: list[Tuple[Reservation, Customer]]):
"""Create RetrievedReservation XML from database entries."""
return _create_xml_from_db(list, OtaMessageType.RETRIEVE)
def create_res_notif_push_message(list: Tuple[Reservation, Customer]):
"""Create Reservation Notification XML from database entries."""
return _create_xml_from_db(list, OtaMessageType.NOTIF)
def create_xml_from_db(list: list[Tuple[Reservation, Customer]]): def _process_single_reservation(reservation: Reservation, customer: Customer, message_type: OtaMessageType):
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, 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
)
unique_id_string = reservation.unique_id
if message_type == OtaMessageType.NOTIF:
UniqueId = NotifUniqueId
RoomStays = NotifRoomStays
HotelReservation = NotifHotelReservation
elif message_type == OtaMessageType.RETRIEVE:
UniqueId = RetrieveUniqueId
RoomStays = RetrieveRoomStays
HotelReservation = RetrieveHotelReservation
else:
raise ValueError(f"Unsupported message type: {message_type}")
# UniqueID
unique_id = UniqueId(
type_value=UniqueIdType2.VALUE_14, id=unique_id_string
)
# 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,
)
room_stay = (
RoomStays.RoomStay(
time_span=time_span,
guest_counts=guest_counts,
)
)
room_stays = RoomStays(
room_stay=[room_stay],
)
res_id_source = "website"
if reservation.fbclid != "":
klick_id = reservation.fbclid
res_id_source = "meta"
elif reservation.gclid != "":
klick_id = reservation.gclid
res_id_source = "google"
# explicitly set klick_id to None otherwise an empty string will be sent
if klick_id in (None, "", "None"):
klick_id = None
else: # extract string from Column object
klick_id = str(klick_id)
hotel_res_id_data = HotelReservationIdData(
res_id_type="13",
res_id_value=klick_id,
res_id_source=res_id_source,
res_id_source_context="99tales",
)
# explicitly set klick_id to None otherwise an empty string will be sent
if klick_id in (None, "", "None"):
klick_id = None
else: # extract string from Column object
klick_id = str(klick_id)
hotel_res_id_data = HotelReservationIdData(
res_id_type="13",
res_id_value=klick_id,
res_id_source=None,
res_id_source_context="99tales",
)
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_code is None:
raise ValueError("Reservation hotel_code is None")
else:
hotel_code = str(reservation.hotel_code)
if reservation.hotel_name is None:
hotel_name = None
else:
hotel_name = 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",
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.info(
f"Creating comment: name={c.name}, text={c.text}, list_items={len(c.list_items)}"
)
comments_data = CommentsData(comments=comments)
comments_xml = alpine_bits_factory.create(
comments_data, OtaMessageType.RETRIEVE
)
res_global_info = (
HotelReservation.ResGlobalInfo(
hotel_reservation_ids=hotel_res_ids,
basic_property_info=basic_property_info,
comments=comments_xml,
)
)
hotel_reservation = 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,
)
return hotel_reservation
def _create_xml_from_db(entries: list[Tuple[Reservation, Customer]] | Tuple[Reservation, Customer], type: OtaMessageType):
"""Create RetrievedReservation XML from database entries. """Create RetrievedReservation XML from database entries.
list of pairs (Reservation, Customer) list of pairs (Reservation, Customer)
@@ -679,187 +896,20 @@ def create_xml_from_db(list: list[Tuple[Reservation, Customer]]):
reservations_list = [] reservations_list = []
for reservation, customer in 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( _LOGGER.info(
f"Creating XML for reservation {reservation.unique_id} and customer {customer.given_name}" f"Creating XML for reservation {reservation.unique_id} and customer {customer.given_name}"
) )
try: try:
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 hotel_reservation = _process_single_reservation(reservation, customer, type)
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
)
unique_id_string = reservation.unique_id
# UniqueID
unique_id = OtaResRetrieveRs.ReservationsList.HotelReservation.UniqueId(
type_value=UniqueIdType2.VALUE_14, id=unique_id_string
)
# 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],
)
res_id_source = "website"
if reservation.fbclid != "":
klick_id = reservation.fbclid
res_id_source = "meta"
elif reservation.gclid != "":
klick_id = reservation.gclid
res_id_source = "google"
# explicitly set klick_id to None otherwise an empty string will be sent
if klick_id in (None, "", "None"):
klick_id = None
else: # extract string from Column object
klick_id = str(klick_id)
hotel_res_id_data = HotelReservationIdData(
res_id_type="13",
res_id_value=klick_id,
res_id_source=res_id_source,
res_id_source_context="99tales",
)
# explicitly set klick_id to None otherwise an empty string will be sent
if klick_id in (None, "", "None"):
klick_id = None
else: # extract string from Column object
klick_id = str(klick_id)
hotel_res_id_data = HotelReservationIdData(
res_id_type="13",
res_id_value=klick_id,
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]
)
if reservation.hotel_code is None:
raise ValueError("Reservation hotel_code is None")
else:
hotel_code = str(reservation.hotel_code)
if reservation.hotel_name is None:
hotel_name = None
else:
hotel_name = str(reservation.hotel_name)
basic_property_info = OtaResRetrieveRs.ReservationsList.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",
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.info(
f"Creating comment: name={c.name}, text={c.text}, list_items={len(c.list_items)}"
)
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) reservations_list.append(hotel_reservation)
@@ -868,21 +918,42 @@ def create_xml_from_db(list: list[Tuple[Reservation, Customer]]):
f"Error creating XML for reservation {reservation.unique_id} and customer {customer.given_name}: {e}" f"Error creating XML for reservation {reservation.unique_id} and customer {customer.given_name}: {e}"
) )
retrieved_reservations = OtaResRetrieveRs.ReservationsList( if type == OtaMessageType.NOTIF:
hotel_reservation=reservations_list retrieved_reservations = OtaHotelResNotifRq.HotelReservations(
) hotel_reservation=reservations_list
)
ota_res_retrieve_rs = OtaResRetrieveRs( ota_hotel_res_notif_rq = OtaHotelResNotifRq(
version="7.000", success="", reservations_list=retrieved_reservations version="7.000", hotel_reservations=retrieved_reservations
) )
try: try:
ota_res_retrieve_rs.model_validate(ota_res_retrieve_rs.model_dump()) ota_hotel_res_notif_rq.model_validate(ota_hotel_res_notif_rq.model_dump())
except Exception as e: except Exception as e:
_LOGGER.error(f"Validation error: {e}") _LOGGER.error(f"Validation error: {e}")
raise raise
return ota_res_retrieve_rs return ota_hotel_res_notif_rq
elif type == OtaMessageType.RETRIEVE:
retrieved_reservations = OtaResRetrieveRs.ReservationsList(
hotel_reservation=reservations_list
)
ota_res_retrieve_rs = OtaResRetrieveRs(
version="7.000", success="", 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
else:
raise ValueError(f"Unsupported message type: {type}")
# Usage examples # Usage examples

View File

@@ -18,7 +18,7 @@ from xml.etree import ElementTree as ET
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, IntEnum from enum import Enum, IntEnum
from alpine_bits_python.alpine_bits_helpers import PhoneTechType, create_xml_from_db from alpine_bits_python.alpine_bits_helpers import PhoneTechType, create_res_retrieve_response
from .generated.alpinebits import OtaNotifReportRq, OtaNotifReportRs, OtaPingRq, OtaPingRs, WarningStatus, OtaReadRq from .generated.alpinebits import OtaNotifReportRq, OtaNotifReportRs, OtaPingRq, OtaPingRs, WarningStatus, OtaReadRq
@@ -559,7 +559,7 @@ class ReadAction(AlpineBitsAction):
f"Reservation: {reservation.id}, Customer: {customer.given_name}" f"Reservation: {reservation.id}, Customer: {customer.given_name}"
) )
res_retrive_rs = create_xml_from_db(reservation_customer_pairs) res_retrive_rs = create_res_retrieve_response(reservation_customer_pairs)
config = SerializerConfig( config = SerializerConfig(
pretty_print=True, xml_declaration=True, encoding="UTF-8" pretty_print=True, xml_declaration=True, encoding="UTF-8"

View File

@@ -71,26 +71,31 @@ event_dispatcher = EventDispatcher()
# Load config at startup # Load config at startup
async def push_listener(customer, reservation, hotel, push): async def push_listener(customer: DBCustomer, reservation: DBReservation, hotel):
push_endpoint = hotel.get("push_endpoint")
server: AlpineBitsServer = app.state.alpine_bits_server server: AlpineBitsServer = app.state.alpine_bits_server
hotel_id = hotel['hotel_id'] hotel_id = hotel['hotel_id']
reservation_hotel_id = reservation.hotel_code
headers = {"Authorization": f"Bearer {push.get('token','')}"} if push.get('token') else {}
action = "OTA_HotelResNotifRQ"
# request = server.handle_request(
# action,)
headers = {"Authorization": f"Bearer {push_endpoint.get('token','')}"} if push_endpoint.get('token') else {}
try: try:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
resp = await client.post(push["url"], json=payload, headers=headers, timeout=10) resp = await client.post(push_endpoint["url"], json=payload, headers=headers, timeout=10)
_LOGGER.info(f"Push event fired to {push['url']} for hotel {hotel['hotel_id']}, status: {resp.status_code}") _LOGGER.info(f"Push event fired to {push_endpoint['url']} for hotel {hotel['hotel_id']}, status: {resp.status_code}")
except Exception as e: except Exception as e:
_LOGGER.error(f"Push event failed for hotel {hotel['hotel_id']}: {e}") _LOGGER.error(f"Push event failed for hotel {hotel['hotel_id']}: {e}")
@@ -114,12 +119,13 @@ async def lifespan(app: FastAPI):
app.state.alpine_bits_server = AlpineBitsServer(config) app.state.alpine_bits_server = AlpineBitsServer(config)
app.state.event_dispatcher = event_dispatcher app.state.event_dispatcher = event_dispatcher
# Register push listeners for hotels with push_endpoint # Register push listeners for hotels with push_endpoint
for hotel in config.get("alpine_bits_auth", []): for hotel in config.get("alpine_bits_auth", []):
push = hotel.get("push_endpoint") push_endpoint = hotel.get("push_endpoint")
if push: if push_endpoint:
event_dispatcher.register("form_processed", partial(push_listener, hotel=hotel, push=push)) event_dispatcher.register("form_processed", partial(push_listener, hotel=hotel))
# Create tables # Create tables
async with engine.begin() as conn: async with engine.begin() as conn:
@@ -284,7 +290,9 @@ async def process_wix_form_submission(request: Request, data: Dict[str, Any], db
contact_id = contact_info.get("contactId") contact_id = contact_info.get("contactId")
name_prefix = data.get("field:anrede") name_prefix = data.get("field:anrede")
email_newsletter = data.get("field:form_field_5a7b", "") != "Non selezionato" email_newsletter_string = data.get("field:form_field_5a7b", "")
yes_values = {"Selezionato", "Angekreuzt"}
email_newsletter = (email_newsletter_string in yes_values)
address_line = None address_line = None
city_name = None city_name = None
postal_code = None postal_code = None