def setUp(self): super(CybersourceNotificationTestsMixin, self).setUp() self.user = UserFactory() self.billing_address = self.make_billing_address() self.basket = create_basket(owner=self.user, site=self.site) self.basket.freeze() self.processor = Cybersource(self.site) self.processor_name = self.processor.NAME
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user logger.info('Valid payment form submitted for basket [%d].', basket.id) # Ensure we aren't attempting to purchase a basket that has already been purchased, frozen, # or merged with another basket. if basket.status != Basket.OPEN: logger.error( 'Basket %d must be in the "Open" state. It is currently in the "%s" state.', basket.id, basket.status) error_msg = _( 'Your basket may have been modified or already purchased. Refresh the page to try again.' ) return self._basket_error_response(error_msg) basket.strategy = request.strategy Applicator().apply(basket, user, self.request) # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, 'device_fingerprint_id': request.session.session_key, } for source, destination in six.iteritems(self.FIELD_MAPPINGS): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) # Freeze the basket since the user is paying for it now. basket.freeze() return response
def setUp(self): super(CybersourceNotifyViewTests, self).setUp() self.user = factories.UserFactory() self.billing_address = self.make_billing_address() self.basket = factories.create_basket() self.basket.owner = self.user self.basket.freeze() self.processor = Cybersource() self.processor_name = self.processor.NAME
def setUp(self): super(CybersourceNotificationTestsMixin, self).setUp() self.toggle_ecommerce_receipt_page(True) self.user = factories.UserFactory() self.billing_address = self.make_billing_address() self.basket = factories.create_basket() self.basket.owner = self.user self.basket.freeze() self.processor = Cybersource(self.site) self.processor_name = self.processor.NAME
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user sdn_check_failure = self.check_sdn(request, data) if sdn_check_failure is not None: return sdn_check_failure # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, # Fall back to order number when there is no session key (JWT auth) 'device_fingerprint_id': request.session.session_key or basket.order_number, } for source, destination in self.FIELD_MAPPINGS.items(): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', # TODO: transaction_id is None in logs. This should be fixed. parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) basket_add_organization_attribute(basket, data) # Freeze the basket since the user is paying for it now. basket.freeze() return response
def setUp(self): super(CybersourceNotifyViewTests, self).setUp() self.site.siteconfiguration.enable_otto_receipt_page = True self.user = factories.UserFactory() self.billing_address = self.make_billing_address() self.basket = factories.create_basket() self.basket.owner = self.user self.basket.freeze() self.processor = Cybersource(self.site) self.processor_name = self.processor.NAME
def test_client_side_checkout(self): """ Verify the view returns the data necessary to initiate client-side checkout. """ seat = self.create_seat(self.course) basket = self.create_basket_and_add_product(seat) response = self.client.get(self.get_full_url(self.path)) self.assertEqual(response.status_code, 200) expected = { 'enable_client_side_checkout': True, 'payment_url': Cybersource(self.site).client_side_payment_url, } self.assertDictContainsSubset(expected, response.context) payment_form = response.context['payment_form'] self.assertIsInstance(payment_form, PaymentForm) self.assertEqual(payment_form.initial['basket'], basket)
def test_duplicate_order_attempt_logging(self): """ Verify that attempts at creation of a duplicate order are logged correctly """ prior_order = create_order() dummy_request = RequestFactory(SERVER_NAME='testserver.fake').get('') dummy_mixin = OrderCreationMixin() dummy_mixin.payment_processor = Cybersource(self.site) with LogCapture(self.DUPLICATE_ORDER_LOGGER_NAME) as lc: with self.assertRaises(ValueError): dummy_mixin.create_order(dummy_request, prior_order.basket, None) lc.check((self.DUPLICATE_ORDER_LOGGER_NAME, 'ERROR', self.get_duplicate_order_error_message( payment_processor='Cybersource', order=prior_order)), )
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, 'device_fingerprint_id': request.session.session_key, } for source, destination in six.iteritems(self.FIELD_MAPPINGS): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) # Freeze the basket since the user is paying for it now. basket.freeze() return response
def payment_processor(self): return Cybersource(self.request.site)
class CybersourceNotifyView(EdxOrderPlacementMixin, View): """ Validates a response from CyberSource and processes the associated basket/order appropriately. """ payment_processor = Cybersource() @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): return super(CybersourceNotifyView, self).dispatch(request, *args, **kwargs) def _get_billing_address(self, cybersource_response): return BillingAddress( first_name=cybersource_response['req_bill_to_forename'], last_name=cybersource_response['req_bill_to_surname'], line1=cybersource_response['req_bill_to_address_line1'], # Address line 2 is optional line2=cybersource_response.get('req_bill_to_address_line2', ''), # Oscar uses line4 for city line4=cybersource_response['req_bill_to_address_city'], postcode=cybersource_response['req_bill_to_address_postal_code'], # State is optional state=cybersource_response.get('req_bill_to_address_state', ''), country=Country.objects.get(iso_3166_1_a2=cybersource_response[ 'req_bill_to_address_country'])) def _get_basket(self, basket_id): if not basket_id: return None try: basket_id = int(basket_id) basket = Basket.objects.get(id=basket_id) basket.strategy = strategy.Default() return basket except (ValueError, ObjectDoesNotExist): return None @transaction.non_atomic_requests def post(self, request): """Process a CyberSource merchant notification and place an order for paid products as appropriate.""" # Note (CCB): Orders should not be created until the payment processor has validated the response's signature. # This validation is performed in the handle_payment method. After that method succeeds, the response can be # safely assumed to have originated from CyberSource. cybersource_response = request.POST.dict() basket = None transaction_id = None try: transaction_id = cybersource_response.get('transaction_id') order_number = cybersource_response.get('req_reference_number') basket_id = OrderNumberGenerator().basket_id(order_number) logger.info( 'Received CyberSource merchant notification for transaction [%s], associated with basket [%d].', transaction_id, basket_id) basket = self._get_basket(basket_id) if not basket: logger.error('Received payment for non-existent basket [%s].', basket_id) return HttpResponse(status=400) finally: # Store the response in the database regardless of its authenticity. ppr = self.payment_processor.record_processor_response( cybersource_response, transaction_id=transaction_id, basket=basket) try: # Explicitly delimit operations which will be rolled back if an exception occurs. with transaction.atomic(): try: self.handle_payment(cybersource_response, basket) except InvalidSignatureError: logger.exception( 'Received an invalid CyberSource response. The payment response was recorded in entry [%d].', ppr.id) return HttpResponse(status=400) except (UserCancelled, TransactionDeclined) as exception: logger.info( 'CyberSource payment did not complete for basket [%d] because [%s]. ' 'The payment response was recorded in entry [%d].', basket.id, exception.__class__.__name__, ppr.id) return HttpResponse() except PaymentError: logger.exception( 'CyberSource payment failed for basket [%d]. The payment response was recorded in entry [%d].', basket.id, ppr.id) return HttpResponse() except: # pylint: disable=bare-except logger.exception( 'Attempts to handle payment for basket [%d] failed.', basket.id) return HttpResponse(status=500) try: # Note (CCB): In the future, if we do end up shipping physical products, we will need to # properly implement shipping methods. For more, see # http://django-oscar.readthedocs.org/en/latest/howto/how_to_configure_shipping.html. shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) # Note (CCB): This calculation assumes the payment processor has not sent a partial authorization, # thus we use the amounts stored in the database rather than those received from the payment processor. order_total = OrderTotalCalculator().calculate( basket, shipping_charge) billing_address = self._get_billing_address(cybersource_response) user = basket.owner self.handle_order_placement(order_number, user, basket, None, shipping_method, shipping_charge, billing_address, order_total) return HttpResponse() except: # pylint: disable=bare-except logger.exception(self.order_placement_failure_msg, basket.id) return HttpResponse(status=500)
def get_expected_transaction_parameters(self, basket, transaction_uuid, include_level_2_3_details=True, processor=None, use_sop_profile=False, **kwargs): """ Builds expected transaction parameters dictionary. Note: Callers should separately validate the transaction_uuid parameter to ensure it is a valid UUID. """ processor = processor or Cybersource(self.site) configuration = settings.PAYMENT_PROCESSOR_CONFIG['edx'][processor.NAME] access_key = configuration['sop_access_key'] if use_sop_profile else configuration['access_key'] profile_id = configuration['sop_profile_id'] if use_sop_profile else configuration['profile_id'] secret_key = configuration['sop_secret_key'] if use_sop_profile else configuration['secret_key'] expected = { 'access_key': access_key, 'profile_id': profile_id, 'transaction_uuid': transaction_uuid, 'signed_field_names': '', 'unsigned_field_names': '', 'signed_date_time': datetime.datetime.utcnow().strftime(ISO_8601_FORMAT), 'locale': settings.LANGUAGE_CODE, 'transaction_type': 'sale', 'reference_number': basket.order_number, 'amount': unicode(basket.total_incl_tax), 'currency': basket.currency, 'override_custom_receipt_page': basket.site.siteconfiguration.build_ecommerce_url( reverse('cybersource_redirect') ), 'override_custom_cancel_page': processor.cancel_page_url, } if include_level_2_3_details: expected.update({ 'line_item_count': basket.lines.count(), 'amex_data_taa1': basket.site.name, 'purchasing_level': '3', 'user_po': 'BLANK', }) for index, line in enumerate(basket.lines.all()): expected['item_{}_code'.format(index)] = line.product.get_product_class().slug expected['item_{}_discount_amount '.format(index)] = str(line.discount_value) expected['item_{}_gross_net_indicator'.format(index)] = 'Y' expected['item_{}_name'.format(index)] = line.product.title expected['item_{}_quantity'.format(index)] = line.quantity expected['item_{}_sku'.format(index)] = line.stockrecord.partner_sku expected['item_{}_tax_amount'.format(index)] = str(line.line_tax) expected['item_{}_tax_rate'.format(index)] = '0' expected['item_{}_total_amount '.format(index)] = str(line.line_price_incl_tax_incl_discounts) expected['item_{}_unit_of_measure'.format(index)] = 'ITM' expected['item_{}_unit_price'.format(index)] = str(line.unit_price_incl_tax) if not use_sop_profile: expected['consumer_id'] = basket.owner.username # Add the extra parameters expected.update(kwargs.get('extra_parameters', {})) # Generate a signature expected['signed_field_names'] = ','.join(sorted(expected.keys())) expected['signature'] = self.generate_signature(secret_key, expected) return expected
def payment_processor(self): return Cybersource()
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user hit_count = checkSDN(request, data['first_name'] + ' ' + data['last_name'], data['city'], data['country']) if hit_count > 0: logger.info( 'SDNCheck function called for basket [%d]. It received %d hit(s).', request.basket.id, hit_count, ) response_to_return = { 'error': 'There was an error submitting the basket', 'sdn_check_failure': { 'hit_count': hit_count } } return JsonResponse(response_to_return, status=403) logger.info( 'SDNCheck function called for basket [%d]. It did not receive a hit.', request.basket.id, ) # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, # Fall back to order number when there is no session key (JWT auth) 'device_fingerprint_id': request.session.session_key or basket.order_number, } for source, destination in six.iteritems(self.FIELD_MAPPINGS): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', # TODO: transaction_id is None in logs. This should be fixed. parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) basket_add_organization_attribute(basket, data) # Freeze the basket since the user is paying for it now. basket.freeze() return response