def get_redirect_to_email_confirmation_if_required(request, offer, product): """ Render the email confirmation template if email confirmation is required to redeem the offer. We require email confirmation via account activation before an offer can be redeemed if the site is configured to require account activation or if the offer is restricted for use to learners with a specific email domain. The learner needs to activate their account before we allow them to redeem email domain-restricted offers, otherwise anyone could create an account using an email address with a privileged domain and use the coupon code associated with the offer. Arguments: request (HttpRequest): The current HttpRequest. offer (ConditionalOffer): The offer to be redeemed. product (Product): The Returns: HttpResponse or None: An HttpResponse that redirects to the email confirmation view if required. """ require_account_activation = request.site.siteconfiguration.require_account_activation or offer.email_domains if require_account_activation and not request.user.account_details(request).get('is_active'): response = absolute_redirect(request, 'offers:email_confirmation') course_id = product.course and product.course.id if course_id: response['Location'] += '?{params}'.format(params=urlencode({'course_id': course_id})) return response return None
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 verify_enterprise_needs(self, basket): failed_enterprise_consent_code = self.request.GET.get( CONSENT_FAILED_PARAM) if failed_enterprise_consent_code: messages.error( self.request, _("Could not apply the code '{code}'; it requires data sharing consent." ).format(code=failed_enterprise_consent_code)) if has_enterprise_offer(basket) and basket.total_incl_tax == Decimal( 0): raise RedirectException(response=absolute_redirect( self.request, 'checkout:free-checkout'), )
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_to_payment_error(self): return absolute_redirect(self.request, 'payment_error')
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) 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. 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. order_number = request.POST.get('req_reference_number') old_basket_id = OrderNumberGenerator().basket_id(order_number) old_basket = Basket.objects.get(id=old_basket_id) new_basket = Basket.objects.create(owner=old_basket.owner, site=request.site) # We intentionally avoid thawing the old basket here to prevent order # numbers from being reused. For more, refer to commit a1efc68. new_basket.merge(old_basket, add_quantities=False) logger.info( 'Created new basket [%d] from old basket [%d] for declined transaction.', new_basket.id, old_basket_id, ) message = _( 'An error occurred while processing your payment. You have not been charged. ' 'Please double-check the information you provided and try again. ' 'For help, {link_start}contact support{link_end}.').format( link_start='<a href="{}">'.format( request.site.siteconfiguration.payment_support_url), link_end='</a>', ) messages.error(request, mark_safe(message)) # TODO: # 1. TransactionDeclined message may be mishandled by Payment MFE since it contains HTML. # 2. There are sometimes messages from CyberSource that would make a more helpful message for users. # 3. We could have similar handling of other exceptions like UserCancelled and AuthorizationError return absolute_redirect(request, 'basket:summary') except: # pylint: disable=bare-except # logging handled by validate_notification, because not all exceptions are problematic 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 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. order_number = request.POST.get('req_reference_number') old_basket_id = OrderNumberGenerator().basket_id(order_number) old_basket = Basket.objects.get(id=old_basket_id) new_basket = Basket.objects.create(owner=old_basket.owner, site=request.site) # We intentionally avoid thawing the old basket here to prevent order # numbers from being reused. For more, refer to commit a1efc68. new_basket.merge(old_basket, add_quantities=False) logger.info( 'Created new basket [%d] from old basket [%d] for declined transaction.', new_basket.id, old_basket_id, ) messages.error(self.request, _('transation 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 return absolute_redirect(request, 'basket:summary') 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')