Commit 16692741 by Stephen Sanchez

:Allow the receipt page to support Oscar Orders.

parent 2c080af6
......@@ -42,6 +42,26 @@ class EcommerceAPI(object):
}
return jwt.encode(data, self.key)
def get_order(self, user, order_number):
"""
Retrieve a paid order.
Arguments
user -- User associated with the requested order.
order_number -- The unique identifier for the order.
Returns a tuple with the order number, order status, API response data.
"""
def get():
"""Internal service call to retrieve an order. """
headers = {
'Content-Type': 'application/json',
'Authorization': 'JWT {}'.format(self._get_jwt(user))
}
url = '{base_url}/orders/{order_number}/'.format(base_url=self.url, order_number=order_number)
return requests.get(url, headers=headers, timeout=self.timeout)
return self._call_ecommerce_service(get)
def create_order(self, user, sku):
"""
Create a new order.
......@@ -52,21 +72,35 @@ class EcommerceAPI(object):
Returns a tuple with the order number, order status, API response data.
"""
headers = {
'Content-Type': 'application/json',
'Authorization': 'JWT {}'.format(self._get_jwt(user))
}
def create():
"""Internal service call to create an order. """
headers = {
'Content-Type': 'application/json',
'Authorization': 'JWT {}'.format(self._get_jwt(user))
}
url = '{}/orders/'.format(self.url)
return requests.post(url, data=json.dumps({'sku': sku}), headers=headers, timeout=self.timeout)
return self._call_ecommerce_service(create)
@staticmethod
def _call_ecommerce_service(call):
"""
Makes a call to the E-Commerce Service. There are a number of common errors that could occur across any
request to the E-Commerce Service that this helper method can wrap each call with. This method helps ensure
calls to the E-Commerce Service will conform to the same output.
url = '{}/orders/'.format(self.url)
Arguments
call -- A callable function that makes a request to the E-Commerce Service.
Returns a tuple with the order number, order status, API response data.
"""
try:
response = requests.post(url, data=json.dumps({'sku': sku}), headers=headers, timeout=self.timeout)
response = call()
data = response.json()
except Timeout:
msg = 'E-Commerce API request timed out.'
log.error(msg)
raise TimeoutError(msg)
except ValueError:
msg = 'E-Commerce API response is not valid JSON.'
log.exception(msg)
......
......@@ -2,6 +2,8 @@
Tests for Shopping Cart views
"""
from collections import OrderedDict
import copy
import mock
import pytz
from urlparse import urlparse
from decimal import Decimal
......@@ -27,6 +29,9 @@ import ddt
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from commerce.api import EcommerceAPI
from commerce.constants import OrderStatus
from commerce.tests import EcommerceApiTestMixin
from student.roles import CourseSalesAdminRole
from util.date_utils import get_default_time_display
from util.testing import UrlResetMixin
......@@ -866,6 +871,106 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
'line_desc': 'Honor Code Certificate for course Test Course'
})
@ddt.data(0, 1, 2)
@override_settings(
ECOMMERCE_API_URL=EcommerceApiTestMixin.ECOMMERCE_API_URL,
ECOMMERCE_API_SIGNING_KEY=EcommerceApiTestMixin.ECOMMERCE_API_SIGNING_KEY
)
def test_show_ecom_receipt_json(self, num_items):
# set up the get request to return an order with X number of line items.
# Log in the student. Use a false order ID for the E-Commerce Application.
self.login_user()
url = reverse('shoppingcart.views.show_receipt', args=['EDX-100042'])
with self.mock_get_order(num_items=num_items):
resp = self.client.get(url, HTTP_ACCEPT="application/json")
# Should have gotten a successful response
self.assertEqual(resp.status_code, 200)
# Parse the response as JSON and check the contents
json_resp = json.loads(resp.content)
self.assertEqual(json_resp.get('currency'), self.mock_get_order.ORDER['currency'])
self.assertEqual(json_resp.get('purchase_datetime'), 'Apr 07, 2015 at 17:59 UTC')
self.assertEqual(json_resp.get('total_cost'), self.mock_get_order.ORDER['total_excl_tax'])
self.assertEqual(json_resp.get('status'), self.mock_get_order.ORDER['status'])
self.assertEqual(json_resp.get('billed_to'), {
'first_name': self.mock_get_order.ORDER['billing_address']['first_name'],
'last_name': self.mock_get_order.ORDER['billing_address']['last_name'],
'street1': self.mock_get_order.ORDER['billing_address']['line1'],
'street2': self.mock_get_order.ORDER['billing_address']['line2'],
'city': self.mock_get_order.ORDER['billing_address']['line4'],
'state': self.mock_get_order.ORDER['billing_address']['state'],
'postal_code': self.mock_get_order.ORDER['billing_address']['postcode'],
'country': self.mock_get_order.ORDER['billing_address']['country']['display_name']
})
self.assertEqual(len(json_resp.get('items')), num_items)
for item in json_resp.get('items'):
self.assertEqual(item, {
'unit_cost': self.mock_get_order.LINE['unit_price_excl_tax'],
'quantity': self.mock_get_order.LINE['quantity'],
'line_cost': self.mock_get_order.LINE['line_price_excl_tax'],
'line_desc': self.mock_get_order.LINE['description']
})
class mock_get_order(object): # pylint: disable=invalid-name
"""Mocks calls to EcommerceAPI.get_order. """
patch = None
ORDER = copy.deepcopy(EcommerceApiTestMixin.ECOMMERCE_API_SUCCESSFUL_BODY)
ORDER['total_excl_tax'] = 40.0
ORDER['currency'] = 'USD'
ORDER['sources'] = [{'transactions': [
{'date_created': '2015-04-07 17:59:06.274587+00:00'},
{'date_created': '2015-04-08 13:33:06.150000+00:00'},
{'date_created': '2015-04-09 10:45:06.200000+00:00'},
]}]
ORDER['billing_address'] = {
'first_name': 'Philip',
'last_name': 'Fry',
'line1': 'Robot Arms Apts',
'line2': '22 Robot Street',
'line4': 'New New York',
'state': 'NY',
'postcode': '11201',
'country': {
'display_name': 'United States',
},
}
LINE = {
"title": "Honor Code Certificate for course Test Course",
"description": "Honor Code Certificate for course Test Course",
"status": "Paid",
"line_price_excl_tax": 40.0,
"quantity": 1,
"unit_price_excl_tax": 40.0
}
def __init__(self, **kwargs):
result = copy.deepcopy(self.ORDER)
result['lines'] = [copy.deepcopy(self.LINE) for _ in xrange(kwargs['num_items'])]
default_kwargs = {
'return_value': (
EcommerceApiTestMixin.ORDER_NUMBER,
OrderStatus.COMPLETE,
result,
)
}
default_kwargs.update(kwargs)
self.patch = mock.patch.object(EcommerceAPI, 'get_order', mock.Mock(**default_kwargs))
def __enter__(self):
self.patch.start()
return self.patch.new
def __exit__(self, exc_type, exc_val, exc_tb): # pylint: disable=unused-argument
self.patch.stop()
def test_show_receipt_json_multiple_items(self):
# Two different item types
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
......
......@@ -5,7 +5,7 @@ urlpatterns = patterns(
'shoppingcart.views',
url(r'^postpay_callback/$', 'postpay_callback'), # Both the ~accept and ~reject callback pages are handled here
url(r'^receipt/(?P<ordernum>[0-9]*)/$', 'show_receipt'),
url(r'^receipt/(?P<ordernum>[-\w]+)/$', 'show_receipt'),
url(r'^donation/$', 'donate', name='donation'),
url(r'^csv_report/$', 'csv_report', name='payment_csv_report'),
# These following URLs are only valid if the ENABLE_SHOPPING_CART feature flag is set
......
import logging
import datetime
import decimal
import dateutil
import pytz
from ipware.ip import get_ip
from django.db.models import Q
......@@ -12,6 +13,9 @@ from django.http import (
HttpResponseBadRequest, HttpResponseForbidden, Http404
)
from django.utils.translation import ugettext as _
from commerce.api import EcommerceAPI
from commerce.exceptions import InvalidConfigurationError, ApiError
from commerce.http import InternalRequestErrorResponse
from course_modes.models import CourseMode
from util.json_request import JsonResponse
from django.views.decorators.http import require_POST, require_http_methods
......@@ -820,20 +824,91 @@ def show_receipt(request, ordernum):
Displays a receipt for a particular order.
404 if order is not yet purchased or request.user != order.user
"""
is_json_request = 'application/json' in request.META.get('HTTP_ACCEPT', "")
try:
order = Order.objects.get(id=ordernum)
except Order.DoesNotExist:
raise Http404('Order not found!')
except (Order.DoesNotExist, ValueError):
if is_json_request:
return _get_external_order(request, ordernum)
else:
raise Http404('Order not found!')
if order.user != request.user or order.status not in ['purchased', 'refunded']:
raise Http404('Order not found!')
if 'application/json' in request.META.get('HTTP_ACCEPT', ""):
if is_json_request:
return _show_receipt_json(order)
else:
return _show_receipt_html(request, order)
def _get_external_order(request, order_number):
"""Get the order context from the external E-Commerce Service.
Get information about an order. This function makes a request to the E-Commerce Service to see if there is
order information that can be used to render a receipt for the user.
Args:
request (Request): The request for the the receipt.
order_number (str) : The order number.
Returns:
dict: A serializable dictionary of the receipt page context based on an order returned from the E-Commerce
Service.
"""
try:
api = EcommerceAPI()
order_number, order_status, order_data = api.get_order(request.user, order_number)
billing = order_data.get('billing_address', {})
country = billing.get('country', {})
# In order to get the date this order was paid, we need to check for payment sources, and associated
# transactions.
payment_dates = []
for source in order_data.get('sources', []):
for transaction in source.get('transactions', []):
payment_dates.append(dateutil.parser.parse(transaction['date_created']))
payment_date = sorted(payment_dates, reverse=True).pop()
order_info = {
'orderNum': order_number,
'currency': order_data['currency'],
'status': order_status,
'purchase_datetime': get_default_time_display(payment_date),
'total_cost': order_data['total_excl_tax'],
'billed_to': {
'first_name': billing.get('first_name', ''),
'last_name': billing.get('last_name', ''),
'street1': billing.get('line1', ''),
'street2': billing.get('line2', ''),
'city': billing.get('line4', ''), # 'line4' is the City, from the E-Commerce Service
'state': billing.get('state', ''),
'postal_code': billing.get('postcode', ''),
'country': country.get('display_name', ''),
},
'items': [
{
'quantity': item['quantity'],
'unit_cost': item['unit_price_excl_tax'],
'line_cost': item['line_price_excl_tax'],
'line_desc': item['description']
}
for item in order_data['lines']
]
}
return JsonResponse(order_info)
except InvalidConfigurationError:
msg = u"E-Commerce API not setup. Cannot request Order [{order_number}] for User [{user_id}] ".format(
user_id=request.user.id, order_number=order_number
)
log.debug(msg)
return JsonResponse(status=500, object={'error_message': msg})
except ApiError as err:
# The API will handle logging of the error.
return InternalRequestErrorResponse(err.message)
def _show_receipt_json(order):
"""Render the receipt page as JSON.
......
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