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
70d837ab
Commit
70d837ab
authored
Apr 27, 2015
by
Jim Abramson
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7777 from edx/jsa/baskets
use commerce api v2.
parents
323e466e
efde11d5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
401 additions
and
211 deletions
+401
-211
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
+50
-26
lms/djangoapps/commerce/urls.py
+4
-2
lms/djangoapps/commerce/views.py
+29
-26
lms/djangoapps/shoppingcart/tests/test_views.py
+21
-18
lms/djangoapps/verify_student/tests/test_integration.py
+1
-1
lms/djangoapps/verify_student/tests/test_views.py
+0
-0
lms/djangoapps/verify_student/views.py
+65
-36
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 @
70d837ab
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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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
()
response
=
self
.
_post_to_view
()
# Validate the response content
msg
=
Messages
.
ORDER_COMPLETED
.
format
(
order_number
=
self
.
ORDER_NUMBER
)
self
.
assertResponseMessage
(
response
,
msg
)
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
self
.
_test_successful_ecommerce_api_call
()
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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -918,24 +918,27 @@ 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'
:
[
{
'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'
,
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'
},
]}],
'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'
,
},
},
}
...
...
lms/djangoapps/verify_student/tests/test_integration.py
View file @
70d837ab
...
...
@@ -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 @
70d837ab
This diff is collapsed.
Click to expand it.
lms/djangoapps/verify_student/views.py
View file @
70d837ab
...
...
@@ -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"
)
)
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"
)
# 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'
)
)
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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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 @
70d837ab
...
...
@@ -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