diff --git a/enterprise_access/apps/api/v1/views/bffs/common.py b/enterprise_access/apps/api/v1/views/bffs/common.py index cebc0150..cbd35287 100644 --- a/enterprise_access/apps/api/v1/views/bffs/common.py +++ b/enterprise_access/apps/api/v1/views/bffs/common.py @@ -51,7 +51,7 @@ def load_route_data_and_build_response(self, request, handler_class, response_bu # Create the context based on the request context = HandlerContext(request=request) except Exception as exc: # pylint: disable=broad-except - logger.exception("Could not create the handler context.") + logger.exception("Could not instantiate the handler context for the request.") error = { 'user_message': 'An error occurred while processing the request.', 'developer_message': f'Could not create the handler context. Error: {exc}', @@ -62,14 +62,33 @@ def load_route_data_and_build_response(self, request, handler_class, response_bu try: # Create the route handler handler = handler_class(context) + except Exception as exc: # pylint: disable=broad-except + logger.exception( + "Could not instantiate route handler (%s) for request user %s and enterprise customer %s.", + handler_class.__name__, + context.lms_user_id, + context.enterprise_customer_uuid, + ) + context.add_error( + user_message='An error occurred while processing the request.', + developer_message=f'Could not instantiate route handler ({handler_class.__name__}). Error: {exc}', + ) + try: # Load and process route data handler.load_and_process() except Exception as exc: # pylint: disable=broad-except - logger.exception("Could not create route handler or load/process route data.") + logger.exception( + "Could not load/process route handler (%s) for request user %s and enterprise customer %s.", + handler_class.__name__, + context.lms_user_id, + context.enterprise_customer_uuid, + ) context.add_error( user_message='An error occurred while processing the request.', - developer_message=f'Could not create route handler or load/process route data. Error: {exc}', + developer_message=( + f'Could not load/process data for route handler ({handler_class.__name__}). Error: {exc}', + ), ) # Build the response data and status code diff --git a/enterprise_access/apps/bffs/context.py b/enterprise_access/apps/bffs/context.py index be547611..82ad70f8 100644 --- a/enterprise_access/apps/bffs/context.py +++ b/enterprise_access/apps/bffs/context.py @@ -4,6 +4,7 @@ import logging from rest_framework import status +from requests.exceptions import HTTPError from enterprise_access.apps.bffs import serializers from enterprise_access.apps.bffs.api import ( @@ -29,6 +30,14 @@ class HandlerContext: enterprise_customer_slug: The enterprise customer slug associated with this request. lms_user_id: The id associated with the authenticated user. enterprise_features: A dictionary to store enterprise features associated with the authenticated user. + enterprise_customer: The enterprise customer associated with the request. + active_enterprise_customer: The active enterprise customer associated with the request user. + all_linked_enterprise_customer_users: A list of all linked enterprise customer users + associated with the request user. + staff_enterprise_customer: The enterprise customer, if resolved as a staff request user. + is_request_user_linked_to_enterprise_customer: A boolean indicating if the request user is linked + to the resolved enterprise customer. + status_code: The HTTP status code to return in the response. """ def __init__(self, request): @@ -118,6 +127,12 @@ def is_request_user_linked_to_enterprise_customer(self): def staff_enterprise_customer(self): return self.data.get('staff_enterprise_customer') + def set_status_code(self, status_code): + """ + Sets the status code for the response. + """ + self._status_code = status_code + def _initialize_common_context_data(self): """ Initializes common context data, like enterprise customer UUID and user ID. @@ -137,10 +152,40 @@ def _initialize_common_context_data(self): self._enterprise_customer_slug = enterprise_customer_slug # Initialize the enterprise customer users metatata derived from the LMS - self._initialize_enterprise_customer_users() + try: + self._initialize_enterprise_customer_users() + except Exception as exc: # pylint: disable=broad-except + logger.exception( + 'Error initializing enterprise customer users for request user %s, ' + 'enterprise customer uuid %s and/or slug %s', + self.lms_user_id, + enterprise_customer_uuid, + enterprise_customer_slug, + ) + self.add_error( + user_message='Error initializing enterprise customer users', + developer_message=f'Could not initialize enterprise customer users. Error: {exc}', + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + return if not self.enterprise_customer: # If no enterprise customer is found, return early + logger.info( + 'No enterprise customer found for request user %s, enterprise customer uuid %s, ' + 'and/or enterprise slug %s', + self.user.id, + enterprise_customer_uuid, + enterprise_customer_slug, + ) + self.add_error( + user_message='No enterprise customer found', + developer_message=( + f'No enterprise customer found for request user {self.user.id} and enterprise uuid ' + f'{enterprise_customer_uuid}, and/or enterprise slug {enterprise_customer_slug}' + ), + status_code=status.HTTP_404_NOT_FOUND, + ) return # Otherwise, update the enterprise customer UUID and slug if not already set @@ -153,18 +198,10 @@ def _initialize_enterprise_customer_users(self): """ Initializes the enterprise customer users for the request user. """ - try: - enterprise_customer_users_data = get_and_cache_enterprise_customer_users( - self.request, - traverse_pagination=True - ) - except Exception as exc: # pylint: disable=broad-except - logger.exception('Error retrieving linked enterprise customers') - self.add_error( - user_message='Error retrieving linked enterprise customers', - developer_message=f'Could not fetch enterprise customer users. Error: {exc}' - ) - return + enterprise_customer_users_data = get_and_cache_enterprise_customer_users( + self.request, + traverse_pagination=True + ) # Set enterprise features from the response self._enterprise_features = enterprise_customer_users_data.get('enterprise_features', {}) @@ -179,12 +216,21 @@ def _initialize_enterprise_customer_users(self): enterprise_customer_uuid=self.enterprise_customer_uuid, ) except Exception as exc: # pylint: disable=broad-except - logger.exception('Error transforming enterprise customer users data') + logger.exception( + 'Error transforming enterprise customer users metadata for request user %s, ' + 'enterprise customer uuid %s and/or slug %s', + self.lms_user_id, + self.enterprise_customer_uuid, + self.enterprise_customer_slug, + ) self.add_error( - user_message='Error transforming enterprise customer users data', - developer_message=f'Could not transform enterprise customer users data. Error: {exc}' + user_message='Could not transform enterprise customer metadata', + developer_message=( + f'Unable to transform enterprise customer users metadata. Error: {exc}' + ), ) + # Update the context data with the transformed enterprise customer users data self.data.update({ 'enterprise_customer': transformed_data.get('enterprise_customer'), 'active_enterprise_customer': transformed_data.get('active_enterprise_customer'), @@ -192,19 +238,28 @@ def _initialize_enterprise_customer_users(self): 'staff_enterprise_customer': transformed_data.get('staff_enterprise_customer'), }) - def add_error(self, **kwargs): + def add_error(self, status_code=None, **kwargs): """ Adds an error to the context. - Output fields determined by the ErrorSerializer + + Args: + user_message (str): A user-friendly message describing the error. + developer_message (str): A message describing the error for developers. + [status_code] (int): The HTTP status code to return in the response. """ serializer = serializers.ErrorSerializer(data=kwargs) serializer.is_valid(raise_exception=True) self.errors.append(serializer.data) + if status_code: + self.set_status_code(status_code) def add_warning(self, **kwargs): """ Adds a warning to the context. - Output fields determined by the WarningSerializer + + Args: + user_message (str): A user-friendly message describing the error. + developer_message (str): A message describing the error for developers. """ serializer = serializers.WarningSerializer(data=kwargs) serializer.is_valid(raise_exception=True) diff --git a/enterprise_access/apps/bffs/handlers.py b/enterprise_access/apps/bffs/handlers.py index a8b6a758..91e10a31 100644 --- a/enterprise_access/apps/bffs/handlers.py +++ b/enterprise_access/apps/bffs/handlers.py @@ -42,19 +42,26 @@ def load_and_process(self): """ raise NotImplementedError("Subclasses must implement `load_and_process` method.") - def add_error(self, **kwargs): + def add_error(self, user_message, developer_message, status_code=None): """ Adds an error to the context. Output fields determined by the ErrorSerializer """ - self.context.add_error(**kwargs) + self.context.add_error( + user_message=user_message, + developer_message=developer_message, + status_code=status_code, + ) - def add_warning(self, **kwargs): + def add_warning(self, user_message, developer_message): """ Adds an error to the context. Output fields determined by the WarningSerializer """ - self.context.add_warning(**kwargs) + self.context.add_warning( + user_message=user_message, + developer_message=developer_message, + ) class BaseLearnerPortalHandler(BaseHandler, BaseLearnerDataMixin): @@ -84,10 +91,7 @@ def load_and_process(self): The method in this class simply calls common learner logic to ensure the context is set up. """ if not self.context.enterprise_customer: - self.add_error( - user_message="An error occurred while loading the learner portal handler.", - developer_message="Enterprise customer not found in the context.", - ) + logger.info('No enterprise customer found in the context for request user %s', self.context.lms_user_id) return try: @@ -100,11 +104,15 @@ def load_and_process(self): # Retrieve default enterprise courses and enroll in the redeemable ones self.load_default_enterprise_enrollment_intentions() self.enroll_in_redeemable_default_enterprise_enrollment_intentions() - except Exception as e: # pylint: disable=broad-exception-caught - logger.exception("Error loading learner portal handler") + except Exception as exc: # pylint: disable=broad-exception-caught + logger.exception( + "Error loading/processing learner portal handler for request user %s and enterprise customer %s", + self.context.lms_user_id, + self.context.enterprise_customer_uuid, + ) self.add_error( - user_message="An error occurred while loading and processing common learner logic.", - developer_message=f"Error: {e}", + user_message="Could not load and/or process common data", + developer_message=f"Unable to load and/or process common learner portal data: {exc}", ) def transform_enterprise_customers(self): @@ -113,6 +121,7 @@ def transform_enterprise_customers(self): """ for customer_record_key in ('enterprise_customer', 'active_enterprise_customer', 'staff_enterprise_customer'): if not (customer_record := getattr(self.context, customer_record_key, None)): + logger.warning(f"No {customer_record_key} found in the context for request user {self.context.lms_user_id}") continue self.context.data[customer_record_key] = self.transform_enterprise_customer(customer_record) @@ -121,6 +130,10 @@ def transform_enterprise_customers(self): self.transform_enterprise_customer_user(enterprise_customer_user) for enterprise_customer_user in enterprise_customer_users ] + else: + logger.warning( + f"No linked enterprise customer users found in the context for request user {self.context.lms_user_id}" + ) def load_and_process_subsidies(self): """ @@ -159,8 +172,13 @@ def transform_enterprise_customer(self, enterprise_customer): Returns: The transformed enterprise customer data. """ - if not enterprise_customer or not enterprise_customer.get('enable_learner_portal', False): - # If the enterprise customer does not exist or the learner portal is not enabled, return None + if not enterprise_customer: + # If the enterprise customer does not exist, return None. + return None + + if not enterprise_customer.get('enable_learner_portal', False): + # If the enterprise customer's learner portal is not enabled, log an info message and return None. + logger.info(f"Learner portal is not enabled for enterprise customer {enterprise_customer.get('uuid')}") return None # Learner Portal is enabled, so transform the enterprise customer data. @@ -192,41 +210,24 @@ def load_subscription_licenses(self): self.context.data['enterprise_customer_user_subsidies'].update({ 'subscriptions': subscriptions_data, }) - except Exception as e: # pylint: disable=broad-exception-caught - logger.exception("Error loading subscription licenses") + 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="An error occurred while loading subscription licenses.", - developer_message=f"Error: {e}", + user_message="Unable to retrieve subscription licenses", + developer_message=f"Unable to fetch subscription licenses. Error: {exc}", ) - def transform_subscription_licenses(self, subscription_licenses): - """ - Transform subscription licenses data if needed. - """ - return [ - { - 'uuid': subscription_license.get('uuid'), - 'status': subscription_license.get('status'), - 'user_email': subscription_license.get('user_email'), - 'activation_date': subscription_license.get('activation_date'), - 'last_remind_date': subscription_license.get('last_remind_date'), - 'revoked_date': subscription_license.get('revoked_date'), - 'activation_key': subscription_license.get('activation_key'), - 'subscription_plan': subscription_license.get('subscription_plan', {}), - } - for subscription_license in subscription_licenses - ] - def transform_subscriptions_result(self, subscriptions_result): """ Transform subscription licenses data if needed. """ subscription_licenses = subscriptions_result.get('results', []) subscription_licenses_by_status = {} - - transformed_licenses = self.transform_subscription_licenses(subscription_licenses) - - for subscription_license in transformed_licenses: + for subscription_license in subscription_licenses: status = subscription_license.get('status') if status not in subscription_licenses_by_status: subscription_licenses_by_status[status] = [] @@ -234,7 +235,7 @@ def transform_subscriptions_result(self, subscriptions_result): return { 'customer_agreement': subscriptions_result.get('customer_agreement'), - 'subscription_licenses': transformed_licenses, + 'subscription_licenses': subscription_licenses, 'subscription_licenses_by_status': subscription_licenses_by_status, } @@ -290,10 +291,14 @@ def process_subscription_licenses(self): This method is called after `load_subscription_licenses` to handle further actions based on the loaded data. """ - if not self.subscriptions or self.current_activated_license: - # Skip processing if: - # - there is no subscriptions data - # - user already has an activated license(s) + if not self.subscriptions: + # Skip process if there are no subscriptions data + logger.warning("No subscription data found for the request user %s", self.context.lms_user_id) + return + + if self.current_activated_license: + # Skip processing if request user already has an activated license(s) + logger.info("User %s already has an activated license", self.context.lms_user_id) return # Check if there are 'assigned' licenses that need to be activated @@ -332,23 +337,25 @@ def check_and_activate_assigned_license(self): enterprise_customer_uuid=self.context.enterprise_customer_uuid, lms_user_id=self.context.lms_user_id, ) - except Exception as e: # pylint: disable=broad-exception-caught - logger.exception(f"Error activating license {subscription_license.get('uuid')}") + except Exception as exc: # pylint: disable=broad-exception-caught + license_uuid = subscription_license.get('uuid') + logger.exception(f"Error activating license {license_uuid}") self.add_error( - user_message="An error occurred while activating a subscription license.", - developer_message=f"License UUID: {subscription_license.get('uuid')}, Error: {e}", + user_message="Unable to activate subscription license", + developer_message=f"Could not activate subscription license {license_uuid}, Error: {exc}", ) return # Update the subscription_license data with the activation status and date; the activated license is not # returned from the API, so we need to manually update the license object we have available. - transformed_activated_subscription_licenses = self.transform_subscription_licenses([activated_license]) + transformed_activated_subscription_licenses = [activated_license] activated_licenses.append(transformed_activated_subscription_licenses[0]) else: - logger.error(f"Activation key not found for license {subscription_license.get('uuid')}") + license_uuid = subscription_license.get('uuid') + logger.error(f"Activation key not found for license {license_uuid}") self.add_error( - user_message="An error occurred while activating a subscription license.", - developer_message=f"Activation key not found for license {subscription_license.get('uuid')}", + user_message="No subscription license activation key found", + developer_message=f"Activation key not found for license {license_uuid}", ) # Update the subscription_licenses_by_status data with the activated licenses @@ -424,18 +431,26 @@ def check_and_auto_apply_license(self): lms_user_id=self.context.lms_user_id, ) # Update the context with the auto-applied license data - transformed_auto_applied_licenses = self.transform_subscription_licenses([auto_applied_license]) - licenses = self.subscription_licenses + transformed_auto_applied_licenses - subscription_licenses_by_status['activated'] = transformed_auto_applied_licenses + licenses = self.subscription_licenses + [auto_applied_license] + subscription_licenses_by_status['activated'] = [auto_applied_license] self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({ 'subscription_licenses': licenses, 'subscription_licenses_by_status': subscription_licenses_by_status, }) - except Exception as e: # pylint: disable=broad-exception-caught - logger.exception("Error auto-applying license") + except Exception as exc: # pylint: disable=broad-exception-caught + logger.exception( + "Error auto-applying subscription license for user %s and " + "enterprise customer %s and customer agreement %s", + self.context.lms_user_id, + self.context.enterprise_customer_uuid, + customer_agreement.get('uuid'), + ) self.add_error( - user_message="An error occurred while auto-applying a license.", - developer_message=f"Customer agreement UUID: {customer_agreement.get('uuid')}, Error: {e}", + user_message="Unable to auto-apply a subscription license.", + developer_message=( + f"Could not auto-apply a subscription license for " + f"customer agreement {customer_agreement.get('uuid')}, Error: {exc}", + ) ) def load_default_enterprise_enrollment_intentions(self): @@ -450,10 +465,14 @@ def load_default_enterprise_enrollment_intentions(self): ) self.context.data['default_enterprise_enrollment_intentions'] = default_enterprise_enrollment_intentions except Exception as e: # pylint: disable=broad-exception-caught - logger.exception("Error loading default enterprise courses") + logger.exception( + "Error loading default enterprise enrollment intentions for user %s and enterprise customer %s", + self.context.lms_user_id, + self.context.enterprise_customer_uuid, + ) self.add_error( - user_message="An error occurred while loading default enterprise courses.", - developer_message=f"Error: {e}", + user_message="Could not load default enterprise enrollment intentions", + developer_message=f"Could not load default enterprise enrollment intentions. Error: {e}", ) def enroll_in_redeemable_default_enterprise_enrollment_intentions(self): @@ -464,7 +483,24 @@ def enroll_in_redeemable_default_enterprise_enrollment_intentions(self): needs_enrollment = enrollment_statuses.get('needs_enrollment', {}) needs_enrollment_enrollable = needs_enrollment.get('enrollable', []) - if not (needs_enrollment_enrollable and self.current_activated_license): + if not needs_enrollment_enrollable: + # Skip enrolling in default enterprise courses if there are no enrollable courses for which to enroll + logger.info( + "No default enterprise enrollment intentions courses for which to enroll " + "for request user %s and enterprise customer %s", + self.context.lms_user_id, + self.context.enterprise_customer_uuid, + ) + return + + if not self.current_activated_license: + # Skip enrolling in default enterprise courses if there is no activated license + logger.info( + "No activated license found for request user %s and enterprise customer %s. " + "Skipping realization of default enterprise enrollment intentions.", + self.context.lms_user_id, + self.context.enterprise_customer_uuid, + ) return license_uuids_by_course_run_key = {} @@ -481,21 +517,30 @@ def enroll_in_redeemable_default_enterprise_enrollment_intentions(self): response_payload = self._request_default_enrollment_realizations(license_uuids_by_course_run_key) if failures := response_payload.get('failures'): + # Log and add error if there are failures realizing default enrollments + failures_str = json.dumps(failures) + logger.error( + 'Default realization enrollment failures for request user %s and ' + 'enterprise customer %s: %s', + self.context.lms_user_id, + self.context.enterprise_customer_uuid, + failures_str, + ) self.add_error( user_message='There were failures realizing default enrollments', - developer_message='Default realization enrollment failures: ' + json.dumps(failures), + developer_message='Default realization enrollment failures: ' + failures_str, ) if not self.context.data.get('default_enterprise_enrollment_realizations'): self.context.data['default_enterprise_enrollment_realizations'] = [] - if response_payload['successes']: + if successful_enrollments := response_payload.get('successes', []): # Invalidate the default enterprise enrollment intentions and enterprise course enrollments cache # as the previously redeemable enrollment intentions have been processed/enrolled. self.invalidate_default_enrollment_intentions_cache() self.invalidate_enrollments_cache() - for enrollment in response_payload['successes']: + for enrollment in successful_enrollments: course_run_key = enrollment.get('course_run_key') self.context.data['default_enterprise_enrollment_realizations'].append({ 'course_key': course_run_key, @@ -523,7 +568,7 @@ def _request_default_enrollment_realizations(self, license_uuids_by_course_run_k bulk_enrollment_payload, ) except Exception as exc: # pylint: disable=broad-exception-caught - logger.exception('Error actualizing default enrollments') + logger.exception('Error realizing default enterprise enrollment intentions') self.add_error( user_message='There was an exception realizing default enrollments', developer_message=f'Default realization enrollment exception: {exc}', @@ -565,10 +610,14 @@ def load_and_process(self): # Load data specific to the dashboard route self.load_enterprise_course_enrollments() except Exception as e: # pylint: disable=broad-exception-caught - logger.exception("Error retrieving enterprise_course_enrollments") + logger.exception( + "Error loading and/or processing dashboard data for user %s and enterprise customer %s", + self.context.lms_user_id, + self.context.enterprise_customer_uuid, + ) self.add_error( - user_message="An error occurred while processing the learner dashboard.", - developer_message=f"Error: {e}", + user_message="Could not load and/or processing the learner dashboard.", + developer_message=f"Failed to load and/or processing the learner dashboard data: {e}", ) def load_enterprise_course_enrollments(self): @@ -585,9 +634,9 @@ def load_enterprise_course_enrollments(self): is_active=True, ) self.context.data['enterprise_course_enrollments'] = enterprise_course_enrollments - except Exception as e: # pylint: disable=broad-exception-caught + except Exception as exc: # pylint: disable=broad-exception-caught logger.exception("Error retrieving enterprise course enrollments") self.add_error( - user_message="An error occurred while retrieving enterprise course enrollments.", - developer_message=f"Error: {e}", + user_message="Could not retrieve your enterprise course enrollments.", + developer_message=f"Failed to retrieve enterprise course enrollments: {exc}", ) diff --git a/enterprise_access/apps/bffs/response_builder.py b/enterprise_access/apps/bffs/response_builder.py index 984f2d6f..84a329ab 100644 --- a/enterprise_access/apps/bffs/response_builder.py +++ b/enterprise_access/apps/bffs/response_builder.py @@ -134,7 +134,7 @@ def build(self): return serialized_data, self.status_code except Exception as exc: # pylint: disable=broad-except logger.exception('Could not serialize the response data.') - self.context.add_error( + self.context.add_warning( user_message='An error occurred while processing the response data.', developer_message=f'Could not serialize the response data. Error: {exc}', ) diff --git a/enterprise_access/apps/bffs/serializers.py b/enterprise_access/apps/bffs/serializers.py index 3e74573c..ad9a7656 100644 --- a/enterprise_access/apps/bffs/serializers.py +++ b/enterprise_access/apps/bffs/serializers.py @@ -179,7 +179,7 @@ class CustomerAgreementSerializer(BaseBffSerializer): required=False, allow_null=True, default=False, ) button_label_in_modal_v2 = serializers.CharField(required=False, allow_null=True) - expired_subscription_modal_messaging_v2 = serializers.CharField(required=False, allow_null=True) + expired_subscription_modal_messaging_v2 = serializers.CharField(required=False, allow_blank=True, allow_null=True) modal_header_text_v2 = serializers.CharField(required=False, allow_null=True)