Skip to content

Commit

Permalink
feat: serialize additional subscriptions-related fields within the Le…
Browse files Browse the repository at this point in the history
…arner BFF (#621)
  • Loading branch information
adamstankiewicz authored Jan 15, 2025
1 parent 8c37be7 commit 6c6c844
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 16 deletions.
21 changes: 21 additions & 0 deletions enterprise_access/apps/api/v1/tests/test_bff_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ def setUp(self):
'assigned': [],
'revoked': [],
},
'subscription_license': None,
'subscription_plan': None,
'show_expiration_notifications': False,
},
},
'errors': [],
Expand Down Expand Up @@ -279,6 +282,9 @@ def test_dashboard_with_subscriptions(
'assigned': [],
'revoked': [],
},
'subscription_license': self.expected_subscription_license,
'subscription_plan': self.expected_subscription_license['subscription_plan'],
'show_expiration_notifications': True,
},
},
})
Expand Down Expand Up @@ -347,6 +353,9 @@ def test_dashboard_with_subscriptions_license_activation(
'assigned': [],
'revoked': [],
},
'subscription_license': expected_activated_subscription_license,
'subscription_plan': expected_activated_subscription_license['subscription_plan'],
'show_expiration_notifications': True,
},
},
})
Expand Down Expand Up @@ -620,6 +629,15 @@ def test_dashboard_with_subscriptions_license_auto_apply(
if has_existing_revoked_license
else []
)
expected_subscription_license = None
expected_subscription_plan = None
if should_auto_apply or has_existing_activated_license:
expected_subscription_license = expected_activated_subscription_license
elif has_existing_revoked_license:
expected_subscription_license = expected_revoked_subscription_license
if expected_subscription_license:
expected_subscription_plan = expected_subscription_license['subscription_plan']

expected_licenses = []
expected_licenses.extend(expected_activated_licenses)
expected_licenses.extend(expected_revoked_licenses)
Expand All @@ -641,6 +659,9 @@ def test_dashboard_with_subscriptions_license_auto_apply(
'assigned': [],
'revoked': expected_revoked_licenses,
},
'subscription_license': expected_subscription_license,
'subscription_plan': expected_subscription_plan,
'show_expiration_notifications': True,
},
},
})
Expand Down
90 changes: 78 additions & 12 deletions enterprise_access/apps/bffs/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import logging

from enterprise_access.apps.api_client.constants import LicenseStatuses
from enterprise_access.apps.api_client.license_manager_client import LicenseManagerUserApiClient
from enterprise_access.apps.api_client.lms_client import LmsApiClient
from enterprise_access.apps.bffs.api import (
Expand Down Expand Up @@ -208,37 +209,93 @@ def load_subscription_licenses(self):
include_revoked=True,
current_plans_only=False,
)
except Exception as exc: # pylint: disable=broad-exception-caught
logger.exception(
"Error loading subscription licenses for request user %s and enterprise customer %s",
self.context.lms_user_id,
self.context.enterprise_customer_uuid,
)
self.add_error(
user_message="Unable to retrieve subscription licenses",
developer_message=f"Unable to fetch subscription licenses. Error: {exc}",
)
return

try:
subscriptions_data = self.transform_subscriptions_result(subscriptions_result)
self.context.data['enterprise_customer_user_subsidies'].update({
'subscriptions': subscriptions_data,
})
except Exception as exc: # pylint: disable=broad-exception-caught
logger.exception(
"Error loading subscription licenses for request user %s and enterprise customer %s",
"Error transforming subscription licenses for request user %s and enterprise customer %s",
self.context.lms_user_id,
self.context.enterprise_customer_uuid,
)
self.add_error(
user_message="Unable to retrieve subscription licenses",
developer_message=f"Unable to fetch subscription licenses. Error: {exc}",
user_message="Unable to transform subscription licenses",
developer_message=f"Unable to transform subscription licenses. Error: {exc}",
)

def _extract_subscription_license(self, subscription_licenses_by_status):
"""
Extract subscription licenses from the subscription licenses by status.
"""
license_status_priority_order = [
LicenseStatuses.ACTIVATED,
LicenseStatuses.ASSIGNED,
LicenseStatuses.REVOKED,
]
subscription_license = next(
(
license
for status in license_status_priority_order
for license in subscription_licenses_by_status.get(status, [])
),
None,
)
return subscription_license

def transform_subscriptions_result(self, subscriptions_result):
"""
Transform subscription licenses data if needed.
"""
subscription_licenses = subscriptions_result.get('results', [])
subscription_licenses_by_status = {}
for subscription_license in subscription_licenses:

# Sort licenses by whether the associated subscription plans
# are current; current plans should be prioritized over non-current plans.
ordered_subscription_licenses = sorted(
subscription_licenses,
key=lambda license: not license.get('subscription_plan', {}).get('is_current'),
)

# Group licenses by status
for subscription_license in ordered_subscription_licenses:
status = subscription_license.get('status')
if status not in subscription_licenses_by_status:
subscription_licenses_by_status[status] = []
subscription_licenses_by_status[status].append(subscription_license)

customer_agreement = subscriptions_result.get('customer_agreement')
subscription_license = self._extract_subscription_license(subscription_licenses_by_status)
subscription_plan = subscription_license.get('subscription_plan') if subscription_license else None

# Determine if expiration notifications should be shown
if not customer_agreement:
show_expiration_notifications = False
else:
disable_expiration_notifications = customer_agreement.get('disable_expiration_notifications', False)
custom_expiration_messaging = customer_agreement.get('has_custom_license_expiration_messaging_v2', False)
show_expiration_notifications = not (disable_expiration_notifications or custom_expiration_messaging)

return {
'customer_agreement': subscriptions_result.get('customer_agreement'),
'customer_agreement': customer_agreement,
'subscription_licenses': subscription_licenses,
'subscription_licenses_by_status': subscription_licenses_by_status,
'subscription_license': subscription_license,
'subscription_plan': subscription_plan,
'show_expiration_notifications': show_expiration_notifications,
}

def _current_subscription_licenses_for_status(self, status):
Expand All @@ -256,7 +313,7 @@ def current_activated_licenses(self):
"""
Returns list of current, activated licenses, if any, for the user.
"""
activated_licenses = self._current_subscription_licenses_for_status('activated')
activated_licenses = self._current_subscription_licenses_for_status(LicenseStatuses.ACTIVATED)
return activated_licenses

@property
Expand All @@ -273,15 +330,15 @@ def current_revoked_licenses(self):
Returns a revoked license for the user iff the related subscription plan is current,
otherwise returns None.
"""
return self._current_subscription_licenses_for_status('revoked')
return self._current_subscription_licenses_for_status(LicenseStatuses.REVOKED)

@property
def current_assigned_licenses(self):
"""
Returns an assigned license for the user iff the related subscription plan is current,
otherwise returns None.
"""
return self._current_subscription_licenses_for_status('assigned')
return self._current_subscription_licenses_for_status(LicenseStatuses.ASSIGNED)

def process_subscription_licenses(self):
"""
Expand Down Expand Up @@ -364,7 +421,7 @@ def check_and_activate_assigned_license(self):
updated_activated_licenses = self.current_activated_licenses
updated_activated_licenses.extend(activated_licenses)
if updated_activated_licenses:
subscription_licenses_by_status['activated'] = updated_activated_licenses
subscription_licenses_by_status[LicenseStatuses.ACTIVATED] = updated_activated_licenses

activated_license_uuids = {license['uuid'] for license in activated_licenses}
remaining_assigned_licenses = [
Expand All @@ -373,9 +430,9 @@ def check_and_activate_assigned_license(self):
if subscription_license['uuid'] not in activated_license_uuids
]
if remaining_assigned_licenses:
subscription_licenses_by_status['assigned'] = remaining_assigned_licenses
subscription_licenses_by_status[LicenseStatuses.ASSIGNED] = remaining_assigned_licenses
else:
subscription_licenses_by_status.pop('assigned', None)
subscription_licenses_by_status.pop(LicenseStatuses.ASSIGNED, None)

self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({
'subscription_licenses_by_status': subscription_licenses_by_status,
Expand All @@ -389,12 +446,19 @@ def check_and_activate_assigned_license(self):
updated_subscription_licenses.append(activated_license)
break
updated_subscription_licenses.append(subscription_license)

if updated_subscription_licenses:
self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({
'subscription_licenses': updated_subscription_licenses,
})

# Update the subscription_license and subscription_plan data given the activated license
subscription_license = self._extract_subscription_license(subscription_licenses_by_status)
subscription_plan = subscription_license.get('subscription_plan') if subscription_license else None
self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({
'subscription_license': subscription_license,
'subscription_plan': subscription_plan,
})

def check_and_auto_apply_license(self):
"""
Check if auto-apply licenses are available and apply them to the user.
Expand Down Expand Up @@ -438,6 +502,8 @@ def check_and_auto_apply_license(self):
self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({
'subscription_licenses': licenses,
'subscription_licenses_by_status': subscription_licenses_by_status,
'subscription_license': auto_applied_license,
'subscription_plan': auto_applied_license.get('subscription_plan'),
})
except Exception as exc: # pylint: disable=broad-exception-caught
logger.exception(
Expand Down
21 changes: 21 additions & 0 deletions enterprise_access/apps/bffs/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@ def subscription_licenses_by_status(self):
"""
return self.subscriptions.get('subscription_licenses_by_status', {})

@property
def subscription_license(self):
"""
Get subscription license from the context.
"""
return self.subscriptions.get('subscription_license', None)

@property
def subscription_plan(self):
"""
Get subscription plan from the context.
"""
return self.subscriptions.get('subscription_plan', {})

@property
def show_subscription_expiration_notifications(self):
"""
Get whether subscription expiration notifications should be shown from the context.
"""
return self.subscriptions.get('show_expiration_notifications', False)


class LearnerSubsidiesDataMixin(LearnerSubscriptionsDataMixin):
"""
Expand Down
3 changes: 0 additions & 3 deletions enterprise_access/apps/bffs/response_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,6 @@ def build(self):
'enterprise_course_enrollments': self.enterprise_course_enrollments,
})

# Add any errors and warnings to the response
self.add_errors_warnings_to_response()

# Serialize and validate the response
try:
serializer = LearnerDashboardResponseSerializer(data=self.response_data)
Expand Down
5 changes: 4 additions & 1 deletion enterprise_access/apps/bffs/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,15 @@ class SubscriptionLicenseStatusSerializer(BaseBffSerializer):

class SubscriptionsSerializer(BaseBffSerializer):
"""
Serializer for enterprise customer user subsidies.
Serializer for subscriptions subsidies.
"""

customer_agreement = CustomerAgreementSerializer(required=False, allow_null=True)
subscription_licenses = SubscriptionLicenseSerializer(many=True, required=False, default=list)
subscription_licenses_by_status = SubscriptionLicenseStatusSerializer(required=False)
subscription_license = SubscriptionLicenseSerializer(required=False, allow_null=True)
subscription_plan = SubscriptionPlanSerializer(required=False, allow_null=True)
show_expiration_notifications = serializers.BooleanField(required=False)


class EnterpriseCustomerUserSubsidiesSerializer(BaseBffSerializer):
Expand Down
3 changes: 3 additions & 0 deletions enterprise_access/apps/bffs/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ def test_load_and_process(
'customer_agreement': None,
'subscription_licenses': [],
'subscription_licenses_by_status': {},
'subscription_license': None,
'subscription_plan': None,
'show_expiration_notifications': False,
}
self.assertEqual(actual_subscriptions, expected_subscriptions)

Expand Down

0 comments on commit 6c6c844

Please sign in to comment.