Commit 4de3881c by Stephen Sanchez

Merge pull request #52 from edx/sanchez/add_receipt_page_logic

XCOM-167: Adding logic to allow direction to the LMS receipt page.
parents 9980e5b3 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
......@@ -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)
......@@ -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)
SingleSeatCybersource().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] = SingleSeatCybersource()._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."""
......
......@@ -117,7 +117,7 @@ ORDERS_ENDPOINT_RATE_LIMIT = '40/minute'
# PAYMENT PROCESSING
PAYMENT_PROCESSORS = (
'ecommerce.extensions.payment.processors.Cybersource',
'ecommerce.extensions.payment.processors.SingleSeatCybersource',
)
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
......
......@@ -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
......
......@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment