Commit 17716208 by Ahsan Ulhaq

Use Course Title with SailThru Purchase Call

ECOM-6873
parent 816a6c53
...@@ -3,9 +3,8 @@ from celery import shared_task ...@@ -3,9 +3,8 @@ from celery import shared_task
from celery.exceptions import Ignore from celery.exceptions import Ignore
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from edx_rest_api_client import exceptions from edx_rest_api_client import exceptions
from edx_rest_api_client.client import EdxRestApiClient
from ecommerce_worker.utils import get_configuration from ecommerce_worker.utils import get_configuration, get_ecommerce_client
logger = get_task_logger(__name__) # pylint: disable=invalid-name logger = get_task_logger(__name__) # pylint: disable=invalid-name
...@@ -36,13 +35,8 @@ def fulfill_order(self, order_number, site_code=None): ...@@ -36,13 +35,8 @@ def fulfill_order(self, order_number, site_code=None):
Returns: Returns:
None None
""" """
ecommerce_api_root = get_configuration('ECOMMERCE_API_ROOT', site_code=site_code)
max_fulfillment_retries = get_configuration('MAX_FULFILLMENT_RETRIES', site_code=site_code) max_fulfillment_retries = get_configuration('MAX_FULFILLMENT_RETRIES', site_code=site_code)
signing_key = get_configuration('JWT_SECRET_KEY', site_code=site_code) api = get_ecommerce_client(site_code=site_code)
issuer = get_configuration('JWT_ISSUER', site_code=site_code)
service_username = get_configuration('ECOMMERCE_SERVICE_USERNAME', site_code=site_code)
api = EdxRestApiClient(ecommerce_api_root, signing_key=signing_key, issuer=issuer, username=service_username)
try: try:
logger.info('Requesting fulfillment of order [%s].', order_number) logger.info('Requesting fulfillment of order [%s].', order_number)
api.orders(order_number).fulfill.put() api.orders(order_number).fulfill.put()
......
...@@ -8,7 +8,7 @@ from sailthru.sailthru_client import SailthruClient ...@@ -8,7 +8,7 @@ from sailthru.sailthru_client import SailthruClient
from sailthru.sailthru_error import SailthruClientError from sailthru.sailthru_error import SailthruClientError
from ecommerce_worker.cache import Cache from ecommerce_worker.cache import Cache
from ecommerce_worker.utils import get_configuration from ecommerce_worker.utils import get_configuration, get_ecommerce_client
logger = get_task_logger(__name__) # pylint: disable=invalid-name logger = get_task_logger(__name__) # pylint: disable=invalid-name
cache = Cache() # pylint: disable=invalid-name cache = Cache() # pylint: disable=invalid-name
...@@ -83,7 +83,7 @@ def update_course_enrollment(self, email, course_url, purchase_incomplete, mode, ...@@ -83,7 +83,7 @@ def update_course_enrollment(self, email, course_url, purchase_incomplete, mode,
_schedule_retry(self, config) _schedule_retry(self, config)
# Get course data from Sailthru content library or cache # Get course data from Sailthru content library or cache
course_data = _get_course_content(course_url, sailthru_client, site_code, config) course_data = _get_course_content(course_id, course_url, sailthru_client, site_code, config)
# build item description # build item description
item = _build_purchase_item(course_id, course_url, cost_in_cents, mode, course_data) item = _build_purchase_item(course_id, course_url, cost_in_cents, mode, course_data)
...@@ -167,12 +167,13 @@ def _record_purchase(sailthru_client, email, item, purchase_incomplete, message_ ...@@ -167,12 +167,13 @@ def _record_purchase(sailthru_client, email, item, purchase_incomplete, message_
return True return True
def _get_course_content(course_url, sailthru_client, site_code, config): def _get_course_content(course_id, course_url, sailthru_client, site_code, config):
"""Get course information using the Sailthru content api or from cache. """Get course information using the Sailthru content api or from cache.
If there is an error, just return with an empty response. If there is an error, just return with an empty response.
Arguments: Arguments:
course_id (str): course key of the course
course_url (str): LMS url for course info page. course_url (str): LMS url for course info page.
sailthru_client (object): SailthruClient sailthru_client (object): SailthruClient
site_code (str): site code site_code (str): site code
...@@ -188,17 +189,53 @@ def _get_course_content(course_url, sailthru_client, site_code, config): ...@@ -188,17 +189,53 @@ def _get_course_content(course_url, sailthru_client, site_code, config):
try: try:
sailthru_response = sailthru_client.api_get("content", {"id": course_url}) sailthru_response = sailthru_client.api_get("content", {"id": course_url})
if not sailthru_response.is_ok(): if not sailthru_response.is_ok():
return {} response = {}
else:
response = sailthru_response.json response = sailthru_response.json
cache.set(cache_key, response, config.get('SAILTHRU_CACHE_TTL_SECONDS')) cache.set(cache_key, response, config.get('SAILTHRU_CACHE_TTL_SECONDS'))
except SailthruClientError: except SailthruClientError:
response = {} response = {}
if not response:
logger.error('Could not get course data from Sailthru on enroll/purchase event. '
'Calling Ecommerce Course API to get course info for enrollment confirmation email')
response = _get_course_content_from_ecommerce(course_id, site_code=site_code)
if response:
cache.set(cache_key, response, config.get('SAILTHRU_CACHE_TTL_SECONDS'))
return response return response
def _get_course_content_from_ecommerce(course_id, site_code=None):
"""
Get course information using the Ecommerce course api.
In case of error returns empty response.
Arguments:
course_id (str): course key of the course
site_code (str): site code
Returns:
course information from Ecommerce
"""
api = get_ecommerce_client(site_code=site_code)
try:
api_response = api.courses(course_id).get()
except Exception: # pylint: disable=broad-except
logger.exception(
'An error occurred while retrieving data for course run [%s] from the Catalog API.',
course_id,
exc_info=True
)
return {}
return {
'title': api_response.get('name'),
'verification_deadline': api_response.get('verification_deadline')
}
def _update_unenrolled_list(sailthru_client, email, course_url, unenroll): def _update_unenrolled_list(sailthru_client, email, course_url, unenroll):
"""Maintain a list of courses the user has unenrolled from in the Sailthru user record """Maintain a list of courses the user has unenrolled from in the Sailthru user record
......
"""Tests of sailthru worker code.""" """Tests of sailthru worker code."""
import json
import logging import logging
from decimal import Decimal from decimal import Decimal
from unittest import TestCase from unittest import TestCase
import httpretty
from mock import patch from mock import patch
from sailthru.sailthru_error import SailthruClientError from sailthru.sailthru_error import SailthruClientError
from ecommerce_worker.sailthru.v1.tasks import update_course_enrollment, _update_unenrolled_list, _get_course_content from ecommerce_worker.sailthru.v1.tasks import (
update_course_enrollment, _update_unenrolled_list, _get_course_content, _get_course_content_from_ecommerce
)
from ecommerce_worker.utils import get_configuration from ecommerce_worker.utils import get_configuration
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -26,6 +30,29 @@ class SailthruTests(TestCase): ...@@ -26,6 +30,29 @@ class SailthruTests(TestCase):
self.course_id2 = 'edX/toy/2016_Fall' self.course_id2 = 'edX/toy/2016_Fall'
self.course_url2 = 'http://lms.testserver.fake/courses/edX/toy/2016_Fall/info' self.course_url2 = 'http://lms.testserver.fake/courses/edX/toy/2016_Fall/info'
def mock_ecommerce_api(self, body, course_id, status=200):
""" Mock GET requests to the ecommerce course API endpoint. """
httpretty.reset()
httpretty.register_uri(
httpretty.GET, '{}/courses/{}/'.format(
get_configuration('ECOMMERCE_API_ROOT').strip('/'), unicode(course_id)
),
status=status,
body=json.dumps(body), content_type='application/json',
)
def ecom_course_data(self, course_id):
""" Returns dummy course data. """
return {
"id": course_id,
"url": "https://test-ecommerce.edx.org/api/v2/courses/{}/".format(course_id),
"name": "test course",
"verification_deadline": "2016-12-01T23:59:00Z",
"type": "verified",
"products_url": "https://test-ecommerce.edx.org/api/v2/courses/{}/products/".format(course_id),
"last_edited": "2016-12-28T15:20:58Z"
}
@patch('ecommerce_worker.sailthru.v1.tasks.get_configuration') @patch('ecommerce_worker.sailthru.v1.tasks.get_configuration')
@patch('ecommerce_worker.sailthru.v1.tasks.logger.error') @patch('ecommerce_worker.sailthru.v1.tasks.logger.error')
def test_sailthru_disabled(self, mock_log_error, mock_get_configuration): def test_sailthru_disabled(self, mock_log_error, mock_get_configuration):
...@@ -318,6 +345,7 @@ class SailthruTests(TestCase): ...@@ -318,6 +345,7 @@ class SailthruTests(TestCase):
unit_cost=Decimal(99)) unit_cost=Decimal(99))
self.assertTrue(mock_log_error.called) self.assertTrue(mock_log_error.called)
@httpretty.activate
@patch('ecommerce_worker.sailthru.v1.tasks.SailthruClient') @patch('ecommerce_worker.sailthru.v1.tasks.SailthruClient')
def test_get_course_content(self, mock_sailthru_client): def test_get_course_content(self, mock_sailthru_client):
""" """
...@@ -325,23 +353,66 @@ class SailthruTests(TestCase): ...@@ -325,23 +353,66 @@ class SailthruTests(TestCase):
""" """
config = {'SAILTHRU_CACHE_TTL_SECONDS': 100} config = {'SAILTHRU_CACHE_TTL_SECONDS': 100}
mock_sailthru_client.api_get.return_value = MockSailthruResponse({"title": "The title"}) mock_sailthru_client.api_get.return_value = MockSailthruResponse({"title": "The title"})
response_json = _get_course_content('course:123', mock_sailthru_client, None, config) response_json = _get_course_content(self.course_id, 'course:123', mock_sailthru_client, None, config)
self.assertEquals(response_json, {"title": "The title"}) self.assertEquals(response_json, {"title": "The title"})
mock_sailthru_client.api_get.assert_called_with('content', {'id': 'course:123'}) mock_sailthru_client.api_get.assert_called_with('content', {'id': 'course:123'})
# test second call uses cache # test second call uses cache
mock_sailthru_client.reset_mock() mock_sailthru_client.reset_mock()
response_json = _get_course_content('course:123', mock_sailthru_client, None, config) response_json = _get_course_content(self.course_id, 'course:123', mock_sailthru_client, None, config)
self.assertEquals(response_json, {"title": "The title"}) self.assertEquals(response_json, {"title": "The title"})
mock_sailthru_client.api_get.assert_not_called() mock_sailthru_client.api_get.assert_not_called()
# test error from Sailthru # test error from Sailthru
mock_sailthru_client.api_get.return_value = MockSailthruResponse({}, error='Got an error') mock_sailthru_client.api_get.return_value = MockSailthruResponse({}, error='Got an error')
self.assertEquals(_get_course_content('course:124', mock_sailthru_client, None, config), {}) data = self.ecom_course_data(self.course_id)
expected_response = {
'title': 'test course',
'verification_deadline': '2016-12-01T23:59:00Z'
}
self.mock_ecommerce_api(data, self.course_id)
self.assertEquals(
_get_course_content(self.course_id, 'course:124', mock_sailthru_client, None, config), expected_response
)
# test Sailthru exception
data = self.ecom_course_data(self.course_id)
expected_response = {
'title': 'test course',
'verification_deadline': '2016-12-01T23:59:00Z'
}
mock_sailthru_client.api_get.side_effect = SailthruClientError
self.mock_ecommerce_api(data, self.course_id)
self.assertEquals(
_get_course_content(self.course_id, 'course:125', mock_sailthru_client, None, config), expected_response
)
# test exception # test Sailthru and Ecommerce exception
mock_sailthru_client.api_get.side_effect = SailthruClientError mock_sailthru_client.api_get.side_effect = SailthruClientError
self.assertEquals(_get_course_content('course:125', mock_sailthru_client, None, config), {}) self.mock_ecommerce_api({}, self.course_id2, status=500)
self.assertEquals(
_get_course_content(self.course_id2, 'course:126', mock_sailthru_client, None, config), {}
)
@httpretty.activate
def test_get_course_content_from_ecommerce(self):
"""
Test routine that fetches data from ecommerce.
"""
data = self.ecom_course_data(self.course_id)
expected_response = {
'title': 'test course',
'verification_deadline': '2016-12-01T23:59:00Z'
}
self.mock_ecommerce_api(data, self.course_id)
response_json = _get_course_content_from_ecommerce(self.course_id, None)
self.assertEquals(response_json, expected_response)
# test error getting data
self.mock_ecommerce_api({}, self.course_id2, status=500)
response_json = _get_course_content_from_ecommerce(self.course_id2, None)
self.assertEquals(response_json, {})
@patch('ecommerce_worker.sailthru.v1.tasks.SailthruClient') @patch('ecommerce_worker.sailthru.v1.tasks.SailthruClient')
def test_update_unenrolled_list_new(self, mock_sailthru_client): def test_update_unenrolled_list_new(self, mock_sailthru_client):
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
import os import os
import sys import sys
from edx_rest_api_client.client import EdxRestApiClient
from ecommerce_worker.configuration import CONFIGURATION_MODULE from ecommerce_worker.configuration import CONFIGURATION_MODULE
...@@ -42,3 +44,17 @@ def get_configuration(variable, site_code=None): ...@@ -42,3 +44,17 @@ def get_configuration(variable, site_code=None):
if setting_value is None: if setting_value is None:
raise RuntimeError('Worker is improperly configured: {} is unset in {}.'.format(variable, module)) raise RuntimeError('Worker is improperly configured: {} is unset in {}.'.format(variable, module))
return setting_value return setting_value
def get_ecommerce_client(site_code=None):
"""
Get client for fetching data from ecommerce API
Returns:
EdxRestApiClient object
"""
ecommerce_api_root = get_configuration('ECOMMERCE_API_ROOT', site_code=site_code)
signing_key = get_configuration('JWT_SECRET_KEY', site_code=site_code)
issuer = get_configuration('JWT_ISSUER', site_code=site_code)
service_username = get_configuration('ECOMMERCE_SERVICE_USERNAME', site_code=site_code)
return EdxRestApiClient(ecommerce_api_root, signing_key=signing_key, issuer=issuer, username=service_username)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment