diff --git a/hubspot_sync/task_helpers.py b/hubspot_sync/task_helpers.py index 25d574d830..1b17a7dc13 100644 --- a/hubspot_sync/task_helpers.py +++ b/hubspot_sync/task_helpers.py @@ -4,6 +4,7 @@ from django.conf import settings +from courses.models import CourseRun, ProgramEnrollment from courses.utils import is_uai_order from ecommerce.models import Order, Product from hubspot_sync import tasks @@ -41,6 +42,21 @@ def sync_hubspot_user(user: User): ) +def _order_is_for_program_enrolled_course(order: Order) -> bool: + """Return True if any line is a course run belonging to a program the purchaser is already enrolled in.""" + course_ids = [ + line.purchased_object.course_id + for line in order.lines.all() + if line.purchased_object and isinstance(line.purchased_object, CourseRun) + ] + if not course_ids: + return False + return ProgramEnrollment.objects.filter( + user=order.purchaser, + program__all_requirements__course__in=course_ids, + ).exists() + + def sync_hubspot_deal(order: Order): """ Trigger celery task to sync an order to Hubspot if it has lines. @@ -60,6 +76,15 @@ def sync_hubspot_deal(order: Order): ) return + if _order_is_for_program_enrolled_course(order): + log.info( + "Skipping HubSpot deal sync for order %d: user %s (user_id=%d) is already enrolled in a program containing this course", + order.id, + order.purchaser.edx_username or order.purchaser.email, + order.purchaser.id, + ) + return + if order.lines.first() is not None: is_uai = is_uai_order(order) diff --git a/hubspot_sync/task_helpers_test.py b/hubspot_sync/task_helpers_test.py index d71426aa03..340232477f 100644 --- a/hubspot_sync/task_helpers_test.py +++ b/hubspot_sync/task_helpers_test.py @@ -1,9 +1,13 @@ """Tests for hubspot_sync.task_helpers""" import pytest +import reversion +from reversion.models import Version from b2b.factories import ContractPageFactory -from ecommerce.factories import ProductFactory +from courses.factories import CourseRunFactory, ProgramEnrollmentFactory, ProgramFactory +from courses.models import ProgramRequirementNodeType +from ecommerce.factories import LineFactory, OrderFactory, ProductFactory from hubspot_sync.task_helpers import ( sync_hubspot_cart_add, sync_hubspot_deal, @@ -172,6 +176,100 @@ def test_sync_hubspot_product(mocker, mock_exception_log, raise_exc): mock_exception_log.assert_not_called() +def test_sync_hubspot_deal_skips_for_course_in_enrolled_program(mocker, settings): + """sync_hubspot_deal should skip if the order is for a course run in a program the user is enrolled in.""" + settings.MITOL_HUBSPOT_API_PRIVATE_TOKEN = "faketoken" # noqa: S105 + + mock_sync = mocker.patch( + "hubspot_sync.task_helpers.tasks.sync_deal_with_hubspot_targeted.apply_async" + ) + mocker.patch("hubspot_sync.task_helpers.is_uai_order", return_value=False) + + course_run = CourseRunFactory.create() + program = ProgramFactory.create() + program.requirements_root.add_child( + node_type=ProgramRequirementNodeType.COURSE, + course=course_run.course, + ) + + with reversion.create_revision(): + product = ProductFactory.create(purchasable_object=course_run) + + order = OrderFactory.create() + LineFactory.create( + order=order, + product_version=Version.objects.get_for_object(product).first(), + purchased_object=course_run, + ) + ProgramEnrollmentFactory.create(user=order.purchaser, program=program) + + sync_hubspot_deal(order) + + mock_sync.assert_not_called() + + +def test_sync_hubspot_deal_proceeds_for_course_not_in_program( + mocker, mock_exception_log, settings +): + """sync_hubspot_deal should proceed if the order is for a course not in any program.""" + settings.MITOL_HUBSPOT_API_PRIVATE_TOKEN = "faketoken" # noqa: S105 + + mock_sync = mocker.patch( + "hubspot_sync.task_helpers.tasks.sync_deal_with_hubspot_targeted.apply_async" + ) + mocker.patch("hubspot_sync.task_helpers.is_uai_order", return_value=False) + + course_run = CourseRunFactory.create() + + with reversion.create_revision(): + product = ProductFactory.create(purchasable_object=course_run) + + order = OrderFactory.create() + LineFactory.create( + order=order, + product_version=Version.objects.get_for_object(product).first(), + purchased_object=course_run, + ) + + sync_hubspot_deal(order) + + mock_sync.assert_called_once() + + +def test_sync_hubspot_deal_proceeds_when_not_enrolled_in_program( + mocker, mock_exception_log, settings +): + """sync_hubspot_deal should proceed if the user is not enrolled in the program containing this course.""" + settings.MITOL_HUBSPOT_API_PRIVATE_TOKEN = "faketoken" # noqa: S105 + + mock_sync = mocker.patch( + "hubspot_sync.task_helpers.tasks.sync_deal_with_hubspot_targeted.apply_async" + ) + mocker.patch("hubspot_sync.task_helpers.is_uai_order", return_value=False) + + course_run = CourseRunFactory.create() + program = ProgramFactory.create() + program.requirements_root.add_child( + node_type=ProgramRequirementNodeType.COURSE, + course=course_run.course, + ) + + with reversion.create_revision(): + product = ProductFactory.create(purchasable_object=course_run) + + order = OrderFactory.create() + LineFactory.create( + order=order, + product_version=Version.objects.get_for_object(product).first(), + purchased_object=course_run, + ) + # Intentionally do not enroll the user in the program + + sync_hubspot_deal(order) + + mock_sync.assert_called_once() + + @pytest.mark.parametrize("raise_exc", [True, False]) def test_sync_hubspot_cart_add(mocker, mock_exception_log, user, raise_exc): """sync_hubspot_cart_add should call sync_cart_add_event_with_hubspot.apply_async and log any exception"""