diff --git a/lms/envs/common.py b/lms/envs/common.py index 917dd025e96f..004d21cdff89 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3172,3 +3172,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.DiscountEligibilityStep"], + }, +} diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py index c930ae2da878..559158904df3 100644 --- a/openedx/features/discounts/applicability.py +++ b/openedx/features/discounts/applicability.py @@ -10,24 +10,24 @@ from datetime import datetime, timedelta - from zoneinfo import ZoneInfo + from crum import get_current_request, impersonate from django.conf import settings 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 -from lms.djangoapps.courseware.utils import is_mode_upsellable +from common.djangoapps.student.models import CourseEnrollment +from common.djangoapps.track import segment from lms.djangoapps.courseware.toggles import COURSEWARE_MFE_MILESTONES_STREAK_DISCOUNT +from lms.djangoapps.courseware.utils import is_mode_upsellable from lms.djangoapps.experiments.models import ExperimentData from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group from openedx.features.discounts.models import DiscountPercentageConfig, DiscountRestrictionConfig -from common.djangoapps.student.models import CourseEnrollment -from common.djangoapps.track import segment - # .. toggle_name: discounts.enable_first_purchase_discount_override # .. toggle_implementation: WaffleFlag @@ -125,10 +125,11 @@ 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. + _user, _course_key, is_eligible = DiscountEligibilityCheckRequested.run_filter( + user=user, course_key=course.id, is_eligible=True + ) + if not is_eligible: return False return True @@ -179,10 +180,11 @@ 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. + _user, _course_key, is_eligible = DiscountEligibilityCheckRequested.run_filter( + user=user, course_key=course.id, is_eligible=True + ) + if not is_eligible: return False # Turn holdback on diff --git a/openedx/features/discounts/tests/test_applicability.py b/openedx/features/discounts/tests/test_applicability.py index 472120b38705..ad9e768d1f13 100644 --- a/openedx/features/discounts/tests/test_applicability.py +++ b/openedx/features/discounts/tests/test_applicability.py @@ -3,14 +3,13 @@ from datetime import datetime, timedelta from unittest.mock import Mock, patch +from zoneinfo import ZoneInfo 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 @@ -20,7 +19,8 @@ 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.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 @@ -50,6 +50,14 @@ def setUp(self): self.mock_holdback = holdback_patcher.start() self.addCleanup(holdback_patcher.stop) + # By default, the filter passes eligibility through unchanged. + discount_filter_patcher = patch( + 'openedx.features.discounts.applicability.DiscountEligibilityCheckRequested.run_filter', + side_effect=lambda user, course_key, is_eligible: (user, course_key, is_eligible), + ) + 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) @@ -134,17 +142,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 marks the user as ineligible, + 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 = lambda user, course_key, is_eligible: ( + user, course_key, False ) applicability = can_receive_discount(user=self.user, course=self.course)