Adding guests to conversion_import

This commit is contained in:
Jonas Linter
2025-11-17 09:22:35 +01:00
parent 9b82be9a6e
commit 0c37254317
6 changed files with 964551 additions and 8 deletions

View File

@@ -131,6 +131,19 @@ class ConversionService:
reservation_type = reservation_elem.get("type")
booking_channel = reservation_elem.get("bookingChannel")
# Extract guest information from guest element
guest_elem = reservation_elem.find("guest")
guest_first_name = None
guest_last_name = None
guest_email = None
guest_country_code = None
if guest_elem is not None:
guest_first_name = guest_elem.get("firstName")
guest_last_name = guest_elem.get("lastName")
guest_email = guest_elem.get("email")
guest_country_code = guest_elem.get("countryCode")
# Advertising/tracking data
advertising_medium = reservation_elem.get("advertisingMedium")
advertising_partner = reservation_elem.get("advertisingPartner")
@@ -159,14 +172,20 @@ class ConversionService:
"Invalid creation time format: %s", creation_time_str
)
# Find matching reservation, customer, and hashed_customer using advertising data
# Find matching reservation, customer, and hashed_customer using advertising data and guest details
matched_reservation = None
matched_customer = None
matched_hashed_customer = None
if advertising_campagne:
match_result = await self._find_matching_entities(
advertising_campagne, hotel_id, reservation_date
advertising_campagne,
hotel_id,
reservation_date,
guest_first_name,
guest_last_name,
guest_email,
advertising_partner,
)
matched_reservation = match_result["reservation"]
matched_customer = match_result["customer"]
@@ -250,6 +269,11 @@ class ConversionService:
creation_time=creation_time,
reservation_type=reservation_type,
booking_channel=booking_channel,
# Guest information
guest_first_name=guest_first_name,
guest_last_name=guest_last_name,
guest_email=guest_email,
guest_country_code=guest_country_code,
# Advertising data
advertising_medium=advertising_medium,
advertising_partner=advertising_partner,
@@ -295,16 +319,26 @@ class ConversionService:
advertising_campagne: str,
hotel_id: str | None,
reservation_date: Any,
guest_first_name: str | None = None,
guest_last_name: str | None = None,
guest_email: str | None = None,
advertising_partner: str | None = None,
) -> dict[str, Any]:
"""Find matching Reservation, Customer, and HashedCustomer using advertising data.
The advertisingCampagne field contains a truncated (64 char) version of
fbclid/gclid, so we use prefix matching.
fbclid/gclid, so we use prefix matching. When multiple matches exist,
uses guest details (first_name, last_name, email) and utm_medium
(matched against advertisingPartner) to narrow down to a single match.
Args:
advertising_campagne: Truncated tracking ID from conversion XML
hotel_id: Hotel ID for additional filtering
reservation_date: Reservation date for additional filtering
guest_first_name: Guest first name for disambiguation
guest_last_name: Guest last name for disambiguation
guest_email: Guest email for disambiguation
advertising_partner: Partner info (matches utm_medium for additional filtering)
Returns:
Dictionary with 'reservation', 'customer', and 'hashed_customer' keys
@@ -344,16 +378,39 @@ class ConversionService:
)
return result
# If multiple matches, try to narrow down using guest details and advertising_partner
if len(reservations) > 1:
_LOGGER.warning(
"Multiple reservations match advertisingCampagne %s (hotel=%s): found %d matches. Using first match.",
_LOGGER.debug(
"Multiple reservations match advertisingCampagne %s (hotel=%s): found %d matches. "
"Attempting to narrow down using guest details.",
advertising_campagne,
hotel_id,
len(reservations),
)
# Use the first matching reservation
matched_reservation = reservations[0]
matched_reservation = self._filter_reservations_by_guest_details(
reservations,
guest_first_name,
guest_last_name,
guest_email,
advertising_partner,
)
if matched_reservation is None:
# If we still can't narrow it down, use the first match and log warning
_LOGGER.warning(
"Could not narrow down multiple reservations for advertisingCampagne %s "
"(hotel=%s, guest=%s %s, email=%s). Using first match.",
advertising_campagne,
hotel_id,
guest_first_name,
guest_last_name,
guest_email,
)
matched_reservation = reservations[0]
else:
matched_reservation = reservations[0]
result["reservation"] = matched_reservation
# Get associated customer and hashed_customer
@@ -373,11 +430,91 @@ class ConversionService:
result["hashed_customer"] = hashed_result.scalar_one_or_none()
_LOGGER.info(
"Matched conversion to reservation_id=%s, customer_id=%s, hashed_customer_id=%s (advertisingCampagne=%s)",
"Matched conversion to reservation_id=%s, customer_id=%s, hashed_customer_id=%s "
"(advertisingCampagne=%s, guest=%s %s, email=%s)",
result["reservation"].id if result["reservation"] else None,
result["customer"].id if result["customer"] else None,
result["hashed_customer"].id if result["hashed_customer"] else None,
advertising_campagne,
guest_first_name,
guest_last_name,
guest_email,
)
return result
def _filter_reservations_by_guest_details(
self,
reservations: list[Reservation],
guest_first_name: str | None,
guest_last_name: str | None,
guest_email: str | None,
advertising_partner: str | None,
) -> Reservation | None:
"""Filter reservations using guest details to find a single match.
First tries to match by guest name and email. If that doesn't yield a single match,
tries matching by advertising_partner against utm_medium.
Args:
reservations: List of candidate reservations
guest_first_name: Guest first name
guest_last_name: Guest last name
guest_email: Guest email
advertising_partner: Partner info (e.g., "Facebook_Mobile_Feed")
Returns:
Single best-match Reservation, or None if no good match found
"""
candidates = reservations
# Try to narrow down by guest name and email
if guest_first_name or guest_last_name or guest_email:
# First try exact match on all available fields
for reservation in candidates:
customer = reservation.customer
if customer:
name_match = True
email_match = True
if guest_first_name:
name_match = name_match and (
customer.given_name
and customer.given_name.lower() == guest_first_name.lower()
)
if guest_last_name:
name_match = name_match and (
customer.surname
and customer.surname.lower() == guest_last_name.lower()
)
if guest_email:
email_match = (
customer.email_address
and customer.email_address.lower() == guest_email.lower()
)
if name_match and email_match:
_LOGGER.debug(
"Found exact match on guest name/email for %s %s",
guest_first_name,
guest_last_name,
)
return reservation
# Try to narrow down by advertising_partner matching utm_medium
if advertising_partner:
for reservation in candidates:
if (
reservation.utm_medium
and reservation.utm_medium.lower() == advertising_partner.lower()
):
_LOGGER.debug(
"Found match on advertising_partner=%s matching utm_medium",
advertising_partner,
)
return reservation
# No single clear match found
return None