"""Tests of openedx.features.discounts.applicability""" from datetime import datetime, timedelta from unittest.mock import Mock, patch import ddt import pytest from zoneinfo import ZoneInfo from django.contrib.sites.models import Site from django.utils.timezone import now from edx_toggles.toggles.testutils import override_waffle_flag from enterprise.models import EnterpriseCustomer, EnterpriseCustomerUser from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from lms.djangoapps.experiments.models import ExperimentData from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.features.discounts.models import DiscountRestrictionConfig from openedx.features.discounts.utils import REV1008_EXPERIMENT_ID from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order from ..applicability import DISCOUNT_APPLICABILITY_FLAG, _is_in_holdback_and_bucket, can_receive_discount @ddt.ddt class TestApplicability(ModuleStoreTestCase): """ Applicability determines if this combination of user and course can receive a discount. Make sure that all of the business conditions work. """ def setUp(self): super().setUp() self.site, _ = Site.objects.get_or_create(domain='example.com') self.user = UserFactory.create() self.course = CourseFactory.create(run='test', display_name='test') CourseModeFactory.create(course_id=self.course.id, mode_slug='verified') now_time = datetime.now(tz=ZoneInfo("UTC")).strftime("%Y-%m-%d %H:%M:%S%z") ExperimentData.objects.create( user=self.user, experiment_id=REV1008_EXPERIMENT_ID, key=str(self.course.id), value=now_time ) holdback_patcher = patch( 'openedx.features.discounts.applicability._is_in_holdback_and_bucket', return_value=False ) self.mock_holdback = holdback_patcher.start() self.addCleanup(holdback_patcher.stop) def test_can_receive_discount(self): # Right now, no one should be able to receive the discount applicability = can_receive_discount(user=self.user, course=self.course) assert applicability is False @override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True) def test_can_receive_discount_course_requirements(self): """ Ensure first purchase offer banner only displays for courses with a non-expired verified mode """ CourseEnrollmentFactory( is_active=True, course_id=self.course.id, user=self.user ) applicability = can_receive_discount(user=self.user, course=self.course) assert applicability is True no_verified_mode_course = CourseFactory(end=now() + timedelta(days=30)) applicability = can_receive_discount(user=self.user, course=no_verified_mode_course) assert applicability is False course_that_has_ended = CourseFactory(end=now() - timedelta(days=30)) applicability = can_receive_discount(user=self.user, course=course_that_has_ended) assert applicability is False disabled_course = CourseFactory() CourseModeFactory.create(course_id=disabled_course.id, mode_slug='verified') # lint-amnesty, pylint: disable=no-member disabled_course_overview = CourseOverview.get_from_id(disabled_course.id) # lint-amnesty, pylint: disable=no-member DiscountRestrictionConfig.objects.create(disabled=True, course=disabled_course_overview) applicability = can_receive_discount(user=self.user, course=disabled_course) assert applicability is False @ddt.data(*( [[]] + [[mode] for mode in CourseMode.ALL_MODES] + [ [mode1, mode2] for mode1 in CourseMode.ALL_MODES for mode2 in CourseMode.ALL_MODES if mode1 != mode2 ] )) @override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True) def test_can_receive_discount_previous_verified_enrollment(self, existing_enrollments): """ Ensure that only users who have not already purchased courses receive the discount. """ CourseEnrollmentFactory( is_active=True, course_id=self.course.id, user=self.user ) for mode in existing_enrollments: CourseEnrollmentFactory.create(mode=mode, user=self.user) applicability = can_receive_discount(user=self.user, course=self.course) assert applicability == all(mode in CourseMode.UPSELL_TO_VERIFIED_MODES for mode in existing_enrollments) @ddt.data( None, CourseMode.VERIFIED, CourseMode.PROFESSIONAL, ) @override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True) def test_can_receive_discount_entitlement(self, entitlement_mode): """ Ensure that only users who have not already purchased courses receive the discount. """ CourseEnrollmentFactory( is_active=True, course_id=self.course.id, user=self.user ) if entitlement_mode is not None: CourseEntitlementFactory.create(mode=entitlement_mode, user=self.user) applicability = can_receive_discount(user=self.user, course=self.course) assert applicability == (entitlement_mode is None) @override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True) def test_can_receive_discount_false_enterprise(self): """ Ensure that enterprise users do not receive the discount. """ enterprise_customer = EnterpriseCustomer.objects.create( name='Test EnterpriseCustomer', site=self.site ) EnterpriseCustomerUser.objects.create( user_id=self.user.id, enterprise_customer=enterprise_customer ) applicability = can_receive_discount(user=self.user, course=self.course) assert applicability is False @override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True) def test_holdback_denies_discount(self): """ Ensure that users in the holdback do not receive the discount. """ self.mock_holdback.return_value = True applicability = can_receive_discount(user=self.user, course=self.course) assert not applicability @ddt.data( (0, True), (1, False), ) @ddt.unpack @pytest.mark.skip(reason="fix under work by revenue team") def test_holdback_group_ids(self, group_number, in_holdback): with patch('openedx.features.discounts.applicability.stable_bucketing_hash_group', return_value=group_number): assert _is_in_holdback_and_bucket(self.user) == in_holdback @pytest.mark.skip(reason="fix under work by revenue team") def test_holdback_expiry(self): with patch('openedx.features.discounts.applicability.stable_bucketing_hash_group', return_value=0): with patch( 'openedx.features.discounts.applicability.datetime', Mock(now=Mock(return_value=datetime(2020, 8, 1, 0, 1, tzinfo=ZoneInfo("UTC"))), wraps=datetime), ): assert not _is_in_holdback_and_bucket(self.user)