feat: fetch program subscription details (#32023)

* feat: fetch program subscription details
This commit is contained in:
Mohammad Ahtasham ul Hassan
2023-04-14 16:33:12 +05:00
committed by GitHub
parent dc63e525f8
commit e973266b2f
9 changed files with 216 additions and 19 deletions

View File

@@ -8,27 +8,25 @@ from copy import deepcopy
from unittest import mock
import ddt
from edx_toggles.toggles.testutils import override_waffle_switch
import httpretty
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_switch
from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order
from pytz import utc
from testfixtures import LogCapture
from xmodule.data import CertificatesDisplayBehaviors
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory as ModuleStoreCourseFactory
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory
from common.djangoapps.student.tests.factories import AnonymousUserFactory, CourseEnrollmentFactory, UserFactory
from common.djangoapps.util.date_utils import strftime_localized
from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
from lms.djangoapps.commerce.utils import EcommerceService
from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=wrong-import-order
from openedx.core.djangoapps.catalog.tests.factories import (
CourseFactory,
CourseRunFactory,
@@ -47,12 +45,15 @@ from openedx.core.djangoapps.programs.utils import (
ProgramProgressMeter,
get_certificates,
get_logged_in_program_certificate_url,
get_programs_subscription_data,
is_user_enrolled_in_program_type
)
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from common.djangoapps.student.tests.factories import AnonymousUserFactory, CourseEnrollmentFactory, UserFactory
from common.djangoapps.util.date_utils import strftime_localized
from xmodule.data import CertificatesDisplayBehaviors
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory as ModuleStoreCourseFactory
ECOMMERCE_URL_ROOT = 'https://ecommerce.example.com'
UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
@@ -1729,3 +1730,71 @@ class TestProgramEnrollment(SharedModuleStoreTestCase):
)
mock_get_programs_by_type.return_value = [self.program]
assert is_user_enrolled_in_program_type(user=self.user, program_type_slug=self.MICROBACHELORS)
@skip_unless_lms
class TestGetProgramsSubscriptionData(TestCase):
"""
Tests for the get_programs_subscription_data utility function.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.mock_program_subscription_data = [
{'id': uuid.uuid4(), 'resource_id': uuid.uuid4(),
'resource_type': 'program', 'resource_data': None, 'trial_end': '1970-01-01T00:02:03Z',
'price': '100.00', 'currency': 'USD', 'sub_type': 'stripe', 'identifier': 'dummy_1',
'next_payment_date': '1970-01-01T00:02:03Z', 'status': 'active',
'customer': 1, 'subscription_state': 'active'},
{'id': uuid.uuid4(), 'resource_id': uuid.uuid4(),
'resource_type': 'program', 'resource_data': None, 'trial_end': '1970-01-01T03:25:12Z',
'price': '1000.00', 'currency': 'USD', 'sub_type': 'stripe', 'identifier': 'dummy_2',
'next_payment_date': '1970-05-23T12:05:21Z', 'status': 'subscription_initiated',
'customer': 1, 'subscription_state': 'notStarted'}
]
@mock.patch(UTILS_MODULE + ".get_subscription_api_client")
@mock.patch(UTILS_MODULE + ".log.info")
def test_get_programs_subscription_data(self, mock_log, mock_get_subscription_api_client):
# mock return values
mock_client = mock.Mock()
mock_get_subscription_api_client.return_value = mock_client
mock_response = {"results": self.mock_program_subscription_data, "next": None}
mock_client.get.return_value = mock.Mock(json=lambda: mock_response, raise_for_status=lambda: None)
# call the function
user = mock.Mock()
result = get_programs_subscription_data(user)
# assert expected behavior
mock_log.assert_called_once_with(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}")
mock_get_subscription_api_client.assert_called_once_with(user)
mock_client.get.assert_called_once_with(settings.SUBSCRIPTIONS_API_PATH, params={"page": 1})
assert result == self.mock_program_subscription_data
@mock.patch(UTILS_MODULE + ".get_subscription_api_client")
@mock.patch(UTILS_MODULE + ".log.info")
def test_get_programs_subscription_data_with_uuid(self, mock_log, mock_get_subscription_api_client):
mock_client = mock.Mock()
mock_get_subscription_api_client.return_value = mock_client
subscription_data = self.mock_program_subscription_data[0]
program_uuid = subscription_data['resource_id']
mock_response = {"results": subscription_data, "next": None}
mock_client.get.return_value = mock.Mock(json=lambda: mock_response, raise_for_status=lambda: None)
user = mock.Mock()
result = get_programs_subscription_data(user, program_uuid=program_uuid)
mock_log.assert_called_once_with(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}"
f" for program_uuid: {str(program_uuid)}")
mock_get_subscription_api_client.assert_called_once_with(user)
mock_client.get.assert_called_once_with(
settings.SUBSCRIPTIONS_API_PATH,
params={
"most_active_and_recent": True,
"resource_id": program_uuid,
}
)
assert result == subscription_data

View File

@@ -8,6 +8,7 @@ from copy import deepcopy
from itertools import chain
from urllib.parse import urljoin, urlparse, urlunparse
import requests
from dateutil.parser import parse
from django.conf import settings
from django.contrib.auth import get_user_model
@@ -15,10 +16,10 @@ from django.contrib.sites.models import Site
from django.core.cache import cache
from django.urls import reverse
from django.utils.functional import cached_property
from edx_rest_api_client.auth import SuppliedJwtAuth
from opaque_keys.edx.keys import CourseKey
from pytz import utc
from requests.exceptions import RequestException
from xmodule.modulestore.django import modulestore
from common.djangoapps.course_modes.api import get_paid_modes_for_course
from common.djangoapps.course_modes.models import CourseMode
@@ -35,15 +36,17 @@ from openedx.core.djangoapps.catalog.constants import PathwayType
from openedx.core.djangoapps.catalog.utils import (
get_fulfillable_course_runs_for_entitlement,
get_pathways,
get_programs,
get_programs
)
from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credentials.utils import get_credentials, get_credentials_records_url
from openedx.core.djangoapps.enrollments.api import get_enrollments
from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.djangoapps.programs import ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from xmodule.modulestore.django import modulestore
# The datetime module's strftime() methods require a year >= 1900.
DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
@@ -82,6 +85,9 @@ def get_program_urls(program_data):
'commerce_api_url': reverse('commerce_api:v0:baskets:create'),
'buy_button_url': ecommerce_service.get_checkout_page_url(*skus),
'program_record_url': program_record_url,
'buy_subscription_url': settings.SUBSCRIPTIONS_BUY_SUBSCRIPTION_URL,
'manage_subscription_url': settings.ORDER_HISTORY_MICROFRONTEND_URL,
'subscriptions_learner_help_center_url': settings.SUBSCRIPTIONS_LEARNER_HELP_CENTER_URL
}
return urls
@@ -151,6 +157,7 @@ class ProgramProgressMeter:
will only inspect this one program, not all programs the user may be
engaged with.
"""
def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False, include_course_entitlements=True):
self.site = site
self.user = user
@@ -544,6 +551,7 @@ class ProgramDataExtender:
program_data (dict): Representation of a program.
user (User): The user whose enrollments to inspect.
"""
def __init__(self, program_data, user, mobile_only=False):
self.data = program_data
self.user = user
@@ -865,6 +873,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
program_data (dict): Representation of a program.
user (User): The user whose enrollments to inspect.
"""
def __init__(self, program_data, user):
super().__init__(program_data, user)
@@ -1028,3 +1037,51 @@ def is_user_enrolled_in_program_type(user, program_type_slug, paid_modes_only=Fa
elif course_run_id in course_runs:
return True
return False
def get_subscription_api_client(user):
"""
Returns an API client which can be used to make Subscriptions API requests.
"""
scopes = [
'user_id',
'email',
'profile'
]
jwt = create_jwt_for_user(user, scopes=scopes)
client = requests.Session()
client.auth = SuppliedJwtAuth(jwt)
return client
def get_programs_subscription_data(user, program_uuid=None):
"""
Returns the subscription data for a user's program if uuid is specified
else return data for user's all subscriptions.
"""
client = get_subscription_api_client(user)
api_path = f"{settings.SUBSCRIPTIONS_API_PATH}"
subscription_data = []
log.info(f"B2C_SUBSCRIPTIONS: Requesting Program subscription data for user: {user}" +
(f" for program_uuid: {program_uuid}" if program_uuid is not None else ""))
try:
if program_uuid:
response = client.get(api_path, params={'resource_id': program_uuid, 'most_active_and_recent': True})
response.raise_for_status()
subscription_data = response.json().get('results', [])
else:
next_page = 1
while next_page:
response = client.get(api_path, params=dict(page=next_page))
response.raise_for_status()
subscription_data.extend(response.json().get('results', []))
next_page = response.json().get('next')
except Exception as exc: # pylint: disable=broad-except
log.exception(
f"B2C_SUBSCRIPTIONS: Failed to retrieve Program Subscription Data for user: {user} with error: {exc}"
+ f" for program_uuid: {str(program_uuid)}" if program_uuid is not None else ""
)
return subscription_data