Commit a5c9b891 by jsa

Report course_key and certificate_type to cybersource.

XCOM-274
parent 23bf2cd4
...@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__) ...@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
PaymentEvent = get_model('order', 'PaymentEvent') PaymentEvent = get_model('order', 'PaymentEvent')
PaymentEventType = get_model('order', 'PaymentEventType') PaymentEventType = get_model('order', 'PaymentEventType')
PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse') PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse')
ProductClass = get_model('catalogue', 'ProductClass')
Source = get_model('payment', 'Source') Source = get_model('payment', 'Source')
SourceType = get_model('payment', 'SourceType') SourceType = get_model('payment', 'SourceType')
...@@ -149,7 +150,13 @@ class Cybersource(BasePaymentProcessor): ...@@ -149,7 +150,13 @@ class Cybersource(BasePaymentProcessor):
u'override_custom_cancel_page': self.cancel_page_url, u'override_custom_cancel_page': self.cancel_page_url,
} }
# TODO Include edX-specific data (e.g. course_id, seat type) # XCOM-274: when internal reporting across all processors is
# operational, these custom fields will no longer be needed and should
# be removed.
single_seat = self.get_single_seat(basket)
if single_seat:
parameters[u'merchant_defined_data1'] = single_seat.attr.course_key
parameters[u'merchant_defined_data2'] = single_seat.attr.certificate_type
# Sign all fields # Sign all fields
signed_field_names = parameters.keys() signed_field_names = parameters.keys()
...@@ -158,6 +165,20 @@ class Cybersource(BasePaymentProcessor): ...@@ -158,6 +165,20 @@ class Cybersource(BasePaymentProcessor):
return parameters return parameters
@staticmethod
def get_single_seat(basket):
"""
Return the first product encountered in the basket with the product
class of 'seat'. Return None if no such products were found.
"""
try:
seat_class = ProductClass.objects.get(slug='seat')
except ProductClass.DoesNotExist:
# this occurs in test configurations where the seat product class is not in use
return None
products = [line.product for line in basket.lines.all() if line.product.product_class == seat_class]
return products[0] if products else None
def handle_processor_response(self, response, basket=None): def handle_processor_response(self, response, basket=None):
""" """
Handle a response from the payment processor. Handle a response from the payment processor.
......
...@@ -31,10 +31,26 @@ class PaymentProcessorTestCaseMixin(PaymentEventsMixin): ...@@ -31,10 +31,26 @@ class PaymentProcessorTestCaseMixin(PaymentEventsMixin):
# This value is used to test the NAME attribute on the processor. # This value is used to test the NAME attribute on the processor.
processor_name = None processor_name = None
COURSE_KEY = 'test-course-key'
CERTIFICATE_TYPE = 'test-certificate-type'
def setUp(self): def setUp(self):
super(PaymentProcessorTestCaseMixin, self).setUp() super(PaymentProcessorTestCaseMixin, self).setUp()
# certain logic and tests expect to find products with class 'seat' in
# baskets, so that's done explicitly in this setup.
self.product_class = factories.ProductClassFactory(slug='seat', requires_shipping=False, track_stock=False)
self.product_class.save()
factories.ProductAttributeFactory(code='course_key', product_class=self.product_class, type="text").save()
factories.ProductAttributeFactory(code='certificate_type', product_class=self.product_class, type="text").save()
self.product = factories.ProductFactory(upc='dummy-upc', title='dummy-title', product_class=self.product_class)
self.product.attr.course_key = self.COURSE_KEY
self.product.attr.certificate_type = self.CERTIFICATE_TYPE
self.product.save()
self.processor = self.processor_class() # pylint: disable=not-callable self.processor = self.processor_class() # pylint: disable=not-callable
self.basket = factories.create_basket() self.basket = factories.create_basket(empty=True)
self.basket.add_product(self.product)
self.basket.owner = factories.UserFactory() self.basket.owner = factories.UserFactory()
self.basket.save() self.basket.save()
...@@ -94,6 +110,8 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase ...@@ -94,6 +110,8 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase
u'consumer_id': self.basket.owner.username, u'consumer_id': self.basket.owner.username,
u'override_custom_receipt_page': u'{}?basket_id={}'.format(self.processor.receipt_page_url, self.basket.id), u'override_custom_receipt_page': u'{}?basket_id={}'.format(self.processor.receipt_page_url, self.basket.id),
u'override_custom_cancel_page': self.processor.cancel_page_url, u'override_custom_cancel_page': self.processor.cancel_page_url,
u'merchant_defined_data1': self.COURSE_KEY,
u'merchant_defined_data2': self.CERTIFICATE_TYPE,
} }
signed_field_names = expected.keys() + [u'transaction_uuid'] signed_field_names = expected.keys() + [u'transaction_uuid']
...@@ -173,3 +191,39 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase ...@@ -173,3 +191,39 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase
response = self.generate_notification(self.processor.secret_key, self.basket, auth_amount=u'0.00') response = self.generate_notification(self.processor.secret_key, self.basket, auth_amount=u'0.00')
self.assertRaises(PartialAuthorizationError, self.processor.handle_processor_response, response, self.assertRaises(PartialAuthorizationError, self.processor.handle_processor_response, response,
basket=self.basket) basket=self.basket)
def test_get_single_seat(self):
"""
The single-seat helper for cybersource reporting should correctly
and return the first 'seat' product encountered in a basket.
"""
get_single_seat = processors.Cybersource.get_single_seat
# finds the seat when it's the only product in the basket.
self.assertEqual(get_single_seat(self.basket), self.product)
# finds the first seat added, when there's more than one.
basket = factories.create_basket(empty=True)
other_seat = factories.ProductFactory(upc='other-upc', title='other-title', product_class=self.product_class)
other_seat.save()
basket.add_product(self.product)
basket.add_product(other_seat)
self.assertEqual(get_single_seat(basket), self.product)
# finds the seat when there's a mixture of product classes.
basket = factories.create_basket(empty=True)
other_product = factories.ProductFactory(upc='not-a-seat', title='not-a-seat')
other_product.save()
basket.add_product(other_product)
basket.add_product(self.product)
self.assertEqual(get_single_seat(basket), self.product)
self.assertNotEqual(get_single_seat(basket), other_product)
# returns None when there's no seats.
basket = factories.create_basket(empty=True)
basket.add_product(other_product)
self.assertIsNone(get_single_seat(basket))
# returns None for an empty basket.
basket = factories.create_basket(empty=True)
self.assertIsNone(get_single_seat(basket))
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