diff --git a/lms/envs/common.py b/lms/envs/common.py index 828c7874152b..6f31f5c76ec2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3293,3 +3293,15 @@ def _should_send_certificate_events(settings): SSL_AUTH_DN_FORMAT_STRING = ( "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" ) + +# .. setting_name: OPEN_EDX_FILTERS_CONFIG +# .. setting_default: {} +# .. setting_description: Configuration dict for openedx-filters pipeline steps. +# Keys are filter type strings; values are dicts with 'fail_silently' (bool) and +# 'pipeline' (list of dotted-path strings to PipelineStep subclasses). +OPEN_EDX_FILTERS_CONFIG = { + "org.openedx.learning.discount.eligibility.check.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.discounts.DiscountEligibilityEnterpriseStep"], + }, +} diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py index 89ddc1d59b71..e7b348116e4c 100644 --- a/openedx/features/discounts/applicability.py +++ b/openedx/features/discounts/applicability.py @@ -9,6 +9,7 @@ """ +import logging from datetime import datetime, timedelta from zoneinfo import ZoneInfo @@ -17,6 +18,7 @@ from django.utils import timezone from django.utils.dateparse import parse_datetime from edx_toggles.toggles import WaffleFlag +from openedx_filters.learning.filters import DiscountEligibilityCheckRequested from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.entitlements.models import CourseEntitlement @@ -28,6 +30,8 @@ from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group from openedx.features.discounts.models import DiscountPercentageConfig, DiscountRestrictionConfig +log = logging.getLogger(__name__) + # .. toggle_name: discounts.enable_first_purchase_discount_override # .. toggle_implementation: WaffleFlag # .. toggle_default: False @@ -124,11 +128,13 @@ def can_show_streak_discount_coupon(user, course): if not is_mode_upsellable(user, enrollment): return False - # We can't import this at Django load time within the openedx tests settings context - from openedx.features.enterprise_support.utils import is_enterprise_learner - - # Don't give discount to enterprise users - if is_enterprise_learner(user): + # Allow plugins to mark this user as ineligible for the discount. + try: + DiscountEligibilityCheckRequested.run_filter( + user=user, course_key=course.id, + ) + except DiscountEligibilityCheckRequested.DiscountIneligible as exc: + log.info("User is ineligible for streak discount: %s", exc.message) return False return True @@ -179,11 +185,13 @@ def can_receive_discount(user, course, discount_expiration_date=None): if CourseEntitlement.objects.filter(user=user).exists(): return False - # We can't import this at Django load time within the openedx tests settings context - from openedx.features.enterprise_support.utils import is_enterprise_learner - - # Don't give discount to enterprise users - if is_enterprise_learner(user): + # Allow plugins to mark this user as ineligible for the discount. + try: + DiscountEligibilityCheckRequested.run_filter( + user=user, course_key=course.id, + ) + except DiscountEligibilityCheckRequested.DiscountIneligible as exc: + log.info("User is ineligible for discount: %s", exc.message) return False # Turn holdback on diff --git a/openedx/features/discounts/tests/test_applicability.py b/openedx/features/discounts/tests/test_applicability.py index 3256bc543ec4..e60ad57d46cd 100644 --- a/openedx/features/discounts/tests/test_applicability.py +++ b/openedx/features/discounts/tests/test_applicability.py @@ -10,22 +10,28 @@ 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 openedx_filters.learning.filters import DiscountEligibilityCheckRequested 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.courseware.toggles import COURSEWARE_MFE_MILESTONES_STREAK_DISCOUNT 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, # pylint: disable=wrong-import-order + ModuleStoreTestCase, # lint-amnesty, pylint: disable=wrong-import-order ) -from xmodule.modulestore.tests.factories import CourseFactory # 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 +from ..applicability import ( + DISCOUNT_APPLICABILITY_FLAG, + _is_in_holdback_and_bucket, + can_receive_discount, + can_show_streak_discount_coupon, +) @ddt.ddt @@ -52,6 +58,13 @@ def setUp(self): self.mock_holdback = holdback_patcher.start() self.addCleanup(holdback_patcher.stop) + # By default, the filter does not raise, meaning the user is eligible. + discount_filter_patcher = patch( + 'openedx.features.discounts.applicability.DiscountEligibilityCheckRequested.run_filter', + ) + self.mock_discount_filter = discount_filter_patcher.start() + self.addCleanup(discount_filter_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) @@ -136,17 +149,13 @@ def test_can_receive_discount_entitlement(self, entitlement_mode): assert applicability == (entitlement_mode is None) @override_waffle_flag(DISCOUNT_APPLICABILITY_FLAG, active=True) - def test_can_receive_discount_false_enterprise(self): + def test_can_receive_discount_false_when_filter_marks_ineligible(self): """ - Ensure that enterprise users do not receive the discount. + Ensure that when the eligibility filter raises DiscountIneligible, + no discount is received. """ - enterprise_customer = EnterpriseCustomer.objects.create( - name='Test EnterpriseCustomer', - site=self.site - ) - EnterpriseCustomerUser.objects.create( - user_id=self.user.id, - enterprise_customer=enterprise_customer + self.mock_discount_filter.side_effect = DiscountEligibilityCheckRequested.DiscountIneligible( + message="test: user is ineligible for discount" ) applicability = can_receive_discount(user=self.user, course=self.course) @@ -180,3 +189,17 @@ def test_holdback_expiry(self): 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) + + @override_waffle_flag(COURSEWARE_MFE_MILESTONES_STREAK_DISCOUNT, active=True) + def test_can_show_streak_discount_coupon_false_when_filter_marks_ineligible(self): + """ + Ensure that when the eligibility filter raises DiscountIneligible, + the streak discount coupon is not shown. + """ + CourseEnrollmentFactory(is_active=True, course_id=self.course.id, user=self.user) + self.mock_discount_filter.side_effect = DiscountEligibilityCheckRequested.DiscountIneligible( + message="test: user is ineligible for streak discount" + ) + + applicability = can_show_streak_discount_coupon(user=self.user, course=self.course) + assert applicability is False diff --git a/requirements/constraints.txt b/requirements/constraints.txt index bc9040cccb3a..2a041ca12507 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -44,7 +44,7 @@ django-stubs<6 # The team that owns this package will manually bump this package rather than having it pulled in automatically. # This is to allow them to better control its deployment and to do it in a process that works better # for them. -edx-enterprise==8.1.2 +edx-enterprise==8.2.0 # Date: 2023-07-26 # Our legacy Sass code is incompatible with anything except this ancient libsass version. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 562e0203ea51..47a13b6a92c0 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -483,7 +483,7 @@ edx-drf-extensions==10.6.0 # enterprise-integrated-channels # openedx-authz # openedx-core -edx-enterprise==8.1.2 +edx-enterprise==8.2.0 # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in @@ -862,7 +862,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.5.0 +openedx-filters==3.7.0 # via # -r requirements/edx/kernel.in # edx-enterprise diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index cf79082f7282..b1f243974c78 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -756,7 +756,7 @@ edx-drf-extensions==10.6.0 # enterprise-integrated-channels # openedx-authz # openedx-core -edx-enterprise==8.1.2 +edx-enterprise==8.2.0 # via # -c requirements/constraints.txt # -r requirements/edx/doc.txt @@ -1414,7 +1414,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.5.0 +openedx-filters==3.7.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 040ac2ed126c..0ba44cc01cab 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -573,7 +573,7 @@ edx-drf-extensions==10.6.0 # enterprise-integrated-channels # openedx-authz # openedx-core -edx-enterprise==8.1.2 +edx-enterprise==8.2.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1042,7 +1042,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.5.0 +openedx-filters==3.7.0 # via # -r requirements/edx/base.txt # edx-enterprise diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 7f7ec671e202..4a7f098801d3 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -589,7 +589,7 @@ edx-drf-extensions==10.6.0 # enterprise-integrated-channels # openedx-authz # openedx-core -edx-enterprise==8.1.2 +edx-enterprise==8.2.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1082,7 +1082,7 @@ openedx-events==11.2.0 # openedx-authz # openedx-core # ora2 -openedx-filters==3.5.0 +openedx-filters==3.7.0 # via # -r requirements/edx/base.txt # edx-enterprise