Skip to content

feat: add DiscountEligibilityCheckRequested filter#335

Draft
pwnage101 wants to merge 8 commits into
openedx:mainfrom
pwnage101:pwnage101/ENT-11564
Draft

feat: add DiscountEligibilityCheckRequested filter#335
pwnage101 wants to merge 8 commits into
openedx:mainfrom
pwnage101:pwnage101/ENT-11564

Conversation

@pwnage101
Copy link
Copy Markdown
Contributor

@pwnage101 pwnage101 commented Mar 4, 2026

Adds a new DiscountEligibilityCheckRequested filter to the learning subdomain, enabling plugins to mark users as ineligible for LMS-controlled course discounts without coupling the core platform to enterprise-specific logic.

ENT-11564

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com


Blocks:

Adds a new DiscountEligibilityCheckRequested filter to the learning
subdomain, enabling plugins to mark users as ineligible for LMS-
controlled course discounts without coupling the core platform to
enterprise-specific logic.

ENT-11564

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread openedx_filters/learning/filters.py Outdated
user: Any,
course_key: Any,
is_eligible: bool,
) -> tuple:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make type more explicit.

Copy link
Copy Markdown
Contributor Author

@pwnage101 pwnage101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to also bump the version.

Comment thread openedx_filters/learning/filters.py Outdated
org.openedx.learning.discount.eligibility.check.requested.v1

Trigger:
- Repository: openedx/edx-platform
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should reference openedx/openedx-platform, not the legacy openedx/edx-platform path.

Comment thread openedx_filters/learning/filters.py Outdated
- bool: the (possibly overridden) eligibility flag.
"""
data = super().run_pipeline(user=user, course_key=course_key, is_eligible=is_eligible)
return data.get("user"), data.get("course_key"), data.get("is_eligible")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict dict access here: data["user"], data["course_key"], data["is_eligible"]. Per the convention established on #333, data.get(...) lets a buggy pipeline step silently drop a key — None would then propagate into the caller and falsy-evaluate as ineligible. We want a KeyError raised inside the pipeline instead.

Comment thread openedx_filters/learning/filters.py Outdated
user: Any,
course_key: Any,
is_eligible: bool,
) -> tuple:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tighten the return annotation from bare tuple to tuple[Any, Any, bool] (or, better, concrete User / CourseKey types alongside fixing the input annotations below). Per #333, leaving this as a wide tuple allows a buggy step to return None for required values without the type checker flagging it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for CourseKey, omitting for User as the type is not referenced anywhere in the file and we would need to do something like User = get_user_model(), which I imagine we shouldn't do just to get a Type annotation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You actually can use AbstractBaseUser from django. See my draft PR for a different ticket: https://github.com/openedx/openedx-filters/pull/336/changes#diff-0c79edd111d62405dc0829ac0b0b971c73b410de87c0133a7190b3de08b40ed9

from django.contrib.auth.base_user import AbstractBaseUser
[...]
    def run_filter(cls, user: AbstractBaseUser, course_key: CourseKey) -> tuple[AbstractBaseUser, CourseKey]:

Comment thread openedx_filters/learning/filters.py Outdated
Comment on lines +1473 to +1474
user: Any,
course_key: Any,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring below already names User and CourseKey or course object — lift those into the annotations (user: User, course_key: CourseKey) so input and output align and Any doesn't drift in.

Comment on lines +818 to +832
def test_run_filter_returns_false_when_pipeline_sets_ineligible(self):
user = Mock()
course_key = Mock()

with patch(
"openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline",
return_value={"user": user, "course_key": course_key, "is_eligible": False},
):
returned_user, returned_course_key, is_eligible = DiscountEligibilityCheckRequested.run_filter(
user=user, course_key=course_key, is_eligible=True
)

self.assertFalse(is_eligible)
self.assertIs(returned_user, user)
self.assertIs(returned_course_key, course_key)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the minimal-test convention from #334, the canonical test for a new filter in this repo verifies inputs pass through unchanged when no pipeline is configured — mirror test_schedule_requested directly above. The override-to-False case exercises pipeline-step behavior and belongs alongside the pipeline-step implementation in edx-enterprise, not here.

Comment thread openedx_filters/learning/filters.py Outdated
Comment on lines +1624 to +1629
def run_filter(
cls,
user: Any,
course_key: CourseKey,
is_eligible: bool,
) -> tuple[Any, CourseKey | None, bool | None]:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Simplify the output type to remove ambiguity---None values should not be allowed.

Suggested change
def run_filter(
cls,
user: Any,
course_key: CourseKey,
is_eligible: bool,
) -> tuple[Any, CourseKey | None, bool | None]:
def run_filter(
cls,
user: AbstractBaseUser,
course_key: CourseKey,
is_eligible: bool,
) -> tuple[AbstractBaseUser, CourseKey, bool]:

Copy link
Copy Markdown
Contributor Author

@pwnage101 pwnage101 May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also my comment on an older thread about AbstractBaseUser: #335 (comment)

cls,
user: Any,
course_key: CourseKey,
is_eligible: bool,
Copy link
Copy Markdown
Contributor Author

@pwnage101 pwnage101 May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: since it only takes one pipeline step to deem the entire discount ineligible, every pipeline step implementation must take care to skip itself when is_eligible=False to prevent clobbering a prior step which set it to False. This pushes complexity into 3rd party code when there's really a much simpler solution:

We could make pipeline steps simply raise an OpenEdxFilterException exception. There are many examples in this same file, including StudentRegistrationRequested.PreventRegistration:
https://github.com/edx/openedx-filters/blob/main/openedx_filters/learning/filters.py#L161

This has the side-benefit of providing a clean way to pass error messages from the caller without polluting the run_filter() signature. See my in-progress work for a different ticket: https://github.com/openedx/openedx-filters/pull/336/changes

Notably:

class CoursewareAccessChecksRequested(OpenEdxPublicFilter):
   [...]

    class PreventCoursewareAccess(OpenEdxFilterException):
        [...]

        def __init__(
            self,
            error_code: str,
            developer_message: str,
            user_message: str,
        ) -> None:
            super().__init__()
            self.error_code = error_code
            self.developer_message = developer_message
            self.user_message = user_message

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants