More refactoring

This commit is contained in:
Jonas Linter
2025-11-19 16:25:18 +01:00
parent 8547326ffa
commit 0854352726
9 changed files with 301001 additions and 340 deletions

View File

@@ -558,7 +558,7 @@ class ConversionService:
if existing_conversion:
# Update existing conversion - only update reservation metadata and advertising data
# Don't overwrite guest info (will be handled by matching logic which uses hashed data)
# Guest info is stored in ConversionGuest table, not here
# Don't clear reservation/customer links (matching logic will update if needed)
existing_conversion.reservation_number = reservation_number
existing_conversion.reservation_date = reservation_date
@@ -568,19 +568,6 @@ class ConversionService:
existing_conversion.advertising_medium = advertising_medium
existing_conversion.advertising_partner = advertising_partner
existing_conversion.advertising_campagne = advertising_campagne
# Update guest info only if new data is provided (not None)
if guest_first_name:
existing_conversion.guest_first_name = guest_first_name
if guest_last_name:
existing_conversion.guest_last_name = guest_last_name
if guest_email:
existing_conversion.guest_email = guest_email
if guest_country_code:
existing_conversion.guest_country_code = guest_country_code
if guest_birth_date:
existing_conversion.guest_birth_date = guest_birth_date
existing_conversion.updated_at = datetime.now()
conversion = existing_conversion
_LOGGER.info(
@@ -590,6 +577,7 @@ class ConversionService:
)
else:
# Create new conversion entry (without matching - will be done later)
# Note: Guest information (first_name, last_name, email, etc) is stored in ConversionGuest table
conversion = Conversion(
# Links to existing entities (nullable, will be filled in after matching)
reservation_id=None,
@@ -597,19 +585,13 @@ class ConversionService:
hashed_customer_id=None,
# Reservation metadata
hotel_id=hotel_id,
guest_id=guest_id, # Links to ConversionGuest
pms_reservation_id=pms_reservation_id,
reservation_number=reservation_number,
reservation_date=reservation_date,
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,
guest_birth_date=guest_birth_date,
guest_id=guest_id,
# Advertising data
advertising_medium=advertising_medium,
advertising_partner=advertising_partner,
@@ -628,6 +610,8 @@ class ConversionService:
await session.flush()
# Create or update ConversionGuest and link it to the conversion
# The conversion is linked to ConversionGuest via composite FK (hotel_id, guest_id)
# So we just need to ensure ConversionGuest exists - the FK is already set via hotel_id + guest_id
conversion_guest = await self._get_or_create_conversion_guest(
hotel_id=hotel_id,
guest_id=guest_id,
@@ -638,8 +622,7 @@ class ConversionService:
guest_birth_date=guest_birth_date,
session=session,
)
if conversion_guest:
conversion.conversion_guest_id = conversion_guest.id
# guest_id is already set on conversion, so the composite FK relationship is established
# Batch-load existing room reservations to avoid N+1 queries
room_numbers = [
@@ -863,6 +846,7 @@ class ConversionService:
matched_reservation = match_result["reservation"]
matched_customer = match_result["customer"]
matched_hashed_customer = match_result["hashed_customer"]
match_type = match_result.get("match_type") # "id" or "guest_details"
# Update the conversion with matched entities if found
if matched_reservation or matched_customer or matched_hashed_customer:
@@ -875,6 +859,15 @@ class ConversionService:
conversion.hashed_customer_id = (
matched_hashed_customer.id if matched_hashed_customer else None
)
# Set attribution flags based on match type
if match_type == "id":
conversion.directly_attributable = True
conversion.guest_matched = False
elif match_type == "guest_details":
conversion.directly_attributable = False
conversion.guest_matched = True
conversion.updated_at = datetime.now()
# Update stats
@@ -902,22 +895,23 @@ class ConversionService:
) -> dict[str, Any]:
"""Find matching Reservation, Customer, and HashedCustomer.
Uses two strategies:
1. Advertising data matching (fbclid/gclid/utm_campaign) with guest details fallback
2. If no advertising data match, falls back to email/name-based matching
Uses two strategies with separate attribution:
1. ID-based matching (fbclid/gclid/md5_unique_id) - directly_attributable
2. Guest detail matching (email/name) - guest_matched only
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 matching
guest_last_name: Guest last name for matching
guest_email: Guest email for matching
guest_first_name: Guest first name (hashed) for matching
guest_last_name: Guest last name (hashed) for matching
guest_email: Guest email (hashed) for matching
advertising_partner: Partner info (matches utm_medium for additional filtering)
session: AsyncSession to use. If None, uses self.session.
Returns:
Dictionary with 'reservation', 'customer', and 'hashed_customer' keys
Dictionary with 'reservation', 'customer', 'hashed_customer', and 'match_type' keys.
match_type is either 'id' (high confidence) or 'guest_details' (lower confidence)
"""
if session is None:
@@ -926,9 +920,10 @@ class ConversionService:
"reservation": None,
"customer": None,
"hashed_customer": None,
"match_type": None, # "id" or "guest_details"
}
# Strategy 1: Try to match by advertising data (fbclid/gclid/utm_campaign)
# Strategy 1: Try to match by advertising data (fbclid/gclid/md5_unique_id) - ID-based, high confidence
if advertising_campagne:
matched_reservation = await self._match_by_advertising(
advertising_campagne,
@@ -942,19 +937,20 @@ class ConversionService:
if matched_reservation:
result["reservation"] = matched_reservation
result["match_type"] = "id" # Matched by ID
_LOGGER.info(
"Matched conversion by advertising data (advertisingCampagne=%s, hotel=%s)",
"Matched conversion by advertising ID data (advertisingCampagne=%s, hotel=%s)",
advertising_campagne,
hotel_id,
)
else:
_LOGGER.debug(
"No match found by advertising data (advertisingCampagne=%s), "
"falling back to email/name matching",
"No match found by advertising ID data (advertisingCampagne=%s), "
"falling back to guest details matching",
advertising_campagne,
)
# Strategy 2: If no advertising match, try email/name-based matching
# Strategy 2: If no ID-based match, try email/name-based matching - guest details, lower confidence
if not result["reservation"] and (
guest_email or guest_first_name or guest_last_name
):
@@ -964,6 +960,7 @@ class ConversionService:
if matched_reservation:
result["reservation"] = matched_reservation
result["match_type"] = "guest_details" # Matched by guest details only
_LOGGER.info(
"Matched conversion by guest details (name=%s %s, email=%s, hotel=%s)",
guest_first_name,
@@ -1484,6 +1481,7 @@ class ConversionService:
matched_reservation = match_result["reservation"]
matched_customer = match_result["customer"]
matched_hashed_customer = match_result["hashed_customer"]
match_type = match_result.get("match_type") # "id" or "guest_details"
# Update the conversion with matched entities if found
if matched_reservation or matched_customer or matched_hashed_customer:
@@ -1496,6 +1494,15 @@ class ConversionService:
conversion.hashed_customer_id = (
matched_hashed_customer.id if matched_hashed_customer else None
)
# Set attribution flags based on match type
if match_type == "id":
conversion.directly_attributable = True
conversion.guest_matched = False
elif match_type == "guest_details":
conversion.directly_attributable = False
conversion.guest_matched = True
conversion.updated_at = datetime.now()
# Update stats if provided