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
be1523f2
Commit
be1523f2
authored
Aug 20, 2014
by
Will Daly
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #4880 from edx/will/use-new-cybersource-api
CyberSource API Update
parents
c2e804ec
1ad8d4f2
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
1028 additions
and
358 deletions
+1028
-358
lms/djangoapps/shoppingcart/processors/CyberSource.py
+32
-27
lms/djangoapps/shoppingcart/processors/CyberSource2.py
+340
-161
lms/djangoapps/shoppingcart/processors/__init__.py
+81
-23
lms/djangoapps/shoppingcart/processors/helpers.py
+31
-0
lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py
+31
-14
lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py
+342
-0
lms/djangoapps/shoppingcart/tests/payment_fake.py
+63
-78
lms/djangoapps/shoppingcart/tests/test_payment_fake.py
+9
-6
lms/djangoapps/shoppingcart/views.py
+9
-14
lms/djangoapps/verify_student/tests/test_views.py
+56
-13
lms/djangoapps/verify_student/views.py
+7
-2
lms/envs/acceptance.py
+0
-14
lms/envs/aws.py
+1
-0
lms/envs/common.py
+10
-0
lms/envs/dev.py
+5
-1
lms/envs/devstack.py
+3
-1
lms/envs/test.py
+8
-4
No files found.
lms/djangoapps/shoppingcart/processors/CyberSource.py
View file @
be1523f2
### Implementation of support for the Cybersource Credit card processor
### The name of this file should be used as the key of the dict in the CC_PROCESSOR setting
### Implementes interface as specified by __init__.py
"""
Implementation the CyberSource credit card processor.
IMPORTANT: CyberSource will deprecate this version of the API ("Hosted Order Page") in September 2014.
We are keeping this implementation in the code-base for now, but we should
eventually replace this module with the newer implementation (in `CyberSource2.py`)
To enable this implementation, add the following to Django settings:
CC_PROCESSOR_NAME = "CyberSource"
CC_PROCESSOR = {
"CyberSource": {
"SHARED_SECRET": "<shared secret>",
"MERCHANT_ID": "<merchant ID>",
"SERIAL_NUMBER": "<serial number>",
"PURCHASE_ENDPOINT": "<purchase endpoint>"
}
}
"""
import
time
import
hmac
import
binascii
...
...
@@ -16,26 +32,11 @@ from django.utils.translation import ugettext as _
from
edxmako.shortcuts
import
render_to_string
from
shoppingcart.models
import
Order
from
shoppingcart.processors.exceptions
import
*
from
shoppingcart.processors.helpers
import
get_processor_config
from
microsite_configuration
import
microsite
def
get_cybersource_config
():
"""
This method will return any microsite specific cybersource configuration, otherwise
we return the default configuration
"""
config_key
=
microsite
.
get_value
(
'cybersource_config_key'
)
config
=
{}
if
config_key
:
# The microsite CyberSource configuration will be subkeys inside of the normal default
# CyberSource configuration
config
=
settings
.
CC_PROCESSOR
[
'CyberSource'
][
'microsites'
][
config_key
]
else
:
config
=
settings
.
CC_PROCESSOR
[
'CyberSource'
]
return
config
def
process_postpay_callback
(
params
):
def
process_postpay_callback
(
params
,
**
kwargs
):
"""
The top level call to this module, basically
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
...
...
@@ -70,7 +71,7 @@ def processor_hash(value):
"""
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
"""
shared_secret
=
get_
cybersource
_config
()
.
get
(
'SHARED_SECRET'
,
''
)
shared_secret
=
get_
processor
_config
()
.
get
(
'SHARED_SECRET'
,
''
)
hash_obj
=
hmac
.
new
(
shared_secret
.
encode
(
'utf-8'
),
value
.
encode
(
'utf-8'
),
sha1
)
return
binascii
.
b2a_base64
(
hash_obj
.
digest
())[:
-
1
]
# last character is a '\n', which we don't want
...
...
@@ -80,9 +81,9 @@ def sign(params, signed_fields_key='orderPage_signedFields', full_sig_key='order
params needs to be an ordered dict, b/c cybersource documentation states that order is important.
Reverse engineered from PHP version provided by cybersource
"""
merchant_id
=
get_
cybersource
_config
()
.
get
(
'MERCHANT_ID'
,
''
)
order_page_version
=
get_
cybersource
_config
()
.
get
(
'ORDERPAGE_VERSION'
,
'7'
)
serial_number
=
get_
cybersource
_config
()
.
get
(
'SERIAL_NUMBER'
,
''
)
merchant_id
=
get_
processor
_config
()
.
get
(
'MERCHANT_ID'
,
''
)
order_page_version
=
get_
processor
_config
()
.
get
(
'ORDERPAGE_VERSION'
,
'7'
)
serial_number
=
get_
processor
_config
()
.
get
(
'SERIAL_NUMBER'
,
''
)
params
[
'merchantID'
]
=
merchant_id
params
[
'orderPage_timestamp'
]
=
int
(
time
.
time
()
*
1000
)
...
...
@@ -115,7 +116,7 @@ def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='si
raise
CCProcessorSignatureException
()
def
render_purchase_form_html
(
cart
):
def
render_purchase_form_html
(
cart
,
**
kwargs
):
"""
Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource
"""
...
...
@@ -124,9 +125,11 @@ def render_purchase_form_html(cart):
'params'
:
get_signed_purchase_params
(
cart
),
})
def
get_signed_purchase_params
(
cart
):
def
get_signed_purchase_params
(
cart
,
**
kwargs
):
return
sign
(
get_purchase_params
(
cart
))
def
get_purchase_params
(
cart
):
total_cost
=
cart
.
total_cost
amount
=
"{0:0.2f}"
.
format
(
total_cost
)
...
...
@@ -139,8 +142,10 @@ def get_purchase_params(cart):
return
params
def
get_purchase_endpoint
():
return
get_cybersource_config
()
.
get
(
'PURCHASE_ENDPOINT'
,
''
)
return
get_processor_config
()
.
get
(
'PURCHASE_ENDPOINT'
,
''
)
def
payment_accepted
(
params
):
"""
...
...
lms/djangoapps/shoppingcart/processors/CyberSource2.py
View file @
be1523f2
### Implementation of support for the Cybersource Credit card processor using the new
### Secure Acceptance API. The previous Hosted Order Page API is being deprecated as of 9/14
### It is mostly the same as the CyberSource.py file, but we have a new file so that we can
### maintain some backwards-compatibility in case of a need to quickly roll back (i.e.
### configuration change rather than code rollback )
"""
Implementation of the CyberSource credit card processor using the newer "Secure Acceptance API".
The previous Hosted Order Page API is being deprecated as of 9/14.
For now, we're keeping the older implementation in the code-base so we can
quickly roll-back by updating the configuration. Eventually, we should replace
the original implementation with this version.
To enable this implementation, add the following Django settings:
CC_PROCESSOR_NAME = "CyberSource2"
CC_PROCESSOR = {
"CyberSource2": {
"SECRET_KEY": "<secret key>",
"ACCESS_KEY": "<access key>",
"PROFILE_ID": "<profile ID>",
"PURCHASE_ENDPOINT": "<purchase endpoint>"
}
}
### The name of this file should be used as the key of the dict in the CC_PROCESSOR setting
### Implementes interface as specified by __init__.py
"""
import
hmac
import
binascii
import
re
import
json
import
uuid
from
textwrap
import
dedent
from
datetime
import
datetime
from
collections
import
OrderedDict
,
defaultdict
from
decimal
import
Decimal
,
InvalidOperation
from
hashlib
import
sha256
from
textwrap
import
dedent
from
django.conf
import
settings
from
django.utils.translation
import
ugettext
as
_
from
edxmako.shortcuts
import
render_to_string
from
shoppingcart.models
import
Order
from
shoppingcart.processors.exceptions
import
*
from
shoppingcart.processors.helpers
import
get_processor_config
from
microsite_configuration
import
microsite
from
django.core.urlresolvers
import
reverse
def
get_cybersource_config
():
"""
This method will return any microsite specific cybersource configuration, otherwise
we return the default configuration
def
process_postpay_callback
(
params
):
"""
config_key
=
microsite
.
get_value
(
'cybersource_config_key'
)
config
=
{}
if
config_key
:
# The microsite CyberSource configuration will be subkeys inside of the normal default
# CyberSource configuration
config
=
settings
.
CC_PROCESSOR
[
'CyberSource2'
][
'microsites'
][
config_key
]
else
:
config
=
settings
.
CC_PROCESSOR
[
'CyberSource2'
]
Handle a response from the payment processor.
return
config
Concrete implementations should:
1) Verify the parameters and determine if the payment was successful.
2) If successful, mark the order as purchased and call `purchased_callbacks` of the cart items.
3) If unsuccessful, try to figure out why and generate a helpful error message.
4) Return a dictionary of the form:
{'success': bool, 'order': Order, 'error_html': str}
Args:
params (dict): Dictionary of parameters received from the payment processor.
Keyword Args:
Can be used to provide additional information to concrete implementations.
Returns:
dict
def
process_postpay_callback
(
params
):
"""
The top level call to this module, basically
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
on the external Hosted Order Page.
It is expected to verify the callback and determine if the payment was successful.
It returns {'success':bool, 'order':Order, 'error_html':str}
If successful this function must have the side effect of marking the order purchased and calling the
purchased_callbacks of the cart items.
If unsuccessful this function should not have those side effects but should try to figure out why and
return a helpful-enough error message in error_html.
"""
try
:
result
=
payment_accepted
(
params
)
valid_params
=
verify_signatures
(
params
)
result
=
_payment_accepted
(
valid_params
[
'req_reference_number'
],
valid_params
[
'auth_amount'
],
valid_params
[
'req_currency'
],
valid_params
[
'decision'
]
)
if
result
[
'accepted'
]:
# SUCCESS CASE first, rest are some sort of oddity
record_purchase
(
params
,
result
[
'order'
])
return
{
'success'
:
True
,
'order'
:
result
[
'order'
],
'error_html'
:
''
}
_record_purchase
(
params
,
result
[
'order'
])
return
{
'success'
:
True
,
'order'
:
result
[
'order'
],
'error_html'
:
''
}
else
:
return
{
'success'
:
False
,
'order'
:
result
[
'order'
],
'error_html'
:
get_processor_decline_html
(
params
)}
return
{
'success'
:
False
,
'order'
:
result
[
'order'
],
'error_html'
:
_get_processor_decline_html
(
params
)
}
except
CCProcessorException
as
error
:
return
{
'success'
:
False
,
'order'
:
None
,
# due to exception we may not have the order
'error_html'
:
get_processor_exception_html
(
error
)}
return
{
'success'
:
False
,
'order'
:
None
,
# due to exception we may not have the order
'error_html'
:
_get_processor_exception_html
(
error
)
}
def
processor_hash
(
value
):
"""
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
Calculate the base64-encoded, SHA-256 hash used by CyberSource.
Args:
value (string): The value to encode.
Returns:
string
"""
secret_key
=
get_
cybersource
_config
()
.
get
(
'SECRET_KEY'
,
''
)
secret_key
=
get_
processor
_config
()
.
get
(
'SECRET_KEY'
,
''
)
hash_obj
=
hmac
.
new
(
secret_key
,
value
,
sha256
)
return
binascii
.
b2a_base64
(
hash_obj
.
digest
())[:
-
1
]
# last character is a '\n', which we don't want
def
sign
(
params
,
signed_fields_key
=
'signed_field_names'
,
full_sig_key
=
'signature'
):
def
verify_signatures
(
params
):
"""
params needs to be an ordered dict, b/c cybersource documentation states that order is important.
Reverse engineered from PHP version provided by cybersource
Use the signature we receive in the POST back from CyberSource to verify
the identity of the sender (CyberSource) and that the contents of the message
have not been tampered with.
Args:
params (dictionary): The POST parameters we received from CyberSource.
Returns:
dict: Contains the parameters we will use elsewhere, converted to the
appropriate types
Raises:
CCProcessorSignatureException: The calculated signature does not match
the signature we received.
CCProcessorDataException: The parameters we received from CyberSource were not valid
(missing keys, wrong types)
"""
# Validate the signature to ensure that the message is from CyberSource
# and has not been tampered with.
signed_fields
=
params
.
get
(
'signed_field_names'
,
''
)
.
split
(
','
)
data
=
u","
.
join
([
u"{0}={1}"
.
format
(
k
,
params
.
get
(
k
,
''
))
for
k
in
signed_fields
])
returned_sig
=
params
.
get
(
'signature'
,
''
)
if
processor_hash
(
data
)
!=
returned_sig
:
raise
CCProcessorSignatureException
()
# Validate that we have the paramters we expect and can convert them
# to the appropriate types.
# Usually validating the signature is sufficient to validate that these
# fields exist, but since we're relying on CyberSource to tell us
# which fields they included in the signature, we need to be careful.
valid_params
=
{}
required_params
=
[
(
'req_reference_number'
,
int
),
(
'req_currency'
,
str
),
(
'decision'
,
str
),
(
'auth_amount'
,
Decimal
),
]
for
key
,
key_type
in
required_params
:
if
key
not
in
params
:
raise
CCProcessorDataException
(
_
(
u"The payment processor did not return a required parameter: {parameter}"
)
.
format
(
parameter
=
key
)
)
try
:
valid_params
[
key
]
=
key_type
(
params
[
key
])
except
(
ValueError
,
TypeError
,
InvalidOperation
):
raise
CCProcessorDataException
(
_
(
u"The payment processor returned a badly-typed value {value} for parameter {parameter}."
)
.
format
(
value
=
params
[
key
],
parameter
=
key
)
)
return
valid_params
def
sign
(
params
):
"""
Sign the parameters dictionary so CyberSource can validate our identity.
The params dict should contain a key 'signed_field_names' that is a comma-separated
list of keys in the dictionary. The order of this list is important!
Args:
params (dict): Dictionary of parameters; must include a 'signed_field_names' key
Returns:
dict: The same parameters dict, with a 'signature' key calculated from the other values.
"""
fields
=
u","
.
join
(
params
.
keys
())
params
[
signed_fields_key
]
=
fields
params
[
'signed_field_names'
]
=
fields
signed_fields
=
params
.
get
(
signed_fields_key
,
''
)
.
split
(
','
)
signed_fields
=
params
.
get
(
'signed_field_names'
,
''
)
.
split
(
','
)
values
=
u","
.
join
([
u"{0}={1}"
.
format
(
i
,
params
.
get
(
i
,
''
))
for
i
in
signed_fields
])
params
[
full_sig_key
]
=
processor_hash
(
values
)
params
[
signed_fields_key
]
=
fields
params
[
'signature'
]
=
processor_hash
(
values
)
params
[
'signed_field_names'
]
=
fields
return
params
def
render_purchase_form_html
(
cart
):
def
render_purchase_form_html
(
cart
,
callback_url
=
None
):
"""
Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource
Args:
cart (Order): The order model representing items in the user's cart.
Keyword Args:
callback_url (unicode): The URL that CyberSource should POST to when the user
completes a purchase. If not provided, then CyberSource will use
the URL provided by the administrator of the account
(CyberSource config, not LMS config).
Returns:
unicode: The rendered HTML form.
"""
return
render_to_string
(
'shoppingcart/cybersource_form.html'
,
{
'action'
:
get_purchase_endpoint
(),
'params'
:
get_signed_purchase_params
(
cart
),
'params'
:
get_signed_purchase_params
(
cart
,
callback_url
=
callback_url
),
})
def
get_signed_purchase_params
(
cart
):
def
get_signed_purchase_params
(
cart
,
callback_url
=
None
):
"""
This method will return a digitally signed set of CyberSource parameters
Args:
cart (Order): The order model representing items in the user's cart.
Keyword Args:
callback_url (unicode): The URL that CyberSource should POST to when the user
completes a purchase. If not provided, then CyberSource will use
the URL provided by the administrator of the account
(CyberSource config, not LMS config).
Returns:
dict
"""
return
sign
(
get_purchase_params
(
cart
))
return
sign
(
get_purchase_params
(
cart
,
callback_url
=
callback_url
))
def
get_purchase_params
(
cart
):
def
get_purchase_params
(
cart
,
callback_url
=
None
):
"""
This method will build out a dictionary of parameters needed by CyberSource to complete the transaction
Args:
cart (Order): The order model representing items in the user's cart.
Keyword Args:
callback_url (unicode): The URL that CyberSource should POST to when the user
completes a purchase. If not provided, then CyberSource will use
the URL provided by the administrator of the account
(CyberSource config, not LMS config).
Returns:
dict
"""
total_cost
=
cart
.
total_cost
amount
=
"{0:0.2f}"
.
format
(
total_cost
)
...
...
@@ -127,8 +258,8 @@ def get_purchase_params(cart):
params
[
'currency'
]
=
cart
.
currency
params
[
'orderNumber'
]
=
"OrderId: {0:d}"
.
format
(
cart
.
id
)
params
[
'access_key'
]
=
get_
cybersource
_config
()
.
get
(
'ACCESS_KEY'
,
''
)
params
[
'profile_id'
]
=
get_
cybersource
_config
()
.
get
(
'PROFILE_ID'
,
''
)
params
[
'access_key'
]
=
get_
processor
_config
()
.
get
(
'ACCESS_KEY'
,
''
)
params
[
'profile_id'
]
=
get_
processor
_config
()
.
get
(
'PROFILE_ID'
,
''
)
params
[
'reference_number'
]
=
cart
.
id
params
[
'transaction_type'
]
=
'sale'
...
...
@@ -136,91 +267,99 @@ def get_purchase_params(cart):
params
[
'signed_date_time'
]
=
datetime
.
utcnow
()
.
strftime
(
'
%
Y-
%
m-
%
dT
%
H:
%
M:
%
SZ'
)
params
[
'signed_field_names'
]
=
'access_key,profile_id,amount,currency,transaction_type,reference_number,signed_date_time,locale,transaction_uuid,signed_field_names,unsigned_field_names,orderNumber'
params
[
'unsigned_field_names'
]
=
''
params
[
'transaction_uuid'
]
=
uuid
.
uuid4
()
params
[
'transaction_uuid'
]
=
uuid
.
uuid4
()
.
hex
params
[
'payment_method'
]
=
'card'
if
hasattr
(
cart
,
'context'
)
and
'request_domain'
in
cart
.
context
:
params
[
'override_custom_receipt_page'
]
=
'{0}{1}'
.
format
(
cart
.
context
[
'request_domain'
],
reverse
(
'shoppingcart.views.postpay_callback'
)
)
if
callback_url
is
not
None
:
params
[
'override_custom_receipt_page'
]
=
callback_url
return
params
def
get_purchase_endpoint
():
"""
Helper function to return the CyberSource endpoint configuration
"""
return
get_cybersource_config
()
.
get
(
'PURCHASE_ENDPOINT'
,
''
)
Return the URL of the payment end-point for CyberSource.
Returns:
unicode
def
payment_accepted
(
params
):
"""
Check that cybersource has accepted the payment
params: a dictionary of POST parameters returned by CyberSource in their post-payment callback
returns: true if the payment was correctly accepted, for the right amount
false if the payment was not accepted
return
get_processor_config
()
.
get
(
'PURCHASE_ENDPOINT'
,
''
)
raises: CCProcessorDataException if the returned message did not provide required parameters
CCProcessorWrongAmountException if the amount charged is different than the order amount
def
_payment_accepted
(
order_id
,
auth_amount
,
currency
,
decision
):
"""
#make sure required keys are present and convert their values to the right type
valid_params
=
{}
for
key
,
key_type
in
[(
'req_reference_number'
,
int
),
(
'req_currency'
,
str
),
(
'decision'
,
str
)]:
if
key
not
in
params
:
raise
CCProcessorDataException
(
_
(
"The payment processor did not return a required parameter: {0}"
.
format
(
key
))
)
try
:
valid_params
[
key
]
=
key_type
(
params
[
key
])
except
ValueError
:
raise
CCProcessorDataException
(
_
(
"The payment processor returned a badly-typed value {0} for param {1}."
.
format
(
params
[
key
],
key
))
)
Check that CyberSource has accepted the payment.
Args:
order_num (int): The ID of the order associated with this payment.
auth_amount (Decimal): The amount the user paid using CyberSource.
currency (str): The currency code of the payment.
decision (str): "ACCEPT" if the payment was accepted.
Returns:
dictionary of the form:
{
'accepted': bool,
'amnt_charged': int,
'currency': string,
'order': Order
}
Raises:
CCProcessorDataException: The order does not exist.
CCProcessorWrongAmountException: The user did not pay the correct amount.
"""
try
:
order
=
Order
.
objects
.
get
(
id
=
valid_params
[
'req_reference_number'
]
)
order
=
Order
.
objects
.
get
(
id
=
order_id
)
except
Order
.
DoesNotExist
:
raise
CCProcessorDataException
(
_
(
"The payment processor accepted an order whose number is not in our system."
))
if
valid_params
[
'decision'
]
==
'ACCEPT'
:
try
:
# Moved reading of charged_amount here from the valid_params loop above because
# only 'ACCEPT' messages have a 'ccAuthReply_amount' parameter
charged_amt
=
Decimal
(
params
[
'auth_amount'
])
except
InvalidOperation
:
raise
CCProcessorDataException
(
_
(
"The payment processor returned a badly-typed value {0} for param {1}."
.
format
(
params
[
'auth_amount'
],
'auth_amount'
))
)
if
charged_amt
==
order
.
total_cost
and
valid_params
[
'req_currency'
]
==
order
.
currency
:
return
{
'accepted'
:
True
,
'amt_charged'
:
charged_amt
,
'currency'
:
valid_params
[
'req_currency'
],
'order'
:
order
}
if
decision
==
'ACCEPT'
:
if
auth_amount
==
order
.
total_cost
and
currency
==
order
.
currency
:
return
{
'accepted'
:
True
,
'amt_charged'
:
auth_amount
,
'currency'
:
currency
,
'order'
:
order
}
else
:
raise
CCProcessorWrongAmountException
(
_
(
"The amount charged by the processor {0} {1} is different than the total cost of the order {2} {3}."
.
format
(
charged_amt
,
valid_params
[
'req_currency'
],
order
.
total_cost
,
order
.
currency
))
_
(
u"The amount charged by the processor {charged_amount} {charged_amount_currency} is different "
u"than the total cost of the order {total_cost} {total_cost_currency}."
)
.
format
(
charged_amount
=
auth_amount
,
charged_amount_currency
=
currency
,
total_cost
=
order
.
total_cost
,
total_cost_currency
=
order
.
currency
)
)
else
:
return
{
'accepted'
:
False
,
'amt_charged'
:
0
,
'currency'
:
'usd'
,
'order'
:
order
}
return
{
'accepted'
:
False
,
'amt_charged'
:
0
,
'currency'
:
'usd'
,
'order'
:
order
}
def
record_purchase
(
params
,
order
):
def
_
record_purchase
(
params
,
order
):
"""
Record the purchase and run purchased_callbacks
Args:
params (dict): The parameters we received from CyberSource.
order (Order): The order associated with this payment.
Returns:
None
"""
# Usually, the credit card number will have the form "xxxxxxxx1234"
# Parse the string to retrieve the digits.
# If we can't find any digits, use placeholder values instead.
ccnum_str
=
params
.
get
(
'req_card_number'
,
''
)
mm
=
re
.
search
(
"
\
d"
,
ccnum_str
)
if
mm
:
...
...
@@ -228,6 +367,7 @@ def record_purchase(params, order):
else
:
ccnum
=
"####"
# Mark the order as purchased and store the billing information
order
.
purchase
(
first
=
params
.
get
(
'req_bill_to_forename'
,
''
),
last
=
params
.
get
(
'req_bill_to_surname'
,
''
),
...
...
@@ -243,57 +383,96 @@ def record_purchase(params, order):
)
def
get_processor_decline_html
(
params
):
"""
Have to parse through the error codes to return a helpful message"""
payment_support_email
=
microsite
.
get_value
(
'payment_support_email'
,
settings
.
PAYMENT_SUPPORT_EMAIL
)
def
_
get_processor_decline_html
(
params
):
"""
Return HTML indicating that the user's payment was declined.
msg
=
dedent
(
_
(
"""
<p class="error_msg">
Sorry! Our payment processor did not accept your payment.
The decision they returned was <span class="decision">{decision}</span>,
and the reason was <span class="reason">{reason_code}:{reason_msg}</span>.
You were not charged. Please try a different form of payment.
Contact us with payment-related questions at {email}.
</p>
"""
))
return
msg
.
format
(
decision
=
params
[
'decision'
],
reason_code
=
params
[
'reason_code'
],
reason_msg
=
REASONCODE_MAP
[
params
[
'reason_code'
]],
email
=
payment_support_email
Args:
params (dict): Parameters we received from CyberSource.
Returns:
unicode: The rendered HTML.
"""
payment_support_email
=
microsite
.
get_value
(
'payment_support_email'
,
settings
.
PAYMENT_SUPPORT_EMAIL
)
return
_format_error_html
(
_
(
"Sorry! Our payment processor did not accept your payment. "
"The decision they returned was {decision}, "
"and the reason was {reason}. "
"You were not charged. Please try a different form of payment. "
"Contact us with payment-related questions at {email}."
)
.
format
(
decision
=
'<span class="decision">{decision}</span>'
.
format
(
decision
=
params
[
'decision'
]),
reason
=
'<span class="reason">{reason_code}:{reason_msg}</span>'
.
format
(
reason_code
=
params
[
'reason_code'
],
reason_msg
=
REASONCODE_MAP
.
get
(
params
[
'reason_code'
])
),
email
=
payment_support_email
)
)
def
get_processor_exception_html
(
exception
):
"""Return error HTML associated with exception"""
def
_get_processor_exception_html
(
exception
):
"""
Return HTML indicating that an error occurred.
Args:
exception (CCProcessorException): The exception that occurred.
Returns:
unicode: The rendered HTML.
"""
payment_support_email
=
microsite
.
get_value
(
'payment_support_email'
,
settings
.
PAYMENT_SUPPORT_EMAIL
)
if
isinstance
(
exception
,
CCProcessorDataException
):
msg
=
dedent
(
_
(
"""
<p class="error_msg">
Sorry! Our payment processor sent us back a payment confirmation that had inconsistent data!
We apologize that we cannot verify whether the charge went through and take further action on your order.
The specific error message is: <span class="exception_msg">{msg}</span>.
Your credit card may possibly have been charged. Contact us with payment-specific questions at {email}.
</p>
"""
.
format
(
msg
=
exception
.
message
,
email
=
payment_support_email
)))
return
msg
return
_format_error_html
(
_
(
u"Sorry! Our payment processor sent us back a payment confirmation that had inconsistent data! "
u"We apologize that we cannot verify whether the charge went through and take further action on your order. "
u"The specific error message is: {msg} "
u"Your credit card may possibly have been charged. Contact us with payment-specific questions at {email}."
)
.
format
(
msg
=
u'<span class="exception_msg">{msg}</span>'
.
format
(
msg
=
exception
.
message
),
email
=
payment_support_email
)
)
elif
isinstance
(
exception
,
CCProcessorWrongAmountException
):
msg
=
dedent
(
_
(
"""
<p class="error_msg">
Sorry! Due to an error your purchase was charged for a different amount than the order total!
The specific error message is: <span class="exception_msg">{msg}</span>.
Your credit card has probably been charged. Contact us with payment-specific questions at {email}.
</p>
"""
.
format
(
msg
=
exception
.
message
,
email
=
payment_support_email
)))
return
msg
# fallthrough case, which basically never happens
return
'<p class="error_msg">EXCEPTION!</p>'
return
_format_error_html
(
_
(
u"Sorry! Due to an error your purchase was charged for a different amount than the order total! "
u"The specific error message is: {msg}. "
u"Your credit card has probably been charged. Contact us with payment-specific questions at {email}."
)
.
format
(
msg
=
u'<span class="exception_msg">{msg}</span>'
.
format
(
msg
=
exception
.
message
),
email
=
payment_support_email
)
)
elif
isinstance
(
exception
,
CCProcessorSignatureException
):
return
_format_error_html
(
_
(
u"Sorry! Our payment processor sent us back a corrupted message regarding your charge, so we are "
u"unable to validate that the message actually came from the payment processor. "
u"The specific error message is: {msg}. "
u"We apologize that we cannot verify whether the charge went through and take further action on your order. "
u"Your credit card may possibly have been charged. Contact us with payment-specific questions at {email}."
)
.
format
(
msg
=
u'<span class="exception_msg">{msg}</span>'
.
format
(
msg
=
exception
.
message
),
email
=
payment_support_email
)
)
else
:
return
_format_error_html
(
_
(
u"Sorry! Your payment could not be processed because an unexpected exception occurred. "
u"Please contact us at {email} for assistance."
)
.
format
(
email
=
payment_support_email
)
)
def
_format_error_html
(
msg
):
""" Format an HTML error message """
return
'<p class="error_msg">{msg}</p>'
.
format
(
msg
=
msg
)
CARDTYPE_MAP
=
defaultdict
(
lambda
:
"UNKNOWN"
)
...
...
lms/djangoapps/shoppingcart/processors/__init__.py
View file @
be1523f2
"""
Public API for payment processor implementations.
The specific implementation is determined at runtime using Django settings:
CC_PROCESSOR_NAME: The name of the Python module (in `shoppingcart.processors`) to use.
CC_PROCESSOR: Dictionary of configuration options for specific processor implementations,
keyed to processor names.
"""
from
django.conf
import
settings
### Now code that determines, using settings, which actual processor implementation we're using.
processor_name
=
settings
.
CC_PROCESSOR
.
keys
()[
0
]
module
=
__import__
(
'shoppingcart.processors.'
+
processor_name
,
fromlist
=
[
'render_purchase_form_html'
'process_postpay_callback'
,
])
# Import the processor implementation, using `CC_PROCESSOR_NAME`
# as the name of the Python module in `shoppingcart.processors`
PROCESSOR_MODULE
=
__import__
(
'shoppingcart.processors.'
+
settings
.
CC_PROCESSOR_NAME
,
fromlist
=
[
'render_purchase_form_html'
,
'process_postpay_callback'
,
'get_purchase_endpoint'
,
'get_signed_purchase_params'
,
]
)
def
render_purchase_form_html
(
cart
,
**
kwargs
):
"""
Render an HTML form with POSTs to the hosted payment processor.
Args:
cart (Order): The order model representing items in the user's cart.
Returns:
unicode: the rendered HTML form
def
render_purchase_form_html
(
*
args
,
**
kwargs
):
"""
The top level call to this module to begin the purchase.
Given a shopping cart,
Renders the HTML form for display on user's browser, which POSTS to Hosted Processors
Returns the HTML as a string
return
PROCESSOR_MODULE
.
render_purchase_form_html
(
cart
,
**
kwargs
)
def
process_postpay_callback
(
params
,
**
kwargs
):
"""
return
module
.
render_purchase_form_html
(
*
args
,
**
kwargs
)
Handle a response from the payment processor.
Concrete implementations should:
1) Verify the parameters and determine if the payment was successful.
2) If successful, mark the order as purchased and call `purchased_callbacks` of the cart items.
3) If unsuccessful, try to figure out why and generate a helpful error message.
4) Return a dictionary of the form:
{'success': bool, 'order': Order, 'error_html': str}
Args:
params (dict): Dictionary of parameters received from the payment processor.
Keyword Args:
Can be used to provide additional information to concrete implementations.
Returns:
dict
def
process_postpay_callback
(
*
args
,
**
kwargs
):
"""
The top level call to this module after the purchase.
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
on the external payment page.
It is expected to verify the callback and determine if the payment was successful.
It returns {'success':bool, 'order':Order, 'error_html':str}
If successful this function must have the side effect of marking the order purchased and calling the
purchased_callbacks of the cart items.
If unsuccessful this function should not have those side effects but should try to figure out why and
return a helpful-enough error message in error_html.
return
PROCESSOR_MODULE
.
process_postpay_callback
(
params
,
**
kwargs
)
def
get_purchase_endpoint
():
"""
Return the URL of the current payment processor's endpoint.
Returns:
unicode
"""
return
PROCESSOR_MODULE
.
get_purchase_endpoint
()
def
get_signed_purchase_params
(
cart
,
**
kwargs
):
"""
Return the parameters to send to the current payment processor.
Args:
cart (Order): The order model representing items in the user's cart.
Keyword Args:
Can be used to provide additional information to concrete implementations.
Returns:
dict
"""
return
module
.
process_postpay_callback
(
*
args
,
**
kwargs
)
return
PROCESSOR_MODULE
.
get_signed_purchase_params
(
cart
,
**
kwargs
)
lms/djangoapps/shoppingcart/processors/helpers.py
0 → 100644
View file @
be1523f2
"""
Helper methods for credit card processing modules.
These methods should be shared among all processor implementations,
but should NOT be imported by modules outside this package.
"""
from
django.conf
import
settings
from
microsite_configuration
import
microsite
def
get_processor_config
():
"""
Return a dictionary of configuration settings for the active credit card processor.
If we're in a microsite and overrides are available, return those instead.
Returns:
dict
"""
# Retrieve the configuration settings for the active credit card processor
config
=
settings
.
CC_PROCESSOR
.
get
(
settings
.
CC_PROCESSOR_NAME
,
{}
)
# Check whether we're in a microsite that overrides our configuration
# If so, find the microsite-specific configuration in the 'microsites'
# sub-key of the normal processor configuration.
config_key
=
microsite
.
get_value
(
'cybersource_config_key'
)
if
config_key
:
config
=
config
[
'microsites'
][
config_key
]
return
config
lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py
View file @
be1523f2
...
...
@@ -7,13 +7,29 @@ from django.test.utils import override_settings
from
django.conf
import
settings
from
student.tests.factories
import
UserFactory
from
shoppingcart.models
import
Order
,
OrderItem
from
shoppingcart.processors.CyberSource
import
*
from
shoppingcart.processors.exceptions
import
*
from
shoppingcart.processors.helpers
import
get_processor_config
from
shoppingcart.processors.exceptions
import
(
CCProcessorException
,
CCProcessorSignatureException
,
CCProcessorDataException
,
CCProcessorWrongAmountException
)
from
shoppingcart.processors.CyberSource
import
(
render_purchase_form_html
,
process_postpay_callback
,
processor_hash
,
verify_signatures
,
sign
,
REASONCODE_MAP
,
record_purchase
,
get_processor_decline_html
,
get_processor_exception_html
,
payment_accepted
,
)
from
mock
import
patch
,
Mock
from
microsite_configuration
import
microsite
import
mock
TEST_CC_PROCESSOR_NAME
=
"CyberSource"
TEST_CC_PROCESSOR
=
{
'CyberSource'
:
{
'SHARED_SECRET'
:
'secret'
,
...
...
@@ -43,24 +59,25 @@ def fakemicrosite(name, default=None):
else
:
return
None
@override_settings
(
CC_PROCESSOR
=
TEST_CC_PROCESSOR
)
class
CyberSourceTests
(
TestCase
):
def
setUp
(
self
):
pass
@override_settings
(
CC_PROCESSOR_NAME
=
TEST_CC_PROCESSOR_NAME
,
CC_PROCESSOR
=
TEST_CC_PROCESSOR
)
class
CyberSourceTests
(
TestCase
):
def
test_override_settings
(
self
):
self
.
assertEqual
(
settings
.
CC_PROCESSOR
[
'CyberSource'
][
'MERCHANT_ID'
],
'edx_test'
)
self
.
assertEqual
(
settings
.
CC_PROCESSOR
[
'CyberSource'
][
'SHARED_SECRET'
],
'secret'
)
def
test_microsite_no_override_settings
(
self
):
self
.
assertEqual
(
get_
cybersource
_config
()[
'MERCHANT_ID'
],
'edx_test'
)
self
.
assertEqual
(
get_
cybersource
_config
()[
'SHARED_SECRET'
],
'secret'
)
self
.
assertEqual
(
get_
processor
_config
()[
'MERCHANT_ID'
],
'edx_test'
)
self
.
assertEqual
(
get_
processor
_config
()[
'SHARED_SECRET'
],
'secret'
)
@
mock.
patch
(
"microsite_configuration.microsite.get_value"
,
fakemicrosite
)
@patch
(
"microsite_configuration.microsite.get_value"
,
fakemicrosite
)
def
test_microsite_override_settings
(
self
):
self
.
assertEqual
(
get_
cybersource
_config
()[
'MERCHANT_ID'
],
'edx_test_override'
)
self
.
assertEqual
(
get_
cybersource
_config
()[
'SHARED_SECRET'
],
'secret_override'
)
self
.
assertEqual
(
get_
processor
_config
()[
'MERCHANT_ID'
],
'edx_test_override'
)
self
.
assertEqual
(
get_
processor
_config
()[
'SHARED_SECRET'
],
'secret_override'
)
def
test_hash
(
self
):
"""
...
...
@@ -258,7 +275,7 @@ class CyberSourceTests(TestCase):
order1
=
Order
.
get_cart_for_user
(
student1
)
item1
=
OrderItem
(
order
=
order1
,
user
=
student1
,
unit_cost
=
1.0
,
line_cost
=
1.0
)
item1
.
save
()
html
=
render_purchase_form_html
(
order1
)
render_purchase_form_html
(
order1
)
((
template
,
context
),
render_kwargs
)
=
render
.
call_args
self
.
assertEqual
(
template
,
'shoppingcart/cybersource_form.html'
)
...
...
lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py
0 → 100644
View file @
be1523f2
# -*- coding: utf-8 -*-
"""
Tests for the newer CyberSource API implementation.
"""
from
mock
import
patch
from
django.test
import
TestCase
import
ddt
from
student.tests.factories
import
UserFactory
from
shoppingcart.models
import
Order
,
OrderItem
from
shoppingcart.processors.CyberSource2
import
(
processor_hash
,
process_postpay_callback
,
render_purchase_form_html
,
get_signed_purchase_params
)
@ddt.ddt
class
CyberSource2Test
(
TestCase
):
"""
Test the CyberSource API implementation. As much as possible,
this test case should use ONLY the public processor interface
(defined in shoppingcart.processors.__init__.py).
Some of the tests in this suite rely on Django settings
to be configured a certain way.
"""
COST
=
"10.00"
CALLBACK_URL
=
"/test_callback_url"
def
setUp
(
self
):
""" Create a user and an order. """
self
.
user
=
UserFactory
()
self
.
order
=
Order
.
get_cart_for_user
(
self
.
user
)
self
.
order_item
=
OrderItem
.
objects
.
create
(
order
=
self
.
order
,
user
=
self
.
user
,
unit_cost
=
self
.
COST
,
line_cost
=
self
.
COST
)
def
test_render_purchase_form_html
(
self
):
# Verify that the HTML form renders with the payment URL specified
# in the test settings.
# This does NOT test that all the form parameters are correct;
# we verify that by testing `get_signed_purchase_params()` directly.
html
=
render_purchase_form_html
(
self
.
order
,
callback_url
=
self
.
CALLBACK_URL
)
self
.
assertIn
(
'<form action="/shoppingcart/payment_fake" method="post">'
,
html
)
self
.
assertIn
(
'transaction_uuid'
,
html
)
self
.
assertIn
(
'signature'
,
html
)
self
.
assertIn
(
self
.
CALLBACK_URL
,
html
)
def
test_get_signed_purchase_params
(
self
):
params
=
get_signed_purchase_params
(
self
.
order
,
callback_url
=
self
.
CALLBACK_URL
)
# Check the callback URL override
self
.
assertEqual
(
params
[
'override_custom_receipt_page'
],
self
.
CALLBACK_URL
)
# Parameters determined by the order model
self
.
assertEqual
(
params
[
'amount'
],
'10.00'
)
self
.
assertEqual
(
params
[
'currency'
],
'usd'
)
self
.
assertEqual
(
params
[
'orderNumber'
],
'OrderId: {order_id}'
.
format
(
order_id
=
self
.
order
.
id
))
self
.
assertEqual
(
params
[
'reference_number'
],
self
.
order
.
id
)
# Parameters determined by the Django (test) settings
self
.
assertEqual
(
params
[
'access_key'
],
'0123456789012345678901'
)
self
.
assertEqual
(
params
[
'profile_id'
],
'edx'
)
# Some fields will change depending on when the test runs,
# so we just check that they're set to a non-empty string
self
.
assertGreater
(
len
(
params
[
'signed_date_time'
]),
0
)
self
.
assertGreater
(
len
(
params
[
'transaction_uuid'
]),
0
)
# Constant parameters
self
.
assertEqual
(
params
[
'transaction_type'
],
'sale'
)
self
.
assertEqual
(
params
[
'locale'
],
'en'
)
self
.
assertEqual
(
params
[
'payment_method'
],
'card'
)
self
.
assertEqual
(
params
[
'signed_field_names'
],
","
.
join
([
'amount'
,
'currency'
,
'orderNumber'
,
'access_key'
,
'profile_id'
,
'reference_number'
,
'transaction_type'
,
'locale'
,
'signed_date_time'
,
'signed_field_names'
,
'unsigned_field_names'
,
'transaction_uuid'
,
'payment_method'
,
'override_custom_receipt_page'
])
)
self
.
assertEqual
(
params
[
'unsigned_field_names'
],
''
)
# Check the signature
self
.
assertEqual
(
params
[
'signature'
],
self
.
_signature
(
params
))
# We patch the purchased callback because
# (a) we're using the OrderItem base class, which doesn't implement this method, and
# (b) we want to verify that the method gets called on success.
@patch.object
(
OrderItem
,
'purchased_callback'
)
def
test_process_payment_success
(
self
,
purchased_callback
):
# Simulate a callback from CyberSource indicating that payment was successful
params
=
self
.
_signed_callback_params
(
self
.
order
.
id
,
self
.
COST
,
self
.
COST
)
result
=
process_postpay_callback
(
params
)
# Expect that we processed the payment successfully
self
.
assertTrue
(
result
[
'success'
],
msg
=
"Payment was not successful: {error}"
.
format
(
error
=
result
.
get
(
'error_html'
))
)
self
.
assertEqual
(
result
[
'error_html'
],
''
)
# Expect that the item's purchased callback was invoked
purchased_callback
.
assert_called_with
()
# Expect that the order has been marked as purchased
self
.
assertEqual
(
result
[
'order'
]
.
status
,
'purchased'
)
def
test_process_payment_rejected
(
self
):
# Simulate a callback from CyberSource indicating that the payment was rejected
params
=
self
.
_signed_callback_params
(
self
.
order
.
id
,
self
.
COST
,
self
.
COST
,
accepted
=
False
)
result
=
process_postpay_callback
(
params
)
# Expect that we get an error message
self
.
assertFalse
(
result
[
'success'
])
self
.
assertIn
(
u"did not accept your payment"
,
result
[
'error_html'
])
def
test_process_payment_invalid_signature
(
self
):
# Simulate a callback from CyberSource indicating that the payment was rejected
params
=
self
.
_signed_callback_params
(
self
.
order
.
id
,
self
.
COST
,
self
.
COST
,
signature
=
"invalid!"
)
result
=
process_postpay_callback
(
params
)
# Expect that we get an error message
self
.
assertFalse
(
result
[
'success'
])
self
.
assertIn
(
u"corrupted message regarding your charge"
,
result
[
'error_html'
])
def
test_process_payment_invalid_order
(
self
):
# Use an invalid order ID
params
=
self
.
_signed_callback_params
(
"98272"
,
self
.
COST
,
self
.
COST
)
result
=
process_postpay_callback
(
params
)
# Expect an error
self
.
assertFalse
(
result
[
'success'
])
self
.
assertIn
(
u"inconsistent data"
,
result
[
'error_html'
])
def
test_process_invalid_payment_amount
(
self
):
# Change the payment amount (no longer matches the database order record)
params
=
self
.
_signed_callback_params
(
self
.
order
.
id
,
"145.00"
,
"145.00"
)
result
=
process_postpay_callback
(
params
)
# Expect an error
self
.
assertFalse
(
result
[
'success'
])
self
.
assertIn
(
u"different amount than the order total"
,
result
[
'error_html'
])
def
test_process_amount_paid_not_decimal
(
self
):
# Change the payment amount to a non-decimal
params
=
self
.
_signed_callback_params
(
self
.
order
.
id
,
self
.
COST
,
"abcd"
)
result
=
process_postpay_callback
(
params
)
# Expect an error
self
.
assertFalse
(
result
[
'success'
])
self
.
assertIn
(
u"badly-typed value"
,
result
[
'error_html'
])
@patch.object
(
OrderItem
,
'purchased_callback'
)
def
test_process_no_credit_card_digits
(
self
,
callback
):
# Use a credit card number with no digits provided
params
=
self
.
_signed_callback_params
(
self
.
order
.
id
,
self
.
COST
,
self
.
COST
,
card_number
=
'nodigits'
)
result
=
process_postpay_callback
(
params
)
# Expect that we processed the payment successfully
self
.
assertTrue
(
result
[
'success'
],
msg
=
"Payment was not successful: {error}"
.
format
(
error
=
result
.
get
(
'error_html'
))
)
self
.
assertEqual
(
result
[
'error_html'
],
''
)
# Expect that the order has placeholders for the missing credit card digits
self
.
assertEqual
(
result
[
'order'
]
.
bill_to_ccnum
,
'####'
)
@ddt.data
(
'req_reference_number'
,
'req_currency'
,
'decision'
,
'auth_amount'
)
def
test_process_missing_parameters
(
self
,
missing_param
):
# Remove a required parameter
params
=
self
.
_signed_callback_params
(
self
.
order
.
id
,
self
.
COST
,
self
.
COST
)
del
params
[
missing_param
]
# Recalculate the signature with no signed fields so we can get past
# signature validation.
params
[
'signed_field_names'
]
=
'reason_code,message'
params
[
'signature'
]
=
self
.
_signature
(
params
)
result
=
process_postpay_callback
(
params
)
# Expect an error
self
.
assertFalse
(
result
[
'success'
])
self
.
assertIn
(
u"did not return a required parameter"
,
result
[
'error_html'
])
def
_signed_callback_params
(
self
,
order_id
,
order_amount
,
paid_amount
,
accepted
=
True
,
signature
=
None
,
card_number
=
'xxxxxxxxxxxx1111'
):
"""
Construct parameters that could be returned from CyberSource
to our payment callback.
Some values can be overridden to simulate different test scenarios,
but most are fake values captured from interactions with
a CyberSource test account.
Args:
order_id (string or int): The ID of the `Order` model.
order_amount (string): The cost of the order.
paid_amount (string): The amount the user paid using CyberSource.
Keyword Args:
accepted (bool): Whether the payment was accepted or rejected.
signature (string): If provided, use this value instead of calculating the signature.
card_numer (string): If provided, use this value instead of the default credit card number.
Returns:
dict
"""
# Parameters sent from CyberSource to our callback implementation
# These were captured from the CC test server.
params
=
{
# Parameters that change based on the test
"decision"
:
"ACCEPT"
if
accepted
else
"REJECT"
,
"req_reference_number"
:
str
(
order_id
),
"req_amount"
:
order_amount
,
"auth_amount"
:
paid_amount
,
"req_card_number"
:
card_number
,
# Stub values
"utf8"
:
u"✓"
,
"req_bill_to_address_country"
:
"US"
,
"auth_avs_code"
:
"X"
,
"req_card_expiry_date"
:
"01-2018"
,
"bill_trans_ref_no"
:
"85080648RYI23S6I"
,
"req_bill_to_address_state"
:
"MA"
,
"signed_field_names"
:
","
.
join
([
"transaction_id"
,
"decision"
,
"req_access_key"
,
"req_profile_id"
,
"req_transaction_uuid"
,
"req_transaction_type"
,
"req_reference_number"
,
"req_amount"
,
"req_currency"
,
"req_locale"
,
"req_payment_method"
,
"req_override_custom_receipt_page"
,
"req_bill_to_forename"
,
"req_bill_to_surname"
,
"req_bill_to_email"
,
"req_bill_to_address_line1"
,
"req_bill_to_address_city"
,
"req_bill_to_address_state"
,
"req_bill_to_address_country"
,
"req_bill_to_address_postal_code"
,
"req_card_number"
,
"req_card_type"
,
"req_card_expiry_date"
,
"message"
,
"reason_code"
,
"auth_avs_code"
,
"auth_avs_code_raw"
,
"auth_response"
,
"auth_amount"
,
"auth_code"
,
"auth_trans_ref_no"
,
"auth_time"
,
"bill_trans_ref_no"
,
"signed_field_names"
,
"signed_date_time"
]),
"req_payment_method"
:
"card"
,
"req_transaction_type"
:
"sale"
,
"auth_code"
:
"888888"
,
"req_locale"
:
"en"
,
"reason_code"
:
"100"
,
"req_bill_to_address_postal_code"
:
"02139"
,
"req_bill_to_address_line1"
:
"123 Fake Street"
,
"req_card_type"
:
"001"
,
"req_bill_to_address_city"
:
"Boston"
,
"signed_date_time"
:
"2014-08-18T14:07:10Z"
,
"req_currency"
:
"usd"
,
"auth_avs_code_raw"
:
"I1"
,
"transaction_id"
:
"4083708299660176195663"
,
"auth_time"
:
"2014-08-18T140710Z"
,
"message"
:
"Request was processed successfully."
,
"auth_response"
:
"100"
,
"req_profile_id"
:
"0000001"
,
"req_transaction_uuid"
:
"ddd9935b82dd403f9aa4ba6ecf021b1f"
,
"auth_trans_ref_no"
:
"85080648RYI23S6I"
,
"req_bill_to_surname"
:
"Doe"
,
"req_bill_to_forename"
:
"John"
,
"req_bill_to_email"
:
"john@example.com"
,
"req_override_custom_receipt_page"
:
"http://localhost:8000/shoppingcart/postpay_callback/"
,
"req_access_key"
:
"abcd12345"
,
}
# Calculate the signature
params
[
'signature'
]
=
signature
if
signature
is
not
None
else
self
.
_signature
(
params
)
return
params
def
_signature
(
self
,
params
):
"""
Calculate the signature from a dictionary of params.
NOTE: This method uses the processor's hashing method. That method
is a thin wrapper of standard library calls, and it seemed overly complex
to rewrite that code in the test suite.
Args:
params (dict): Dictionary with a key 'signed_field_names',
which is a comma-separated list of keys in the dictionary
to include in the signature.
Returns:
string
"""
return
processor_hash
(
","
.
join
([
"{0}={1}"
.
format
(
signed_field
,
params
[
signed_field
])
for
signed_field
in
params
[
'signed_field_names'
]
.
split
(
","
)
])
)
lms/djangoapps/shoppingcart/tests/payment_fake.py
View file @
be1523f2
# -*- coding: utf-8 -*-
"""
Fake payment page for use in acceptance tests.
This view is enabled in the URLs by the feature flag `ENABLE_PAYMENT_FAKE`.
...
...
@@ -21,7 +22,7 @@ from edxmako.shortcuts import render_to_response
# We use the same hashing function as the software under test,
# because it mainly uses standard libraries, and I want
# to avoid duplicating that code.
from
shoppingcart.processors.CyberSource
import
processor_hash
from
shoppingcart.processors.CyberSource
2
import
processor_hash
class
PaymentFakeView
(
View
):
...
...
@@ -51,7 +52,7 @@ class PaymentFakeView(View):
* Triggers a POST to `postpay_callback()` on submit.
* Has hidden fields for all the data CyberSource sends to the callback.
- Most of this data is duplicated from the request POST params (e.g. `amount`
and `course_id`
)
- Most of this data is duplicated from the request POST params (e.g. `amount`)
- Other params contain fake data (always the same user name and address.
- Still other params are calculated (signatures)
...
...
@@ -63,7 +64,7 @@ class PaymentFakeView(View):
served by the shopping cart app.
"""
if
self
.
_is_signature_valid
(
request
.
POST
):
return
self
.
_payment_page_response
(
request
.
POST
,
'/shoppingcart/postpay_callback/'
)
return
self
.
_payment_page_response
(
request
.
POST
)
else
:
return
render_to_response
(
'shoppingcart/test/fake_payment_error.html'
)
...
...
@@ -91,22 +92,17 @@ class PaymentFakeView(View):
Return a bool indicating whether the client sent
us a valid signature in the payment page request.
"""
# Calculate the fields signature
fields_sig
=
processor_hash
(
post_params
.
get
(
'orderPage_signedFields'
))
# Retrieve the list of signed fields
signed_fields
=
post_params
.
get
(
'
orderPage_signedField
s'
)
.
split
(
','
)
signed_fields
=
post_params
.
get
(
'
signed_field_name
s'
)
.
split
(
','
)
# Calculate the public signature
hash_val
=
","
.
join
([
"{0}={1}"
.
format
(
key
,
post_params
[
key
])
for
key
in
signed_fields
])
+
",signedFieldsPublicSignature={0}"
.
format
(
fields_sig
)
])
public_sig
=
processor_hash
(
hash_val
)
return
public_sig
==
post_params
.
get
(
'orderPage_signaturePublic'
)
return
(
public_sig
==
post_params
.
get
(
'signature'
)
)
@classmethod
def
response_post_params
(
cls
,
post_params
):
...
...
@@ -117,88 +113,76 @@ class PaymentFakeView(View):
# Indicate whether the payment was successful
"decision"
:
"ACCEPT"
if
cls
.
PAYMENT_STATUS_RESPONSE
==
"success"
else
"REJECT"
,
# Reflect back whatever the client sent us,
# defaulting to `None` if a paramter wasn't received
"course_id"
:
post_params
.
get
(
'course_id'
),
"orderAmount"
:
post_params
.
get
(
'amount'
),
"ccAuthReply_amount"
:
post_params
.
get
(
'amount'
),
"orderPage_transactionType"
:
post_params
.
get
(
'orderPage_transactionType'
),
"orderPage_serialNumber"
:
post_params
.
get
(
'orderPage_serialNumber'
),
"orderNumber"
:
post_params
.
get
(
'orderNumber'
),
"orderCurrency"
:
post_params
.
get
(
'currency'
),
"match"
:
post_params
.
get
(
'match'
),
"merchantID"
:
post_params
.
get
(
'merchantID'
),
# Send fake user data
"billTo_firstName"
:
"John"
,
"billTo_lastName"
:
"Doe"
,
"billTo_street1"
:
"123 Fake Street"
,
"billTo_state"
:
"MA"
,
"billTo_city"
:
"Boston"
,
"billTo_postalCode"
:
"02134"
,
"billTo_country"
:
"us"
,
# Send fake data for other fields
"card_cardType"
:
"001"
,
"card_accountNumber"
:
"############1111"
,
"card_expirationMonth"
:
"08"
,
"card_expirationYear"
:
"2019"
,
"paymentOption"
:
"card"
,
"orderPage_environment"
:
"TEST"
,
"orderPage_requestToken"
:
"unused"
,
"reconciliationID"
:
"39093601YKVO1I5D"
,
"ccAuthReply_authorizationCode"
:
"888888"
,
"ccAuthReply_avsCodeRaw"
:
"I1"
,
"reasonCode"
:
"100"
,
"requestID"
:
"3777139938170178147615"
,
"ccAuthReply_reasonCode"
:
"100"
,
"ccAuthReply_authorizedDateTime"
:
"2013-08-28T181954Z"
,
"ccAuthReply_processorResponse"
:
"100"
,
"ccAuthReply_avsCode"
:
"X"
,
# We don't use these signatures
"transactionSignature"
:
"unused="
,
"decision_publicSignature"
:
"unused="
,
"orderAmount_publicSignature"
:
"unused="
,
"orderNumber_publicSignature"
:
"unused="
,
"orderCurrency_publicSignature"
:
"unused="
,
# Reflect back parameters we were sent by the client
"req_amount"
:
post_params
.
get
(
'amount'
),
"auth_amount"
:
post_params
.
get
(
'amount'
),
"req_reference_number"
:
post_params
.
get
(
'reference_number'
),
"req_transaction_uuid"
:
post_params
.
get
(
'transaction_uuid'
),
"req_access_key"
:
post_params
.
get
(
'access_key'
),
"req_transaction_type"
:
post_params
.
get
(
'transaction_type'
),
"req_override_custom_receipt_page"
:
post_params
.
get
(
'override_custom_receipt_page'
),
"req_payment_method"
:
post_params
.
get
(
'payment_method'
),
"req_currency"
:
post_params
.
get
(
'currency'
),
"req_locale"
:
post_params
.
get
(
'locale'
),
"signed_date_time"
:
post_params
.
get
(
'signed_date_time'
),
# Fake data
"req_bill_to_address_city"
:
"Boston"
,
"req_card_number"
:
"xxxxxxxxxxxx1111"
,
"req_bill_to_address_state"
:
"MA"
,
"req_bill_to_address_line1"
:
"123 Fake Street"
,
"utf8"
:
u"✓"
,
"reason_code"
:
"100"
,
"req_card_expiry_date"
:
"01-2018"
,
"req_bill_to_forename"
:
"John"
,
"req_bill_to_surname"
:
"Doe"
,
"auth_code"
:
"888888"
,
"req_bill_to_address_postal_code"
:
"02139"
,
"message"
:
"Request was processed successfully."
,
"auth_response"
:
"100"
,
"auth_trans_ref_no"
:
"84997128QYI23CJT"
,
"auth_time"
:
"2014-08-18T110622Z"
,
"bill_trans_ref_no"
:
"84997128QYI23CJT"
,
"auth_avs_code"
:
"X"
,
"req_bill_to_email"
:
"john@example.com"
,
"auth_avs_code_raw"
:
"I1"
,
"req_profile_id"
:
"0000001"
,
"req_card_type"
:
"001"
,
"req_bill_to_address_country"
:
"US"
,
"transaction_id"
:
"4083599817820176195662"
,
}
# Indicate which fields we are including in the signature
# Order is important
signed_fields
=
[
'
billTo_lastName'
,
'orderAmount'
,
'cours
e_id'
,
'
billTo_street1'
,
'card_accountNumber'
,
'orderAmount_publicSignature
'
,
'
orderPage_serialNumber'
,
'orderCurrency'
,
'reconciliationID
'
,
'
decision'
,
'ccAuthReply_processorResponse'
,
'billTo_stat
e'
,
'
billTo_firstName'
,
'card_expirationYear'
,
'billTo_city
'
,
'
billTo_postalCode'
,
'orderPage_requestToken'
,
'ccAuthReply_amount
'
,
'
orderCurrency_publicSignature'
,
'orderPage_transactionTyp
e'
,
'
ccAuthReply_authorizationCode'
,
'decision_publicSignatur
e'
,
'
match'
,
'ccAuthReply_avsCodeRaw'
,
'paymentOption
'
,
'
billTo_country'
,
'reasonCode'
,
'ccAuthReply_reasonC
ode'
,
'
orderPage_environment'
,
'card_expirationMonth'
,
'merchantID
'
,
'
orderNumber_publicSignature'
,
'requestID'
,
'orderNumber
'
,
'
ccAuthReply_authorizedDateTime'
,
'card_cardType'
,
'ccAuthReply_avsCod
e'
'
transaction_id'
,
'decision'
,
'req_access_key'
,
'req_profil
e_id'
,
'
req_transaction_uuid'
,
'req_transaction_type'
,
'req_reference_number
'
,
'
req_amount'
,
'req_currency'
,
'req_locale
'
,
'
req_payment_method'
,
'req_override_custom_receipt_pag
e'
,
'
req_bill_to_forename'
,
'req_bill_to_surname
'
,
'
req_bill_to_email'
,
'req_bill_to_address_line1
'
,
'
req_bill_to_address_city'
,
'req_bill_to_address_stat
e'
,
'
req_bill_to_address_country'
,
'req_bill_to_address_postal_cod
e'
,
'
req_card_number'
,
'req_card_type'
,
'req_card_expiry_date
'
,
'
message'
,
'reason_code'
,
'auth_avs_c
ode'
,
'
auth_avs_code_raw'
,
'auth_response'
,
'auth_amount
'
,
'
auth_code'
,
'auth_trans_ref_no'
,
'auth_time
'
,
'
bill_trans_ref_no'
,
'signed_field_names'
,
'signed_date_tim
e'
]
# Add the list of signed fields
resp_params
[
'signedFields'
]
=
","
.
join
(
signed_fields
)
# Calculate the fields signature
signed_fields_sig
=
processor_hash
(
resp_params
[
'signedFields'
])
resp_params
[
'signed_field_names'
]
=
","
.
join
(
signed_fields
)
# Calculate the public signature
hash_val
=
","
.
join
([
"{0}={1}"
.
format
(
key
,
resp_params
[
key
])
for
key
in
signed_fields
])
+
",signedFieldsPublicSignature={0}"
.
format
(
signed_fields_sig
)
resp_params
[
'signedDataPublicSignature'
]
=
processor_hash
(
hash_val
)
])
resp_params
[
'signature'
]
=
processor_hash
(
hash_val
)
return
resp_params
def
_payment_page_response
(
self
,
post_params
,
callback_url
):
def
_payment_page_response
(
self
,
post_params
):
"""
Render the payment page to a response. This is an HTML form
that triggers a POST request to `callback_url`.
...
...
@@ -210,9 +194,10 @@ class PaymentFakeView(View):
we either:
1) Use fake static data (e.g. always send user name "John Doe")
2) Use the same info we received (e.g. send the same `
course_id` and `
amount`)
2) Use the same info we received (e.g. send the same `amount`)
3) Dynamically calculate signatures using a shared secret
"""
callback_url
=
post_params
.
get
(
'override_custom_receipt_page'
,
'/shoppingcart/postpay_callback/'
)
# Build the context dict used to render the HTML form,
# filling in values for the hidden input fields.
...
...
lms/djangoapps/shoppingcart/tests/test_payment_fake.py
View file @
be1523f2
...
...
@@ -3,8 +3,8 @@ Tests for the fake payment page used in acceptance tests.
"""
from
django.test
import
TestCase
from
shoppingcart.processors.CyberSource
import
sign
,
verify_signatures
,
\
CCProcessorSignatureException
from
shoppingcart.processors.CyberSource
2
import
sign
,
verify_signatures
from
shoppingcart.processors.exceptions
import
CCProcessorSignatureException
from
shoppingcart.tests.payment_fake
import
PaymentFakeView
from
collections
import
OrderedDict
...
...
@@ -16,16 +16,19 @@ class PaymentFakeViewTest(TestCase):
"""
CLIENT_POST_PARAMS
=
OrderedDict
([
(
'match'
,
'on'
),
(
'course_id'
,
'edx/999/2013_Spring'
),
(
'amount'
,
'25.00'
),
(
'currency'
,
'usd'
),
(
'
orderPage_transactionT
ype'
,
'sale'
),
(
'
transaction_t
ype'
,
'sale'
),
(
'orderNumber'
,
'33'
),
(
'access_key'
,
'123456789'
),
(
'merchantID'
,
'edx'
),
(
'djch'
,
'012345678912'
),
(
'orderPage_version'
,
2
),
(
'orderPage_serialNumber'
,
'1234567890'
),
(
'profile_id'
,
"00000001"
),
(
'reference_number'
,
10
),
(
'locale'
,
'en'
),
(
'signed_date_time'
,
'2014-08-18T13:59:31Z'
),
])
def
setUp
(
self
):
...
...
@@ -58,7 +61,7 @@ class PaymentFakeViewTest(TestCase):
post_params
=
sign
(
self
.
CLIENT_POST_PARAMS
)
# Tamper with the signature
post_params
[
'
orderPage_signaturePublic
'
]
=
"invalid"
post_params
[
'
signature
'
]
=
"invalid"
# Simulate a POST request from the payment workflow
# page to the fake payment page.
...
...
lms/djangoapps/shoppingcart/views.py
View file @
be1523f2
...
...
@@ -72,21 +72,16 @@ def show_cart(request):
total_cost
=
cart
.
total_cost
cart_items
=
cart
.
orderitem_set
.
all
()
# add the request protocol, domain, and port to the cart object so that any specific
# CC_PROCESSOR implementation can construct callback URLs, if necessary
cart
.
context
=
{
'request_domain'
:
'{0}://{1}'
.
format
(
'https'
if
request
.
is_secure
()
else
'http'
,
request
.
get_host
()
)
callback_url
=
request
.
build_absolute_uri
(
reverse
(
"shoppingcart.views.postpay_callback"
)
)
form_html
=
render_purchase_form_html
(
cart
,
callback_url
=
callback_url
)
context
=
{
'shoppingcart_items'
:
cart_items
,
'amount'
:
total_cost
,
'form_html'
:
form_html
,
}
form_html
=
render_purchase_form_html
(
cart
)
return
render_to_response
(
"shoppingcart/list.html"
,
{
'shoppingcart_items'
:
cart_items
,
'amount'
:
total_cost
,
'form_html'
:
form_html
,
})
return
render_to_response
(
"shoppingcart/list.html"
,
context
)
@login_required
...
...
lms/djangoapps/verify_student/tests/test_views.py
View file @
be1523f2
...
...
@@ -24,17 +24,23 @@ from django.conf import settings
from
django.core.urlresolvers
import
reverse
from
django.core.exceptions
import
ObjectDoesNotExist
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
,
mixed_store_config
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
student.tests.factories
import
UserFactory
from
student.models
import
CourseEnrollment
from
course_modes.tests.factories
import
CourseModeFactory
from
course_modes.models
import
CourseMode
from
verify_student.views
import
render_to_response
from
verify_student.models
import
SoftwareSecurePhotoVerification
from
reverification.tests.factories
import
MidcourseReverificationWindowFactory
# Since we don't need any XML course fixtures, use a modulestore configuration
# that disables the XML modulestore.
MODULESTORE_CONFIG
=
mixed_store_config
(
settings
.
COMMON_TEST_DATA_ROOT
,
{},
include_xml
=
False
)
def
mock_render_to_response
(
*
args
,
**
kwargs
):
return
render_to_response
(
*
args
,
**
kwargs
)
...
...
@@ -58,8 +64,8 @@ class StartView(TestCase):
self
.
assertHttpForbidden
(
self
.
client
.
get
(
self
.
start_url
()))
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestVerifyView
(
TestCase
):
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
TestVerifyView
(
ModuleStore
TestCase
):
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
(
username
=
"rusty"
,
password
=
"test"
)
self
.
client
.
login
(
username
=
"rusty"
,
password
=
"test"
)
...
...
@@ -93,8 +99,8 @@ class TestVerifyView(TestCase):
self
.
assertIn
(
"You are upgrading your registration for"
,
response
.
content
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestVerifiedView
(
TestCase
):
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
TestVerifiedView
(
ModuleStore
TestCase
):
"""
Tests for VerifiedView.
"""
...
...
@@ -121,8 +127,8 @@ class TestVerifiedView(TestCase):
self
.
assertIn
(
'dashboard'
,
response
.
_headers
.
get
(
'location'
)[
1
])
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestReverifyView
(
TestCase
):
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
TestReverifyView
(
ModuleStore
TestCase
):
"""
Tests for the reverification views
...
...
@@ -167,8 +173,8 @@ class TestReverifyView(TestCase):
self
.
assertTrue
(
context
[
'error'
])
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestPhotoVerificationResultsCallback
(
TestCase
):
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
TestPhotoVerificationResultsCallback
(
ModuleStore
TestCase
):
"""
Tests for the results_callback view.
"""
...
...
@@ -379,8 +385,8 @@ class TestPhotoVerificationResultsCallback(TestCase):
self
.
assertIsNotNone
(
CourseEnrollment
.
objects
.
get
(
course_id
=
self
.
course_id
))
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestMidCourseReverifyView
(
TestCase
):
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
TestMidCourseReverifyView
(
ModuleStore
TestCase
):
""" Tests for the midcourse reverification views """
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
(
username
=
"rusty"
,
password
=
"test"
)
...
...
@@ -490,8 +496,8 @@ class TestMidCourseReverifyView(TestCase):
self
.
assertEquals
(
response
.
status_code
,
200
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestReverificationBanner
(
TestCase
):
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
TestReverificationBanner
(
ModuleStore
TestCase
):
""" Tests for the midcourse reverification failed toggle banner off """
@patch.dict
(
settings
.
FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
...
...
@@ -511,3 +517,40 @@ class TestReverificationBanner(TestCase):
self
.
client
.
post
(
reverse
(
'verify_student_toggle_failed_banner_off'
))
photo_verification
=
SoftwareSecurePhotoVerification
.
objects
.
get
(
user
=
self
.
user
,
window
=
self
.
window
)
self
.
assertFalse
(
photo_verification
.
display
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
TestCreateOrder
(
ModuleStoreTestCase
):
""" Tests for the create order view. """
def
setUp
(
self
):
""" Create a user and course. """
self
.
user
=
UserFactory
.
create
(
username
=
"test"
,
password
=
"test"
)
self
.
course
=
CourseFactory
.
create
()
for
mode
in
(
'audit'
,
'honor'
,
'verified'
):
CourseModeFactory
(
mode_slug
=
mode
,
course_id
=
self
.
course
.
id
)
self
.
client
.
login
(
username
=
"test"
,
password
=
"test"
)
def
test_create_order_already_verified
(
self
):
# Verify the student so we don't need to submit photos
self
.
_verify_student
()
# Create an order
url
=
reverse
(
'verify_student_create_order'
)
params
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
}
response
=
self
.
client
.
post
(
url
,
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/"
)
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
()
lms/djangoapps/verify_student/views.py
View file @
be1523f2
...
...
@@ -25,7 +25,7 @@ from course_modes.models import CourseMode
from
student.models
import
CourseEnrollment
from
student.views
import
reverification_info
from
shoppingcart.models
import
Order
,
CertificateItem
from
shoppingcart.processors
.CyberSource
import
(
from
shoppingcart.processors
import
(
get_signed_purchase_params
,
get_purchase_endpoint
)
from
verify_student.models
import
(
...
...
@@ -219,7 +219,12 @@ def create_order(request):
enrollment_mode
=
current_mode
.
slug
CertificateItem
.
add_to_order
(
cart
,
course_id
,
amount
,
enrollment_mode
)
params
=
get_signed_purchase_params
(
cart
)
callback_url
=
request
.
build_absolute_uri
(
reverse
(
"shoppingcart.views.postpay_callback"
)
)
params
=
get_signed_purchase_params
(
cart
,
callback_url
=
callback_url
)
return
HttpResponse
(
json
.
dumps
(
params
),
content_type
=
"text/json"
)
...
...
lms/envs/acceptance.py
View file @
be1523f2
...
...
@@ -117,20 +117,6 @@ FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False
# verification.
FEATURES
[
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
]
=
True
# Configure the payment processor to use the fake processing page
# Since both the fake payment page and the shoppingcart app are using
# the same settings, we can generate this randomly and guarantee
# that they are using the same secret.
RANDOM_SHARED_SECRET
=
''
.
join
(
choice
(
string
.
letters
+
string
.
digits
+
string
.
punctuation
)
for
x
in
range
(
250
)
)
CC_PROCESSOR
[
'CyberSource'
][
'SHARED_SECRET'
]
=
RANDOM_SHARED_SECRET
CC_PROCESSOR
[
'CyberSource'
][
'MERCHANT_ID'
]
=
"edx"
CC_PROCESSOR
[
'CyberSource'
][
'SERIAL_NUMBER'
]
=
"0123456789012345678901"
CC_PROCESSOR
[
'CyberSource'
][
'PURCHASE_ENDPOINT'
]
=
"/shoppingcart/payment_fake"
# HACK
# Setting this flag to false causes imports to not load correctly in the lettuce python files
# We do not yet understand why this occurs. Setting this to true is a stopgap measure
...
...
lms/envs/aws.py
View file @
be1523f2
...
...
@@ -301,6 +301,7 @@ SEGMENT_IO_LMS_KEY = AUTH_TOKENS.get('SEGMENT_IO_LMS_KEY')
if
SEGMENT_IO_LMS_KEY
:
FEATURES
[
'SEGMENT_IO_LMS'
]
=
ENV_TOKENS
.
get
(
'SEGMENT_IO_LMS'
,
False
)
CC_PROCESSOR_NAME
=
AUTH_TOKENS
.
get
(
'CC_PROCESSOR_NAME'
,
CC_PROCESSOR_NAME
)
CC_PROCESSOR
=
AUTH_TOKENS
.
get
(
'CC_PROCESSOR'
,
CC_PROCESSOR
)
SECRET_KEY
=
AUTH_TOKENS
[
'SECRET_KEY'
]
...
...
lms/envs/common.py
View file @
be1523f2
...
...
@@ -757,7 +757,10 @@ EMBARGO_SITE_REDIRECT_URL = None
##### shoppingcart Payment #####
PAYMENT_SUPPORT_EMAIL
=
'payment@example.com'
##### Using cybersource by default #####
CC_PROCESSOR_NAME
=
'CyberSource'
CC_PROCESSOR
=
{
'CyberSource'
:
{
'SHARED_SECRET'
:
''
,
...
...
@@ -765,8 +768,15 @@ CC_PROCESSOR = {
'SERIAL_NUMBER'
:
''
,
'ORDERPAGE_VERSION'
:
'7'
,
'PURCHASE_ENDPOINT'
:
''
,
},
'CyberSource2'
:
{
"PURCHASE_ENDPOINT"
:
''
,
"SECRET_KEY"
:
''
,
"ACCESS_KEY"
:
''
,
"PROFILE_ID"
:
''
,
}
}
# Setting for PAID_COURSE_REGISTRATION, DOES NOT AFFECT VERIFIED STUDENTS
PAID_COURSE_REGISTRATION_CURRENCY
=
[
'usd'
,
'$'
]
...
...
lms/envs/dev.py
View file @
be1523f2
...
...
@@ -281,9 +281,13 @@ if SEGMENT_IO_LMS_KEY:
CC_PROCESSOR
[
'CyberSource'
][
'SHARED_SECRET'
]
=
os
.
environ
.
get
(
'CYBERSOURCE_SHARED_SECRET'
,
''
)
CC_PROCESSOR
[
'CyberSource'
][
'MERCHANT_ID'
]
=
os
.
environ
.
get
(
'CYBERSOURCE_MERCHANT_ID'
,
''
)
CC_PROCESSOR
[
'CyberSource'
][
'SERIAL_NUMBER'
]
=
os
.
environ
.
get
(
'CYBERSOURCE_SERIAL_NUMBER'
,
''
)
#CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_PURCHASE_ENDPOINT', '')
CC_PROCESSOR
[
'CyberSource'
][
'PURCHASE_ENDPOINT'
]
=
'/shoppingcart/payment_fake/'
CC_PROCESSOR
[
'CyberSource2'
][
'SECRET_KEY'
]
=
os
.
environ
.
get
(
'CYBERSOURCE_SECRET_KEY'
,
''
)
CC_PROCESSOR
[
'CyberSource2'
][
'ACCESS_KEY'
]
=
os
.
environ
.
get
(
'CYBERSOURCE_ACCESS_KEY'
,
''
)
CC_PROCESSOR
[
'CyberSource2'
][
'PROFILE_ID'
]
=
os
.
environ
.
get
(
'CYBERSOURCE_PROFILE_ID'
,
''
)
CC_PROCESSOR
[
'CyberSource2'
][
'PURCHASE_ENDPOINT'
]
=
'/shoppingcart/payment_fake/'
########################## USER API ##########################
EDX_API_KEY
=
None
...
...
lms/envs/devstack.py
View file @
be1523f2
...
...
@@ -83,7 +83,9 @@ PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon
FEATURES
[
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
]
=
True
FEATURES
[
'ENABLE_PAYMENT_FAKE'
]
=
True
CC_PROCESSOR
[
'CyberSource'
][
'PURCHASE_ENDPOINT'
]
=
'/shoppingcart/payment_fake/'
for
processor
in
CC_PROCESSOR
.
values
():
processor
[
'PURCHASE_ENDPOINT'
]
=
'/shoppingcart/payment_fake/'
#####################################################################
# See if the developer has any local overrides.
...
...
lms/envs/test.py
View file @
be1523f2
...
...
@@ -220,6 +220,7 @@ OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
###################### Payment ##############################3
# Enable fake payment processing page
FEATURES
[
'ENABLE_PAYMENT_FAKE'
]
=
True
# Configure the payment processor to use the fake processing page
# Since both the fake payment page and the shoppingcart app are using
# the same settings, we can generate this randomly and guarantee
...
...
@@ -231,10 +232,13 @@ RANDOM_SHARED_SECRET = ''.join(
for
x
in
range
(
250
)
)
CC_PROCESSOR
[
'CyberSource'
][
'SHARED_SECRET'
]
=
RANDOM_SHARED_SECRET
CC_PROCESSOR
[
'CyberSource'
][
'MERCHANT_ID'
]
=
"edx"
CC_PROCESSOR
[
'CyberSource'
][
'SERIAL_NUMBER'
]
=
"0123456789012345678901"
CC_PROCESSOR
[
'CyberSource'
][
'PURCHASE_ENDPOINT'
]
=
"/shoppingcart/payment_fake"
CC_PROCESSOR_NAME
=
'CyberSource2'
CC_PROCESSOR
[
'CyberSource2'
][
'SECRET_KEY'
]
=
RANDOM_SHARED_SECRET
CC_PROCESSOR
[
'CyberSource2'
][
'ACCESS_KEY'
]
=
"0123456789012345678901"
CC_PROCESSOR
[
'CyberSource2'
][
'PROFILE_ID'
]
=
"edx"
CC_PROCESSOR
[
'CyberSource2'
][
'PURCHASE_ENDPOINT'
]
=
"/shoppingcart/payment_fake"
FEATURES
[
'STORE_BILLING_INFO'
]
=
True
########################### SYSADMIN DASHBOARD ################################
FEATURES
[
'ENABLE_SYSADMIN_DASHBOARD'
]
=
True
...
...
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