181 lines
7.5 KiB
Python
181 lines
7.5 KiB
Python
"""Tests of openedx.features.discounts.applicability"""
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import Mock, patch
|
|
|
|
import ddt
|
|
import pytest
|
|
import pytz
|
|
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=pytz.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=pytz.UTC)), wraps=datetime),
|
|
):
|
|
assert not _is_in_holdback_and_bucket(self.user)
|