def redirect_on_transaction_declined(self): redirect_url = get_payment_microfrontend_or_basket_url(self.request) redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) return JsonResponse({ 'redirectTo': redirect_url, }, status=400)
def test_decline(self): """ Verify the view reports an error if the transaction is only authorized pending review. """ basket = self._create_valid_basket() data = self._generate_data(basket.id) order_number = OrderNumberGenerator().order_number_from_basket_id( self.site.siteconfiguration.partner, basket.id, ) # This response has been pruned to only the needed data. self._prep_request_success( """{"links":{"_self":{"href":"/pts/v2/payments/6021683934456376603262","method":"GET"}},"id":"6021683934456376603262","status":"DECLINED","error_information":{"reason":"PROCESSOR_DECLINED","message":"Decline - General decline of the card. No other information provided by the issuing bank."},"client_reference_information":{"code":"%s"},"processor_information":{"transaction_id":"460282531937765","network_transaction_id":"460282531937765","response_code":"005","avs":{"code":"D","code_raw":"D"},"card_verification":{"result_code":"M","result_code_raw":"M"}},"payment_information":{"account_features":{"category":"F"}}}""" % order_number # pylint: disable=line-too-long ) response = self.client.post(self.path, data) assert response.status_code == 400 assert response['content-type'] == JSON request = RequestFactory(SERVER_NAME='testserver.fake').post( self.path, data) request.site = self.site assert json.loads( response.content )['redirectTo'] == get_payment_microfrontend_or_basket_url(request) # Ensure the basket is frozen basket = Basket.objects.get(pk=basket.pk) self.assertEqual(basket.status, Basket.MERGED) assert Basket.objects.count() == 2
def redirect_on_transaction_declined(self): redirect_url = get_payment_microfrontend_or_basket_url(self.request) redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) # TODO: Remove as part of PCI-81 redirect_url = add_flex_microform_flag_to_url(redirect_url, self.request) return HttpResponseRedirect(redirect_url)
def _redirect_response_to_basket_or_payment(self, request, invalid_code=None): redirect_url = get_payment_microfrontend_or_basket_url(request) redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) redirect_url = add_invalid_code_message_to_url(redirect_url, invalid_code) return HttpResponseRedirect(redirect_url, status=303)
def _redirect_response_to_basket_or_payment(self, request, skus): redirect_url = get_payment_microfrontend_or_basket_url(request) redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) # If a user is eligible and bucketed, REV1074 experiment information will be added to their url if waffle.flag_is_active( self.request, 'REV1074.enable_experiment'): # pragma: no cover if skus: redirect_url = add_REV1074_information_to_url_if_eligible( redirect_url, request, skus[0]) return HttpResponseRedirect(redirect_url, status=303)
def redirect_on_transaction_declined(self): redirect_url = get_payment_microfrontend_or_basket_url(self.request) redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) # TODO: Remove as part of PCI-81 redirect_url = add_flex_microform_flag_to_url(redirect_url, self.request, force_flag=False) return JsonResponse({ 'redirectTo': redirect_url, }, status=400)
def _redirect_response_to_basket_or_payment(self, request, skus, invalid_code=None): redirect_url = get_payment_microfrontend_or_basket_url(request) # If a user is eligible and bucketed, REV1074 experiment information will be added to their url REV1074_is_active = waffle.flag_is_active(self.request, 'REV1074.enable_experiment') if REV1074_is_active and skus and not invalid_code: # pragma: no cover redirect_url = add_REV1074_information_to_url_if_eligible(redirect_url, request, skus[0]) redirect_url += '?basket_id=' + str(request.basket.id) else: # pragma: no cover redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) redirect_url = add_invalid_code_message_to_url(redirect_url, invalid_code) return HttpResponseRedirect(redirect_url, status=303)
def _redirect_response_to_basket_or_payment(self, request, skus): redirect_url = get_payment_microfrontend_or_basket_url(request) # If a user is eligible and bucketed, REV1074 experiment information will be added to their url if waffle.flag_is_active(self.request, 'REV1074.enable_experiment') and skus: # pragma: no cover redirect_url = add_REV1074_information_to_url_if_eligible(redirect_url, request, skus[0]) redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) basket = 'basket_id=' + str(request.basket.id) has_params = urlparse(redirect_url).query redirect_url += '&' + basket if has_params else '?' + basket else: # pragma: no cover redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) return HttpResponseRedirect(redirect_url, status=303)
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument """Process a CyberSource merchant notification and place an order for paid products as appropriate.""" notification = request.POST.dict() try: basket = self.validate_notification(notification) monitoring_utils.set_custom_metric('payment_response_validation', 'success') except DuplicateReferenceNumber: # CyberSource has told us that they've declined an attempt to pay # for an existing order. If this happens, we can redirect the browser # to the receipt page for the existing order. monitoring_utils.set_custom_metric('payment_response_validation', 'redirect-to-receipt') return self.redirect_to_receipt_page(notification) except TransactionDeclined: # Declined transactions are the most common cause of errors during payment # processing and tend to be easy to correct (e.g., an incorrect CVV may have # been provided). The recovery path is not as clear for other exceptions, # so we let those drop through to the payment error page. self._merge_old_basket_into_new(request) messages.error(self.request, _('transaction declined'), extra_tags='transaction-declined-message') monitoring_utils.set_custom_metric('payment_response_validation', 'redirect-to-payment-page') # TODO: # 1. There are sometimes messages from CyberSource that would make a more helpful message for users. # 2. We could have similar handling of other exceptions like UserCancelled and AuthorizationError redirect_url = get_payment_microfrontend_or_basket_url(self.request) return HttpResponseRedirect(redirect_url) except: # pylint: disable=bare-except # logging handled by validate_notification, because not all exceptions are problematic monitoring_utils.set_custom_metric('payment_response_validation', 'redirect-to-error-page') return absolute_redirect(request, 'payment_error') try: order = self.create_order(request, basket, self._get_billing_address(notification)) self.handle_post_order(order) return self.redirect_to_receipt_page(notification) except: # pylint: disable=bare-except transaction_id, order_number, basket_id = self.get_ids_from_notification(notification) logger.exception( 'Error processing order for transaction [%s], with order [%s] and basket [%d].', transaction_id, order_number, basket_id ) return absolute_redirect(request, 'payment_error')
def test_authorized_pending_review_request_reversal_failed(self): """ Verify the view reports an error if the transaction is only authorized pending review. """ basket = self._create_valid_basket() data = self._generate_data(basket.id) order_number = OrderNumberGenerator().order_number_from_basket_id( self.site.siteconfiguration.partner, basket.id, ) # This response has been pruned to only the needed data. self._prep_request_success( """{"links":{"_self":{"href":"/pts/v2/payments/6038898237296087603031","method":"GET"}},"id":"6038898237296087603031","submit_time_utc":"2020-10-28T12:57:04Z","status":"AUTHORIZED_PENDING_REVIEW","error_information":{"reason":"AVS_FAILED","message":"Soft Decline - The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the Address Verification Service (AVS) check."},"client_reference_information":{"code":"%s"},"processor_information":{"approval_code":"028252","transaction_id":"580302466249046","network_transaction_id":"580302466249046","response_code":"000","avs":{"code":"N","code_raw":"N"},"card_verification":{"result_code":"M","result_code_raw":"M"}},"payment_information":{"account_features":{"category":"C"}},"order_information":{"amount_details":{"authorized_amount":"25.00","currency":"USD"}}}""" % order_number # pylint: disable=line-too-long ) self._prep_request_invalid( """{"submitTimeUtc":"2020-09-30T18:53:23Z","status":"INVALID_REQUEST","reason":"DUPLICATE_REQUEST","message":"Declined - The\u00a0merchantReferenceCode\u00a0sent with this authorization request matches the merchantReferenceCode of another authorization request that you sent in the last 15 minutes."}""", # pylint: disable=line-too-long '6038898237296087603031') response = self.client.post(self.path, data) assert response.status_code == 400 assert response['content-type'] == JSON request = RequestFactory(SERVER_NAME='testserver.fake').post( self.path, data) request.site = self.site assert json.loads( response.content )['redirectTo'] == get_payment_microfrontend_or_basket_url(request) # Ensure the basket is frozen basket = Basket.objects.get(pk=basket.pk) self.assertEqual(basket.status, Basket.MERGED) assert Basket.objects.count() == 2 # Ensure that 2 requests were sent to cybersource assert self.mock_cybersource_request.call_count == 2 # Ensure that 2 requests and 2 responses were recorded as PaymentProcessorResponses assert PaymentProcessorResponse.objects.all().count() == 4
def get(self, request): # pylint: disable=too-many-statements """ Looks up the passed code and adds the matching product to a basket, then applies the voucher and if the basket total is FREE places the order and enrolls the user in the course. """ template_name = 'coupons/_offer_error.html' code = request.GET.get('code') sku = request.GET.get('sku') failure_url = request.GET.get('failure_url') site_configuration = request.site.siteconfiguration if not code: return render(request, template_name, {'error': _('Code not provided.')}) if not sku: return render(request, template_name, {'error': _('SKU not provided.')}) try: voucher = Voucher.objects.get(code=code) except Voucher.DoesNotExist: msg = 'No voucher found with code {code}'.format(code=code) return render(request, template_name, {'error': _(msg)}) try: product = StockRecord.objects.get(partner_sku=sku).product except StockRecord.DoesNotExist: return render(request, template_name, {'error': _('The product does not exist.')}) valid_voucher, msg = voucher_is_valid(voucher, [product], request) if not valid_voucher: logger.warning( '[Code Redemption Failure] The voucher is not valid for this product. ' 'User: %s, Product: %s, Code: %s, Message: %s', request.user.username, product.id, voucher.code, msg) return render(request, template_name, {'error': msg}) offer = voucher.best_offer if not offer.is_email_valid(request.user.email): logger.warning( '[Code Redemption Failure] Unable to apply offer because the user\'s email ' 'does not meet the domain requirements. ' 'User: %s, Offer: %s, Code: %s', request.user.username, offer.id, voucher.code) return render( request, template_name, {'error': _('You are not eligible to use this coupon.')}) email_confirmation_response = get_redirect_to_email_confirmation_if_required( request, offer, product) if email_confirmation_response: return email_confirmation_response try: enterprise_customer = get_enterprise_customer_from_voucher( request.site, voucher) except EnterpriseDoesNotExist as e: # If an EnterpriseException is caught while pulling the EnterpriseCustomer, that means there's no # corresponding EnterpriseCustomer in the Enterprise service (which should never happen). logger.exception(six.text_type(e)) return render( request, template_name, { 'error': _('Couldn\'t find a matching Enterprise Customer for this coupon.' ) }) if enterprise_customer and product.is_course_entitlement_product: return render( request, template_name, { 'error': _('This coupon code is not valid for entitlement course product. Try a different course.' ) }) if enterprise_customer is not None and enterprise_customer_user_needs_consent( request.site, enterprise_customer['id'], product.course.id, request.user.username, ): consent_token = get_enterprise_customer_data_sharing_consent_token( request.user.access_token, product.course.id, enterprise_customer['id']) received_consent_token = request.GET.get('consent_token') if received_consent_token: # If the consent token is set, then the user is returning from the consent view. Render out an error # if the computed token doesn't match the one received from the redirect URL. if received_consent_token != consent_token: logger.warning( '[Code Redemption Failure] Unable to complete code redemption because of ' 'invalid consent. User: %s, Offer: %s, Code: %s', request.user.username, offer.id, voucher.code) return render(request, template_name, { 'error': _('Invalid data sharing consent token provided.') }) else: # The user hasn't been redirected to the interstitial consent view to collect consent, so # redirect them now. redirect_url = get_enterprise_course_consent_url( request.site, code, sku, consent_token, product.course.id, enterprise_customer['id'], failure_url=failure_url) return HttpResponseRedirect(redirect_url) try: basket = prepare_basket(request, [product], voucher) except AlreadyPlacedOrderException: msg = _('You have already purchased {course} seat.').format( course=product.course.name) return render(request, template_name, {'error': msg}) if basket.total_excl_tax == 0: try: order = self.place_free_order(basket) return HttpResponseRedirect( get_receipt_page_url( site_configuration, order.number, disable_back_button=True, ), ) except: # pylint: disable=bare-except logger.exception( 'Failed to create a free order for basket [%d]', basket.id) return absolute_redirect(self.request, 'checkout:error') if enterprise_customer: if is_voucher_applied(basket, voucher): message = _( 'A discount has been applied, courtesy of {enterprise_customer_name}.' ).format( enterprise_customer_name=enterprise_customer.get('name')) messages.info(self.request, message) else: # Display a generic message to the user if a condition-specific # message has not already been added by an unsatified Condition class. if not messages.get_messages(self.request): messages.warning( self.request, _('This coupon code is not valid for this course. Try a different course.' )) self.request.basket.vouchers.remove(voucher) # The coupon_redeem_redirect query param is used to communicate to the Payment MFE that it may redirect # and should not display the payment form before making that determination. # TODO: It would be cleaner if the user could be redirected to their final destination up front. redirect_url = get_payment_microfrontend_or_basket_url( self.request) + "?coupon_redeem_redirect=1" return HttpResponseRedirect(redirect_url)
def redirect_on_transaction_declined(self): redirect_url = get_payment_microfrontend_or_basket_url(self.request) return HttpResponseRedirect(redirect_url)