Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
efde11d5
Commit
efde11d5
authored
Apr 21, 2015
by
jsa
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use ecommerce api v2.
XCOM-213 XCOM-214
parent
960ec06f
Show whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
498 additions
and
312 deletions
+498
-312
common/static/js/spec_helpers/ajax_helpers.js
+17
-2
common/test/acceptance/pages/lms/pay_and_verify.py
+1
-1
lms/djangoapps/commerce/api.py
+43
-12
lms/djangoapps/commerce/constants.py
+0
-5
lms/djangoapps/commerce/tests/__init__.py
+31
-23
lms/djangoapps/commerce/tests/test_api.py
+19
-15
lms/djangoapps/commerce/tests/test_views.py
+46
-22
lms/djangoapps/commerce/urls.py
+4
-2
lms/djangoapps/commerce/views.py
+29
-26
lms/djangoapps/shoppingcart/tests/test_views.py
+9
-6
lms/djangoapps/verify_student/tests/test_integration.py
+1
-1
lms/djangoapps/verify_student/tests/test_views.py
+114
-118
lms/djangoapps/verify_student/views.py
+64
-35
lms/static/js/spec/photocapture_spec.js
+3
-3
lms/static/js/spec/student_account/enrollment_spec.js
+1
-1
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
+18
-15
lms/static/js/student_account/enrollment.js
+2
-2
lms/static/js/verify_student/pay_and_verify.js
+1
-1
lms/static/js/verify_student/photocapture.js
+4
-4
lms/static/js/verify_student/views/make_payment_step_view.js
+62
-13
lms/static/sass/_developer.scss
+19
-0
lms/templates/verify_student/make_payment_step.underscore
+9
-4
lms/templates/verify_student/pay_and_verify.html
+1
-1
No files found.
common/static/js/spec_helpers/ajax_helpers.js
View file @
efde11d5
define
([
'sinon'
,
'underscore'
],
function
(
sinon
,
_
)
{
var
fakeServer
,
fakeRequests
,
expectRequest
,
expectJsonRequest
,
respondWithJson
,
respondWithError
,
respondWithTextError
,
respon
se
WithNoContent
;
var
fakeServer
,
fakeRequests
,
expectRequest
,
expectJsonRequest
,
expectPostRequest
,
respondWithJson
,
respondWithError
,
respondWithTextError
,
respon
d
WithNoContent
;
/* These utility methods are used by Jasmine tests to create a mock server or
* get reference to mock requests. In either case, the cleanup (restore) is done with
...
...
@@ -68,6 +68,20 @@ define(['sinon', 'underscore'], function(sinon, _) {
expect
(
JSON
.
parse
(
request
.
requestBody
)).
toEqual
(
jsonRequest
);
};
/**
* Intended for use with POST requests using application/x-www-form-urlencoded.
*/
expectPostRequest
=
function
(
requests
,
url
,
body
,
requestIndex
)
{
var
request
;
if
(
_
.
isUndefined
(
requestIndex
))
{
requestIndex
=
requests
.
length
-
1
;
}
request
=
requests
[
requestIndex
];
expect
(
request
.
url
).
toEqual
(
url
);
expect
(
request
.
method
).
toEqual
(
"POST"
);
expect
(
_
.
difference
(
request
.
requestBody
.
split
(
'&'
),
body
.
split
(
'&'
))).
toEqual
([]);
};
respondWithJson
=
function
(
requests
,
jsonResponse
,
requestIndex
)
{
if
(
_
.
isUndefined
(
requestIndex
))
{
requestIndex
=
requests
.
length
-
1
;
...
...
@@ -122,6 +136,7 @@ define(['sinon', 'underscore'], function(sinon, _) {
'requests'
:
fakeRequests
,
'expectRequest'
:
expectRequest
,
'expectJsonRequest'
:
expectJsonRequest
,
'expectPostRequest'
:
expectPostRequest
,
'respondWithJson'
:
respondWithJson
,
'respondWithError'
:
respondWithError
,
'respondWithTextError'
:
respondWithTextError
,
...
...
common/test/acceptance/pages/lms/pay_and_verify.py
View file @
efde11d5
...
...
@@ -76,7 +76,7 @@ class PaymentAndVerificationFlow(PageObject):
def
proceed_to_payment
(
self
):
"""Interact with the payment button."""
self
.
q
(
css
=
"
#pay_
button"
)
.
click
()
self
.
q
(
css
=
"
.payment-
button"
)
.
click
()
FakePaymentPage
(
self
.
browser
,
self
.
_course_id
)
.
wait_for_page
()
...
...
lms/djangoapps/commerce/api.py
View file @
efde11d5
...
...
@@ -60,26 +60,57 @@ class EcommerceAPI(object):
}
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
):
data
=
self
.
_call_ecommerce_service
(
get
)
return
data
[
'number'
],
data
[
'status'
],
data
def
get_processors
(
self
,
user
):
"""
Create a new order
.
Retrieve the list of available payment processors
.
Arguments
user -- User for which the order should be created.
sku -- SKU of the course seat being ordered.
Returns a list of strings.
"""
def
get
():
"""Internal service call to retrieve the processor list. """
headers
=
{
'Content-Type'
:
'application/json'
,
'Authorization'
:
'JWT {}'
.
format
(
self
.
_get_jwt
(
user
))
}
url
=
'{base_url}/payment/processors/'
.
format
(
base_url
=
self
.
url
)
return
requests
.
get
(
url
,
headers
=
headers
,
timeout
=
self
.
timeout
)
Returns a tuple with the order number, order status, API response data.
return
self
.
_call_ecommerce_service
(
get
)
def
create_basket
(
self
,
user
,
sku
,
payment_processor
=
None
):
"""Create a new basket and immediately trigger checkout.
Note that while the API supports deferring checkout to a separate step,
as well as adding multiple products to the basket, this client does not
currently need that capability, so that case is not supported.
Args:
user: the django.auth.User for which the basket should be created.
sku: a string containing the SKU of the course seat being ordered.
payment_processor: (optional) the name of the payment processor to
use for checkout.
Returns:
A dictionary containing {id, order, payment_data}.
Raises:
TimeoutError: the request to the API server timed out.
InvalidResponseError: the API server response was not understood.
"""
def
create
():
"""Internal service call to create a
n order
. """
"""Internal service call to create a
basket
. """
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
)
url
=
'{}/baskets/'
.
format
(
self
.
url
)
data
=
{
'products'
:
[{
'sku'
:
sku
}],
'checkout'
:
True
,
'payment_processor_name'
:
payment_processor
}
return
requests
.
post
(
url
,
data
=
json
.
dumps
(
data
),
headers
=
headers
,
timeout
=
self
.
timeout
)
return
self
.
_call_ecommerce_service
(
create
)
@staticmethod
...
...
@@ -92,7 +123,7 @@ class EcommerceAPI(object):
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.
Returns a
dict of JSON-decoded
API response data.
"""
try
:
response
=
call
()
...
...
@@ -109,7 +140,7 @@ class EcommerceAPI(object):
status_code
=
response
.
status_code
if
status_code
==
HTTP_200_OK
:
return
data
[
'number'
],
data
[
'status'
],
data
return
data
else
:
msg
=
u'Response from E-Commerce API was invalid: (
%(status)
d) -
%(msg)
s'
msg_kwargs
=
{
...
...
lms/djangoapps/commerce/constants.py
View file @
efde11d5
...
...
@@ -4,13 +4,8 @@
class
OrderStatus
(
object
):
"""Constants representing all known order statuses. """
OPEN
=
'Open'
ORDER_CANCELLED
=
'Order Cancelled'
BEING_PROCESSED
=
'Being Processed'
PAYMENT_CANCELLED
=
'Payment Cancelled'
PAID
=
'Paid'
FULFILLMENT_ERROR
=
'Fulfillment Error'
COMPLETE
=
'Complete'
REFUNDED
=
'Refunded'
class
Messages
(
object
):
...
...
lms/djangoapps/commerce/tests/__init__.py
View file @
efde11d5
...
...
@@ -6,7 +6,6 @@ import jwt
import
mock
from
commerce.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
class
EcommerceApiTestMixin
(
object
):
...
...
@@ -14,12 +13,19 @@ class EcommerceApiTestMixin(object):
ECOMMERCE_API_URL
=
'http://example.com/api'
ECOMMERCE_API_SIGNING_KEY
=
'edx'
BASKET_ID
=
7
ORDER_NUMBER
=
'100004'
PROCESSOR
=
'test-processor'
PAYMENT_DATA
=
{
'payment_processor_name'
:
PROCESSOR
,
'payment_form_data'
:
{},
'payment_page_url'
:
'http://example.com/pay'
,
}
ORDER_DATA
=
{
'number'
:
ORDER_NUMBER
}
ECOMMERCE_API_SUCCESSFUL_BODY
=
{
'status'
:
OrderStatus
.
COMPLETE
,
'number'
:
ORDER_NUMBER
,
'payment_processor'
:
'cybersource'
,
'payment_parameters'
:
{
'orderNumber'
:
ORDER_NUMBER
}
'id'
:
BASKET_ID
,
'order'
:
{
'number'
:
ORDER_NUMBER
},
# never both None.
'payment_data'
:
PAYMENT_DATA
,
}
ECOMMERCE_API_SUCCESSFUL_BODY_JSON
=
json
.
dumps
(
ECOMMERCE_API_SUCCESSFUL_BODY
)
# pylint: disable=invalid-name
...
...
@@ -28,14 +34,18 @@ class EcommerceApiTestMixin(object):
expected_jwt
=
jwt
.
encode
({
'username'
:
user
.
username
,
'email'
:
user
.
email
},
key
)
self
.
assertEqual
(
request
.
headers
[
'Authorization'
],
'JWT {}'
.
format
(
expected_jwt
))
def
assertValid
OrderRequest
(
self
,
request
,
user
,
jwt_signing_key
,
sku
):
def
assertValid
BasketRequest
(
self
,
request
,
user
,
jwt_signing_key
,
sku
,
processor
):
""" Verifies that an order request to the E-Commerce Service is valid. """
self
.
assertValidJWTAuthHeader
(
request
,
user
,
jwt_signing_key
)
self
.
assertEqual
(
request
.
body
,
'{{"sku": "{}"}}'
.
format
(
sku
))
expected_body_data
=
{
'products'
:
[{
'sku'
:
sku
}],
'checkout'
:
True
,
'payment_processor_name'
:
processor
}
self
.
assertEqual
(
json
.
loads
(
request
.
body
),
expected_body_data
)
self
.
assertEqual
(
request
.
headers
[
'Content-Type'
],
'application/json'
)
def
_mock_ecommerce_api
(
self
,
status
=
200
,
body
=
None
):
def
_mock_ecommerce_api
(
self
,
status
=
200
,
body
=
None
,
is_payment_required
=
False
):
"""
Mock calls to the E-Commerce API.
...
...
@@ -43,27 +53,25 @@ class EcommerceApiTestMixin(object):
"""
self
.
assertTrue
(
httpretty
.
is_enabled
(),
'Test is missing @httpretty.activate decorator.'
)
url
=
self
.
ECOMMERCE_API_URL
+
'/orders/'
body
=
body
or
self
.
ECOMMERCE_API_SUCCESSFUL_BODY_JSON
url
=
self
.
ECOMMERCE_API_URL
+
'/baskets/'
if
body
is
None
:
response_data
=
{
'id'
:
self
.
BASKET_ID
,
'payment_data'
:
None
,
'order'
:
None
}
if
is_payment_required
:
response_data
[
'payment_data'
]
=
self
.
PAYMENT_DATA
else
:
response_data
[
'order'
]
=
{
'number'
:
self
.
ORDER_NUMBER
}
body
=
json
.
dumps
(
response_data
)
httpretty
.
register_uri
(
httpretty
.
POST
,
url
,
status
=
status
,
body
=
body
)
class
mock_create_
order
(
object
):
# pylint: disable=invalid-name
""" Mocks calls to EcommerceAPI.create_
order
. """
class
mock_create_
basket
(
object
):
# pylint: disable=invalid-name
""" Mocks calls to EcommerceAPI.create_
basket
. """
patch
=
None
def
__init__
(
self
,
**
kwargs
):
default_kwargs
=
{
'return_value'
:
(
EcommerceApiTestMixin
.
ORDER_NUMBER
,
OrderStatus
.
COMPLETE
,
EcommerceApiTestMixin
.
ECOMMERCE_API_SUCCESSFUL_BODY
)
}
default_kwargs
=
{
'return_value'
:
EcommerceApiTestMixin
.
ECOMMERCE_API_SUCCESSFUL_BODY
}
default_kwargs
.
update
(
kwargs
)
self
.
patch
=
mock
.
patch
.
object
(
EcommerceAPI
,
'create_order'
,
mock
.
Mock
(
**
default_kwargs
))
self
.
patch
=
mock
.
patch
.
object
(
EcommerceAPI
,
'create_basket'
,
mock
.
Mock
(
**
default_kwargs
))
def
__enter__
(
self
):
self
.
patch
.
start
()
...
...
lms/djangoapps/commerce/tests/test_api.py
View file @
efde11d5
...
...
@@ -10,7 +10,6 @@ import httpretty
from
requests
import
Timeout
from
commerce.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
from
commerce.exceptions
import
InvalidResponseError
,
TimeoutError
,
InvalidConfigurationError
from
commerce.tests
import
EcommerceApiTestMixin
from
student.tests.factories
import
UserFactory
...
...
@@ -26,7 +25,7 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
def
setUp
(
self
):
super
(
EcommerceAPITests
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'commerce:
order
s'
)
self
.
url
=
reverse
(
'commerce:
basket
s'
)
self
.
user
=
UserFactory
()
self
.
api
=
EcommerceAPI
()
...
...
@@ -48,35 +47,40 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
self
.
assertRaises
(
InvalidConfigurationError
,
EcommerceAPI
)
@httpretty.activate
def
test_create_order
(
self
):
@data
(
True
,
False
)
def
test_create_basket
(
self
,
is_payment_required
):
""" Verify the method makes a call to the E-Commerce API with the correct headers and data. """
self
.
_mock_ecommerce_api
()
number
,
status
,
body
=
self
.
api
.
create_order
(
self
.
user
,
self
.
SKU
)
self
.
_mock_ecommerce_api
(
is_payment_required
=
is_payment_required
)
response_data
=
self
.
api
.
create_basket
(
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
# Validate the request sent to the E-Commerce API endpoint.
request
=
httpretty
.
last_request
()
self
.
assertValid
OrderRequest
(
request
,
self
.
user
,
self
.
ECOMMERCE_API_SIGNING_KEY
,
self
.
SKU
)
self
.
assertValid
BasketRequest
(
request
,
self
.
user
,
self
.
ECOMMERCE_API_SIGNING_KEY
,
self
.
SKU
,
self
.
PROCESSOR
)
# Validate the data returned by the method
self
.
assertEqual
(
number
,
self
.
ORDER_NUMBER
)
self
.
assertEqual
(
status
,
OrderStatus
.
COMPLETE
)
self
.
assertEqual
(
body
,
self
.
ECOMMERCE_API_SUCCESSFUL_BODY
)
self
.
assertEqual
(
response_data
[
'id'
],
self
.
BASKET_ID
)
if
is_payment_required
:
self
.
assertEqual
(
response_data
[
'order'
],
None
)
self
.
assertEqual
(
response_data
[
'payment_data'
],
self
.
PAYMENT_DATA
)
else
:
self
.
assertEqual
(
response_data
[
'order'
],
{
"number"
:
self
.
ORDER_NUMBER
})
self
.
assertEqual
(
response_data
[
'payment_data'
],
None
)
@httpretty.activate
@data
(
400
,
401
,
405
,
406
,
429
,
500
,
503
)
def
test_create_
order
_with_invalid_http_status
(
self
,
status
):
def
test_create_
basket
_with_invalid_http_status
(
self
,
status
):
""" If the E-Commerce API returns a non-200 status, the method should raise an InvalidResponseError. """
self
.
_mock_ecommerce_api
(
status
=
status
,
body
=
json
.
dumps
({
'user_message'
:
'FAIL!'
}))
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
order
,
self
.
user
,
self
.
SKU
)
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
basket
,
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
@httpretty.activate
def
test_create_
order
_with_invalid_json
(
self
):
def
test_create_
basket
_with_invalid_json
(
self
):
""" If the E-Commerce API returns un-parseable data, the method should raise an InvalidResponseError. """
self
.
_mock_ecommerce_api
(
body
=
'TOTALLY NOT JSON!'
)
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
order
,
self
.
user
,
self
.
SKU
)
self
.
assertRaises
(
InvalidResponseError
,
self
.
api
.
create_
basket
,
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
@httpretty.activate
def
test_create_
order
_with_timeout
(
self
):
def
test_create_
basket
_with_timeout
(
self
):
""" If the call to the E-Commerce API times out, the method should raise a TimeoutError. """
def
request_callback
(
_request
,
_uri
,
_headers
):
...
...
@@ -85,4 +89,4 @@ class EcommerceAPITests(EcommerceApiTestMixin, TestCase):
self
.
_mock_ecommerce_api
(
body
=
request_callback
)
self
.
assertRaises
(
TimeoutError
,
self
.
api
.
create_
order
,
self
.
user
,
self
.
SKU
)
self
.
assertRaises
(
TimeoutError
,
self
.
api
.
create_
basket
,
self
.
user
,
self
.
SKU
,
self
.
PROCESSOR
)
lms/djangoapps/commerce/tests/test_views.py
View file @
efde11d5
...
...
@@ -9,7 +9,7 @@ from django.test.utils import override_settings
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
commerce.constants
import
OrderStatus
,
Messages
from
commerce.constants
import
Messages
from
commerce.exceptions
import
TimeoutError
,
ApiError
from
commerce.tests
import
EcommerceApiTestMixin
from
course_modes.models
import
CourseMode
...
...
@@ -22,7 +22,7 @@ from student.tests.tests import EnrollmentEventTestMixin
@ddt
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
class
Order
sViewTests
(
EnrollmentEventTestMixin
,
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
class
Basket
sViewTests
(
EnrollmentEventTestMixin
,
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
"""
Tests for the commerce orders view.
"""
...
...
@@ -48,6 +48,11 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
actual
=
json
.
loads
(
response
.
content
)[
'detail'
]
self
.
assertEqual
(
actual
,
expected_msg
)
def
assertResponsePaymentData
(
self
,
response
):
""" Asserts correctness of a JSON body containing payment information. """
actual_response
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
actual_response
,
self
.
PAYMENT_DATA
)
def
assertValidEcommerceInternalRequestErrorResponse
(
self
,
response
):
""" Asserts the response is a valid response sent when the E-Commerce API is unavailable. """
self
.
assertEqual
(
response
.
status_code
,
500
)
...
...
@@ -60,8 +65,8 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
self
.
assert_no_events_were_emitted
()
def
setUp
(
self
):
super
(
Order
sViewTests
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'commerce:
order
s'
)
super
(
Basket
sViewTests
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'commerce:
basket
s'
)
self
.
user
=
UserFactory
()
self
.
_login
()
...
...
@@ -113,7 +118,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
"""
If the call to the E-Commerce API times out, the view should log an error and return an HTTP 503 status.
"""
with
self
.
mock_create_
order
(
side_effect
=
TimeoutError
):
with
self
.
mock_create_
basket
(
side_effect
=
TimeoutError
):
response
=
self
.
_post_to_view
()
self
.
assertValidEcommerceInternalRequestErrorResponse
(
response
)
...
...
@@ -123,22 +128,24 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
"""
If the E-Commerce API raises an error, the view should return an HTTP 503 status.
"""
with
self
.
mock_create_
order
(
side_effect
=
ApiError
):
with
self
.
mock_create_
basket
(
side_effect
=
ApiError
):
response
=
self
.
_post_to_view
()
self
.
assertValidEcommerceInternalRequestErrorResponse
(
response
)
self
.
assertUserNotEnrolled
()
def
_test_successful_ecommerce_api_call
(
self
):
def
_test_successful_ecommerce_api_call
(
self
,
is_completed
=
True
):
"""
Verifies that the view contacts the E-Commerce API with the correct data and headers.
"""
with
self
.
mock_create_order
():
response
=
self
.
_post_to_view
()
# Validate the response content
if
is_completed
:
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
self
.
ORDER_NUMBER
)
self
.
assertResponseMessage
(
response
,
msg
)
else
:
self
.
assertResponsePaymentData
(
response
)
@data
(
True
,
False
)
def
test_course_with_honor_seat_sku
(
self
,
user_is_active
):
...
...
@@ -151,26 +158,30 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
self
.
user
.
is_active
=
user_is_active
self
.
user
.
save
()
# pylint: disable=no-member
return_value
=
{
'id'
:
self
.
BASKET_ID
,
'payment_data'
:
None
,
'order'
:
{
'number'
:
self
.
ORDER_NUMBER
}}
with
self
.
mock_create_basket
(
return_value
=
return_value
):
self
.
_test_successful_ecommerce_api_call
()
def
test_order_not_complete
(
self
):
with
self
.
mock_create_order
(
return_value
=
(
self
.
ORDER_NUMBER
,
OrderStatus
.
OPEN
,
self
.
ECOMMERCE_API_SUCCESSFUL_BODY
)):
response
=
self
.
_post_to_view
()
self
.
assertEqual
(
response
.
status_code
,
202
)
msg
=
Messages
.
ORDER_INCOMPLETE_ENROLLED
.
format
(
order_number
=
self
.
ORDER_NUMBER
)
self
.
assertResponseMessage
(
response
,
msg
)
@data
(
True
,
False
)
def
test_course_with_paid_seat_sku
(
self
,
user_is_active
):
"""
If the course has a SKU, the view should return data that the client
will use to redirect the user to an external payment processor.
"""
# Set user's active flag
self
.
user
.
is_active
=
user_is_active
self
.
user
.
save
()
# pylint: disable=no-member
# TODO Eventually we should NOT be enrolling users directly from this view.
self
.
assertTrue
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
return_value
=
{
'id'
:
self
.
BASKET_ID
,
'payment_data'
:
self
.
PAYMENT_DATA
,
'order'
:
None
}
with
self
.
mock_create_basket
(
return_value
=
return_value
):
self
.
_test_successful_ecommerce_api_call
(
False
)
def
_test_course_without_sku
(
self
):
"""
Validates the view bypasses the E-Commerce API when the course has no CourseModes with SKUs.
"""
# Place an order
with
self
.
mock_create_
order
()
as
api_mock
:
with
self
.
mock_create_
basket
()
as
api_mock
:
response
=
self
.
_post_to_view
()
# Validate the response content
...
...
@@ -199,7 +210,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
"""
If the E-Commerce Service is not configured, the view should enroll the user.
"""
with
self
.
mock_create_
order
()
as
api_mock
:
with
self
.
mock_create_
basket
()
as
api_mock
:
response
=
self
.
_post_to_view
()
# Validate the response
...
...
@@ -219,7 +230,7 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
CourseModeFactory
.
create
(
course_id
=
self
.
course
.
id
,
mode_slug
=
mode
,
mode_display_name
=
mode
,
sku
=
uuid4
()
.
hex
.
decode
(
'ascii'
))
with
self
.
mock_create_
order
()
as
api_mock
:
with
self
.
mock_create_
basket
()
as
api_mock
:
response
=
self
.
_post_to_view
()
# The view should return an error status code
...
...
@@ -274,4 +285,17 @@ class OrdersViewTests(EnrollmentEventTestMixin, EcommerceApiTestMixin, ModuleSto
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
self
.
assertIsNotNone
(
get_enrollment
(
self
.
user
.
username
,
unicode
(
self
.
course
.
id
)))
self
.
_test_successful_ecommerce_api_call
()
with
self
.
mock_create_basket
():
self
.
_test_successful_ecommerce_api_call
(
False
)
class
OrdersViewTests
(
BasketsViewTests
):
"""
Ensures that /orders/ points to and behaves like /baskets/, for backward
compatibility with stale js clients during updates.
(XCOM-214) remove after release.
"""
def
setUp
(
self
):
super
(
OrdersViewTests
,
self
)
.
setUp
()
self
.
url
=
reverse
(
'commerce:orders'
)
lms/djangoapps/commerce/urls.py
View file @
efde11d5
...
...
@@ -4,10 +4,12 @@ Defines the URL routes for this app.
from
django.conf.urls
import
patterns
,
url
from
.views
import
Order
sView
,
checkout_cancel
from
.views
import
Basket
sView
,
checkout_cancel
urlpatterns
=
patterns
(
''
,
url
(
r'^
orders/$'
,
OrdersView
.
as_view
(),
name
=
"order
s"
),
url
(
r'^
baskets/$'
,
BasketsView
.
as_view
(),
name
=
"basket
s"
),
url
(
r'^checkout/cancel/$'
,
checkout_cancel
,
name
=
"checkout_cancel"
),
# (XCOM-214) For backwards compatibility with js clients during intial release
url
(
r'^orders/$'
,
BasketsView
.
as_view
(),
name
=
"orders"
),
)
lms/djangoapps/commerce/views.py
View file @
efde11d5
...
...
@@ -3,31 +3,31 @@ import logging
from
django.conf
import
settings
from
django.views.decorators.cache
import
cache_page
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.status
import
HTTP_406_NOT_ACCEPTABLE
,
HTTP_
202_ACCEPTED
,
HTTP_
409_CONFLICT
from
rest_framework.status
import
HTTP_406_NOT_ACCEPTABLE
,
HTTP_409_CONFLICT
from
rest_framework.views
import
APIView
from
commerce.api
import
EcommerceAPI
from
commerce.constants
import
OrderStatus
,
Messages
from
commerce.exceptions
import
ApiError
,
InvalidConfigurationError
from
commerce.constants
import
Messages
from
commerce.exceptions
import
ApiError
,
InvalidConfigurationError
,
InvalidResponseError
from
commerce.http
import
DetailResponse
,
InternalRequestErrorResponse
from
course_modes.models
import
CourseMode
from
courseware
import
courses
from
edxmako.shortcuts
import
render_to_response
from
enrollment.api
import
add_enrollment
from
microsite_configuration
import
microsite
from
student.models
import
CourseEnrollment
from
openedx.core.lib.api.authentication
import
SessionAuthenticationAllowInactiveUser
from
student.models
import
CourseEnrollment
from
util.json_request
import
JsonResponse
log
=
logging
.
getLogger
(
__name__
)
class
Order
sView
(
APIView
):
""" Creates a
n order
with a course seat and enrolls users. """
class
Basket
sView
(
APIView
):
""" Creates a
basket
with a course seat and enrolls users. """
# LMS utilizes User.user_is_active to indicate email verification, not whether an account is active. Sigh!
authentication_classes
=
(
SessionAuthenticationAllowInactiveUser
,)
...
...
@@ -63,7 +63,7 @@ class OrdersView(APIView):
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Attempt to create the
order
and enroll the user.
Attempt to create the
basket
and enroll the user.
"""
user
=
request
.
user
valid
,
course_key
,
error
=
self
.
_is_data_valid
(
request
)
...
...
@@ -103,28 +103,31 @@ class OrdersView(APIView):
# Make the API call
try
:
order_number
,
order_status
,
_body
=
api
.
create_order
(
user
,
honor_mode
.
sku
)
if
order_status
==
OrderStatus
.
COMPLETE
:
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
order_number
)
response_data
=
api
.
create_basket
(
user
,
honor_mode
.
sku
,
payment_processor
=
"cybersource"
,
)
payment_data
=
response_data
[
"payment_data"
]
if
payment_data
is
not
None
:
# it is time to start the payment flow.
# NOTE this branch does not appear to be used at the moment.
return
JsonResponse
(
payment_data
)
elif
response_data
[
'order'
]:
# the order was completed immediately because there was no charge.
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
response_data
[
'order'
][
'number'
])
log
.
debug
(
msg
)
return
DetailResponse
(
msg
)
else
:
#
TODO Before this functionality is fully rolled-out, this branch should be updated to NOT enroll the
#
user. Enrollments must be initiated by the E-Commerce API only
.
#
Enroll in the honor mode directly as a failsafe.
#
This MUST be removed when this code handles paid modes
.
self
.
_enroll
(
course_key
,
user
)
msg
=
u'Order
%(order_number)
s was received with
%(status)
s status. Expected
%(complete_status)
s. '
\
u'User
%(username)
s was enrolled in
%(course_id)
s by LMS.'
msg_kwargs
=
{
'order_number'
:
order_number
,
'status'
:
order_status
,
'complete_status'
:
OrderStatus
.
COMPLETE
,
'username'
:
user
.
username
,
'course_id'
:
course_id
,
}
log
.
error
(
msg
,
msg_kwargs
)
msg
=
Messages
.
ORDER_INCOMPLETE_ENROLLED
.
format
(
order_number
=
order_number
)
return
DetailResponse
(
msg
,
status
=
HTTP_202_ACCEPTED
)
msg
=
u'Unexpected response from basket endpoint.'
log
.
error
(
msg
+
u' Could not enroll user
%(username)
s in course
%(course_id)
s.'
,
{
'username'
:
user
.
id
,
'course_id'
:
course_id
},
)
raise
InvalidResponseError
(
msg
)
except
ApiError
as
err
:
# The API will handle logging of the error.
return
InternalRequestErrorResponse
(
err
.
message
)
...
...
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
efde11d5
...
...
@@ -918,15 +918,17 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
"""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'
:
[
ORDER
=
{
'status'
:
OrderStatus
.
COMPLETE
,
'number'
:
EcommerceApiTestMixin
.
ORDER_NUMBER
,
'total_excl_tax'
:
40.0
,
'currency'
:
'USD'
,
'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'
]
=
{
]}],
'billing_address'
:
{
'first_name'
:
'Philip'
,
'last_name'
:
'Fry'
,
'line1'
:
'Robot Arms Apts'
,
...
...
@@ -937,6 +939,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
'country'
:
{
'display_name'
:
'United States'
,
},
},
}
LINE
=
{
...
...
lms/djangoapps/verify_student/tests/test_integration.py
View file @
efde11d5
...
...
@@ -58,4 +58,4 @@ class TestProfEdVerification(ModuleStoreTestCase):
# On the first page of the flow, verify that there's a button allowing the user
# to proceed to the payment processor; this is the only action the user is allowed to take.
self
.
assertContains
(
resp
,
'pay
_
button'
)
self
.
assertContains
(
resp
,
'pay
ment-
button'
)
lms/djangoapps/verify_student/tests/test_views.py
View file @
efde11d5
...
...
@@ -52,6 +52,8 @@ def mock_render_to_response(*args, **kwargs):
render_mock
=
Mock
(
side_effect
=
mock_render_to_response
)
PAYMENT_DATA_KEYS
=
{
'payment_processor_name'
,
'payment_page_url'
,
'payment_form_data'
}
class
StartView
(
TestCase
):
def
start_url
(
self
,
course_id
=
""
):
...
...
@@ -849,166 +851,159 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
self
.
assertEqual
(
response_dict
[
'course_name'
],
mode_display_name
)
class
TestCreateOrder
(
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
class
CheckoutTestMixin
(
object
):
"""
Tests for the create order view.
Mixin implementing test methods that should behave identically regardless
of which backend is used (shoppingcart or ecommerce service). Subclasses
immediately follow for each backend, which inherit from TestCase and
define methods needed to customize test parameters, and patch the
appropriate checkout method.
Though the view endpoint under test is named 'create_order' for backward-
compatibility, the effect of using this endpoint is to choose a specific product
(i.e. course mode) and trigger immediate checkout.
"""
def
setUp
(
self
):
""" Create a user and course. """
super
(
TestCreateOrder
,
self
)
.
setUp
()
super
(
CheckoutTestMixin
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
(
username
=
"test"
,
password
=
"test"
)
self
.
course
=
CourseFactory
.
create
()
for
mode
,
min_price
in
((
'audit'
,
0
),
(
'honor'
,
0
),
(
'verified'
,
100
)):
# Set SKU to empty string to ensure view knows how to handle such values
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
,
min_price
=
min_price
,
sku
=
''
)
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
,
min_price
=
min_price
,
sku
=
self
.
make_sku
())
self
.
client
.
login
(
username
=
"test"
,
password
=
"test"
)
def
_post
(
self
,
data
):
"""
POST to the view being tested and return the response.
def
_assert_checked_out
(
self
,
post_params
,
patched_create_order
,
expected_course_key
,
expected_mode_slug
,
expected_status_code
=
200
):
"""
url
=
reverse
(
'verify_student_create_order'
)
return
self
.
client
.
post
(
url
,
data
)
DRY helper.
def
test_create_order_already_verified
(
self
):
# Verify the student so we don't need to submit photos
self
.
_verify_student
()
Ensures that checkout functions were invoked as
expected during execution of the create_order endpoint.
"""
post_params
.
setdefault
(
'processor'
,
None
)
response
=
self
.
client
.
post
(
reverse
(
'verify_student_create_order'
),
post_params
)
self
.
assertEqual
(
response
.
status_code
,
expected_status_code
)
if
expected_status_code
==
200
:
# ensure we called checkout at all
self
.
assertTrue
(
patched_create_order
.
called
)
# ensure checkout args were correct
args
=
self
.
_get_checkout_args
(
patched_create_order
)
self
.
assertEqual
(
args
[
'user'
],
self
.
user
)
self
.
assertEqual
(
args
[
'course_key'
],
expected_course_key
)
self
.
assertEqual
(
args
[
'course_mode'
]
.
slug
,
expected_mode_slug
)
# ensure response data was correct
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
set
(
data
.
keys
()),
PAYMENT_DATA_KEYS
)
else
:
self
.
assertFalse
(
patched_create_order
.
called
)
def
test_create_order
(
self
,
patched_create_order
):
# Create an order
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
'contribution'
:
100
,
}
response
=
self
.
_post
(
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the information will be sent to the correct callback URL
# (configured by test settings)
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'override_custom_receipt_page'
],
"http://testserver/shoppingcart/postpay_callback/"
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"verified"
)
def
test_create_order_already_verified_prof_ed
(
self
):
# Verify the student so we don't need to submit photos
self
.
_verify_student
()
self
.
_assert_checked_out
(
params
,
patched_create_order
,
self
.
course
.
id
,
'verified'
)
def
test_create_order_prof_ed
(
self
,
patched_create_order
):
# Create a prof ed course
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
# Create an order for a prof ed course
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
response
=
self
.
_post
(
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"professional"
)
def
test_create_order_for_no_id_professional
(
self
):
self
.
_assert_checked_out
(
params
,
patched_create_order
,
course
.
id
,
'professional'
)
def
test_create_order_no_id_professional
(
self
,
patched_create_order
):
# Create a no-id-professional ed course
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
# Create an order for a prof ed course
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
response
=
self
.
_post
(
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"no-id-professional"
)
def
test_create_order_for_multiple_paid_modes
(
self
):
self
.
_assert_checked_out
(
params
,
patched_create_order
,
course
.
id
,
'no-id-professional'
)
def
test_create_order_for_multiple_paid_modes
(
self
,
patched_create_order
):
# Create a no-id-professional ed course
course
=
CourseFactory
.
create
()
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
)
CourseModeFactory
(
mode_slug
=
"no-id-professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
CourseModeFactory
(
mode_slug
=
"professional"
,
course_id
=
course
.
id
,
min_price
=
10
,
sku
=
self
.
make_sku
())
# Create an order for a prof ed course
params
=
{
'course_id'
:
unicode
(
course
.
id
)}
response
=
self
.
_post
(
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# TODO jsa - is this the intended behavior?
self
.
_assert_checked_out
(
params
,
patched_create_order
,
course
.
id
,
'no-id-professional'
)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
[
'merchant_defined_data1'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
data
[
'merchant_defined_data2'
],
"no-id-professional"
)
def
test_create_order_set_donation_amount
(
self
):
# Verify the student so we don't need to submit photos
self
.
_verify_student
()
def
test_create_order_bad_donation_amount
(
self
,
patched_create_order
):
# Create an order
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
'99.9'
}
self
.
_assert_checked_out
(
params
,
patched_create_order
,
None
,
None
,
expected_status_code
=
400
)
def
test_create_order_good_donation_amount
(
self
,
patched_create_order
):
# Create an order
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
'1
.23
'
'contribution'
:
'1
00.0
'
}
self
.
_post
(
params
)
self
.
_assert_checked_out
(
params
,
patched_create_order
,
self
.
course
.
id
,
'verified'
)
def
test_old_clients
(
self
,
patched_create_order
):
# ensure the response to a request from a stale js client is modified so as
# not to break behavior in the browser.
# (XCOM-214) remove after release.
expected_payment_data
=
EcommerceApiTestMixin
.
PAYMENT_DATA
.
copy
()
expected_payment_data
[
'payment_form_data'
]
.
update
({
'foo'
:
'bar'
})
patched_create_order
.
return_value
=
expected_payment_data
# there is no 'processor' parameter in the post payload, so the response should only contain payment form data.
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
}
response
=
self
.
client
.
post
(
reverse
(
'verify_student_create_order'
),
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertTrue
(
patched_create_order
.
called
)
# ensure checkout args were correct
args
=
self
.
_get_checkout_args
(
patched_create_order
)
self
.
assertEqual
(
args
[
'user'
],
self
.
user
)
self
.
assertEqual
(
args
[
'course_key'
],
self
.
course
.
id
)
self
.
assertEqual
(
args
[
'course_mode'
]
.
slug
,
'verified'
)
# ensure response data was correct
data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
data
,
{
'foo'
:
'bar'
})
# Verify that the client's session contains the new donation amount
self
.
assertNotIn
(
'donation_for_course'
,
self
.
client
.
session
)
def
_verify_student
(
self
):
""" Simulate that the student's identity has already been verified. """
attempt
=
SoftwareSecurePhotoVerification
.
objects
.
create
(
user
=
self
.
user
)
attempt
.
mark_ready
()
attempt
.
submit
()
attempt
.
approve
()
@patch
(
'verify_student.views.checkout_with_shoppingcart'
,
return_value
=
EcommerceApiTestMixin
.
PAYMENT_DATA
)
class
TestCreateOrderShoppingCart
(
CheckoutTestMixin
,
ModuleStoreTestCase
):
""" Test view behavior when the shoppingcart is used. """
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
def
test_create_order_with_ecommerce_api
(
self
):
""" Verifies that the view communicates with the E-Commerce API to create orders. """
# Keep track of the original number of orders to verify the old code is not being called.
order_count
=
Order
.
objects
.
count
()
# Add SKU to CourseModes
for
course_mode
in
CourseMode
.
objects
.
filter
(
course_id
=
self
.
course
.
id
):
course_mode
.
sku
=
uuid4
()
.
hex
.
decode
(
'ascii'
)
course_mode
.
save
()
# Mock the E-Commerce Service response
with
self
.
mock_create_order
():
self
.
_verify_student
()
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
}
response
=
self
.
_post
(
params
)
def
make_sku
(
self
):
""" Checkout is handled by shoppingcart when the course mode's sku is empty. """
return
''
# Verify the response is correct.
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
[
'Content-Type'
],
'application/json'
)
self
.
assertEqual
(
json
.
loads
(
response
.
content
),
self
.
ECOMMERCE_API_SUCCESSFUL_BODY
[
'payment_parameters'
])
def
_get_checkout_args
(
self
,
patched_create_order
):
""" Assuming patched_create_order was called, return a mapping containing the call arguments."""
return
dict
(
zip
((
'request'
,
'user'
,
'course_key'
,
'course_mode'
,
'amount'
),
patched_create_order
.
call_args
[
0
]))
# Verify old code is not called (e.g. no Order object created in LMS)
self
.
assertEqual
(
order_count
,
Order
.
objects
.
count
())
def
_add_course_mode_skus
(
self
):
""" Add SKUs to the CourseMode objects for self.course. """
for
course_mode
in
CourseMode
.
objects
.
filter
(
course_id
=
self
.
course
.
id
):
course_mode
.
sku
=
uuid4
()
.
hex
.
decode
(
'ascii'
)
course_mode
.
save
()
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
@patch
(
'verify_student.views.checkout_with_ecommerce_service'
,
return_value
=
EcommerceApiTestMixin
.
PAYMENT_DATA
)
class
TestCreateOrderEcommerceService
(
CheckoutTestMixin
,
EcommerceApiTestMixin
,
ModuleStoreTestCase
):
""" Test view behavior when the ecommerce service is used. """
@override_settings
(
ECOMMERCE_API_URL
=
EcommerceApiTestMixin
.
ECOMMERCE_API_URL
,
ECOMMERCE_API_SIGNING_KEY
=
EcommerceApiTestMixin
.
ECOMMERCE_API_SIGNING_KEY
)
def
test_create_order_with_ecommerce_api_errors
(
self
):
"""
Verifies that the view communicates with the E-Commerce API to create orders, and handles errors
appropriately.
"""
self
.
_add_course_mode_skus
()
def
make_sku
(
self
):
""" Checkout is handled by the ecommerce service when the course mode's sku is nonempty. """
return
uuid4
()
.
hex
.
decode
(
'ascii'
)
with
self
.
mock_create_order
(
side_effect
=
ApiError
):
self
.
_verify_student
()
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'contribution'
:
100
}
self
.
assertRaises
(
ApiError
,
self
.
_post
,
params
)
def
_get_checkout_args
(
self
,
patched_create_order
):
""" Assuming patched_create_order was called, return a mapping containing the call arguments."""
return
dict
(
zip
((
'user'
,
'course_key'
,
'course_mode'
,
'processor'
),
patched_create_order
.
call_args
[
0
]))
class
TestCreateOrderView
(
ModuleStoreTestCase
):
...
...
@@ -1099,7 +1094,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
photo_id_image
=
self
.
IMAGE_DATA
)
json_response
=
json
.
loads
(
response
.
content
)
self
.
assertIsNotNone
(
json_response
.
get
(
'orderNumber'
))
self
.
assertIsNotNone
(
json_response
[
'payment_form_data'
]
.
get
(
'orderNumber'
))
# TODO not canonical
# Verify that the order exists and is configured correctly
order
=
Order
.
objects
.
get
(
user
=
self
.
user
)
...
...
@@ -1135,7 +1130,8 @@ class TestCreateOrderView(ModuleStoreTestCase):
url
=
reverse
(
'verify_student_create_order'
)
data
=
{
'contribution'
:
contribution
,
'course_id'
:
course_id
'course_id'
:
course_id
,
'processor'
:
None
,
}
if
face_image
is
not
None
:
...
...
@@ -1149,9 +1145,9 @@ class TestCreateOrderView(ModuleStoreTestCase):
if
expect_status_code
==
200
:
json_response
=
json
.
loads
(
response
.
content
)
if
expect_success
:
self
.
assert
True
(
json_response
.
get
(
'success'
)
)
self
.
assert
Equal
(
set
(
json_response
.
keys
()),
PAYMENT_DATA_KEYS
)
else
:
self
.
assertFalse
(
json_response
.
get
(
'success'
)
)
self
.
assertFalse
(
json_response
[
'success'
]
)
return
response
...
...
lms/djangoapps/verify_student/views.py
View file @
efde11d5
...
...
@@ -380,6 +380,14 @@ class PayAndVerifyView(View):
# Determine the photo verification status
verification_good_until
=
self
.
_verification_valid_until
(
request
.
user
)
# get available payment processors
if
unexpired_paid_course_mode
.
sku
:
# transaction will be conducted via ecommerce service
processors
=
EcommerceAPI
()
.
get_processors
(
request
.
user
)
else
:
# transaction will be conducted using legacy shopping cart
processors
=
[
settings
.
CC_PROCESSOR_NAME
]
# Render the top-level page
context
=
{
'contribution_amount'
:
contribution_amount
,
...
...
@@ -393,7 +401,7 @@ class PayAndVerifyView(View):
'is_active'
:
json
.
dumps
(
request
.
user
.
is_active
),
'message_key'
:
message
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'p
urchase_endpoint'
:
get_purchase_endpoint
()
,
'p
rocessors'
:
processors
,
'requirements'
:
requirements
,
'user_full_name'
:
full_name
,
'verification_deadline'
:
(
...
...
@@ -644,26 +652,59 @@ class PayAndVerifyView(View):
return
(
has_paid
,
bool
(
is_active
))
def
c
reate_order_with_ecommerce_service
(
user
,
course_key
,
course_mode
):
# pylint: disable=invalid-name
""" Create a new
order
using the E-Commerce API. """
def
c
heckout_with_ecommerce_service
(
user
,
course_key
,
course_mode
,
processor
):
# pylint: disable=invalid-name
""" Create a new
basket and trigger immediate checkout,
using the E-Commerce API. """
try
:
api
=
EcommerceAPI
()
# Make an API call to create the order and retrieve the results
_order_number
,
_order_status
,
data
=
api
.
create_order
(
user
,
course_mode
.
sku
)
response_data
=
api
.
create_basket
(
user
,
course_mode
.
sku
,
processor
)
# Pass the payment parameters directly from the API response.
return
HttpResponse
(
json
.
dumps
(
data
[
'payment_parameters'
]),
content_type
=
'application/json
'
)
return
response_data
.
get
(
'payment_data
'
)
except
ApiError
:
params
=
{
'username'
:
user
.
username
,
'mode'
:
course_mode
.
slug
,
'course_id'
:
unicode
(
course_key
)}
log
.
error
(
'Failed to create order for
%(username)
s
%(mode)
s mode of
%(course_id)
s'
,
params
)
raise
def
checkout_with_shoppingcart
(
request
,
user
,
course_key
,
course_mode
,
amount
):
""" Create an order and trigger checkout using shoppingcart."""
cart
=
Order
.
get_cart_for_user
(
user
)
cart
.
clear
()
enrollment_mode
=
course_mode
.
slug
CertificateItem
.
add_to_order
(
cart
,
course_key
,
amount
,
enrollment_mode
)
# Change the order's status so that we don't accidentally modify it later.
# We need to do this to ensure that the parameters we send to the payment system
# match what we store in the database.
# (Ordinarily we would do this client-side when the user submits the form, but since
# the JavaScript on this page does that immediately, we make the change here instead.
# This avoids a second AJAX call and some additional complication of the JavaScript.)
# If a user later re-enters the verification / payment flow, she will create a new order.
cart
.
start_purchase
()
callback_url
=
request
.
build_absolute_uri
(
reverse
(
"shoppingcart.views.postpay_callback"
)
)
payment_data
=
{
'payment_processor_name'
:
settings
.
CC_PROCESSOR_NAME
,
'payment_page_url'
:
get_purchase_endpoint
(),
'payment_form_data'
:
get_signed_purchase_params
(
cart
,
callback_url
=
callback_url
,
extra_data
=
[
unicode
(
course_key
),
course_mode
.
slug
]
),
}
return
payment_data
@require_POST
@login_required
def
create_order
(
request
):
"""
Submit PhotoVerification and create a new Order for this verified cert
This endpoint is named 'create_order' for backward compatibility, but its
actual use is to add a single product to the user's cart and request
immediate checkout.
"""
# Only submit photos if photo data is provided by the client.
# TODO (ECOM-188): Once the A/B test of decoupling verified / payment
...
...
@@ -724,35 +765,23 @@ def create_order(request):
return
HttpResponseBadRequest
(
_
(
"No selected price or selected price is below minimum."
))
if
current_mode
.
sku
:
return
create_order_with_ecommerce_service
(
request
.
user
,
course_id
,
current_mode
)
# I know, we should check this is valid. All kinds of stuff missing here
cart
=
Order
.
get_cart_for_user
(
request
.
user
)
cart
.
clear
()
enrollment_mode
=
current_mode
.
slug
CertificateItem
.
add_to_order
(
cart
,
course_id
,
amount
,
enrollment_mode
)
# Change the order's status so that we don't accidentally modify it later.
# We need to do this to ensure that the parameters we send to the payment system
# match what we store in the database.
# (Ordinarily we would do this client-side when the user submits the form, but since
# the JavaScript on this page does that immediately, we make the change here instead.
# This avoids a second AJAX call and some additional complication of the JavaScript.)
# If a user later re-enters the verification / payment flow, she will create a new order.
cart
.
start_purchase
()
callback_url
=
request
.
build_absolute_uri
(
reverse
(
"shoppingcart.views.postpay_callback"
)
# if request.POST doesn't contain 'processor' then the service's default payment processor will be used.
payment_data
=
checkout_with_ecommerce_service
(
request
.
user
,
course_id
,
current_mode
,
request
.
POST
.
get
(
'processor'
)
)
params
=
get_signed_purchase_params
(
cart
,
callback_url
=
callback_url
,
extra_data
=
[
unicode
(
course_id
),
current_mode
.
slug
]
)
params
[
'success'
]
=
True
return
HttpResponse
(
json
.
dumps
(
params
),
content_type
=
"text/json"
)
else
:
payment_data
=
checkout_with_shoppingcart
(
request
,
request
.
user
,
course_id
,
current_mode
,
amount
)
if
'processor'
not
in
request
.
POST
:
# (XCOM-214) To be removed after release.
# the absence of this key in the POST payload indicates that the request was initiated from
# a stale js client, which expects a response containing only the 'payment_form_data' part of
# the payment data result.
payment_data
=
payment_data
[
'payment_form_data'
]
return
HttpResponse
(
json
.
dumps
(
payment_data
),
content_type
=
"application/json"
)
@require_POST
...
...
lms/static/js/spec/photocapture_spec.js
View file @
efde11d5
...
...
@@ -4,7 +4,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
describe
(
"Photo Verification"
,
function
()
{
beforeEach
(
function
()
{
setFixtures
(
'<div id="order-error" style="display: none;"></div><input type="radio" name="contribution" value="35" id="contribution-35" checked="checked"><input type="radio" id="contribution-other" name="contribution" value=""><input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="30"><img id="face_image" src="src="data:image/png;base64,dummy"><img id="photo_id_image" src="src="data:image/png;base64,dummy"><button
id="pay_
button">pay button</button>'
);
setFixtures
(
'<div id="order-error" style="display: none;"></div><input type="radio" name="contribution" value="35" id="contribution-35" checked="checked"><input type="radio" id="contribution-other" name="contribution" value=""><input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="30"><img id="face_image" src="src="data:image/png;base64,dummy"><img id="photo_id_image" src="src="data:image/png;base64,dummy"><button
class="payment-
button">pay button</button>'
);
});
it
(
'retake photo'
,
function
()
{
...
...
@@ -27,7 +27,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
});
submitToPaymentProcessing
();
expect
(
window
.
submitForm
).
toHaveBeenCalled
();
expect
(
$
(
"
#pay_
button"
)).
toHaveClass
(
"is-disabled"
);
expect
(
$
(
"
.payment-
button"
)).
toHaveClass
(
"is-disabled"
);
});
it
(
'Error during process'
,
function
()
{
...
...
@@ -44,7 +44,7 @@ define(['backbone', 'jquery', 'js/verify_student/photocapture'],
expect
(
window
.
showSubmissionError
).
toHaveBeenCalled
();
// make sure the button isn't disabled
expect
(
$
(
"
#pay_
button"
)).
not
.
toHaveClass
(
"is-disabled"
);
expect
(
$
(
"
.payment-
button"
)).
not
.
toHaveClass
(
"is-disabled"
);
// but also make sure that it was disabled during the ajax call
expect
(
$
.
fn
.
addClass
).
toHaveBeenCalledWith
(
"is-disabled"
);
...
...
lms/static/js/spec/student_account/enrollment_spec.js
View file @
efde11d5
...
...
@@ -5,7 +5,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
describe
(
'edx.student.account.EnrollmentInterface'
,
function
()
{
var
COURSE_KEY
=
'edX/DemoX/Fall'
,
ENROLL_URL
=
'/commerce/
order
s/'
,
ENROLL_URL
=
'/commerce/
basket
s/'
,
FORWARD_URL
=
'/course_modes/choose/edX/DemoX/Fall/'
,
EMBARGO_MSG_URL
=
'/embargo/blocked-message/enrollment/default/'
;
...
...
lms/static/js/spec/verify_student/make_payment_step_view_spec.js
View file @
efde11d5
...
...
@@ -11,8 +11,6 @@ define([
describe
(
'edx.verify_student.MakePaymentStepView'
,
function
()
{
var
PAYMENT_URL
=
"/pay"
;
var
PAYMENT_PARAMS
=
{
orderId
:
"test-order"
,
signature
:
"abcd1234"
...
...
@@ -21,7 +19,7 @@ define([
var
STEP_DATA
=
{
minPrice
:
"12"
,
currency
:
"usd"
,
p
urchaseEndpoint
:
PAYMENT_URL
,
p
rocessors
:
[
"test-payment-processor"
]
,
courseKey
:
"edx/test/test"
,
courseModeSlug
:
'verified'
};
...
...
@@ -50,15 +48,16 @@ define([
};
var
expectPaymentButtonEnabled
=
function
(
isEnabled
)
{
var
appearsDisabled
=
$
(
'#pay_button'
).
hasClass
(
'is-disabled'
),
isDisabled
=
$
(
'#pay_button'
).
prop
(
'disabled'
);
var
el
=
$
(
'.payment-button'
),
appearsDisabled
=
el
.
hasClass
(
'is-disabled'
),
isDisabled
=
el
.
prop
(
'disabled'
);
expect
(
!
appearsDisabled
).
toEqual
(
isEnabled
);
expect
(
!
isDisabled
).
toEqual
(
isEnabled
);
};
var
expectPaymentDisabledBecauseInactive
=
function
()
{
var
payButton
=
$
(
'
#pay
_button'
);
var
payButton
=
$
(
'
.payment
_button'
);
// Payment button should be hidden
expect
(
payButton
.
length
).
toEqual
(
0
);
...
...
@@ -67,21 +66,22 @@ define([
var
goToPayment
=
function
(
requests
,
kwargs
)
{
var
params
=
{
contribution
:
kwargs
.
amount
||
""
,
course_id
:
kwargs
.
courseId
||
""
course_id
:
kwargs
.
courseId
||
""
,
processor
:
kwargs
.
processor
||
""
};
// Click the "go to payment" button
$
(
'
#pay_
button'
).
click
();
$
(
'
.payment-
button'
).
click
();
// Verify that the request was made to the server
AjaxHelpers
.
expectRequest
(
requests
,
"POST"
,
"/verify_student/create_order/"
,
$
.
param
(
params
)
AjaxHelpers
.
expectPostRequest
(
requests
,
"/verify_student/create_order/"
,
$
.
param
(
params
)
);
// Simulate the server response
if
(
kwargs
.
succeeds
)
{
AjaxHelpers
.
respondWithJson
(
requests
,
PAYMENT_PARAMS
);
// TODO put fixture responses in the right place
AjaxHelpers
.
respondWithJson
(
requests
,
{
payment_page_url
:
'http://payment-page-url/'
,
payment_form_data
:
{
foo
:
'bar'
}}
);
}
else
{
AjaxHelpers
.
respondWithTextError
(
requests
,
400
,
SERVER_ERROR_MSG
);
}
...
...
@@ -95,7 +95,7 @@ define([
expect
(
form
.
serialize
()).
toEqual
(
$
.
param
(
params
));
expect
(
form
.
attr
(
'method'
)).
toEqual
(
"POST"
);
expect
(
form
.
attr
(
'action'
)).
toEqual
(
PAYMENT_URL
);
expect
(
form
.
attr
(
'action'
)).
toEqual
(
'http://payment-page-url/'
);
};
beforeEach
(
function
()
{
...
...
@@ -114,9 +114,10 @@ define([
goToPayment
(
requests
,
{
amount
:
STEP_DATA
.
minPrice
,
courseId
:
STEP_DATA
.
courseKey
,
processor
:
STEP_DATA
.
processors
[
0
],
succeeds
:
true
});
expectPaymentSubmitted
(
view
,
PAYMENT_PARAMS
);
expectPaymentSubmitted
(
view
,
{
foo
:
'bar'
}
);
});
it
(
'by default minimum price is selected if no suggested prices are given'
,
function
()
{
...
...
@@ -129,9 +130,10 @@ define([
goToPayment
(
requests
,
{
amount
:
STEP_DATA
.
minPrice
,
courseId
:
STEP_DATA
.
courseKey
,
processor
:
STEP_DATA
.
processors
[
0
],
succeeds
:
true
});
expectPaymentSubmitted
(
view
,
PAYMENT_PARAMS
);
expectPaymentSubmitted
(
view
,
{
foo
:
'bar'
}
);
});
it
(
'min price is always selected even if contribution amount is provided'
,
function
()
{
...
...
@@ -156,6 +158,7 @@ define([
goToPayment
(
requests
,
{
amount
:
STEP_DATA
.
minPrice
,
courseId
:
STEP_DATA
.
courseKey
,
processor
:
STEP_DATA
.
processors
[
0
],
succeeds
:
false
});
...
...
lms/static/js/student_account/enrollment.js
View file @
efde11d5
...
...
@@ -9,7 +9,7 @@ var edx = edx || {};
edx
.
student
.
account
.
EnrollmentInterface
=
{
urls
:
{
orders
:
'/commerce/order
s/'
,
baskets
:
'/commerce/basket
s/'
,
},
headers
:
{
...
...
@@ -26,7 +26,7 @@ var edx = edx || {};
data
=
JSON
.
stringify
(
data_obj
);
$
.
ajax
({
url
:
this
.
urls
.
order
s
,
url
:
this
.
urls
.
basket
s
,
type
:
'POST'
,
contentType
:
'application/json; charset=utf-8'
,
data
:
data
,
...
...
lms/static/js/verify_student/pay_and_verify.js
View file @
efde11d5
...
...
@@ -59,7 +59,7 @@ var edx = edx || {};
function
(
price
)
{
return
Boolean
(
price
);
}
),
currency
:
el
.
data
(
'course-mode-currency'
),
p
urchaseEndpoint
:
el
.
data
(
'purchase-endpoint
'
),
p
rocessors
:
el
.
data
(
'processors
'
),
verificationDeadline
:
el
.
data
(
'verification-deadline'
),
courseModeSlug
:
el
.
data
(
'course-mode-slug'
),
alreadyVerified
:
el
.
data
(
'already-verified'
),
...
...
lms/static/js/verify_student/photocapture.js
View file @
efde11d5
...
...
@@ -69,7 +69,7 @@ function refereshPageMessage() {
}
var
submitToPaymentProcessing
=
function
()
{
$
(
"
#pay_
button"
).
addClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
true
);
$
(
"
.payment-
button"
).
addClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
true
);
var
contribution_input
=
$
(
"input[name='contribution']:checked"
)
var
contribution
=
0
;
if
(
contribution_input
.
attr
(
'id'
)
==
'contribution-other'
)
{
...
...
@@ -96,7 +96,7 @@ var submitToPaymentProcessing = function() {
}
},
error
:
function
(
xhr
,
status
,
error
)
{
$
(
"
#pay_
button"
).
removeClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
false
);
$
(
"
.payment-
button"
).
removeClass
(
'is-disabled'
).
attr
(
'aria-disabled'
,
false
);
showSubmissionError
()
}
});
...
...
@@ -290,7 +290,7 @@ function waitForFlashLoad(func, flash_object) {
$
(
document
).
ready
(
function
()
{
$
(
".carousel-nav"
).
addClass
(
'sr'
);
$
(
"
#pay_
button"
).
click
(
function
(){
$
(
"
.payment-
button"
).
click
(
function
(){
analytics
.
pageview
(
"Payment Form"
);
submitToPaymentProcessing
();
});
...
...
@@ -306,7 +306,7 @@ $(document).ready(function() {
// prevent browsers from keeping this button checked
$
(
"#confirm_pics_good"
).
prop
(
"checked"
,
false
)
$
(
"#confirm_pics_good"
).
change
(
function
()
{
$
(
"
#pay_
button"
).
toggleClass
(
'disabled'
);
$
(
"
.payment-
button"
).
toggleClass
(
'disabled'
);
$
(
"#reverify_button"
).
toggleClass
(
'disabled'
);
$
(
"#midcourse_reverify_button"
).
toggleClass
(
'disabled'
);
});
...
...
lms/static/js/verify_student/views/make_payment_step_view.js
View file @
efde11d5
...
...
@@ -3,7 +3,7 @@
*/
var
edx
=
edx
||
{};
(
function
(
$
,
_
,
gettext
)
{
(
function
(
$
,
_
,
gettext
,
interpolate_text
)
{
'use strict'
;
edx
.
verify_student
=
edx
.
verify_student
||
{};
...
...
@@ -28,12 +28,45 @@ var edx = edx || {};
};
},
_getProductText
:
function
(
modeSlug
,
isUpgrade
)
{
switch
(
modeSlug
)
{
case
"professional"
:
return
gettext
(
"Professional Education Verified Certificate"
);
case
"no-id-professional"
:
return
gettext
(
"Professional Education"
);
default
:
if
(
isUpgrade
)
{
return
gettext
(
"Verified Certificate upgrade"
);
}
else
{
return
gettext
(
"Verified Certificate"
);
}
}
},
_getPaymentButtonText
:
function
(
processorName
)
{
if
(
processorName
.
toLowerCase
().
substr
(
0
,
11
)
==
'cybersource'
)
{
return
gettext
(
'Pay with Credit Card'
);
}
else
{
// This is mainly for testing as no other processors are supported right now.
// Translators: 'processor' is the name of a third-party payment processing vendor (example: "PayPal")
return
interpolate_text
(
gettext
(
'Pay with {processor}'
),
{
processor
:
processorName
});
}
},
_getPaymentButtonHtml
:
function
(
processorName
)
{
var
self
=
this
;
return
_
.
template
(
'<a class="next action-primary payment-button" id="<%- name %>" tab-index="0"><%- text %></a> '
)({
name
:
processorName
,
text
:
self
.
_getPaymentButtonText
(
processorName
)});
},
postRender
:
function
()
{
var
templateContext
=
this
.
templateContext
(),
hasVisibleReqs
=
_
.
some
(
templateContext
.
requirements
,
function
(
isVisible
)
{
return
isVisible
;
}
);
),
self
=
this
;
// Track a virtual pageview, for easy funnel reconstruction.
window
.
analytics
.
page
(
'payment'
,
this
.
templateName
);
...
...
@@ -59,25 +92,41 @@ var edx = edx || {};
this
.
setPaymentEnabled
(
true
);
}
// render the name of the product being paid for
$
(
'div.payment-buttons span.product-name'
).
append
(
self
.
_getProductText
(
templateContext
.
courseModeSlug
,
templateContext
.
upgrade
)
);
// create a button for each payment processor
_
.
each
(
templateContext
.
processors
,
function
(
processorName
)
{
$
(
'div.payment-buttons'
).
append
(
self
.
_getPaymentButtonHtml
(
processorName
)
);
});
// Handle payment submission
$
(
'
#pay_
button'
).
on
(
'click'
,
_
.
bind
(
this
.
createOrder
,
this
)
);
$
(
'
.payment-
button'
).
on
(
'click'
,
_
.
bind
(
this
.
createOrder
,
this
)
);
},
setPaymentEnabled
:
function
(
isEnabled
)
{
if
(
_
.
isUndefined
(
isEnabled
)
)
{
isEnabled
=
true
;
}
$
(
'
#pay_
button'
)
$
(
'
.payment-
button'
)
.
toggleClass
(
'is-disabled'
,
!
isEnabled
)
.
prop
(
'disabled'
,
!
isEnabled
)
.
attr
(
'aria-disabled'
,
!
isEnabled
);
},
createOrder
:
function
()
{
// This function invokes the create_order endpoint. It will either create an order in
// the lms' shoppingcart or a basket in Otto, depending on which backend the request course
// mode is configured to use. In either case, the checkout process will be triggered,
// and the expected response will consist of an appropriate payment processor endpoint for
// redirection, along with parameters to be passed along in the request.
createOrder
:
function
(
event
)
{
var
paymentAmount
=
this
.
getPaymentAmount
(),
postData
=
{
'processor'
:
event
.
target
.
id
,
'contribution'
:
paymentAmount
,
'course_id'
:
this
.
stepData
.
courseKey
,
'course_id'
:
this
.
stepData
.
courseKey
};
// Disable the payment button to prevent multiple submissions
...
...
@@ -98,21 +147,21 @@ var edx = edx || {};
},
handleCreateOrderResponse
:
function
(
payment
Params
)
{
// At this point, the
order
has been created on the server,
handleCreateOrderResponse
:
function
(
payment
Data
)
{
// At this point, the
basket
has been created on the server,
// and we've received signed payment parameters.
// We need to dynamically construct a form using
// these parameters, then submit it to the payment processor.
// This will send the user to a
hosted order page,
// where she can
enter credit card information
.
// This will send the user to a
n externally-hosted page
// where she can
proceed with payment
.
var
form
=
$
(
'#payment-processor-form'
);
$
(
'input'
,
form
).
remove
();
form
.
attr
(
'action'
,
this
.
stepData
.
purchaseEndpoint
);
form
.
attr
(
'action'
,
paymentData
.
payment_page_url
);
form
.
attr
(
'method'
,
'POST'
);
_
.
each
(
payment
Params
,
function
(
value
,
key
)
{
_
.
each
(
payment
Data
.
payment_form_data
,
function
(
value
,
key
)
{
$
(
'<input>'
).
attr
({
type
:
'hidden'
,
name
:
key
,
...
...
@@ -200,4 +249,4 @@ var edx = edx || {};
});
})(
jQuery
,
_
,
gettext
);
})(
jQuery
,
_
,
gettext
,
interpolate_text
);
lms/static/sass/_developer.scss
View file @
efde11d5
...
...
@@ -50,3 +50,22 @@
padding
:
(
$baseline
*
1
.5
)
$baseline
;
text-align
:
center
;
}
// for verify_student/make_payment_step.underscore
.payment-buttons
{
.purchase
{
float
:
left
;
padding
:
(
$baseline
*.
5
)
0
;
.product-info
,
.product-name
,
.price
{
@extend
%t-ultrastrong
;
color
:
$m-blue-d3
;
}
}
.payment-button
{
float
:
right
;
@include
margin-left
(
(
$baseline
/
2
)
);
}
}
lms/templates/verify_student/make_payment_step.underscore
View file @
efde11d5
...
...
@@ -98,11 +98,16 @@
<% } %>
<% if ( isActive ) { %>
<div class="nav-wizard is-ready center">
<div class="
payment-buttons
nav-wizard is-ready center">
<input type="hidden" name="contribution" value="<%- minPrice %>" />
<a class="next action-primary" id="pay_button" tab-index="0">
<%- gettext( "Continue to payment" ) %> ($<%- minPrice %>)
</a>
<div class="purchase">
<p class="product-info"><span class="product-name"></span> <%- gettext( "price" ) %>: <span class="price">$<%- minPrice %></span></p>
</div>
<div class="pay-options">
<%
// payment buttons will go here
%>
</div>
</div>
<% } %>
...
...
lms/templates/verify_student/pay_and_verify.html
View file @
efde11d5
...
...
@@ -67,7 +67,7 @@ from verify_student.views import PayAndVerifyView
data-course-mode-suggested-prices=
'${course_mode.suggested_prices}'
data-course-mode-currency=
'${course_mode.currency}'
data-contribution-amount=
'${contribution_amount}'
data-p
urchase-endpoint=
'${purchase_endpoint
}'
data-p
rocessors=
'${json.dumps(processors)
}'
data-verification-deadline=
'${verification_deadline}'
data-display-steps=
'${json.dumps(display_steps)}'
data-current-step=
'${current_step}'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment