Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: respect allow_enrollment_in_invite_only_courses #14

Conversation

0x29a
Copy link
Member

@0x29a 0x29a commented Mar 1, 2024

Description

This fix allows enterprise learners to enroll into invite-only enterprise courses using the frontend-app-learner-portal-enterprise MFE.

This issue is easy to reproduce. Let's imagine we have an enterprise customer, with one invitation-only course in its catalog, enough funds and one linked leaner. The learner will see the following if visit the portal:
image
Clicking Enroll will result in some error:
image

Let's follow the request to understand what's causing it. First, frontend is sending a POST request to http://localhost:18270/api/v1/policy-redemption/<per_learner_spend_credit_access_policy_uuid>/redeem/ URL with a body like this:

{
  "lms_user_id": 20026,
  "content_key": "course-v1:PSY+PSY+PSY",
  "metadata": {}
}

Then, this endpoint is failing because of inability to create a subsidy transaction:

2024-03-01 11:40:15,499 ERROR 23 [enterprise_access.apps.api.v1.views.subsidy_access_policy] [user 4] [ip 172.19.0.1] [request_id None] subsidy_access_policy.py:554 - HTTPError occurred in Subsidy API request. when creating transaction in subsidy API
Traceback (most recent call last):
  File "/edx/app/enterprise-access/enterprise_access/apps/subsidy_access_policy/models.py", line 791, in redeem
    return self.subsidy_client.create_subsidy_transaction(**creation_payload)
  File "/edx/venvs/enterprise-access/lib/python3.8/site-packages/edx_enterprise_subsidy_client/client.py", line 336, in create_subsidy_transaction
    response.raise_for_status()
  File "/edx/venvs/enterprise-access/lib/python3.8/site-packages/requests/models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 422 Client Error: Unprocessable Entity for url: http://enterprise-subsidy.app:18280/api/v2/subsidies/d3a489ae-f94e-42d6-af5a-bcb895bb511d/admin/transactions/

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/edx/app/enterprise-access/enterprise_access/apps/api/v1/views/subsidy_access_policy.py", line 537, in redeem
    redemption_result = policy.redeem(lms_user_id, content_key, existing_transactions, metadata)
  File "/edx/app/enterprise-access/enterprise_access/apps/subsidy_access_policy/models.py", line 793, in redeem
    raise SubsidyAPIHTTPError('HTTPError occurred in Subsidy API request.') from exc
enterprise_access.apps.subsidy_access_policy.exceptions.SubsidyAPIHTTPError: HTTPError occurred in Subsidy API request.

enterprise-subsidy service, in turn, is failing to create a transaction because LMS is failing to create an enrollment:

2024-03-01 11:40:15,496 ERROR 9 [enterprise_subsidy.apps.subsidy.models] [user 2] [ip 172.19.0.35] [request_id None] models.py:478 - <Subsidy uuid=d3a489ae-f94e-42d6-af5a-bcb895bb511d, title=Test subsidy> cannot redeem course-v1:PSY+PSY+PSY with price 10000 for user 20026 in policy dd99c210-5c61-4c89-a3d6-c3dad09e5cc1. HTTPError during enrollment.
Traceback (most recent call last):
  File "/edx/app/enterprise-subsidy/enterprise_subsidy/apps/subsidy/models.py", line 463, in redeem
    transaction = self._create_redemption(
  File "/edx/app/enterprise-subsidy/enterprise_subsidy/apps/subsidy/models.py", line 586, in _create_redemption
    raise exc
  File "/edx/app/enterprise-subsidy/enterprise_subsidy/apps/subsidy/models.py", line 576, in _create_redemption
    enterprise_fulfillment_uuid = self.enterprise_client.enroll(lms_user_id, content_key, ledger_transaction)
  File "/edx/app/enterprise-subsidy/enterprise_subsidy/apps/api_client/enterprise.py", line 111, in enroll
    response = self.bulk_enroll_enterprise_learners(customer_uuid, enrollments_info)
  File "/edx/app/enterprise-subsidy/enterprise_subsidy/apps/api_client/enterprise.py", line 163, in bulk_enroll_enterprise_learners
    raise exc
  File "/edx/app/enterprise-subsidy/enterprise_subsidy/apps/api_client/enterprise.py", line 156, in bulk_enroll_enterprise_learners
    response.raise_for_status()
  File "/edx/venvs/enterprise-subsidy/lib/python3.8/site-packages/requests/models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 409 Client Error: Conflict for url: http://edx.devstack.lms:18000/enterprise/api/v1/enterprise-customer/55adc9d9-12de-4d0a-95c5-e01d0998aab8/enroll_learners_in_courses/

And LMS is failing to create an enrollment because edx-enterprise's enroll_learners_in_courses is failing:

2024-03-01 11:40:15,472 ERROR 1670 [enterprise.utils] [user 20024] [ip 172.19.0.27] utils.py:1850 - Failed to enroll user 20026 in course course-v1:PSY+PSY+PSY
Traceback (most recent call last):
  File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/enrollments/data.py", line 151, in create_course_enrollment
    enrollment = CourseEnrollment.enroll(
  File "/edx/app/edxapp/edx-platform/common/djangoapps/student/models/course_enrollment.py", line 690, in enroll
    raise EnrollmentClosedError
common.djangoapps.student.models.course_enrollment.EnrollmentClosedError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/edx/src/edx-enterprise/enterprise/utils.py", line 1840, in customer_admin_enroll_user_with_status
    new_enrollment = lms_update_or_create_enrollment(
  File "/edx/app/edxapp/edx-platform/openedx/features/enterprise_support/enrollments/utils.py", line 138, in lms_update_or_create_enrollment
    raise error
  File "/edx/app/edxapp/edx-platform/openedx/features/enterprise_support/enrollments/utils.py", line 101, in lms_update_or_create_enrollment
    response = enrollment_api.add_enrollment(
  File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/enrollments/api.py", line 265, in add_enrollment
    enrollment = _data_api().create_course_enrollment(
  File "/edx/app/edxapp/edx-platform/openedx/core/djangoapps/enrollments/data.py", line 158, in create_course_enrollment
    raise CourseEnrollmentClosedError(str(err))  # lint-amnesty, pylint: disable=raise-missing-from
openedx.core.djangoapps.enrollments.errors.CourseEnrollmentClosedError
2024-03-01 11:40:15,475 WARNING 1670 [enterprise.utils] [user 20024] [ip 172.19.0.27] utils.py:1894 - Failed to enroll user 20026 in course course-v1:PSY+PSY+PSY
[01/Mar/2024 11:40:15] "POST /enterprise/api/v1/enterprise-customer/55adc9d9-12de-4d0a-95c5-e01d0998aab8/enroll_learners_in_courses/ HTTP/1.1" 409 127

Unlike CourseEnrollmentView, this endpoint doesn't create CourseEnrollmentAllowed object automatically if an enterprise customer has allow_enrollment_in_invite_only_courses set to True. In this PR we fix exactly this.

Testing

Configuring devstack to test this is a bit tedious.

First, get a working enterprise setup like described in the testing steps section of openedx/frontend-app-learner-portal-enterprise#887, but with some differences:

  • Skip adding ENTERPRISE_ALGOLIA_SEARCH_API_KEY to devstack.py.
  • Use 0x29a/bb8626/enroll-enterprise-learners-in-invite-only-courses branch for edx-enterprise repo.
  • Use upstream master branch for frontend-app-learner-portal-enterprise.
  • For Installing frontend-app-learner-portal-enterprise section, run also export ALGOLIA_SEARCH_API_KEY=<your_search_api_key>.
  • Skip Enrolling users step.

Then you have to make Enroll button work, which requires three additional services:

cd $DEVSTACK_WORKSPACE
git clone [email protected]:openedx/enterprise-subsidy.git
cd enterprise-subsidy
make dev.up.build-no-cache
bash provision-enterprise-subsidy.sh
cd $DEVSTACK_WORKSPACE
git clone [email protected]:openedx/license-manager.git
cd license-manager
make docker_build
make dev.provision
make dev.up
cd $DEVSTACK_WORKSPACE
git clone [email protected]:openedx/enterprise-access.git
cd enterprise-access
make docker_build
make dev.provision
make dev.up

Also you need to create some missing role assignments, so these services can work together:

  1. Assign [email protected] to enterprise_subsidy_admin role here: http://localhost:18280/admin/subsidy/enterprisesubsidyroleassignment/
  2. Assign [email protected] and [email protected] to enterprise_catalog_admin role here: http://localhost:18160/admin/catalog/enterprisecatalogroleassignment/

Finally, in this context, some course is enroll-able only if it has price and if organization that own this course has a non-zero balance to subsidize learning. That's why you'll need to do the following:

  1. Add a ledger here: http://localhost:18280/admin/openedx_ledger/ledger/add/.
  2. Add a subsidy here: http://localhost:18280/admin/subsidy/subsidy/add. Be sure to note an enterprise customer UUID you use here.
  3. Add a policy here: http://localhost:18270/admin/subsidy_access_policy/perlearnerspendcreditaccesspolicy/add/. Be sure to use a catalog UUID linked to a customer you set previously and set the spending limit to 1000000 cents.
  4. Add an adjustment here: http://localhost:18280/admin/openedx_ledger/adjustment/, specify $10,000,000.00 amount. After this, verify that the ledger's balance is 10 mil $.
  5. Choose some course in the catalog of the previously chosen enterprise customer, find it here: http://localhost:18160/admin/catalog/contentmetadata/, and correct both course and course run to have first_enrollable_paid_seat_price == 100.

Now you can register some leaner, link it to the previously chosen enterprise customer in http://localhost:18000/admin/enterprise/enterprisecustomer/<enterprise_customer_uuid>/manage_learners, and trying to enroll via the portal. It should fail unless Allow enrollment in invite only courses checkbox on http://localhost:18000/admin/enterprise/enterprisecustomer/<enterprise_customer_uuid>/change/ page is set.

Private ref

Fixes `enroll_learners_in_courses` endpoint (and
`enroll_subsidy_users_in_courses` utility function) to respect
enterprise customer's `allow_enrollment_in_invite_only_courses` flag.
Copy link

@CefBoud CefBoud left a comment

Choose a reason for hiding this comment

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

Good job on this, @0x29a !

  • I tested this (I had some version issues during devstack setup)
  • I read through the code

* fix: create CEA object when enrolling using a license flow
* test: verify that allow_enrollment is called_correctly
* fix: xmlsec issue
    xmlsec/python-xmlsec#314
* build: enable CI for pull requests
* style: fix some pycodestyle issues
@tecoholic
Copy link
Member

Merging this into the target branch as:

  1. The PR is tested and reviewed.
  2. Target branch is an internal branch and will simplify managing the branches.
  3. There are no conflicts.

Finally, if any issues are discovered after merging, it will be handled in tecoholic/create-cea-for-invite-only-courses-before-checkout

@tecoholic tecoholic merged commit 67f418a into tecoholic/create-cea-for-invite-only-courses-before-checkout Aug 12, 2024
7 checks passed
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.

3 participants