Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
ecommerce
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
ecommerce
Commits
e43a6dec
Commit
e43a6dec
authored
Apr 07, 2015
by
Stephen Sanchez
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Adding logic to allow direction to the LMS receipt page.
parent
6e3770d0
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
133 additions
and
13 deletions
+133
-13
ecommerce/extensions/payment/errors.py
+5
-0
ecommerce/extensions/payment/processors.py
+58
-1
ecommerce/extensions/payment/tests/test_processors.py
+53
-11
ecommerce/settings/_oscar.py
+5
-1
ecommerce/settings/local.py
+4
-0
ecommerce/settings/test.py
+8
-0
No files found.
ecommerce/extensions/payment/errors.py
View file @
e43a6dec
...
...
@@ -39,3 +39,8 @@ class UserCancelled(CybersourceError):
class
PaymentDeclined
(
CybersourceError
):
"""Payment declined."""
pass
class
UnsupportedProductError
(
CybersourceError
):
"""Cannot generate a receipt for the given product type in this order. """
pass
ecommerce/extensions/payment/processors.py
View file @
e43a6dec
...
...
@@ -12,7 +12,7 @@ from ecommerce.extensions.order.models import Order
from
ecommerce.extensions.payment.helpers
import
sign
from
ecommerce.extensions.payment.errors
import
(
ExcessiveMerchantDefinedData
,
UserCancelled
,
PaymentDeclined
,
SignatureException
,
CybersourceError
,
WrongAmountException
,
DataException
CybersourceError
,
WrongAmountException
,
DataException
,
UnsupportedProductError
)
from
ecommerce.extensions.payment.constants
import
CybersourceConstants
as
CS
from
ecommerce.extensions.payment.constants
import
ProcessorConstants
as
PC
...
...
@@ -74,6 +74,8 @@ class Cybersource(BasePaymentProcessor):
self
.
profile_id
=
configuration
[
'profile_id'
]
self
.
access_key
=
configuration
[
'access_key'
]
self
.
secret_key
=
configuration
[
'secret_key'
]
self
.
receipt_page_url
=
configuration
[
'receipt_page_url'
]
self
.
cancel_page_url
=
configuration
[
'cancel_page_url'
]
self
.
language_code
=
settings
.
LANGUAGE_CODE
def
get_transaction_parameters
(
...
...
@@ -219,9 +221,13 @@ class Cybersource(BasePaymentProcessor):
if
receipt_page_url
:
parameters
[
CS
.
FIELD_NAMES
.
OVERRIDE_CUSTOM_RECEIPT_PAGE
]
=
receipt_page_url
elif
self
.
receipt_page_url
:
parameters
[
CS
.
FIELD_NAMES
.
OVERRIDE_CUSTOM_RECEIPT_PAGE
]
=
self
.
_generate_receipt_url
(
order
)
if
cancel_page_url
:
parameters
[
CS
.
FIELD_NAMES
.
OVERRIDE_CUSTOM_CANCEL_PAGE
]
=
cancel_page_url
elif
self
.
cancel_page_url
:
parameters
[
CS
.
FIELD_NAMES
.
OVERRIDE_CUSTOM_CANCEL_PAGE
]
=
self
.
cancel_page_url
if
merchant_defined_data
:
if
len
(
merchant_defined_data
)
>
CS
.
MAX_OPTIONAL_FIELDS
:
...
...
@@ -244,6 +250,22 @@ class Cybersource(BasePaymentProcessor):
return
parameters
def
_generate_receipt_url
(
self
,
order
):
"""Generate the full receipt URL based off the order.
Takes the receipt page URL and modifies it to display a single order.
Args:
order (Order): The order the receipt represents
Returns:
string: The string representation of the receipt URL for this order.
"""
return
"{base_url}?payment-order-num={order_number}"
.
format
(
base_url
=
self
.
receipt_page_url
,
order_number
=
order
.
number
)
def
_generate_signature
(
self
,
parameters
):
"""Sign the contents of the provided transaction parameters dictionary.
...
...
@@ -391,3 +413,38 @@ class Cybersource(BasePaymentProcessor):
"The payment processor accepted an order with number [{number}] that is not in our system."
.
format
(
number
=
order_num
)
)
class
SingleSeatCybersource
(
Cybersource
):
"""Payment Processor limited to supporting a single seat. """
def
_generate_receipt_url
(
self
,
order
):
"""Generate the full receipt URL based off the order.
Takes the receipt page URL and modifies it to display a single order.
Args:
order (Order): The order the receipt represents
Returns:
string: The string representation of the receipt URL for this order.
"""
# TODO: Right now, our receipt page only supports the purchase of Course Seats, and assumes that an order
# is relative to a single course. This function will try and get a course ID to construct the URL. Once our
# receipt page supports donations, cohorts, and other products, we will need a generic URL that can be
# constructed simply from the order number.
# This issue should be resolved by completing JIRA Ticket XCOM-202
line
=
order
.
lines
.
all
()[
0
]
if
line
and
line
.
product
.
get_product_class
()
.
name
==
'Seat'
:
course_key
=
line
.
product
.
attribute_values
.
get
(
attribute__name
=
"course_key"
)
.
value
return
"{base_url}{course_key}/?payment-order-num={order_number}"
.
format
(
base_url
=
self
.
receipt_page_url
,
course_key
=
course_key
,
order_number
=
order
.
number
)
else
:
msg
=
(
u'Cannot construct a receipt URL for order [{order_number}]. Receipt page only supports Seat products.'
.
format
(
order_number
=
order
.
number
)
)
logger
.
error
(
msg
)
raise
UnsupportedProductError
(
msg
)
ecommerce/extensions/payment/tests/test_processors.py
View file @
e43a6dec
...
...
@@ -8,15 +8,15 @@ import logging
import
ddt
from
django.conf
import
settings
from
django.contrib.auth
import
get_user_model
from
django.test
import
TestCase
from
django.test
import
TestCase
,
override_settings
import
mock
from
nose.tools
import
raises
from
oscar.test
import
factories
import
ecommerce.extensions.payment.processors
as
processors
from
ecommerce.extensions.order.models
import
Order
from
ecommerce.extensions.payment.processors
import
BasePaymentProcessor
,
Cybersource
from
ecommerce.extensions.payment.errors
import
ExcessiveMerchantDefinedData
from
ecommerce.extensions.payment.processors
import
BasePaymentProcessor
,
Cybersource
,
SingleSeatCybersource
from
ecommerce.extensions.payment.errors
import
ExcessiveMerchantDefinedData
,
UnsupportedProductError
from
ecommerce.extensions.payment.constants
import
CybersourceConstants
as
CS
from
ecommerce.extensions.payment.constants
import
ProcessorConstants
as
PC
from
ecommerce.extensions.fulfillment.status
import
ORDER
...
...
@@ -37,17 +37,32 @@ class PaymentProcessorTestCase(TestCase):
username
=
'Gus'
,
email
=
'gustavo@lospolloshermanos.com'
,
password
=
'the-chicken-man'
)
product_class
=
factories
.
ProductClassFactory
(
name
=
u'𝕽𝖊𝖘𝖙𝖆𝖚𝖗𝖆𝖓𝖙
'
,
self
.
product_class
=
factories
.
ProductClassFactory
(
name
=
'Seat
'
,
requires_shipping
=
False
,
track_stock
=
False
)
product_attribute
=
factories
.
ProductAttributeFactory
(
name
=
'course_key'
,
code
=
'course_key'
,
product_class
=
self
.
product_class
,
type
=
'text'
)
fried_chicken
=
factories
.
ProductFactory
(
structure
=
'parent'
,
title
=
u'𝑭𝒓𝒊𝒆𝒅 𝑪𝒉𝒊𝒄𝒌𝒆𝒏'
,
product_class
=
product_class
,
product_class
=
self
.
product_class
,
stockrecords
=
None
,
)
factories
.
ProductAttributeValueFactory
(
attribute
=
product_attribute
,
product
=
fried_chicken
,
value_text
=
'pollos/chickenX/2015'
)
pollos_hermanos
=
factories
.
ProductFactory
(
structure
=
'child'
,
parent
=
fried_chicken
,
...
...
@@ -56,6 +71,12 @@ class PaymentProcessorTestCase(TestCase):
stockrecords__price_excl_tax
=
D
(
'9.99'
),
)
self
.
attribute_value
=
factories
.
ProductAttributeValueFactory
(
attribute
=
product_attribute
,
product
=
pollos_hermanos
,
value_text
=
'pollos/hermanosX/2015'
)
basket
=
factories
.
create_basket
(
empty
=
True
)
basket
.
add_product
(
pollos_hermanos
,
1
)
...
...
@@ -142,17 +163,38 @@ class CybersourceParameterGenerationTests(CybersourceTests):
merchant_defined_data
=
self
.
MERCHANT_DEFINED_DATA
)
@raises
(
UnsupportedProductError
)
def
test_receipt_error
(
self
):
"""Test that a single seat CyberSource processor will not construct a receipt for an unknown product. """
self
.
product_class
.
name
=
'Not A Seat'
self
.
product_class
.
save
()
self
.
_assert_order_parameters
(
self
.
order
)
@raises
(
ExcessiveMerchantDefinedData
)
def
test_excessive_merchant_defined_data
(
self
):
"""Test that excessive merchant-defined data is not accepted."""
# Generate a list of strings with a number of elements exceeding the maximum number
# of optional fields allowed by CyberSource
excessive_data
=
[
unicode
(
i
)
for
i
in
xrange
(
CS
.
MAX_OPTIONAL_FIELDS
+
1
)]
Cybersource
()
.
get_transaction_parameters
(
self
.
order
,
merchant_defined_data
=
excessive_data
)
SingleSeat
Cybersource
()
.
get_transaction_parameters
(
self
.
order
,
merchant_defined_data
=
excessive_data
)
def
_assert_order_parameters
(
self
,
order
,
receipt_page_url
=
None
,
cancel_page_url
=
None
,
merchant_defined_data
=
None
):
"""Verify that returned transaction parameters match expectations."""
returned_parameters
=
Cybersource
()
.
get_transaction_parameters
(
expected_receipt_page_url
=
receipt_page_url
if
not
receipt_page_url
:
expected_receipt_page_url
=
'{receipt_url}{course_key}/?payment-order-num={order_number}'
.
format
(
receipt_url
=
settings
.
PAYMENT_PROCESSOR_CONFIG
[
'cybersource'
][
'receipt_page_url'
],
course_key
=
self
.
attribute_value
.
value
,
order_number
=
self
.
order
.
number
)
if
not
cancel_page_url
:
cancel_page_url
=
settings
.
PAYMENT_PROCESSOR_CONFIG
[
'cybersource'
][
'cancel_page_url'
]
returned_parameters
=
SingleSeatCybersource
()
.
get_transaction_parameters
(
order
,
receipt_page_url
=
receipt_page_url
,
cancel_page_url
=
cancel_page_url
,
...
...
@@ -172,8 +214,7 @@ class CybersourceParameterGenerationTests(CybersourceTests):
(
CS
.
FIELD_NAMES
.
LOCALE
,
getattr
(
settings
,
'LANGUAGE_CODE'
)),
])
if
receipt_page_url
:
expected_parameters
[
CS
.
FIELD_NAMES
.
OVERRIDE_CUSTOM_RECEIPT_PAGE
]
=
receipt_page_url
expected_parameters
[
CS
.
FIELD_NAMES
.
OVERRIDE_CUSTOM_RECEIPT_PAGE
]
=
expected_receipt_page_url
if
cancel_page_url
:
expected_parameters
[
CS
.
FIELD_NAMES
.
OVERRIDE_CUSTOM_CANCEL_PAGE
]
=
cancel_page_url
...
...
@@ -191,11 +232,12 @@ class CybersourceParameterGenerationTests(CybersourceTests):
# Generate a comma-separated list of keys and values to be signed. CyberSource refers to this
# as a 'Version 1' signature in their documentation.
# pylint: disable=protected-access
expected_parameters
[
CS
.
FIELD_NAMES
.
SIGNATURE
]
=
Cybersource
()
.
_generate_signature
(
expected_parameters
)
expected_parameters
[
CS
.
FIELD_NAMES
.
SIGNATURE
]
=
SingleSeat
Cybersource
()
.
_generate_signature
(
expected_parameters
)
self
.
assertEqual
(
returned_parameters
,
expected_parameters
)
@override_settings
(
PAYMENT_PROCESSORS
=
(
'ecommerce.extensions.payment.processors.SingleSeatCybersource'
,))
@ddt.ddt
class
CybersourcePaymentAcceptanceTests
(
CybersourceTests
):
"""Tests of the CyberSource processor class related to checking response."""
...
...
ecommerce/settings/_oscar.py
View file @
e43a6dec
...
...
@@ -117,7 +117,7 @@ ORDERS_ENDPOINT_RATE_LIMIT = '40/minute'
# PAYMENT PROCESSING
PAYMENT_PROCESSORS
=
(
'ecommerce.extensions.payment.processors.Cybersource'
,
'ecommerce.extensions.payment.processors.
SingleSeat
Cybersource'
,
)
PAYMENT_PROCESSOR_CONFIG
=
{
...
...
@@ -126,6 +126,10 @@ PAYMENT_PROCESSOR_CONFIG = {
'access_key'
:
'set-me-please'
,
'secret_key'
:
'set-me-please'
,
'pay_endpoint'
:
'https://replace-me/'
,
# TODO: XCOM-202 must be completed before any other receipt page is used.
# By design this specific receipt page is expected.
'receipt_page_url'
:
'https://replace-me/verify_student/payment-confirmation/'
,
'cancel_page_url'
:
'https://replace-me/'
,
}
}
# END PAYMENT PROCESSING
...
...
ecommerce/settings/local.py
View file @
e43a6dec
...
...
@@ -106,6 +106,10 @@ PAYMENT_PROCESSOR_CONFIG = {
'access_key'
:
'fake-access-key'
,
'secret_key'
:
'fake-secret-key'
,
'pay_endpoint'
:
'https://replace-me/'
,
# TODO: XCOM-202 must be completed before any other receipt page is used.
# By design this specific receipt page is expected.
'receipt_page_url'
:
'https://replace-me/verify_student/payment-confirmation/'
,
'cancel_page_url'
:
'https://replace-me/'
,
}
}
# END PAYMENT PROCESSING
...
...
ecommerce/settings/test.py
View file @
e43a6dec
...
...
@@ -82,12 +82,20 @@ EDX_API_KEY = 'replace-me'
# PAYMENT PROCESSING
PAYMENT_PROCESSORS
=
(
'ecommerce.extensions.payment.processors.Cybersource'
,
)
PAYMENT_PROCESSOR_CONFIG
=
{
'cybersource'
:
{
'profile_id'
:
'fake-profile-id'
,
'access_key'
:
'fake-access-key'
,
'secret_key'
:
'fake-secret-key'
,
'pay_endpoint'
:
'https://replace-me/'
,
# TODO: XCOM-202 must be completed before any other receipt page is used.
# By design this specific receipt page is expected.
'receipt_page_url'
:
'https://replace-me/verify_student/payment-confirmation/'
,
'cancel_page_url'
:
'https://replace-me/'
,
}
}
# END PAYMENT PROCESSING
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