def form_valid(self, form): self.data = form.cleaned_data basket = self.data['basket'] request = self.request sdn_check_failure = self.check_sdn(request, self.data) if sdn_check_failure is not None: return sdn_check_failure self.basket_id = basket.id self.order_number = basket.order_number basket_add_organization_attribute(basket, self.data) # Freeze the basket since the user is paying for it now. basket.freeze() try: payment_processor_response, transaction_id = self.payment_processor.initiate_payment( basket, request, self.data, ) self.transaction_id = transaction_id except GatewayError: return self.redirect_to_payment_error() else: return self.complete_order(payment_processor_response)
def test_handle_post_order_for_bulk_purchase(self, __): """ Ensure that the bulk purchase order is linked to the provided business client when the method `handle_post_order` is invoked. """ toggle_switch(ENROLLMENT_CODE_SWITCH, True) course = CourseFactory(partner=self.partner) course.create_or_update_seat('verified', True, 50, create_enrollment_code=True) enrollment_code = Product.objects.get( product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) user = UserFactory() basket = BasketFactory(owner=user, site=self.site) basket.add_product(enrollment_code, quantity=1) order = create_order(number=1, basket=basket, user=user) request_data = { 'organization': 'Dummy Business Client', PURCHASER_BEHALF_ATTRIBUTE: 'False', } # Manually add organization and purchaser attributes on the basket for testing basket_add_organization_attribute(basket, request_data) EdxOrderPlacementMixin().handle_post_order(order) # Now verify that a new business client has been created in current # order is now linked with that client through Invoice model. business_client = BusinessClient.objects.get( name=request_data['organization']) assert Invoice.objects.get( order=order).business_client == business_client
def test_handle_post_order_for_seat_purchase(self, __): """ Ensure that the single seat purchase order is not linked any business client when the method `handle_post_order` is invoked. """ toggle_switch(ENROLLMENT_CODE_SWITCH, False) course = CourseFactory(partner=self.partner) verified_product = course.create_or_update_seat('verified', True, 50) user = UserFactory() basket = BasketFactory(owner=user, site=self.site) basket.add_product(verified_product, quantity=1) order = create_order(number=1, basket=basket, user=user) request_data = { 'organization': 'Dummy Business Client', PURCHASER_BEHALF_ATTRIBUTE: 'False', } # Manually add organization and purchaser attributes on the basket for testing basket_add_organization_attribute(basket, request_data) EdxOrderPlacementMixin().handle_post_order(order) # Now verify that the single seat order is not linked to business # client by checking that there is no record for BusinessClient. assert not BusinessClient.objects.all()
def create_order_with_billing_address(self): """ Creates an order object with a bit of extra information for HubSpot unit tests""" enrollment_code = Product.objects.get( product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) user = UserFactory() basket = factories.BasketFactory(owner=user, site=self.site) basket.add_product(enrollment_code, self.QUANTITY) # add organization and purchaser attributes manually to the basket for testing purposes basket_data = { 'organization': 'Dummy Business Client', PURCHASER_BEHALF_ATTRIBUTE: 'True' } basket_add_organization_attribute(basket, basket_data) # add some additional data the billing address to exercise some of the code paths in the unit we are testing billing_address = factories.BillingAddressFactory() billing_address.line2 = 'Suite 321' billing_address.line4 = "City" billing_address.state = "State" billing_address.country.name = "United States of America" # create new order adding in the additional billing address info return create_order(number=2, basket=basket, user=user, billing_address=billing_address)
def test_successful_order_for_bulk_purchase(self): """ Verify the view redirects to the Receipt page when the Order has been successfully placed for bulk purchase and also that the order is linked to the provided business client. """ toggle_switch(ENROLLMENT_CODE_SWITCH, True) course = CourseFactory() course.create_or_update_seat('verified', True, 50, create_enrollment_code=True) enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) self.basket = create_basket(owner=self.user, site=self.site) self.basket.add_product(enrollment_code, quantity=1) # The basket should not have an associated order if no payment was made. self.assertFalse(Order.objects.filter(basket=self.basket).exists()) request_data = self.generate_notification( self.basket, billing_address=self.billing_address, ) request_data.update({'organization': 'Dummy Business Client'}) request_data.update({PURCHASER_BEHALF_ATTRIBUTE: "False"}) # Manually add organization and purchaser attributes on the basket for testing basket_add_organization_attribute(self.basket, request_data) response = self.client.post(self.path, request_data) self.assertTrue(Order.objects.filter(basket=self.basket).exists()) self.assertEqual(response.status_code, 302) # Now verify that a new business client has been created and current # order is now linked with that client through Invoice model. order = Order.objects.filter(basket=self.basket).first() business_client = BusinessClient.objects.get(name=request_data['organization']) assert Invoice.objects.get(order=order).business_client == business_client
def _get_basket(self, payment_id): """ Retrieve a basket using a payment ID. Arguments: payment_id: payment_id received from PayPal. Returns: It will return related basket or log exception and return None if duplicate payment_id received or any other exception occurred. """ try: basket = PaymentProcessorResponse.objects.get( processor_name=self.payment_processor.NAME, transaction_id=payment_id ).basket basket.strategy = strategy.Default() Applicator().apply(basket, basket.owner, self.request) basket_add_organization_attribute(basket, self.request.GET) return basket except MultipleObjectsReturned: logger.warning(u"Duplicate payment ID [%s] received from PayPal.", payment_id) return None except Exception: # pylint: disable=broad-except logger.exception(u"Unexpected error during basket retrieval while executing PayPal payment.") return None
def add_required_attributes_to_basket(self, order, purchased_by_org): """ Utility method that will setup Basket with attributes needed for unit tests """ # add organization and purchaser attributes manually to the basket for testing purposes basket_data = { 'organization': 'Dummy Business Client', PURCHASER_BEHALF_ATTRIBUTE: '{}'.format(purchased_by_org) } basket_add_organization_attribute(order.basket, basket_data)
def test_successful_payment_for_bulk_purchase(self): """ Verify that when a Order has been successfully placed for bulk purchase then that order is linked to the provided business client. """ toggle_switch(ENROLLMENT_CODE_SWITCH, True) course = CourseFactory() course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True) basket = create_basket(owner=self.user, site=self.site) enrollment_code = Product.objects.get( product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) basket.add_product(enrollment_code, quantity=1) basket.strategy = Selector().strategy() data = self.generate_form_data(basket.id) data.update({'organization': 'Dummy Business Client'}) # Manually add organization attribute on the basket for testing basket_add_organization_attribute(basket, data) card_type = 'American Express' label = '1986' charge = stripe.Charge.construct_from( { 'id': '2404', 'source': { 'brand': card_type, 'last4': label, }, }, 'fake-key') billing_address = BillingAddressFactory() with mock.patch.object(Stripe, 'get_address_from_token') as address_mock: address_mock.return_value = billing_address with mock.patch.object(stripe.Charge, 'create') as charge_mock: charge_mock.return_value = charge response = self.client.post(self.path, data) address_mock.assert_called_once_with(data['stripe_token']) self.assert_successful_order_response(response, basket.order_number) self.assert_order_created(basket, billing_address, card_type, label) # Now verify that a new business client has been created and current # order is now linked with that client through Invoice model. order = Order.objects.filter(basket=basket).first() business_client = BusinessClient.objects.get(name=data['organization']) assert Invoice.objects.get( order=order).business_client == business_client
def _get_basket(self, payment_id): """ Retrieve a basket using a payment ID. Arguments: payment_id: payment_id received from PayPal. Returns: It will return related basket or log exception and return None if duplicate payment_id received or any other exception occurred. """ try: basket = PaymentProcessorResponse.objects.get( processor_name=self.payment_processor.NAME, transaction_id=payment_id).basket basket.strategy = strategy.Default() # TODO: Remove as a part of REVMI-124 as this is a hacky solution # The problem is that orders are being created after payment processing, and the discount is not # saved in the database, so it needs to be calculated again in order to save the correct info to the # order. REVMI-124 will create the order before payment processing, when we have the discount context. if waffle.flag_is_active( self.request, DYNAMIC_DISCOUNT_FLAG) and basket.lines.count() == 1: discount_lms_url = get_lms_url('/api/discounts/') lms_discount_client = EdxRestApiClient( discount_lms_url, jwt=self.request.site.siteconfiguration.access_token) ck = basket.lines.first().product.course_id user_id = self.request.user.lms_user_id try: response = lms_discount_client.user(user_id).course( ck).get() self.request.GET = self.request.GET.copy() self.request.GET['discount_jwt'] = response.get('jwt') except (SlumberHttpBaseException, Timeout) as error: logger.warning( 'Failed to get discount jwt from LMS. [%s] returned [%s]', discount_lms_url, error.response) # END TODO Applicator().apply(basket, basket.owner, self.request) basket_add_organization_attribute(basket, self.request.GET) return basket except MultipleObjectsReturned: logger.warning(u"Duplicate payment ID [%s] received from PayPal.", payment_id) return None except Exception: # pylint: disable=broad-except logger.exception( u"Unexpected error during basket retrieval while executing PayPal payment." ) return None
def test_execution_for_bulk_purchase(self): """ Verify redirection to LMS receipt page after attempted payment execution if the Otto receipt page is disabled for bulk purchase and also that the order is linked to the provided business client.. """ toggle_switch(ENROLLMENT_CODE_SWITCH, True) self.mock_oauth2_response() course = CourseFactory(partner=self.partner) course.create_or_update_seat('verified', True, 50, create_enrollment_code=True) self.basket = create_basket(owner=UserFactory(), site=self.site) enrollment_code = Product.objects.get( product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) factories.create_stockrecord(enrollment_code, num_in_stock=2, price_excl_tax='10.00') self.basket.add_product(enrollment_code, quantity=1) # Create a payment record the view can use to retrieve a basket self.mock_payment_creation_response(self.basket) self.processor.get_transaction_parameters(self.basket, request=self.request) self.mock_payment_execution_response(self.basket) self.mock_payment_creation_response(self.basket, find=True) # Manually add organization attribute on the basket for testing self.RETURN_DATA.update({'organization': 'Dummy Business Client'}) self.RETURN_DATA.update({PURCHASER_BEHALF_ATTRIBUTE: 'False'}) basket_add_organization_attribute(self.basket, self.RETURN_DATA) response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA) self.assertRedirects( response, get_receipt_page_url( order_number=self.basket.order_number, site_configuration=self.basket.site.siteconfiguration, disable_back_button=True, ), fetch_redirect_response=False) # Now verify that a new business client has been created and current # order is now linked with that client through Invoice model. order = Order.objects.filter(basket=self.basket).first() business_client = BusinessClient.objects.get( name=self.RETURN_DATA['organization']) assert Invoice.objects.get( order=order).business_client == business_client
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 form_valid(self, form): form_data = form.cleaned_data basket = form_data['basket'] token = form_data['stripe_token'] order_number = basket.order_number basket_add_organization_attribute(basket, self.request.POST) try: billing_address = self.payment_processor.get_address_from_token( token) except Exception: # pylint: disable=broad-except logger.exception( 'An error occurred while parsing the billing address for basket [%d]. No billing address will be ' 'stored for the resulting order [%s].', basket.id, order_number) billing_address = None try: self.handle_payment(token, basket) except Exception: # pylint: disable=broad-except logger.exception( 'An error occurred while processing the Stripe payment for basket [%d].', basket.id) return JsonResponse({}, status=400) shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) order_total = OrderTotalCalculator().calculate(basket, shipping_charge) order = self.handle_order_placement(order_number=order_number, user=basket.owner, basket=basket, shipping_address=None, shipping_method=shipping_method, shipping_charge=shipping_charge, billing_address=billing_address, order_total=order_total, request=self.request) self.handle_post_order(order) receipt_url = get_receipt_page_url( site_configuration=self.request.site.siteconfiguration, order_number=order_number, disable_back_button=True, ) return JsonResponse({'url': receipt_url}, status=201)
def _get_basket(self, payment_id): """ Retrieve a basket using a payment ID. Arguments: payment_id: payment_id received from PayPal. Returns: It will return related basket or log exception and return None if duplicate payment_id received or any other exception occurred. """ try: basket = PaymentProcessorResponse.objects.get( processor_name=self.payment_processor.NAME, transaction_id=payment_id).basket basket.strategy = strategy.Default() # TODO: Remove as a part of REVMI-124 as this is a hacky solution # The problem is that orders are being created after payment processing, and the discount is not # saved in the database, so it needs to be calculated again in order to save the correct info to the # order. REVMI-124 will create the order before payment processing, when we have the discount context. self._add_dynamic_discount_to_request(basket) # END TODO Applicator().apply(basket, basket.owner, self.request) basket_add_organization_attribute(basket, self.request.GET) return basket except MultipleObjectsReturned: logger.warning(u"Duplicate payment ID [%s] received from PayPal.", payment_id) return None except Exception: # pylint: disable=broad-except logger.exception( u"Unexpected error during basket retrieval while executing PayPal payment." ) return None
def _get_basket(self, payment_id): """ Retrieve a basket using a payment ID. Arguments: payment_id: payment_id received from Alipay. Returns: """ try: basket = PaymentProcessorResponse.objects.get( processor_name=self.payment_processor.NAME, transaction_id=payment_id).basket basket.strategy = strategy.Default() Applicator().apply(basket, basket.owner, self.request) basket_add_organization_attribute(basket, self.request.GET) return basket except MultipleObjectsReturned: return None except Exception, e: logger.exception(e) return None
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
def form_valid(self, form): form_data = form.cleaned_data #basket = form_data['basket'] committed_basket = Basket.objects.filter(owner=self.request.user, status="Commited").last() committed_basket.strategy = self.request.strategy committed_basket.save() token = form_data['stripe_token'] order_number = committed_basket.order_number if waffle.flag_is_active( self.request, DYNAMIC_DISCOUNT_FLAG) and committed_basket.lines.count() == 1: discount_lms_url = get_lms_url('/api/discounts/') lms_discount_client = EdxRestApiClient( discount_lms_url, jwt=self.request.site.siteconfiguration.access_token) ck = committed_basket.lines.first().product.course_id user_id = committed_basket.owner.lms_user_id response = lms_discount_client.course(ck).get() self.request.GET = self.request.GET.copy() self.request.GET['discount_jwt'] = response.get('jwt') self.request.POST = self.request.POST.copy() self.request.POST['discount_jwt'] = response.get('jwt') Applicator().apply(committed_basket, self.request.user, self.request) basket_add_organization_attribute(committed_basket, self.request.POST) committed_basket.freeze() try: billing_address = self.payment_processor.get_address_from_token( token) except Exception: # pylint: disable=broad-except logger.exception( 'An error occurred while parsing the billing address for basket [%d]. No billing address will be ' 'stored for the resulting order [%s].', committed_basket.id, order_number) billing_address = None try: self.handle_payment(token, committed_basket) except Exception: # pylint: disable=broad-except logger.exception( 'An error occurred while processing the Stripe payment for basket [%d].', committed_basket.id) return JsonResponse({}, status=400) # Remove paid items from last open baskets user_baskets = Basket.objects.filter(owner=self.request.user, status="Open") if user_baskets.exists(): last_open_basket = user_baskets.last() del_lines = committed_basket.all_lines() open_lines = last_open_basket.all_lines() for line in del_lines: product = line.product filtered_lines = open_lines.filter(product_id=product.id) if filtered_lines.exists(): filtered_lines.delete() last_open_basket.save() try: order = self.create_order(self.request, committed_basket, billing_address=billing_address) send_confirm_purchase_email(None, user=self.request.user, order=order) except Exception: # pylint: disable=broad-except logger.exception( 'An error occurred while processing the Stripe payment for basket [%d].', committed_basket.id) return JsonResponse({}, status=400) self.handle_post_order(order) receipt_url = get_receipt_page_url( site_configuration=self.request.site.siteconfiguration, order_number=order_number, disable_back_button=True, ) return JsonResponse({'url': receipt_url}, status=201)