diff --git a/courses/api.py b/courses/api.py index cce150e9dd..d20b24c8ff 100644 --- a/courses/api.py +++ b/courses/api.py @@ -782,6 +782,8 @@ def process_course_run_grade_certificate(course_run_grade, should_force_create=F Tuple[ CourseRunCertificate, bool, bool ]: A Tuple containing None or CourseRunCertificate object, A bool representing if the certificate is created, A bool representing if a certificate is deleted """ + from hubspot_sync.task_helpers import sync_hubspot_user + user = course_run_grade.user course_run = course_run_grade.course_run @@ -793,6 +795,7 @@ def process_course_run_grade_certificate(course_run_grade, should_force_create=F delete_count, _ = CourseRunCertificate.objects.filter( user=user, course_run=course_run ).delete() + sync_hubspot_user(user) return None, False, (delete_count > 0) elif should_create: @@ -800,12 +803,12 @@ def process_course_run_grade_certificate(course_run_grade, should_force_create=F certificate, created = CourseRunCertificate.objects.get_or_create( user=user, course_run=course_run ) + sync_hubspot_user(user) return certificate, created, False # noqa: TRY300 except IntegrityError: log.warning( f"IntegrityError caught processing certificate for {course_run.courseware_id} for user {user} - certificate was likely already revoked." # noqa: G004 ) - return None, False, False @@ -1009,6 +1012,8 @@ def generate_program_certificate(user, program, force_create=False): # noqa: FB ProgramCertificate (or None if one was not found or created) paired with a boolean indicating whether the certificate was newly created. """ + from hubspot_sync.task_helpers import sync_hubspot_user + existing_cert_queryset = ProgramCertificate.objects.filter( user=user, program=program ) @@ -1028,6 +1033,7 @@ def generate_program_certificate(user, program, force_create=False): # noqa: FB user.username, program.title, ) + sync_hubspot_user(user) _, created = ProgramEnrollment.objects.get_or_create( program=program, user=user, defaults={"active": True, "change_status": None} ) diff --git a/courses/api_test.py b/courses/api_test.py index bf93ee8546..eddb4a8c89 100644 --- a/courses/api_test.py +++ b/courses/api_test.py @@ -1102,10 +1102,14 @@ def test_course_run_certificate( # noqa: PLR0913 exp_certificate, exp_created, exp_deleted, + mocker, ): """ Test that the certificate is generated correctly """ + patched_sync_hubspot_user = mocker.patch( + "hubspot_sync.task_helpers.sync_hubspot_user", + ) passed_grade_with_enrollment.grade = grade passed_grade_with_enrollment.passed = passed if not paid: @@ -1117,16 +1121,20 @@ def test_course_run_certificate( # noqa: PLR0913 certificate, created, deleted = process_course_run_grade_certificate( passed_grade_with_enrollment ) + if created: + patched_sync_hubspot_user.assert_called_once_with(user) assert bool(certificate) is exp_certificate assert created is exp_created assert deleted is exp_deleted -def test_course_run_certificate_idempotent(passed_grade_with_enrollment): +def test_course_run_certificate_idempotent(passed_grade_with_enrollment, mocker, user): """ Test that the certificate generation is idempotent """ - + patched_sync_hubspot_user = mocker.patch( + "hubspot_sync.task_helpers.sync_hubspot_user", + ) # Certificate is created the first time certificate, created, deleted = process_course_run_grade_certificate( passed_grade_with_enrollment @@ -1135,6 +1143,8 @@ def test_course_run_certificate_idempotent(passed_grade_with_enrollment): assert created assert not deleted + patched_sync_hubspot_user.assert_called_once_with(user) + # Existing certificate is simply returned without any create/delete certificate, created, deleted = process_course_run_grade_certificate( passed_grade_with_enrollment @@ -1148,7 +1158,6 @@ def test_course_run_certificate_not_passing(passed_grade_with_enrollment): """ Test that the certificate is not generated if the grade is set to not passed """ - # Initially the certificate is created certificate, created, deleted = process_course_run_grade_certificate( passed_grade_with_enrollment @@ -1425,10 +1434,13 @@ def test_generate_program_certificate_failure_not_all_passed( assert len(ProgramCertificate.objects.all()) == 0 -def test_generate_program_certificate_success_single_requirement_course(user): +def test_generate_program_certificate_success_single_requirement_course(user, mocker): """ Test that generate_program_certificate generates a program certificate for a Program with a single required Course. """ + patched_sync_hubspot_user = mocker.patch( + "hubspot_sync.task_helpers.sync_hubspot_user", + ) course = CourseFactory.create() program = ProgramFactory.create() ProgramRequirementFactory.add_root(program) @@ -1449,12 +1461,16 @@ def test_generate_program_certificate_success_single_requirement_course(user): assert created is True assert isinstance(certificate, ProgramCertificate) assert len(ProgramCertificate.objects.all()) == 1 + patched_sync_hubspot_user.assert_called_once_with(user) -def test_generate_program_certificate_success_multiple_required_courses(user): +def test_generate_program_certificate_success_multiple_required_courses(user, mocker): """ Test that generate_program_certificate generate a program certificate """ + patched_sync_hubspot_user = mocker.patch( + "hubspot_sync.task_helpers.sync_hubspot_user", + ) courses = CourseFactory.create_batch(3) program = ProgramFactory.create() ProgramRequirementFactory.add_root(program) @@ -1476,11 +1492,12 @@ def test_generate_program_certificate_success_multiple_required_courses(user): assert created is True assert isinstance(certificate, ProgramCertificate) assert len(ProgramCertificate.objects.all()) == 1 + patched_sync_hubspot_user.assert_called_once_with(user) def test_generate_program_certificate_success_minimum_electives_not_met(user): """ - Test that generate_program_certificate generate a program certificate + Test that generate_program_certificate does not generate a program certificate if minimum electives have not been met. """ courses = CourseFactory.create_batch(3) @@ -1524,11 +1541,18 @@ def test_generate_program_certificate_success_minimum_electives_not_met(user): assert len(ProgramCertificate.objects.all()) == 0 -def test_force_generate_program_certificate_success(user, program_with_requirements): # noqa: F811 +def test_force_generate_program_certificate_success( + user, + program_with_requirements, # noqa: F811 + mocker, +): """ Test that force creating a program certificate with generate_program_certificate generates a program certificate without matching program certificate requirements. """ + patched_sync_hubspot_user = mocker.patch( + "hubspot_sync.task_helpers.sync_hubspot_user", + ) courses = CourseFactory.create_batch(3) course_runs = CourseRunFactory.create_batch(3, course=factory.Iterator(courses)) CourseRunCertificateFactory.create_batch( @@ -1545,6 +1569,7 @@ def test_force_generate_program_certificate_success(user, program_with_requireme assert created is True assert isinstance(certificate, ProgramCertificate) assert len(ProgramCertificate.objects.all()) == 1 + patched_sync_hubspot_user.assert_called_once_with(user) def test_generate_program_certificate_already_exist( diff --git a/courses/management/commands/import_courserun.py b/courses/management/commands/import_courserun.py index 23c9ea29b5..821fce8663 100644 --- a/courses/management/commands/import_courserun.py +++ b/courses/management/commands/import_courserun.py @@ -275,4 +275,5 @@ def handle(self, *args, **kwargs): # pylint: disable=unused-argument # noqa: C ) ) - self.stdout.write(self.style.SUCCESS(f"{success_count} course runs created")) # noqa: RET503 + self.stdout.write(self.style.SUCCESS(f"{success_count} course runs created")) + return None diff --git a/courses/management/commands/test_manage_certificate.py b/courses/management/commands/test_manage_certificate.py index d2a7031696..4bae23b11b 100644 --- a/courses/management/commands/test_manage_certificate.py +++ b/courses/management/commands/test_manage_certificate.py @@ -122,8 +122,11 @@ def test_certificate_management_revoke_unrevoke_invalid_args( (None, True), ], ) -def test_certificate_management_revoke_unrevoke_success(user, revoke, unrevoke): +def test_certificate_management_revoke_unrevoke_success(user, revoke, unrevoke, mocker): """Test that certificate revoke, un-revoke work as expected and manage the certificate access properly""" + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) course_run = CourseRunFactory.create() certificate = CourseRunCertificateFactory( course_run=course_run, @@ -146,6 +149,9 @@ def test_certificate_management_create(mocker, user, edx_grade_json, revoked): """Test that create operation for certificate management command creates the certificates for a single user when a user is provided """ + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) edx_grade = CurrentGrade(edx_grade_json) course_run = CourseRunFactory.create() CourseRunEnrollmentFactory.create( @@ -185,6 +191,9 @@ def test_certificate_management_create_no_user(mocker, edx_grade_json, user): """Test that create operation for certificate management command attempts to creates the certificates for all the enrolled users in a run when no user is provided """ + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) passed_edx_grade = CurrentGrade(edx_grade_json) course_run = CourseRunFactory.create() users = UserFactory.create_batch(4) diff --git a/courses/management/commands/test_manage_program_certificate.py b/courses/management/commands/test_manage_program_certificate.py index 77e4753898..52f929cc20 100644 --- a/courses/management/commands/test_manage_program_certificate.py +++ b/courses/management/commands/test_manage_program_certificate.py @@ -120,11 +120,18 @@ def test_program_certificate_management_revoke_unrevoke_success(user, revoke, un assert certificate.is_revoked is (False if unrevoke else True) # noqa: SIM211 -def test_program_certificate_management_create(user, program_with_empty_requirements): # noqa: F811 +def test_program_certificate_management_create( + user, + program_with_empty_requirements, # noqa: F811 + mocker, +): """ Test that create operation for program certificate management command creates the program certificate for a user """ + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) courses = CourseFactory.create_batch(2) program_with_empty_requirements.add_requirement(courses[0]) program_with_empty_requirements.add_elective(courses[1]) @@ -145,11 +152,18 @@ def test_program_certificate_management_create(user, program_with_empty_requirem assert generated_certificates.count() == 1 -def test_program_certificate_management_force_create(user, program_with_requirements): # noqa: F811 +def test_program_certificate_management_force_create( + user, + program_with_requirements, # noqa: F811 + mocker, +): """ Test that create operation for program certificate management command forcefully creates the certificate for a user """ + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) courses = CourseFactory.create_batch(3) course_runs = CourseRunFactory.create_batch(3, course=factory.Iterator(courses)) CourseRunGradeFactory.create_batch( diff --git a/courses/signals.py b/courses/signals.py index f1be23e77d..ecfdce47aa 100644 --- a/courses/signals.py +++ b/courses/signals.py @@ -2,6 +2,7 @@ Signals for mitxonline course certificates """ +from django.core.management import call_command from django.db import transaction from django.db.models.signals import post_save from django.dispatch import receiver @@ -11,6 +12,7 @@ CourseRunCertificate, Program, ) +from hubspot_sync.task_helpers import sync_hubspot_user @receiver( @@ -18,7 +20,12 @@ sender=CourseRunCertificate, dispatch_uid="courseruncertificate_post_save", ) -def handle_create_course_run_certificate(sender, instance, created, **kwargs): # pylint: disable=unused-argument # noqa: ARG001 +def handle_create_course_run_certificate( + sender, # pylint: disable=unused-argument # noqa: ARG001 + instance, + created, + **kwargs, # pylint: disable=unused-argument # noqa: ARG001 +): """ When a CourseRunCertificate model is created. """ @@ -34,3 +41,5 @@ def handle_create_course_run_certificate(sender, instance, created, **kwargs): transaction.on_commit( lambda: generate_multiple_programs_certificate(user, programs) ) + call_command("configure_hubspot_properties") + sync_hubspot_user(instance) diff --git a/courses/signals_test.py b/courses/signals_test.py index 926a2b0756..721e2245d2 100644 --- a/courses/signals_test.py +++ b/courses/signals_test.py @@ -20,11 +20,14 @@ # pylint: disable=unused-argument @patch("courses.signals.transaction.on_commit", side_effect=lambda callback: callback()) @patch("courses.signals.generate_multiple_programs_certificate", autospec=True) -def test_create_course_certificate(generate_program_cert_mock, mock_on_commit): +def test_create_course_certificate(generate_program_cert_mock, mock_on_commit, mocker): """ Test that generate_multiple_programs_certificate is called when a course certificate is created """ + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) user = UserFactory.create() course_run = CourseRunFactory.create() program = ProgramFactory.create() @@ -38,11 +41,14 @@ def test_create_course_certificate(generate_program_cert_mock, mock_on_commit): @patch("courses.signals.transaction.on_commit", side_effect=lambda callback: callback()) @patch("courses.signals.generate_multiple_programs_certificate", autospec=True) def test_generate_program_certificate_if_not_live( - generate_program_cert_mock, mock_on_commit + generate_program_cert_mock, mock_on_commit, mocker ): """ Test that generate_multiple_programs_certificate is not called when a program is not live """ + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) user = UserFactory.create() course_run = CourseRunFactory.create() program = ProgramFactory.create(live=False) @@ -57,12 +63,15 @@ def test_generate_program_certificate_if_not_live( @patch("courses.signals.transaction.on_commit", side_effect=lambda callback: callback()) @patch("courses.signals.generate_multiple_programs_certificate", autospec=True) def test_generate_program_certificate_not_called( - generate_program_cert_mock, mock_on_commit + generate_program_cert_mock, mock_on_commit, mocker ): """ Test that generate_multiple_programs_certificate is not called when a course is not associated with program. """ + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) user = UserFactory.create() course = CourseFactory.create() course_run = CourseRunFactory.create(course=course) diff --git a/hubspot_sync/api.py b/hubspot_sync/api.py index b47390dea8..31ee0ee603 100644 --- a/hubspot_sync/api.py +++ b/hubspot_sync/api.py @@ -93,7 +93,7 @@ def make_contact_sync_message_from_user(user: User) -> SimplePublicObjectInput: Returns: SimplePublicObjectInput: Input object for upserting User data to Hubspot """ - from users.serializers import UserSerializer + from hubspot_sync.serializers import HubspotContactSerializer contact_properties_map = { "email": "email", @@ -116,8 +116,10 @@ def make_contact_sync_message_from_user(user: User) -> SimplePublicObjectInput: "type_is_professional": "typeisprofessional", "type_is_educator": "typeiseducator", "type_is_other": "typeisother", + "program_certificates": "program_certificates", + "course_run_certificates": "course_run_certificates", } - properties = UserSerializer(user).data + properties = HubspotContactSerializer(user).data properties.update(properties.pop("legal_address") or {}) properties.update(properties.pop("user_profile") or {}) hubspot_props = transform_object_properties(properties, contact_properties_map) diff --git a/hubspot_sync/api_test.py b/hubspot_sync/api_test.py index 8d17fe6eae..b2ca43e125 100644 --- a/hubspot_sync/api_test.py +++ b/hubspot_sync/api_test.py @@ -5,9 +5,7 @@ import pytest import reversion from django.contrib.contenttypes.models import ContentType -from hubspot.crm.objects import ( - ApiException, -) +from hubspot.crm.objects import ApiException from mitol.common.utils.datetime import now_in_utc from mitol.hubspot_api.factories import HubspotObjectFactory, SimplePublicObjectFactory from mitol.hubspot_api.models import HubspotObject @@ -15,7 +13,9 @@ from courses.constants import ALL_ENROLL_CHANGE_STATUSES from courses.factories import ( + CourseRunCertificateFactory, CourseRunEnrollmentFactory, + ProgramCertificateFactory, ) from ecommerce.factories import LineFactory, OrderFactory, ProductFactory from ecommerce.models import Product @@ -27,18 +27,22 @@ OrderToDealSerializer, ProductSerializer, ) -from openedx.constants import ( - EDX_ENROLLMENT_AUDIT_MODE, - EDX_ENROLLMENT_VERIFIED_MODE, -) +from openedx.constants import EDX_ENROLLMENT_AUDIT_MODE, EDX_ENROLLMENT_VERIFIED_MODE from users.factories import UserFactory pytestmark = [pytest.mark.django_db] @pytest.mark.django_db -def test_make_contact_sync_message(user): +def test_make_contact_sync_message(user, mocker): """Test make_contact_sync_message serializes a user and returns a properly formatted sync message""" + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) + course_certificate_1 = CourseRunCertificateFactory.create(user=user) + course_certificate_2 = CourseRunCertificateFactory.create(user=user) + program_certificate_1 = ProgramCertificateFactory.create(user=user) + program_certificate_2 = ProgramCertificateFactory.create(user=user) contact_sync_message = api.make_contact_sync_message_from_user(user) assert contact_sync_message.properties == { "country": user.legal_address.country, @@ -61,6 +65,12 @@ def test_make_contact_sync_message(user): "typeisprofessional": user.user_profile.type_is_professional, "typeiseducator": user.user_profile.type_is_educator, "typeisother": user.user_profile.type_is_other, + "program_certificates": str(program_certificate_1.program) + + ";" + + str(program_certificate_2.program), + "course_run_certificates": str(course_certificate_1.course_run) + + ";" + + str(course_certificate_2.course_run), } diff --git a/hubspot_sync/management/commands/configure_hubspot_properties.py b/hubspot_sync/management/commands/configure_hubspot_properties.py index 563919b812..b4ea9063e4 100644 --- a/hubspot_sync/management/commands/configure_hubspot_properties.py +++ b/hubspot_sync/management/commands/configure_hubspot_properties.py @@ -15,6 +15,7 @@ ) from courses.constants import ALL_ENROLL_CHANGE_STATUSES +from courses.models import CourseRun, Program from ecommerce import models from ecommerce.constants import ( DISCOUNT_TYPE_DOLLARS_OFF, @@ -637,26 +638,86 @@ } -def upsert_custom_properties(): +def _get_course_run_certificate_hubspot_property(): + """ + Creates a dictionary representation of a Hubspot checkbox, + populated with options using the string representation of all course runs. + + Returns: + dict: dictionary representing the properties for a HubSpot checkbox, + populated with the string representation of all course runs. + """ + course_runs = CourseRun.objects.all() + options_array = [ + { + "value": str(course_run), + "label": str(course_run), + "hidden": False, + } + for course_run in course_runs + ] + return { + "name": "course_run_certificates", + "label": "Course Run certificates", + "description": "Earned course run certificates.", + "groupName": "contactinformation", + "type": "enumeration", + "fieldType": "checkbox", + "options": options_array, + } + + +def _get_program_certificate_hubspot_property(): + """ + Creates a dictionary representation of a Hubspot checkbox, + populated with options using string representation of all programs. + + Returns: + dict: dictionary representing the properties for a HubSpot checkbox, + populated with the string representation of all programs. + """ + programs = Program.objects.all() + options_array = [ + { + "value": str(program), + "label": str(program), + "hidden": False, + } + for program in programs + ] + return { + "name": "program_certificates", + "label": "Program certificates", + "description": "Earned program certificates.", + "groupName": "contactinformation", + "type": "enumeration", + "fieldType": "checkbox", + "options": options_array, + } + + +def _upsert_custom_properties(): """Create or update all custom properties and groups""" - for object_type in CUSTOM_ECOMMERCE_PROPERTIES: - for group in CUSTOM_ECOMMERCE_PROPERTIES[object_type]["groups"]: + for ecommerce_object_type, ecommerce_object in CUSTOM_ECOMMERCE_PROPERTIES.items(): + for group in ecommerce_object["groups"]: sys.stdout.write(f"Adding group {group}\n") - sync_property_group(object_type, group["name"], group["label"]) - for obj_property in CUSTOM_ECOMMERCE_PROPERTIES[object_type]["properties"]: + sync_property_group(ecommerce_object_type, group["name"], group["label"]) + for obj_property in ecommerce_object["properties"]: sys.stdout.write(f"Adding property {obj_property}\n") - sync_object_property(object_type, obj_property) + sync_object_property(ecommerce_object_type, obj_property) + sync_object_property("contacts", _get_course_run_certificate_hubspot_property()) + sync_object_property("contacts", _get_program_certificate_hubspot_property()) -def delete_custom_properties(): +def _delete_custom_properties(): """Delete all custom properties and groups""" - for object_type in CUSTOM_ECOMMERCE_PROPERTIES: - for obj_property in CUSTOM_ECOMMERCE_PROPERTIES[object_type]["properties"]: - if object_property_exists(object_type, obj_property): - delete_object_property(object_type, obj_property) - for group in CUSTOM_ECOMMERCE_PROPERTIES[object_type]["groups"]: - if property_group_exists(object_type, group): - delete_property_group(object_type, group) + for ecommerce_object_type, ecommerce_object in CUSTOM_ECOMMERCE_PROPERTIES.items(): + for obj_property in ecommerce_object["properties"]: + if object_property_exists(ecommerce_object_type, obj_property): + delete_object_property(ecommerce_object_type, obj_property) + for group in ecommerce_object["groups"]: + if property_group_exists(ecommerce_object_type, group): + delete_property_group(ecommerce_object_type, group) class Command(BaseCommand): @@ -679,10 +740,10 @@ def add_arguments(self, parser): def handle(self, *args, **options): # noqa: ARG002 if options["delete"]: print("Uninstalling custom groups and properties...") # noqa: T201 - delete_custom_properties() + _delete_custom_properties() print("Uninstall successful") # noqa: T201 return else: print("Configuring custom groups and properties...") # noqa: T201 - upsert_custom_properties() + _upsert_custom_properties() print("Custom properties configured") # noqa: T201 diff --git a/hubspot_sync/serializers.py b/hubspot_sync/serializers.py index ece3d23951..d614c940a9 100644 --- a/hubspot_sync/serializers.py +++ b/hubspot_sync/serializers.py @@ -6,13 +6,14 @@ from mitol.hubspot_api.api import format_app_id from rest_framework import serializers -from courses.models import CourseRunEnrollment +from courses.models import CourseRunCertificate, CourseRunEnrollment, ProgramCertificate from ecommerce import models from ecommerce.constants import DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_PERCENT_OFF from ecommerce.discounts import resolve_product_version from ecommerce.models import Product from hubspot_sync.api import format_product_name, get_hubspot_id_for_object from main.utils import format_decimal +from users.serializers import UserSerializer """ Map order state to hubspot ids for pipeline stages @@ -274,6 +275,43 @@ class Meta: model = models.Product +class HubspotContactSerializer(UserSerializer): + """User Serializer for Hubspot""" + + program_certificates = serializers.SerializerMethodField() + course_run_certificates = serializers.SerializerMethodField() + + def get_program_certificates(self, instance): + """Return a list of program names that the user has a certificate for.""" + programs_user_has_cert = ProgramCertificate.objects.filter( + user=instance, is_revoked=False + ).select_related("program") + program_name_array = [ + str(program_cert.program) for program_cert in programs_user_has_cert + ] + return ";".join(program_name_array) + + def get_course_run_certificates(self, instance): + """Return a list of course run names that the user has a certificate for.""" + course_runs_user_has_cert = CourseRunCertificate.objects.filter( + user=instance, is_revoked=False + ).select_related("course_run") + course_run_name_array = [ + str(course_run_cert.course_run) + for course_run_cert in course_runs_user_has_cert + ] + return ";".join(course_run_name_array) + + class Meta: + fields = ( + *UserSerializer.Meta.fields, + "program_certificates", + "course_run_certificates", + ) + read_only_fields = fields + model = models.User + + def get_hubspot_serializer(obj: object) -> serializers.ModelSerializer: """Get the appropriate serializer for an object""" if isinstance(obj, models.Order): diff --git a/hubspot_sync/serializers_test.py b/hubspot_sync/serializers_test.py index 628cc1afdc..570183e420 100644 --- a/hubspot_sync/serializers_test.py +++ b/hubspot_sync/serializers_test.py @@ -1,6 +1,7 @@ """ Tests for hubspot_sync serializers """ + # pylint: disable=unused-argument, redefined-outer-name from decimal import Decimal @@ -11,7 +12,12 @@ from mitol.hubspot_api.api import format_app_id from mitol.hubspot_api.models import HubspotObject -from courses.factories import CourseRunEnrollmentFactory, CourseRunFactory +from courses.factories import ( + CourseRunCertificateFactory, + CourseRunEnrollmentFactory, + CourseRunFactory, + ProgramCertificateFactory, +) from ecommerce.constants import ( DISCOUNT_TYPE_DOLLARS_OFF, DISCOUNT_TYPE_FIXED_PRICE, @@ -25,6 +31,7 @@ from ecommerce.models import Order, Product from hubspot_sync.serializers import ( ORDER_STATUS_MAPPING, + HubspotContactSerializer, LineSerializer, OrderToDealSerializer, ProductSerializer, @@ -39,7 +46,10 @@ [ ["course-v1:MITxOnline+SysEngxNAV+R1", "Run 1"], # noqa: PT007 ["course-v1:MITxOnline+SysEngxNAV+R10", "Run 10"], # noqa: PT007 - ["course-v1:MITxOnline+SysEngxNAV", "course-v1:MITxOnline+SysEngxNAV"], # noqa: PT007 + ( + "course-v1:MITxOnline+SysEngxNAV", + "course-v1:MITxOnline+SysEngxNAV", + ), ], ) def test_serialize_product(text_id, expected): @@ -164,3 +174,23 @@ def test_serialize_order_with_coupon( # noqa: PLR0913 "pipeline": settings.HUBSPOT_PIPELINE_ID, "unique_app_id": format_app_id(hubspot_order.id), } + + +def test_serialize_contact(settings, user, mocker): + """Test that HubspotContactSerializer includes program and course run certificates for the user""" + mocker.patch( + "hubspot_sync.management.commands.configure_hubspot_properties._upsert_custom_properties", + ) + program_cert_1 = ProgramCertificateFactory.create(user=user) + program_cert_2 = ProgramCertificateFactory.create(user=user) + course_run_cert_1 = CourseRunCertificateFactory.create(user=user) + course_run_cert_2 = CourseRunCertificateFactory.create(user=user) + serialized_data = HubspotContactSerializer(instance=user).data + assert ( + serialized_data["program_certificates"] + == f"{program_cert_1.program!s};{program_cert_2.program!s}" + ) + assert ( + serialized_data["course_run_certificates"] + == f"{course_run_cert_1.course_run!s};{course_run_cert_2.course_run!s}" + )