Skip to content

Commit

Permalink
feat: emit learner credit unenrollment event
Browse files Browse the repository at this point in the history
event type: org.openedx.enterprise.learner_credit_course_enrollment.revoked.v1
event_name: LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED

The motivation for emitting this event is to support event-driven
creation of openedx_ledger.Reversal objects.

ENT-9213
  • Loading branch information
pwnage101 committed Sep 4, 2024
1 parent f8ea429 commit fa2a4e9
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Unreleased
----------
* nothing unreleased

[4.25.0]
----------
* feat: emit learner credit unenrollment event

[4.24.0]
----------
* fix: customer sorting error in customer support tool endpoint and added user query param
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.24.0"
__version__ = "4.25.0"
3 changes: 3 additions & 0 deletions enterprise/api/v1/views/enterprise_subsidy_fulfillment.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ def unenrolled(self, request, *args, **kwargs):
retrieve_licensed_enrollments (bool): If true, return data related to licensed enrollments instead of
learner credit
"""
LOGGER.warning(
"[DEPRECATION] This view is deprecated for lack of purpose. Logging to confirm utilization drop-off.",
)
queryset = self._get_unenrolled_fulfillments()
serializer_class = self.get_unenrolled_fulfillment_serializer_class()
serializer = serializer_class(queryset, many=True)
Expand Down
70 changes: 70 additions & 0 deletions enterprise/event_bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
Functions for serializing and emiting Open edX event bus signals.
"""
from openedx_events.enterprise.data import (
EnterpriseCourseEnrollment,
EnterpriseCustomerUser,
LearnerCreditEnterpriseCourseEnrollment,
)
from openedx_events.enterprise.signals import LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED


def serialize_learner_credit_course_enrollment(learner_credit_course_enrollment):
"""
Serializes the ``LearnerCreditEnterpriseCourseEnrollment`` into a defined set of attributes
for use in the event-bus signal.
"""
enterprise_course_enrollment = learner_credit_course_enrollment.enterprise_course_enrollment
enterprise_customer_user = enterprise_course_enrollment.enterprise_customer_user

enterprise_customer_user_data = EnterpriseCustomerUser(
id=enterprise_customer_user.id,
created=enterprise_customer_user.created,
modified=enterprise_customer_user.modified,
enterprise_customer_uuid=enterprise_customer_user.enterprise_customer.uuid,
user_id=enterprise_customer_user.user_id,
active=enterprise_customer_user.active,
linked=enterprise_customer_user.linked,
is_relinkable=enterprise_customer_user.is_relinkable,
invite_key=enterprise_customer_user.invite_key.uuid if enterprise_customer_user.invite_key else None,
should_inactivate_other_customers=enterprise_customer_user.should_inactivate_other_customers,
)
enterprise_course_enrollment_data = EnterpriseCourseEnrollment(
id=enterprise_course_enrollment.id,
created=enterprise_course_enrollment.created,
modified=enterprise_course_enrollment.modified,
enterprise_customer_user=enterprise_customer_user_data,
course_id=enterprise_course_enrollment.course_id,
saved_for_later=enterprise_course_enrollment.saved_for_later,
source_slug=enterprise_course_enrollment.source.slug if enterprise_course_enrollment.source else None,
unenrolled=enterprise_course_enrollment.unenrolled,
unenrolled_at=enterprise_course_enrollment.unenrolled_at,
)
data = LearnerCreditEnterpriseCourseEnrollment(
uuid=learner_credit_course_enrollment.uuid,
created=learner_credit_course_enrollment.created,
modified=learner_credit_course_enrollment.modified,
fulfillment_type=learner_credit_course_enrollment.fulfillment_type,
enterprise_course_entitlement_uuid=(
learner_credit_course_enrollment.enterprise_course_entitlement.uuid
if learner_credit_course_enrollment.enterprise_course_entitlement
else None
),
enterprise_course_enrollment=enterprise_course_enrollment_data,
is_revoked=learner_credit_course_enrollment.is_revoked,
transaction_id=learner_credit_course_enrollment.transaction_id,
)
return data


def send_learner_credit_course_enrollment_revoked_event(learner_credit_course_enrollment):
"""
Sends the LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED openedx event.
Args:
learner_credit_course_enrollment (enterprise.models.LearnerCreditEnterpriseCourseEnrollment):
An enterprise learner credit fulfillment record that was revoked.
"""
LEARNER_CREDIT_COURSE_ENROLLMENT_REVOKED.send_event(
learner_credit_course_enrollment=serialize_learner_credit_course_enrollment(learner_credit_course_enrollment),
)
10 changes: 10 additions & 0 deletions enterprise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
json_serialized_course_modes,
)
from enterprise.errors import LinkUserToEnterpriseError
from enterprise.event_bus import send_learner_credit_course_enrollment_revoked_event
from enterprise.logging import getEnterpriseLogger
from enterprise.tasks import send_enterprise_email_notification
from enterprise.utils import (
Expand Down Expand Up @@ -2287,6 +2288,8 @@ def revoke(self):
Marks this object as revoked and marks the associated EnterpriseCourseEnrollment
as "saved for later". This object and the associated EnterpriseCourseEnrollment are both saved.
Subclasses may override this function to additionally emit revocation events.
TODO: revoke entitlements as well?
"""
if self.enterprise_course_enrollment:
Expand Down Expand Up @@ -2330,6 +2333,13 @@ class LearnerCreditEnterpriseCourseEnrollment(EnterpriseFulfillmentSource):
.. no_pii:
"""

def revoke(self):
"""
Revoke this LearnerCreditEnterpriseCourseEnrollment, and emit a revoked event.
"""
super().revoke()
send_learner_credit_course_enrollment_revoked_event(self)

def reactivate(self, transaction_id=None, **kwargs):
"""
Idmpotently reactivates this LearnerCreditEnterpriseCourseEnrollment.
Expand Down
13 changes: 9 additions & 4 deletions tests/test_enterprise/api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4401,9 +4401,9 @@ def test_unsupported_methods(self):
assert update_response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED

@mock.patch("enterprise.api.v1.views.enterprise_subsidy_fulfillment.enrollment_api")
def test_successful_cancel_fulfillment(self, mock_enrollment_api):
def test_successful_cancel_licensed_fulfillment(self, mock_enrollment_api):
"""
Test that we can successfully cancel both licensed and learner credit fulfillments.
Test that we can successfully cancel licensed fulfillments.
"""
mock_enrollment_api.update_enrollment.return_value = mock.Mock()
self.licensed_course_enrollment.is_revoked = False
Expand All @@ -4424,8 +4424,12 @@ def test_successful_cancel_fulfillment(self, mock_enrollment_api):
'is_active': False,
}

mock_enrollment_api.reset_mock()

@mock.patch("enterprise.models.send_learner_credit_course_enrollment_revoked_event")
@mock.patch("enterprise.api.v1.views.enterprise_subsidy_fulfillment.enrollment_api")
def test_successful_cancel_learner_credit_fulfillment(self, mock_enrollment_api, mock_send_revoked_event):
"""
Test that we can successfully cancel learner credit fulfillments, and an openedx event is emitted.
"""
self.learner_credit_course_enrollment.is_revoked = False
self.learner_credit_course_enrollment.save()
response = self.client.post(
Expand All @@ -4443,6 +4447,7 @@ def test_successful_cancel_fulfillment(self, mock_enrollment_api):
assert mock_enrollment_api.update_enrollment.call_args.kwargs == {
'is_active': False,
}
mock_send_revoked_event.assert_called_once_with(self.learner_credit_course_enrollment)

@mock.patch("enterprise.api.v1.views.enterprise_subsidy_fulfillment.enrollment_api")
def test_idempotent_cancel_fulfillment(self, mock_enrollment_api):
Expand Down
48 changes: 48 additions & 0 deletions tests/test_enterprise/test_event_bus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Tests for enterprise/event_bus.py
"""
import unittest

import ddt
from pytest import mark

from enterprise.event_bus import serialize_learner_credit_course_enrollment
from enterprise.models import EnterpriseEnrollmentSource
from test_utils import factories


@ddt.ddt
@mark.django_db
class TestEventBusSerializers(unittest.TestCase):
"""
Test serializers for use with openedx-events events ("event bus").
"""

def setUp(self):
super().setUp()

self.user = factories.UserFactory(is_active=True)
self.enterprise_customer = factories.EnterpriseCustomerFactory()
self.enterprise_user = factories.EnterpriseCustomerUserFactory(
user_id=self.user.id,
enterprise_customer=self.enterprise_customer,
)
self.enterprise_course_enrollment = factories.EnterpriseCourseEnrollmentFactory(
enterprise_customer_user=self.enterprise_user,
source=EnterpriseEnrollmentSource.get_source(EnterpriseEnrollmentSource.API),
)
self.learner_credit_course_enrollment = factories.LearnerCreditEnterpriseCourseEnrollmentFactory(
enterprise_course_enrollment=self.enterprise_course_enrollment,
)

def test_serialize_learner_credit_course_enrollment(self):
"""
Perform a basic test that the serializer drills down two levels into the enterprise user correctly.
"""
data = serialize_learner_credit_course_enrollment(self.learner_credit_course_enrollment)
assert data.uuid == self.learner_credit_course_enrollment.uuid
assert data.enterprise_course_enrollment.id == self.enterprise_course_enrollment.id
assert data.enterprise_course_enrollment.source_slug == self.enterprise_course_enrollment.source.slug
assert data.enterprise_course_enrollment.enterprise_customer_user.id == self.enterprise_user.id
assert data.enterprise_course_enrollment.enterprise_customer_user.enterprise_customer_uuid == \
self.enterprise_customer.uuid

0 comments on commit fa2a4e9

Please sign in to comment.