diff --git a/drf_lint_baseline.json b/drf_lint_baseline.json index b1338f4b53..751686338a 100644 --- a/drf_lint_baseline.json +++ b/drf_lint_baseline.json @@ -81,8 +81,11 @@ "hubspot_sync/serializers.py:171:22:ORM001", "hubspot_sync/serializers.py:183:25:ORM002", "hubspot_sync/serializers.py:186:33:ORM002", + "hubspot_sync/serializers.py:193:21:ORM002", "hubspot_sync/serializers.py:312:33:ORM001", "hubspot_sync/serializers.py:323:36:ORM001", + "hubspot_sync/serializers.py:324:33:ORM001", + "hubspot_sync/serializers.py:335:36:ORM001", "hubspot_sync/serializers.py:64:22:ORM002", "hubspot_sync/serializers.py:76:31:ORM002", "hubspot_sync/serializers.py:80:31:ORM002", diff --git a/hubspot_sync/api.py b/hubspot_sync/api.py index 2abb0d761b..5ed047dbea 100644 --- a/hubspot_sync/api.py +++ b/hubspot_sync/api.py @@ -2191,14 +2191,35 @@ def _ensure_hubspot_contact_for_user( def _sync_cart_add_deal_with_hubspot( order: Order, contact_id: str, hubspot_client: HubspotApi ) -> SimplePublicObject: - """Create cart-add deal and line-item objects and associate them in target account.""" + """Create or update cart-add deal and line-item objects and associate them in target account.""" deal_input = _build_target_deal_message(order, hubspot_client) - wait_for_hubspot_rate_limit() - deal = hubspot_client.crm.objects.basic_api.create( - object_type=HubspotObjectType.DEALS.value, - simple_public_object_input_for_create=deal_input, + # Extract unique_app_id from deal input to check for existing deals + unique_app_id = deal_input.properties.get("unique_app_id") + + # Use the same dual lookup logic as checkout flow to prevent duplicates + existing_deal_id = _find_target_deal_id_by_dealname( + hubspot_client, deal_input.properties.get("dealname") ) + if not existing_deal_id and unique_app_id: + existing_deal_id = _find_target_deal_id_by_unique_app_id( + hubspot_client, unique_app_id + ) + + wait_for_hubspot_rate_limit() + if existing_deal_id: + # Update existing deal + deal = hubspot_client.crm.objects.basic_api.update( + object_type=HubspotObjectType.DEALS.value, + object_id=existing_deal_id, + simple_public_object_input=deal_input, + ) + else: + # Create new deal + deal = hubspot_client.crm.objects.basic_api.create( + object_type=HubspotObjectType.DEALS.value, + simple_public_object_input_for_create=deal_input, + ) wait_for_hubspot_rate_limit() hubspot_client.crm.associations.v4.basic_api.create_default( diff --git a/hubspot_sync/serializers.py b/hubspot_sync/serializers.py index 7ff079400b..42e03ea5fb 100644 --- a/hubspot_sync/serializers.py +++ b/hubspot_sync/serializers.py @@ -187,7 +187,19 @@ def _get_discount(self, instance): return self._discount def get_unique_app_id(self, instance): - """Get the app_id for the object""" + """Get the app_id for the object - based on user and purchasable object for consistency""" + # For consistency between cart-add and checkout, base the unique_app_id on + # the user and purchasable object rather than the order ID + first_line = instance.lines.first() + if first_line and first_line.purchased_object: + # Generate consistent ID based on user and purchasable object + user_identifier = instance.purchaser.global_id + object_type = first_line.purchased_content_type.model + object_id = first_line.purchased_object_id + consistent_id = f"{user_identifier}-{object_type}-{object_id}" + return format_app_id(consistent_id) + + # Fallback to original behavior for orders without lines return format_app_id(instance.id) def get_dealname(self, instance): diff --git a/hubspot_sync/serializers_test.py b/hubspot_sync/serializers_test.py index 56dff18a63..07ef02aa19 100644 --- a/hubspot_sync/serializers_test.py +++ b/hubspot_sync/serializers_test.py @@ -117,6 +117,19 @@ def test_serialize_order(settings, hubspot_order, status): """Test that OrderToDealSerializer produces the correct serialized data""" hubspot_order.state = status serialized_data = OrderToDealSerializer(instance=hubspot_order).data + + # Calculate expected unique_app_id based on new logic (user global_id + purchasable object) + first_line = hubspot_order.lines.first() + if first_line and first_line.purchased_object: + user_identifier = hubspot_order.purchaser.global_id + object_type = first_line.purchased_content_type.model + object_id = first_line.purchased_object_id + consistent_id = f"{user_identifier}-{object_type}-{object_id}" + expected_unique_app_id = format_app_id(consistent_id) + else: + # Fallback to original behavior for orders without lines + expected_unique_app_id = format_app_id(hubspot_order.id) + assert serialized_data == { "dealname": f"MITXONLINE-ORDER-{hubspot_order.id}", "dealstage": ORDER_STATUS_MAPPING[status], @@ -132,7 +145,7 @@ def test_serialize_order(settings, hubspot_order, status): "coupon_code": None, "status": hubspot_order.state, "pipeline": settings.HUBSPOT_PIPELINE_ID, - "unique_app_id": format_app_id(hubspot_order.id), + "unique_app_id": expected_unique_app_id, } @@ -174,6 +187,19 @@ def test_serialize_order_with_coupon( # noqa: PLR0913 ) serialized_data = OrderToDealSerializer(instance=hubspot_order).data + + # Calculate expected unique_app_id based on new logic (user global_id + purchasable object) + first_line = hubspot_order.lines.first() + if first_line and first_line.purchased_object: + user_identifier = hubspot_order.purchaser.global_id + object_type = first_line.purchased_content_type.model + object_id = first_line.purchased_object_id + consistent_id = f"{user_identifier}-{object_type}-{object_id}" + expected_unique_app_id = format_app_id(consistent_id) + else: + # Fallback to original behavior for orders without lines + expected_unique_app_id = format_app_id(hubspot_order.id) + assert serialized_data == { "dealname": f"MITXONLINE-ORDER-{hubspot_order.id}", "dealstage": ORDER_STATUS_MAPPING[hubspot_order.state], @@ -189,7 +215,7 @@ def test_serialize_order_with_coupon( # noqa: PLR0913 "discount_percent": percent_off, "status": hubspot_order.state, "pipeline": settings.HUBSPOT_PIPELINE_ID, - "unique_app_id": format_app_id(hubspot_order.id), + "unique_app_id": expected_unique_app_id, }