diff --git a/enterprise/api/v1/serializers.py b/enterprise/api/v1/serializers.py index 7d9ffaa33..1c101b230 100644 --- a/enterprise/api/v1/serializers.py +++ b/enterprise/api/v1/serializers.py @@ -38,6 +38,7 @@ CourseEnrollmentDowngradeError, CourseEnrollmentPermissionError, get_integrations_for_customers, + get_active_sso_configurations_for_customer, get_last_course_run_end_date, has_course_run_available_for_enrollment, track_enrollment, @@ -227,7 +228,8 @@ class Meta: 'enable_learner_portal_sidebar_message', 'learner_portal_sidebar_content', 'enable_pathways', 'enable_programs', 'enable_demo_data_for_analytics_and_lpr', 'enable_academies', 'enable_one_academy', 'active_integrations', 'show_videos_in_learner_portal_search_results', - 'default_language', 'country', 'enable_slug_login', + 'default_language', 'country', 'enable_slug_login', 'active_sso_configurations', + 'subscriptions', 'coupons', 'offers', 'has_active_offers', 'has_active_coupons', 'has_active_subscriptions' ) identity_providers = EnterpriseCustomerIdentityProviderSerializer(many=True, read_only=True) @@ -237,10 +239,51 @@ class Meta: enterprise_notification_banner = serializers.SerializerMethodField() admin_users = serializers.SerializerMethodField() active_integrations = serializers.SerializerMethodField() + active_sso_configurations = serializers.SerializerMethodField() + subscriptions = serializers.SerializerMethodField() + coupons = serializers.SerializerMethodField() + offers = serializers.SerializerMethodField() + has_active_offers = serializers.SerializerMethodField() + has_active_coupons = serializers.SerializerMethodField() + has_active_subscriptions = serializers.SerializerMethodField() + + def get_offers(self, obj): + # we want to get both active and inactive offers to display data for the card + return obj.offers_for_customer + + def get_coupons(self, obj): + # we want to get both active and inactive coupons to display data for the card + return obj.coupons_for_customer + + def get_subscriptions(self, obj): + # we want to get both active and inactive subs to display data for the card + return obj.subscriptions_for_customer + + def get_has_active_offers(self, obj): + # loop through offers and check if there is at least one + # active offer and return boolean. this is used for the + # checkmark on the support tools customer data table. + return + + def get_has_active_coupons(self, obj): + # loop through coupons and check if there is at least one + # active coupon and return boolean. this is used for the + # checkmark on the support tools customer data table. + return + + def get_has_active_subscriptions(self, obj): + # loop through subs and check if there is at least one + # active sub and return boolean. this is used for the + # checkmark on the support tools customer data table. + return + + def get_active_sso_configurations(self, obj): + return get_active_sso_configurations_for_customer(obj.uuid) def get_active_integrations(self, obj): return get_integrations_for_customers(obj.uuid) + def get_branding_configuration(self, obj): """ Return the serialized branding configuration object OR default object if null diff --git a/enterprise/api/v1/views/enterprise_customer_api_credentials.py b/enterprise/api/v1/views/enterprise_customer_api_credentials.py index 468ecc47f..2eae12801 100644 --- a/enterprise/api/v1/views/enterprise_customer_api_credentials.py +++ b/enterprise/api/v1/views/enterprise_customer_api_credentials.py @@ -115,6 +115,16 @@ def retrieve(self, request, *args, **kwargs): URL: /enterprise/api/v1/enterprise-customer-api-credentials/{enterprise_uuid} """ + + ''' + Note: obtaining data for all API credentials created for a specific enterprise customer + is not straightforward because the data is not stored on the enterprise customer. One potential + method is to iterate through all users linked to the enterprise customer to identify the creator + of the API credentials. However, this approach could impact performance, raise security concerns, and + expand the scope of this task. As an alternative, I recommend displaying a checkmark on the customer + data table and API credentials card to indicate if API credentials are enabled for the enterprise customer. + ''' + user_application = Application.objects.get(user=request.user) serializer = self.get_serializer(instance=user_application) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/enterprise/api_client/ecommerce.py b/enterprise/api_client/ecommerce.py index 947a60631..024d441ed 100644 --- a/enterprise/api_client/ecommerce.py +++ b/enterprise/api_client/ecommerce.py @@ -118,6 +118,36 @@ def create_manual_enrollment_orders(self, enrollments): str(exc) ) + def get_coupons(self, enterprise_customer_uuid): + """ + Get coupons for the enterprise customer. + + Returns: + array of coupons. + """ + api_url = urljoin(f"{self.API_BASE_URL}/", f"enterprise/coupons/{enterprise_customer_uuid}/overview/") + # format each result to include start date/end date name, and uuid + try: + response = self.client.get(api_url) + return response.json().get('results') + except: + LOGGER.exception(f'failed to fetch at url {api_url}') + + def get_offers(self, enterprise_customer_uuid): + """ + Get offers for the enterprise customer. + + Returns: + array of offers. + """ + # format each result to include start date/end date, name, and uuid + api_url = urljoin(f"{self.API_BASE_URL}/", f"enterprise/{enterprise_customer_uuid}/enterprise-admin-offers/") + try: + response = self.client.get(api_url) + return response.json().get('results') + except: + LOGGER.exception(f'failed to fetch at url {api_url}') + class NoAuthEcommerceClient(NoAuthAPIClient): """ diff --git a/enterprise/api_client/license_manager.py b/enterprise/api_client/license_manager.py new file mode 100644 index 000000000..d722adc8f --- /dev/null +++ b/enterprise/api_client/license_manager.py @@ -0,0 +1,35 @@ +""" +API client for calls to the license-manager service. +""" +import logging + +import requests +from django.conf import settings +from enterprise.api_client.client import BackendServiceAPIClient +from urllib.parse import urljoin + +logger = logging.getLogger(__name__) + +class LicenseManagerApiClient(BackendServiceAPIClient): + """ + API client for calls to the license-manager service. + """ + LICENSE_MANAGER_BASE_URL = urljoin(f"{settings.LICENSE_MANAGER_URL}/", "api/v1/") + SUBSCRIPTIONS_ENDPOINT = LICENSE_MANAGER_BASE_URL + 'subscriptions/?enterprise_customer_uuid=' + + def get_customer_subscriptions(self, customer_uuid): + """ + Call license-manager API for data about a SubscriptionPlan. + + Arguments: + subscription_uuid (UUID): UUID of the SubscriptionPlan in license-manager + Returns: + dict: Dictionary representation of json returned from API + """ + try: + endpoint = self.SUBSCRIPTIONS_ENDPOINT + str(customer_uuid) + response = self.client.get(endpoint) + return response.json().get('results') + except requests.exceptions.HTTPError as exc: + logger.exception(exc) + raise \ No newline at end of file diff --git a/enterprise/models.py b/enterprise/models.py index 67566c8f1..81e30b23a 100644 --- a/enterprise/models.py +++ b/enterprise/models.py @@ -43,6 +43,7 @@ from enterprise.api_client.discovery import CourseCatalogApiClient, get_course_catalog_api_service_client from enterprise.api_client.ecommerce import EcommerceApiClient from enterprise.api_client.enterprise_catalog import EnterpriseCatalogApiClient +from enterprise.api_client.license_manager import LicenseManagerApiClient from enterprise.api_client.lms import EnrollmentApiClient, ThirdPartyAuthApiClient from enterprise.api_client.sso_orchestrator import EnterpriseSSOOrchestratorApiClient from enterprise.api_client.xpert_ai import chat_completion @@ -759,6 +760,60 @@ def catalog_contains_course(self, course_run_id): return False + @property + def subscriptions_for_customer(self): + """ + Helper method to get subscriptions for customer. + + Arguments: + customer_uuid (UUID): uuid of an enterprise customer + Returns: + list: a list of subscriptions. + """ + try: + subscriptions = LicenseManagerApiClient().get_customer_subscriptions(self.uuid) + except: + raise + return subscriptions + + @property + def coupons_for_customer(self): + """ + Helper method to get active coupons for customer. + + Arguments: + customer_uuid (UUID): uuid of an enterprise customer + Returns: + list: a list of active coupons + """ + ecommerce_service_worker = get_ecommerce_worker_user() + try: + ecommerce_api_client = EcommerceApiClient(ecommerce_service_worker) + except: + LOGGER.exception('failed') + else: + coupons = ecommerce_api_client.get_coupons(self.uuid) + return coupons + + @property + def offers_for_customer(self): + """ + Helper method to get offers for customer. + + Arguments: + customer_uuid (UUID): uuid of an enterprise customer + Returns: + list: a list of offers + """ + ecommerce_service_worker = get_ecommerce_worker_user() + try: + ecommerce_api_client = EcommerceApiClient(ecommerce_service_worker) + except: + LOGGER.exception('failed') + else: + offers = ecommerce_api_client.get_offers(self.uuid) + return offers + def enroll_user_pending_registration_with_status(self, email, course_mode, *course_ids, **kwargs): """ Create pending enrollments for the user in any number of courses, which will take effect on registration. diff --git a/enterprise/settings/test.py b/enterprise/settings/test.py index b98da6eef..f96d64827 100644 --- a/enterprise/settings/test.py +++ b/enterprise/settings/test.py @@ -139,6 +139,7 @@ def root(*args): LMS_ENROLLMENT_API_PATH = "/api/enrollment/v1/" ECOMMERCE_PUBLIC_URL_ROOT = "http://localhost:18130" ENTERPRISE_CATALOG_INTERNAL_ROOT_URL = "http://localhost:18160" +LICENSE_MANAGER_URL = "http://localhost:18170" ENTERPRISE_ENROLLMENT_API_URL = LMS_INTERNAL_ROOT_URL + LMS_ENROLLMENT_API_PATH diff --git a/enterprise/utils.py b/enterprise/utils.py index b2768148d..9747c3203 100644 --- a/enterprise/utils.py +++ b/enterprise/utils.py @@ -47,6 +47,7 @@ CourseModes, ) from enterprise.logging import getEnterpriseLogger +# from enterprise.api_client.license_manager_client import LicenseManagerApiClient try: from openedx.features.enterprise_support.enrollments.utils import lms_update_or_create_enrollment @@ -728,6 +729,13 @@ def enterprise_customer_invite_key_model(): return apps.get_model('enterprise', 'EnterpriseCustomerInviteKey') +def enterprise_customer_sso_configuration_model(): + """ + Returns the ``EnterpriseCustomerSsoConfiguration`` class. + """ + return apps.get_model('enterprise', 'EnterpriseCustomerSsoConfiguration') + + def get_enterprise_customer(uuid): """ Get the ``EnterpriseCustomer`` instance associated with ``uuid``. @@ -2466,9 +2474,40 @@ def get_integrations_for_customers(customer_uuid): Returns: list: a list of integration channel codes. """ + + ''' + Currently, we are only returning an array of the integrated channel code. We need to + update this to also return the an array of objects that includes a created and last modified date + ''' unique_integrations = [] integrated_channel_choices = get_integrated_channel_choices() for code, choice in integrated_channel_choices.items(): if choice.objects.filter(enterprise_customer__uuid=customer_uuid, active=True): unique_integrations.append(code) return unique_integrations + + +def get_active_sso_configurations_for_customer(customer_uuid): + """ + Helper method to get active sso configurations for each enterprise customer + + Arguments: + customer_uuid (UUID): uuid of an enterprise customer + Returns: + list: a list of active sso configurations + """ + SsoConfigurations = enterprise_customer_sso_configuration_model() + sso_configurations = SsoConfigurations.objects.filter(enterprise_customer__uuid=customer_uuid, + active=True).values() + active_configurations = [] + if sso_configurations: + for sso_configuration in sso_configurations: + active_configurations.append({ + 'created': sso_configuration.get('created'), + 'modified': sso_configuration.get('modified'), + 'is_removed': sso_configuration.get('is_removed'), + 'display_name': sso_configuration.get('display_name'), + 'uuid': sso_configuration.get('uuid'), + 'enterprise_customer_id': sso_configuration.get('enterprise_customer_id'), + }) + return active_configurations \ No newline at end of file