Commit 2ea2b8f6 by Will Daly

Add JSON end-point for shoppingcart receipt information

Add enrollment confirmation page

Fix progress update

Add order info to the payment confirmation page.
parent 7699b5ff
......@@ -26,6 +26,7 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config
)
from xmodule.modulestore.tests.factories import CourseFactory
from util.date_utils import get_default_time_display
from shoppingcart.views import _can_download_report, _get_date_from_str
from shoppingcart.models import (
Order, CertificateItem, PaidCourseRegistration, CourseRegCodeItem,
......@@ -66,6 +67,7 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
@ddt.ddt
class ShoppingCartViewsTests(ModuleStoreTestCase):
def setUp(self):
patcher = patch('student.models.tracker')
......@@ -801,6 +803,103 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(context['order'], self.cart)
self.assertEqual(context['error_html'], 'ERROR_TEST!!!')
@ddt.data(0, 1)
def test_show_receipt_json(self, num_items):
# Create the correct number of items in the order
for __ in range(num_items):
CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor')
self.cart.purchase()
self.login_user()
url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id])
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.cart.currency)
self.assertEqual(json_resp.get('purchase_datetime'), get_default_time_display(self.cart.purchase_time))
self.assertEqual(json_resp.get('total_cost'), self.cart.total_cost)
self.assertEqual(json_resp.get('status'), "purchased")
self.assertEqual(json_resp.get('billed_to'), {
'first_name': self.cart.bill_to_first,
'last_name': self.cart.bill_to_last,
'street1': self.cart.bill_to_street1,
'street2': self.cart.bill_to_street2,
'city': self.cart.bill_to_city,
'state': self.cart.bill_to_state,
'postal_code': self.cart.bill_to_postalcode,
'country': self.cart.bill_to_country
})
self.assertEqual(len(json_resp.get('items')), num_items)
for item in json_resp.get('items'):
self.assertEqual(item, {
'unit_cost': 40,
'quantity': 1,
'line_cost': 40,
'line_desc': 'Honor Code Certificate for course Test Course'
})
def test_show_receipt_json_multiple_items(self):
# Two different item types
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor')
self.cart.purchase()
self.login_user()
url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id])
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('total_cost'), self.cart.total_cost)
items = json_resp.get('items')
self.assertEqual(len(items), 2)
self.assertEqual(items[0], {
'unit_cost': 40,
'quantity': 1,
'line_cost': 40,
'line_desc': 'Registration for Course: Robot Super Course'
})
self.assertEqual(items[1], {
'unit_cost': 40,
'quantity': 1,
'line_cost': 40,
'line_desc': 'Honor Code Certificate for course Test Course'
})
def test_receipt_json_refunded(self):
mock_enrollment = Mock()
mock_enrollment.refundable.side_effect = lambda: True
mock_enrollment.course_id = self.verified_course_key
mock_enrollment.user = self.user
CourseMode.objects.create(
course_id=self.verified_course_key,
mode_slug="verified",
mode_display_name="verified cert",
min_price=self.cost
)
cert = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'verified')
self.cart.purchase()
cert.refund_cert_callback(course_enrollment=mock_enrollment)
self.login_user()
url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id])
resp = self.client.get(url, HTTP_ACCEPT="application/json")
self.assertEqual(resp.status_code, 200)
json_resp = json.loads(resp.content)
self.assertEqual(json_resp.get('status'), 'refunded')
def test_show_receipt_404s(self):
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor')
......
......@@ -14,6 +14,7 @@ from django.views.decorators.http import require_POST, require_http_methods
from django.core.urlresolvers import reverse
from django.views.decorators.csrf import csrf_exempt
from util.bad_request_rate_limiter import BadRequestRateLimiter
from util.date_utils import get_default_time_display
from django.contrib.auth.decorators import login_required
from microsite_configuration import microsite
from edxmako.shortcuts import render_to_response
......@@ -655,15 +656,76 @@ def show_receipt(request, ordernum):
Displays a receipt for a particular order.
404 if order is not yet purchased or request.user != order.user
"""
try:
order = Order.objects.get(id=ordernum)
except Order.DoesNotExist:
raise Http404('Order not found!')
if order.user != request.user or order.status != 'purchased':
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', ""):
return _show_receipt_json(order)
else:
return _show_receipt_html(request, order)
def _show_receipt_json(order):
"""Render the receipt page as JSON.
The included information is deliberately minimal:
as much as possible, the included information should
be common to *all* order items, so the client doesn't
need to handle different item types differently.
Arguments:
request (HttpRequest): The request for the receipt.
order (Order): The order model to display.
Returns:
HttpResponse
"""
order_info = {
'orderNum': order.id,
'currency': order.currency,
'status': order.status,
'purchase_datetime': get_default_time_display(order.purchase_time) if order.purchase_time else None,
'billed_to': {
'first_name': order.bill_to_first,
'last_name': order.bill_to_last,
'street1': order.bill_to_street1,
'street2': order.bill_to_street2,
'city': order.bill_to_city,
'state': order.bill_to_state,
'postal_code': order.bill_to_postalcode,
'country': order.bill_to_country,
},
'total_cost': order.total_cost,
'items': [
{
'quantity': item.qty,
'unit_cost': item.unit_cost,
'line_cost': item.line_cost,
'line_desc': item.line_desc
}
for item in OrderItem.objects.filter(order=order).select_subclasses()
]
}
return JsonResponse(order_info)
def _show_receipt_html(request, order):
"""Render the receipt page as HTML.
Arguments:
request (HttpRequest): The request for the receipt.
order (Order): The order model to display.
Returns:
HttpResponse
"""
order_items = OrderItem.objects.filter(order=order).select_subclasses()
shoppingcart_items = []
course_names_list = []
......
......@@ -1010,6 +1010,7 @@ courseware_js = (
base_vendor_js = [
'js/vendor/jquery.min.js',
'js/vendor/jquery.cookie.js',
'js/vendor/url.min.js',
'js/vendor/underscore-min.js',
'js/vendor/require.js',
'js/RequireJS-namespace-undefine.js',
......
......@@ -56,6 +56,11 @@ var edx = edx || {};
'review-photos-step': {
fullName: el.data('full-name'),
platformName: el.data('platform-name')
},
'enrollment-confirmation-step': {
courseName: el.data('course-name'),
courseStartDate: el.data('course-start-date'),
coursewareUrl: el.data('courseware-url')
}
}
}).render();
......
......@@ -30,18 +30,17 @@ var edx = edx || {};
this.errorModel = obj.errorModel || {};
this.displaySteps = obj.displaySteps || [];
// Determine which step we're starting on
// Depending on how the user enters the flow,
// this could be anywhere in the sequence of steps.
this.currentStepIndex = _.indexOf(
_.pluck( this.displaySteps, 'name' ),
obj.currentStep
);
this.progressView = new edx.verify_student.ProgressView({
el: this.el,
displaySteps: this.displaySteps,
currentStepIndex: this.currentStepIndex
// Determine which step we're starting on
// Depending on how the user enters the flow,
// this could be anywhere in the sequence of steps.
currentStepIndex: _.indexOf(
_.pluck( this.displaySteps, 'name' ),
obj.currentStep
)
});
this.initializeStepViews( obj.stepInfo );
......@@ -140,29 +139,21 @@ var edx = edx || {};
// underscore template.
// When the view is rendered, it will overwrite the existing
// step in the DOM.
stepName = this.displaySteps[ this.currentStepIndex ].name;
stepName = this.displaySteps[ this.progressView.currentStepIndex ].name;
stepView = this.subviews[ stepName ];
stepView.el = stepEl;
stepView.render();
},
nextStep: function() {
this.currentStepIndex = Math.min( this.currentStepIndex + 1, this.displaySteps.length - 1 );
this.progressView.nextStep();
this.render();
},
goToStep: function( stepName ) {
var stepIndex = _.indexOf(
_.pluck( this.displaySteps, 'name' ),
stepName
);
if ( stepIndex >= 0 ) {
this.currentStepIndex = stepIndex;
this.render();
}
},
this.progressView.goToStep( stepName );
this.render();
}
});
})(jQuery, _, Backbone, gettext);
......@@ -3,16 +3,133 @@
*/
var edx = edx || {};
(function( $ ) {
(function( $, _, gettext ) {
'use strict';
edx.verify_student = edx.verify_student || {};
// The "Verify Later" button goes directly to the dashboard,
// The "Verify Now" button reloads this page with the "skip-first-step"
// flag set. This allows the user to navigate back to the confirmation
// if he/she wants to.
// For this reason, we don't need any custom click handlers here.
edx.verify_student.PaymentConfirmationStepView = edx.verify_student.StepView.extend({});
edx.verify_student.PaymentConfirmationStepView = edx.verify_student.StepView.extend({
})( jQuery );
/**
* Retrieve receipt information from the shopping cart.
*
* We make this request from JavaScript to encapsulate
* the verification Django app from the shopping cart app.
*
* This method checks the query string param
* ?payment-order-num, which can be set by the shopping cart
* before redirecting to the payment confirmation page.
* This step then reads the param and requests receipt information
* from the shopping cart. At no point does the "verify student"
* Django app interact directly with the shopping cart.
*
* @param {object} templateContext The original template context.
* @return {object} A JQuery promise that resolves with the modified
* template context.
*/
updateContext: function( templateContext ) {
var view = this;
return $.Deferred(
function( defer ) {
var paymentOrderNum = $.url( '?payment-order-num' );
if ( paymentOrderNum ) {
// If there is a payment order number, try to retrieve
// the receipt information from the shopping cart.
view.getReceiptData( paymentOrderNum ).done(
function( data ) {
// Add the receipt info to the template context
_.extend( templateContext, { receipt: this.receiptContext( data ) } );
defer.resolveWith( view, [ templateContext ]);
}
).fail(function() {
// Display an error
// This can occur if the user does not have access to the receipt
// or the order number is invalid.
defer.rejectWith(
this,
[
gettext( "Error" ),
gettext( "Could not retrieve payment information" )
]
);
});
} else {
// If no payment order is provided, return the original context
// The template is responsible for displaying a default state.
_.extend( templateContext, { receipt: null } );
defer.resolveWith( view, [ templateContext ]);
}
}
).promise();
},
/**
* The "Verify Later" button goes directly to the dashboard,
* The "Verify Now" button reloads this page with the "skip-first-step"
* flag set. This allows the user to navigate back to the confirmation
* if he/she wants to.
* For this reason, we don't need any custom click handlers here.
*/
postRender: function() {},
/**
* Retrieve receipt data from the shoppingcart.
* @param {int} paymentOrderNum The order number of the payment.
* @return {object} JQuery Promise.
*/
getReceiptData: function( paymentOrderNum ) {
return $.ajax({
url: _.sprintf( '/shoppingcart/receipt/%s/', paymentOrderNum ),
type: 'GET',
dataType: 'json',
context: this
});
},
/**
* Construct the template context from data received
* from the shopping cart receipt.
*
* @param {object} data Receipt data received from the server
* @return {object} Receipt template context.
*/
receiptContext: function( data ) {
var view = this,
receiptContext;
receiptContext = {
orderNum: data.orderNum,
currency: data.currency,
purchasedDatetime: data.purchase_datetime,
totalCost: view.formatMoney( data.total_cost ),
isRefunded: data.status === "refunded",
billedTo: {
firstName: data.billed_to.first_name,
lastName: data.billed_to.last_name,
city: data.billed_to.city,
state: data.billed_to.state,
postalCode: data.billed_to.postal_code,
country: data.billed_to.country
},
items: []
};
receiptContext.items = _.map(
data.items,
function( item ) {
return {
lineDescription: item.line_desc,
cost: view.formatMoney( item.line_cost )
};
}
);
return receiptContext;
},
formatMoney: function( moneyStr ) {
return Number( moneyStr ).toFixed(2);
}
});
})( jQuery, _, gettext );
......@@ -18,6 +18,24 @@
this.currentStepIndex = obj.currentStepIndex || 0;
},
nextStep: function() {
this.currentStepIndex = Math.min(
this.currentStepIndex + 1,
this.displaySteps.length - 1
);
},
goToStep: function( stepName ) {
var stepIndex = _.indexOf(
_.pluck( this.displaySteps, 'name' ),
stepName
);
if ( stepIndex >= 0 ) {
this.currentStepIndex = stepIndex;
}
},
render: function() {
var renderedHtml, context;
......
......@@ -64,10 +64,10 @@
this.postRender();
},
handleError: function() {
handleError: function( errorTitle, errorMsg ) {
this.errorModel.set({
errorTitle: gettext( "Error" ),
errorMsg: gettext( "An unexpected error occurred. Please reload the page to try again." ),
errorTitle: errorTitle || gettext( "Error" ),
errorMsg: errorMsg || gettext( "An unexpected error occurred. Please reload the page to try again." ),
shown: true
});
},
......
<p>Enrollment confirmation!</p>
<div class="wrapper-content-main">
<article class="content-main">
<h3 class="title"><%- gettext( "Congratulations! You are now enrolled in the verified track." ) %></h3>
<div class="instruction">
<p><%- gettext( "You are now enrolled as a verified student! Your enrollment details are below.") %></p>
</div>
<ul class="list-info">
<li class="info-item course-info">
<h4 class="title">
<%- gettext( "You are enrolled in " ) %> :
</h4>
<div class="wrapper-report">
<table class="report report-course">
<caption class="sr"><%- gettext( "A list of courses you have just enrolled in as a verified student" ) %></caption>
<thead>
<tr>
<th scope="col" ><%- gettext( "Course" ) %></th>
<th scope="col" ><%- gettext( "Status" ) %></th>
<th scope="col" ><span class="sr"><%- gettext( "Options" ) %></span></th>
</tr>
</thead>
<tbody>
<tr>
<td><%- courseName %></td>
<td>
<%- _.sprintf( gettext( "Starts: %(start)s" ), { start: courseStartDate } ) %>
</td>
<td class="options">
<% if ( coursewareUrl ) { %>
<a class="action action-course" href="<%- coursewareUrl %>"><%- gettext( "Go to Course" ) %></a>
<% } %>
</td>
</tr>
</tbody>
<tfoot>
<tr class="course-actions">
<td colspan="3">
<a class="action action-dashboard" href="/dashboard"><%- gettext("Go to your dashboard") %></a>
</td>
</tr>
</tfoot>
</table>
</div>
</li>
</ul>
</article>
</div>
......@@ -5,47 +5,80 @@
<p><%- gettext( "You are now enrolled as a verified student! Your enrollment details are below.") %></p>
</div>
<% if ( receipt ) { %>
<ul class="list-info">
<li class="info-item course-info">
<h4 class="title">
<%- gettext( "You are enrolled in " ) %> :
</h4>
<li class="info-item payment-info">
<h4 class="title"><%- gettext( "Payment Details" ) %></h4>
<div class="copy">
<p><%- gettext( "Please print this page for your records; it serves as your receipt. You will also receive an email with the same information." ) %></p>
</div>
<div class="wrapper-report">
<table class="report report-course">
<caption class="sr"><%- gettext( "A list of courses you have just enrolled in as a verified student" ) %></caption>
<table class="report report-receipt">
<thead>
<tr>
<th scope="col" ><%- gettext( "Course" ) %></th>
<th scope="col" ><%- gettext( "Status" ) %></th>
<th scope="col" ><span class="sr"><%- gettext( "Options" ) %></span></th>
<th scope="col" ><%- gettext( "Order No." ) %></th>
<th scope="col" ><%- gettext( "Description" ) %></th>
<th scope="col" ><%- gettext( "Date" ) %></th>
<th scope="col" ><%- gettext( "Description" ) %></th>
</tr>
</thead>
<tbody>
<tr>
<td><%- courseName %></td>
<td>
<%- _.sprintf( gettext( "Starts: %(start)s" ), { start: courseStartDate } ) %>
</td>
<td class="options">
<% if ( coursewareUrl ) { %>
<a class="action action-course" href="<%- coursewareUrl %>"><%- gettext( "Go to Course" ) %></a>
<% for ( var i = 0; i < receipt.items.length; i++ ) { %>
<% if ( receipt.isRefunded ) { %>
<td><del><%- receipt.orderNum %></del></td>
<td><del><%- receipt.items[i].lineDescription %></del></td>
<td><del><%- receipt.purchasedDatetime %></del></td>
<td><del><%- receipt.items[i].cost %> ($<%- receipt.currency.toUpperCase() %>)</del></td>
<% } else { %>
<tr>
<td><%- receipt.orderNum %></td>
<td><%- receipt.items[i].lineDescription %></td>
<td><%- receipt.purchasedDatetime %></td>
<td><%- receipt.items[i].cost %> ($<%- receipt.currency.toUpperCase() %>)</td>
</tr>
<% } %>
</td>
</tr>
<% } %>
</tbody>
<tfoot>
<tr class="course-actions">
<td colspan="3">
<a class="action action-dashboard" href="/dashboard"><%- gettext("Go to your dashboard") %></a>
<tr>
<th scope="row" class="total-label" colspan="1"><%- gettext( "Total" ) %></th>
<td claass="total-value" colspan="3">
<span class="value-amount"><%- receipt.totalCost %></span>
<span class="value-currency">(<%- receipt.currency.toUpperCase() %>)</span>
</td>
</tr>
</tfoot>
</table>
<% if ( receipt.isRefunded ) { %>
<div class="msg msg-refunds">
<h4 class="title sr"><%- gettext( "Please Note" ) %>: </h4>
<div class="copy">
<p><%- gettext( "Items with strikethough have been refunded." ) %></p>
</div>
</div>
<% } %>
</div>
<div class="copy">
<p><%- gettext( "Billed To" ) %>:
<span class="name-first"><%- receipt.billedTo.firstName %></span>
<span class="name-last"><%- receipt.billedTo.lastName %></span>
(<span class="address-city"><%- receipt.billedTo.city %></span>,
<span class="address-state"><%- receipt.billedTo.state %></span>
<span class="address-postalcode"><%- receipt.billedTo.postalCode %></span>
<span class="address-country"><%- receipt.billedTo.country.toUpperCase() %></span>)
</p>
</div>
</li>
</ul>
<% } else { %>
<p><%- gettext( "No receipt available." ) %></p>
<% } %>
<% if ( nextStepTitle ) { %>
<nav class="nav-wizard is-ready">
......
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