Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions hubspot_sync/task_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand Down
100 changes: 99 additions & 1 deletion hubspot_sync/task_helpers_test.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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"""
Expand Down