Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    def form_valid(self, form):
        data = form.cleaned_data
        basket = data['basket']
        request = self.request
        user = request.user

        # 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.debug('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
        )

        # 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
Esempio n. 4
0
    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
Esempio n. 5
0
    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
Esempio n. 6
0
    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
Esempio n. 7
0
    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
Esempio n. 8
0
    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
Esempio n. 9
0
    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)), )
Esempio n. 11
0
 def payment_processor(self):
     return Cybersource(self.request.site)
Esempio n. 12
0
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)
Esempio n. 13
0
    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
Esempio n. 14
0
 def payment_processor(self):
     return Cybersource()
Esempio n. 15
0
    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