diff --git a/src/alpine_bits_python/conversion_service.py b/src/alpine_bits_python/conversion_service.py index 16a9258..6cad096 100644 --- a/src/alpine_bits_python/conversion_service.py +++ b/src/alpine_bits_python/conversion_service.py @@ -119,6 +119,28 @@ class ConversionService: f"session must be AsyncSession or SessionMaker, got {type(session)}" ) + @staticmethod + def _update_timestamp_if_modified( + obj: Conversion | ConversionRoom, session: AsyncSession + ) -> bool: + """Update the updated_at timestamp only if the object has been modified. + + Uses SQLAlchemy's change tracking to determine if any scalar attributes + have changed. Only updates the timestamp if actual changes were detected. + + Args: + obj: The ORM object to check and potentially update + session: The session managing this object + + Returns: + True if the object was modified and timestamp was updated, False otherwise + + """ + if session.is_modified(obj, include_collections=False): + obj.updated_at = datetime.now() + return True + return False + @staticmethod def _parse_required_int(value: str | None, field_name: str) -> int: """Parse an integer attribute that must be present.""" @@ -897,26 +919,39 @@ class ConversionService: existing_conversion = existing_result.scalar_one_or_none() if existing_conversion: - # Update existing conversion - only update reservation metadata and advertising data + # Update existing conversion using Pydantic validation # 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 = ( - parsed_reservation.reservation_number + # Preserve reservation/customer links (matching logic will update if needed) + conversion_data = ConversionData( + hotel_id=hotel_id, + pms_reservation_id=pms_reservation_id, + guest_id=parsed_reservation.guest_id, + reservation_number=parsed_reservation.reservation_number, + reservation_date=parsed_reservation.reservation_date, + creation_time=parsed_reservation.creation_time, + reservation_type=parsed_reservation.reservation_type, + booking_channel=parsed_reservation.booking_channel, + advertising_medium=parsed_reservation.advertising_medium, + advertising_partner=parsed_reservation.advertising_partner, + advertising_campagne=parsed_reservation.advertising_campagne, + # Preserve existing values (managed separately) + created_at=existing_conversion.created_at, + reservation_id=existing_conversion.reservation_id, + customer_id=existing_conversion.customer_id, + directly_attributable=existing_conversion.directly_attributable, + guest_matched=existing_conversion.guest_matched, ) - existing_conversion.reservation_date = parsed_reservation.reservation_date - existing_conversion.creation_time = parsed_reservation.creation_time - existing_conversion.reservation_type = parsed_reservation.reservation_type - existing_conversion.booking_channel = parsed_reservation.booking_channel - existing_conversion.advertising_medium = ( - parsed_reservation.advertising_medium + + # Apply validated data, excluding managed fields + validated_dict = conversion_data.model_dump( + exclude={'created_at', 'updated_at', 'reservation_id', 'customer_id', + 'directly_attributable', 'guest_matched'} ) - existing_conversion.advertising_partner = ( - parsed_reservation.advertising_partner - ) - existing_conversion.advertising_campagne = ( - parsed_reservation.advertising_campagne - ) - existing_conversion.updated_at = datetime.now() + for key, value in validated_dict.items(): + setattr(existing_conversion, key, value) + + # Only update timestamp if something actually changed + self._update_timestamp_if_modified(existing_conversion, session) conversion = existing_conversion _LOGGER.debug( "Updated conversion %s (pms_id=%s)", @@ -998,7 +1033,8 @@ class ConversionService: else None ) existing_room_reservation.total_revenue = room_reservation.total_revenue - existing_room_reservation.updated_at = datetime.now() + # Only update timestamp if something actually changed + self._update_timestamp_if_modified(existing_room_reservation, session) _LOGGER.debug( "Updated room reservation %s (pms_id=%s, room=%s)", existing_room_reservation.id, @@ -1358,8 +1394,8 @@ class ConversionService: # ID-based matches are always directly attributable conversion.directly_attributable = True conversion.guest_matched = False - - conversion.updated_at = datetime.now() + # Only update timestamp if something actually changed + self._update_timestamp_if_modified(conversion, session) # Update stats if provided if stats is not None: @@ -1506,7 +1542,8 @@ class ConversionService: elif conversion.reservation_id is None: conversion.directly_attributable = False - conversion.updated_at = datetime.now() + # Only update timestamp if something actually changed + self._update_timestamp_if_modified(conversion, session) return matched_reservation, matched_customer