def send_course_purchase_email(sender, order=None, **kwargs): # pylint: disable=unused-argument """Send course purchase notification email when a course is purchased.""" if waffle.switch_is_active('ENABLE_NOTIFICATIONS'): # We do not currently support email sending for orders with more than one item. if len(order.lines.all()) == ORDER_LINE_COUNT: product = order.lines.first().product credit_provider_id = getattr(product.attr, 'credit_provider', None) if not credit_provider_id: logger.error( 'Failed to send credit receipt notification. Credit seat product [%s] has no provider.', product.id) return elif product.is_seat_product: provider_data = get_credit_provider_details( access_token=order.site.siteconfiguration.access_token, credit_provider_id=credit_provider_id, site_configuration=order.site.siteconfiguration) receipt_page_url = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration) if provider_data: send_notification( order.user, 'CREDIT_RECEIPT', { 'course_title': product.title, 'receipt_page_url': receipt_page_url, 'credit_hours': product.attr.credit_hours, 'credit_provider': provider_data['display_name'], }, order.site) else: logger.info( 'Currently support receipt emails for order with one item.')
def get(self, request, order_number): """Provide confirmation of payment.""" order_id = decode_string(order_number) payment_status = self._get_payment_status(order_id) if payment_status.lower() != 'submitted': return render(request, 'payment/pending_status.html', {'msg': payment_status}) else: try: order = Order.objects.get(number=order_id) # pylint: disable=unused-variable except Order.DoesNotExist: logger.info( 'Edupay Payment: No payment found for order [%s] ', order_id, ) return render(request, 'payment/pending_status.html', {'msg': 'error'}) basket_id = OrderNumberGenerator().basket_id(order_id) basket = Basket.objects.get(id=basket_id) receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration, ) return redirect(receipt_url)
def test_notify_purchaser(self, mock_task): """ Verify the notification is scheduled if the site has notifications enabled and the refund is for a course seat. """ site_configuration = self.site.siteconfiguration site_configuration.send_refund_notifications = True user = UserFactory() course = CourseFactory() price = Decimal(100.00) product = course.create_or_update_seat('verified', True, price, self.partner) basket = create_basket(empty=True) basket.site = self.site basket.add_product(product) order = create_order(basket=basket, user=user) order_url = get_receipt_page_url(site_configuration, order.number) refund = Refund.create_with_lines(order, order.lines.all()) with LogCapture(REFUND_MODEL_LOGGER_NAME) as l: refund._notify_purchaser() # pylint: disable=protected-access msg = 'Course refund notification scheduled for Refund [{}].'.format(refund.id) l.check( (REFUND_MODEL_LOGGER_NAME, 'INFO', msg) ) amount = format_currency(order.currency, price) mock_task.assert_called_once_with( user.email, refund.id, amount, course.name, order.number, order_url, site_code=self.partner.short_code )
def _notify_purchaser(self): """ Notify the purchaser that the refund has been processed. """ site_configuration = self.order.site.siteconfiguration site_code = site_configuration.partner.short_code if not site_configuration.send_refund_notifications: logger.info( 'Refund notifications are disabled for Partner [%s]. No notification will be sent for Refund [%d]', site_code, self.id ) return # NOTE (CCB): The initial version of the refund email only supports refunding a single course. product = self.lines.first().order_line.product product_class = product.get_product_class().name if product_class != SEAT_PRODUCT_CLASS_NAME: logger.warning( ('No refund notification will be sent for Refund [%d]. The notification supports product lines ' 'of type Course, not [%s].'), self.id, product_class ) return course_name = self.lines.first().order_line.product.course.name order_number = self.order.number order_url = get_receipt_page_url(site_configuration, order_number) amount = format_currency(self.currency, self.total_credit_excl_tax) send_course_refund_email.delay(self.user.email, self.id, amount, course_name, order_number, order_url, site_code=site_code) logger.info('Course refund notification scheduled for Refund [%d].', self.id)
def send_email(self, order): """ Sends an email with enrollment code order information. """ # Note (multi-courses): Change from a course_name to a list of course names. product = order.lines.first().product course = Course.objects.get(id=product.attr.course_key) receipt_page_url = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration ) send_notification( order.user, 'ORDER_WITH_CSV', context={ 'contact_url': order.site.siteconfiguration.build_lms_url('/contact'), 'course_name': course.name, 'download_csv_link': order.site.siteconfiguration.build_ecommerce_url( reverse('coupons:enrollment_code_csv', args=[order.number]) ), 'enrollment_code_title': product.title, 'lms_url': order.site.siteconfiguration.build_lms_url(), 'order_number': order.number, 'partner_name': order.site.siteconfiguration.partner.name, 'receipt_page_url': receipt_page_url, }, site=order.site )
def get(self, request, pk): ''' query order GET /payment/wechatpay/order_query/{basket.id} ''' status = self.NOTPAY receipt_url = '' try: basket = Basket.objects.get(owner=request.user, id=pk) order = Order.objects.filter(number=basket.order_number, status='Complete') if basket.status == 'Submitted': status = self.PAID else: status, resp = self.wechatpay_query(basket) if status == self.PAID and not order: post_data = {'original_data': json.dumps({'data': resp})} requests.post(settings.ECOMMERCE_URL_ROOT + reverse('wechatpay:execute'), data=post_data) if status == self.PAID and order: receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration) status = self.SUCCESS except Exception, e: logger.exception(e)
def test_duplicate_reference_code(self): """ Verify that if CyberSource declines to charge for an existing order, we redirect to the receipt page for the existing order. """ notification = self.generate_notification(self.basket, billing_address=self.billing_address) self.client.post(self.path, notification) # Validate that a new order exists in the correct state order = Order.objects.get(basket=self.basket) self.assertIsNotNone(order, 'No order was created for the basket after payment.') # Mutate the notification and re-use it to simulate a duplicate reference # number error from CyberSource. notification.update({ 'decision': 'ERROR', 'reason_code': '104', }) # Re-sign the response. This is necessary because we've tampered with fields # that have already been signed. notification['signature'] = self.generate_signature(self.processor.secret_key, notification) response = self.client.post(self.path, notification) expected_redirect = get_receipt_page_url( self.site.siteconfiguration, order_number=notification.get('req_reference_number'), disable_back_button=True, ) self.assertRedirects(response, expected_redirect, fetch_redirect_response=False)
def _assert_execution_redirect(self, payer_info=None, url_redirect=None): """Verify redirection to Otto receipt page after attempted payment execution.""" self.mock_oauth2_response() # 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) creation_response = self.mock_payment_creation_response(self.basket, find=True) execution_response = self.mock_payment_execution_response( self.basket, payer_info=payer_info) response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA) self.assertRedirects( response, url_redirect or get_receipt_page_url( order_number=self.basket.order_number, site_configuration=self.basket.site.siteconfiguration, disable_back_button=True, ), fetch_redirect_response=False) return creation_response, execution_response
def test_notify_purchaser_course_entielement(self, mock_task): """ Verify the notification is scheduled if the site has notifications enabled and the refund is for a course entitlement. """ site_configuration = self.site.siteconfiguration site_configuration.send_refund_notifications = True user = UserFactory() course_entitlement = create_or_update_course_entitlement( 'verified', 100, self.partner, '111-222-333-444', 'Course Entitlement') basket = create_basket(site=self.site, owner=user, empty=True) basket.add_product(course_entitlement, 1) order = create_order(number=1, basket=basket, user=user) order_url = get_receipt_page_url(site_configuration, order.number) refund = Refund.create_with_lines(order, order.lines.all()) with LogCapture(REFUND_MODEL_LOGGER_NAME) as logger: refund._notify_purchaser() # pylint: disable=protected-access msg = 'Course refund notification scheduled for Refund [{}].'.format( refund.id) logger.check_present((REFUND_MODEL_LOGGER_NAME, 'INFO', msg)) amount = format_currency(order.currency, 100) mock_task.assert_called_once_with(user.email, refund.id, amount, course_entitlement.title, order.number, order_url, site_code=self.partner.short_code)
def _notify_purchaser(self): """ Notify the purchaser that the refund has been processed. """ site_configuration = self.order.site.siteconfiguration site_code = site_configuration.partner.short_code if not site_configuration.send_refund_notifications: logger.info( 'Refund notifications are disabled for Partner [%s]. No notification will be sent for Refund [%d]', site_code, self.id ) return # NOTE (CCB): The initial version of the refund email only supports refunding a single course. product = self.lines.first().order_line.product product_class = product.get_product_class().name if product_class not in [SEAT_PRODUCT_CLASS_NAME, COURSE_ENTITLEMENT_PRODUCT_CLASS_NAME]: logger.warning( ('No refund notification will be sent for Refund [%d]. The notification supports product lines ' 'of type Course, not [%s].'), self.id, product_class ) return if product_class == SEAT_PRODUCT_CLASS_NAME: course_name = self.lines.first().order_line.product.course.name else: course_name = self.lines.first().order_line.product.title order_number = self.order.number order_url = get_receipt_page_url(site_configuration, order_number) amount = format_currency(self.currency, self.total_credit_excl_tax) send_course_refund_email.delay(self.user.email, self.id, amount, course_name, order_number, order_url, site_code=site_code) logger.info('Course refund notification scheduled for Refund [%d].', self.id)
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument """Process a CyberSource merchant notification and place an order for paid products as appropriate.""" try: notification = request.POST.dict() basket = self.validate_notification(notification) except (InvalidBasketError, InvalidSignatureError): return redirect(reverse('payment_error')) except (UserCancelled, TransactionDeclined, PaymentError): order_number = request.POST.get('req_reference_number') basket_id = OrderNumberGenerator().basket_id(order_number) basket = self._get_basket(basket_id) if basket: basket.thaw() messages.error(request, _('Your payment has been canceled.')) return redirect(reverse('basket:summary')) except: # pylint: disable=bare-except return redirect(reverse('payment_error')) try: self.create_order(request, basket, notification) receipt_page_url = get_receipt_page_url( order_number=notification.get('req_reference_number'), site_configuration=self.request.site.siteconfiguration ) self.request.session['fire_tracking_events'] = True return redirect(receipt_page_url) except: # pylint: disable=bare-except return redirect(reverse('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.""" try: notification = request.POST.dict() basket = self.validate_notification(notification) except (InvalidBasketError, InvalidSignatureError): return redirect(reverse('payment_error')) except (UserCancelled, TransactionDeclined, PaymentError): order_number = request.POST.get('req_reference_number') basket_id = OrderNumberGenerator().basket_id(order_number) basket = self._get_basket(basket_id) if basket: basket.thaw() messages.error(request, _('Your payment has been canceled.')) return redirect(reverse('basket:summary')) except: # pylint: disable=bare-except return redirect(reverse('payment_error')) try: self.create_order(request, basket, notification) receipt_page_url = get_receipt_page_url( order_number=notification.get('req_reference_number'), site_configuration=self.request.site.siteconfiguration) self.request.session['fire_tracking_events'] = True return redirect(receipt_page_url) except: # pylint: disable=bare-except return redirect(reverse('payment_error'))
def send_email(self, order): """ Sends an email with enrollment code order information. """ # Note (multi-courses): Change from a course_name to a list of course names. product = order.lines.first().product course = Course.objects.get(id=product.attr.course_key) receipt_page_url = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration) send_notification( order.user, 'ORDER_WITH_CSV', context={ 'contact_url': order.site.siteconfiguration.build_lms_url('/contact'), 'course_name': course.name, 'download_csv_link': order.site.siteconfiguration.build_ecommerce_url( reverse('coupons:enrollment_code_csv', args=[order.number])), 'enrollment_code_title': product.title, 'lms_url': order.site.siteconfiguration.build_lms_url(), 'order_number': order.number, 'partner_name': order.site.siteconfiguration.partner.name, 'receipt_page_url': receipt_page_url, 'order_history_url': order.site.siteconfiguration.build_lms_url('account/settings'), }, site=order.site)
def test_valid_request(self): """ Verify the view completes the transaction if the request is valid. """ 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, ) # The view has already been created, so patch and assert that its reference to basket_add_organization_attribute # has been called with mock.patch( "ecommerce.extensions.payment.views.cybersource.basket_add_organization_attribute" ) as mock_basket_add_org: # This response has been pruned to only the needed data. self._prep_request_success( """{"links":{"_self":{"href":"/pts/v2/payments/6031827608526961004260","method":"GET"}},"id":"6031827608526961004260","submit_time_utc":"2020-10-20T08:32:44Z","status":"AUTHORIZED","client_reference_information":{"code":"%s"},"processor_information":{"approval_code":"307640","transaction_id":"380294307616695","network_transaction_id":"380294307616695","response_code":"000","avs":{"code":"G","code_raw":"G"},"card_verification":{"result_code":"M","result_code_raw":"M"},"consumer_authentication_response":{"code":"99"}},"payment_information":{"tokenized_card":{"type":"001"},"account_features":{"category":"F"}},"order_information":{"amount_details":{"total_amount":"5.00","authorized_amount":"5.00","currency":"USD"}}}""" % order_number # pylint: disable=line-too-long ) response = self.client.post(self.path, data) assert response.status_code == 201 assert response['content-type'] == JSON assert json.loads( response.content)['receipt_page_url'] == get_receipt_page_url( self.site.siteconfiguration, order_number=order_number, disable_back_button=True, ) # Ensure the basket is Submitted basket = Basket.objects.get(pk=basket.pk) self.assertEqual(basket.status, Basket.SUBMITTED) mock_basket_add_org.assert_called_once()
def get(self, request): """Handle an incoming user returned to us by PayPal after approving payment.""" payment_id = request.GET.get('paymentId') payer_id = request.GET.get('PayerID') logger.info(u"Payment [%s] approved by payer [%s]", payment_id, payer_id) paypal_response = request.GET.dict() basket = self._get_basket(payment_id) if not basket: return redirect(self.payment_processor.error_url) receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration, disable_back_button=True, ) try: with transaction.atomic(): try: self.handle_payment(paypal_response, basket) except PaymentError: return redirect(self.payment_processor.error_url) except: # pylint: disable=bare-except logger.exception( 'Attempts to handle payment for basket [%d] failed.', basket.id) return redirect(receipt_url) self.call_handle_order_placement(basket, request) return redirect(receipt_url)
def redirect_to_receipt_page(self, notification): receipt_page_url = get_receipt_page_url( self.request.site.siteconfiguration, order_number=notification.get('req_reference_number') ) return redirect(receipt_page_url)
def get_redirect_url(self, *args, **kwargs): request = self.request site = request.site basket = Basket.get_basket(request.user, site) if not basket.is_empty: # Need to re-apply the voucher to the basket. Applicator().apply(basket, request.user, request) if basket.total_incl_tax != Decimal(0): raise BasketNotFreeError( 'Basket [{}] is not free. User affected [{}]'.format( basket.id, basket.owner.id)) order = self.place_free_order(basket) if has_enterprise_offer(basket): # Skip the receipt page and redirect to the LMS # if the order is free due to an Enterprise-related offer. program_uuid = get_program_uuid(order) if program_uuid: url = get_lms_program_dashboard_url(program_uuid) else: course_run_id = order.lines.all()[:1].get( ).product.course.id url = get_lms_courseware_url(course_run_id) else: receipt_path = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration) url = site.siteconfiguration.build_lms_url(receipt_path) else: # If a user's basket is empty redirect the user to the basket summary # page which displays the appropriate message for empty baskets. url = reverse('basket:summary') return url
def assert_redirects_to_receipt_page(self, code=COUPON_CODE, consent_token=None): response = self.redeem_coupon(code=code, consent_token=consent_token) order = Order.objects.first() receipt_page_url = get_receipt_page_url(self.site.siteconfiguration) expected_url = format_url(base=receipt_page_url, params={'order_number': order.number}) self.assertRedirects(response, expected_url, status_code=302, fetch_redirect_response=False)
def assert_successful_order_response(self, response, order_number): assert response.status_code == 201 receipt_url = get_receipt_page_url( self.site_configuration, order_number, disable_back_button=True, ) assert response.json() == {'url': receipt_url}
def redirect_to_receipt_page(self): receipt_page_url = get_receipt_page_url( self.request.site.siteconfiguration, order_number=self.order_number, disable_back_button=True, ) return redirect(receipt_page_url)
def redirect_to_receipt_page(self): receipt_page_url = get_receipt_page_url( self.request.site.siteconfiguration, order_number=self.order_number, disable_back_button=True, ) return JsonResponse({ 'receipt_page_url': receipt_page_url, }, status=201)
def get(self, request): """Handle an incoming user returned to us by PayPal after approving payment.""" payment_id = request.GET.get('paymentId') payer_id = request.GET.get('PayerID') logger.info(u"Payment [%s] approved by payer [%s]", payment_id, payer_id) paypal_response = request.GET.dict() basket = self._get_basket(payment_id) if not basket: return redirect(self.payment_processor.error_url) receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration) try: with transaction.atomic(): try: self.handle_payment(paypal_response, basket) except PaymentError: return redirect(self.payment_processor.error_url) except: # pylint: disable=bare-except logger.exception( 'Attempts to handle payment for basket [%d] failed.', basket.id) return redirect(receipt_url) try: shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) order_total = OrderTotalCalculator().calculate( basket, shipping_charge) user = basket.owner # Given a basket, order number generation is idempotent. Although we've already # generated this order number once before, it's faster to generate it again # than to retrieve an invoice number from PayPal. order_number = basket.order_number order = self.handle_order_placement( order_number=order_number, user=user, basket=basket, shipping_address=None, shipping_method=shipping_method, shipping_charge=shipping_charge, billing_address=None, order_total=order_total, request=request) self.handle_post_order(order) return redirect(receipt_url) except: # pylint: disable=bare-except logger.exception(self.order_placement_failure_msg, basket.id) return redirect(receipt_url)
def get(self, request): """ Handles an incoming user returned to us by Paystack after approving payment. It redirects user to order receipt page after completing all payment flow of a successfull Paystack transaction. """ reference = request.GET.get('reference') success, response = self.payment_processor.paystack_client.handler( VERIFY_TRANSACTION_CODE, reference) if success: data = response.get('data') metadata = data.get('metadata') order_number = metadata.get('order_number') basket_id = metadata.get('basket_id') transaction_id = data.get('id') logger.info( "Received Paystack payment notification for transaction: %s, associated with basket: %d.", transaction_id, int(basket_id)) basket = self.get_basket(basket_id) if not basket: logger.error( "Received Paystack response for non-existent basket: %d.", basket_id) raise InvalidBasketError if basket.status != Basket.FROZEN: logger.info( "Received Paystack response for basket [%d] which is in a non-frozen state, [%s].", basket.id, basket.status) self.payment_processor.record_processor_response( response, transaction_id=reference, basket=basket) receipt_url = get_receipt_page_url( order_number=order_number, site_configuration=basket.site.siteconfiguration) try: with transaction.atomic(): self.handle_payment(data, basket) self.call_handle_order_placement(basket, request) except Exception: # pylint: disable=broad-except logger.exception( "Attempts to handle payment for basket [%d] failed.", basket.id) self.log_order_placement_exception(order_number, basket.id) else: return redirect(receipt_url) logger.error( "Unable to process Paystack transaction with reference: %s.", reference) return redirect(self.payment_processor.error_url)
def get(self, request): """Handle an incoming user returned to us by PayPal after approving payment.""" payment_id = request.GET.get('paymentId') payer_id = request.GET.get('PayerID') logger.info(u"Payment [%s] approved by payer [%s]", payment_id, payer_id) paypal_response = request.GET.dict() basket = self._get_basket(payment_id) if not basket: return redirect(self.payment_processor.error_url) receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration ) try: with transaction.atomic(): try: self.handle_payment(paypal_response, basket) except PaymentError: return redirect(self.payment_processor.error_url) except: # pylint: disable=bare-except logger.exception('Attempts to handle payment for basket [%d] failed.', basket.id) return redirect(receipt_url) try: shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) order_total = OrderTotalCalculator().calculate(basket, shipping_charge) user = basket.owner # Given a basket, order number generation is idempotent. Although we've already # generated this order number once before, it's faster to generate it again # than to retrieve an invoice number from PayPal. order_number = basket.order_number self.handle_order_placement( order_number=order_number, user=user, basket=basket, shipping_address=None, shipping_method=shipping_method, shipping_charge=shipping_charge, billing_address=None, order_total=order_total, request=request ) return redirect(receipt_url) except: # pylint: disable=bare-except logger.exception(self.order_placement_failure_msg, basket.id) return redirect(receipt_url)
def test_duplicate_payment(self): """ Verify the view errors as expected if there is a duplicate payment attempt after a successful payment. """ # This unit test describes the current state of the code, but I'm not sure if there are other # situations that would result in a duplicate payment that we should handle differently. 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( """{"id":"6028635251536131304003","status":"AUTHORIZED","client_reference_information":{"code":"%s"},"processor_information":{"approval_code":"831000","transaction_id":"558196000003814","network_transaction_id":"558196000003814","card_verification":{"result_code":"3"}},"payment_information":{"tokenized_card":{"type":"001"},"account_features":{"category":"A"}},"order_information":{"amount_details":{"total_amount":"99.00","authorized_amount":"99.00","currency":"USD"}}}""" % order_number # pylint: disable=line-too-long ) response = self.client.post(self.path, data) assert response.status_code == 201 assert response['content-type'] == JSON assert json.loads( response.content)['receipt_page_url'] == get_receipt_page_url( self.site.siteconfiguration, order_number=order_number, disable_back_button=True, ) # Ensure the basket is Submitted basket = Basket.objects.get(pk=basket.pk) self.assertEqual(basket.status, Basket.SUBMITTED) # This response has been pruned to only the needed data. 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 '6028635251536131304003') response = self.client.post(self.path, data) # The original basket is frozen, and the new basket is empty, so currentyl this triggers an error response assert response.status_code == 400 assert response['content-type'] == JSON assert json.loads(response.content) == { 'error': 'There was a problem retrieving your basket. Refresh the page to try again.', 'field_errors': { 'basket': 'There was a problem retrieving your basket. Refresh the page to try again.' } } # Ensure the basket is frozen basket = Basket.objects.get(pk=basket.pk + 1) self.assertEqual(basket.status, Basket.OPEN)
def test_successful_redirect(self): """ Verify redirect to the receipt page. """ self.prepare_basket(0) self.assertEqual(Order.objects.count(), 0) response = self.client.get(self.path) self.assertEqual(Order.objects.count(), 1) order = Order.objects.first() expected_url = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration ) self.assertRedirects(response, expected_url, fetch_redirect_response=False)
def get(self, request): """ """ try: out_trade_no = request.GET.get('out_trade_no') basket = PaymentProcessorResponse.objects.get( transaction_id=out_trade_no).basket receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration) return redirect(receipt_url) except Exception, e: logger.exception(e)
def test_post_checkout_callback(self): """ When the post_checkout signal is emitted, the receiver should attempt to fulfill the newly-placed order and send receipt email. """ credit_provider_id = 'HGW' credit_provider_name = 'Hogwarts' body = {'display_name': credit_provider_name} httpretty.register_uri( httpretty.GET, self.site.siteconfiguration.build_lms_url( 'api/credit/v1/providers/{credit_provider_id}/'.format(credit_provider_id=credit_provider_id) ), body=json.dumps(body), content_type='application/json' ) order = self.prepare_order('credit', credit_provider_id=credit_provider_id) self.mock_access_token_response() send_course_purchase_email(None, user=self.user, order=order) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].from_email, order.site.siteconfiguration.from_email) self.assertEqual(mail.outbox[0].subject, 'Order Receipt') self.assertEqual( mail.outbox[0].body, '\nPayment confirmation for: {course_title}' '\n\nDear {full_name},' '\n\nThank you for purchasing {credit_hours} credit hours from {credit_provider_name} for {course_title}. ' 'A charge will appear on your credit or debit card statement with a company name of "{platform_name}".' '\n\nTo receive your course credit, you must also request credit at the {credit_provider_name} website. ' 'For a link to request credit from {credit_provider_name}, or to see the status of your credit request, ' 'go to your {platform_name} dashboard.' '\n\nTo explore other credit-eligible courses, visit the {platform_name} website. ' 'We add new courses frequently!' '\n\nTo view your payment information, visit the following website.' '\n{receipt_url}' '\n\nThank you. We hope you enjoyed your course!' '\nThe {platform_name} team' '\n\nYou received this message because you purchased credit hours for {course_title}, ' 'an {platform_name} course.\n'.format( course_title=order.lines.first().product.title, full_name=self.user.get_full_name(), credit_hours=2, credit_provider_name=credit_provider_name, platform_name=self.site.name, receipt_url=get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration ) ) )
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 test_post_checkout_callback(self): """ When the post_checkout signal is emitted, the receiver should attempt to fulfill the newly-placed order and send receipt email. """ credit_provider_id = 'HGW' credit_provider_name = 'Hogwarts' body = {'display_name': credit_provider_name} responses.add( responses.GET, self.site.siteconfiguration.build_lms_url( 'api/credit/v1/providers/{credit_provider_id}/'.format(credit_provider_id=credit_provider_id) ), json=body, content_type='application/json' ) order = self.prepare_order('credit', credit_provider_id=credit_provider_id) self.mock_access_token_response() send_course_purchase_email(None, user=self.user, order=order) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].from_email, order.site.siteconfiguration.from_email) self.assertEqual(mail.outbox[0].subject, 'Order Receipt') self.assertEqual( mail.outbox[0].body, '\nPayment confirmation for: {course_title}' '\n\nDear {full_name},' '\n\nThank you for purchasing {credit_hours} credit hours from {credit_provider_name} for {course_title}. ' 'A charge will appear on your credit or debit card statement with a company name of "{platform_name}".' '\n\nTo receive your course credit, you must also request credit at the {credit_provider_name} website. ' 'For a link to request credit from {credit_provider_name}, or to see the status of your credit request, ' 'go to your {platform_name} dashboard.' '\n\nTo explore other credit-eligible courses, visit the {platform_name} website. ' 'We add new courses frequently!' '\n\nTo view your payment information, visit the following website.' '\n{receipt_url}' '\n\nThank you. We hope you enjoyed your course!' '\nThe {platform_name} team' '\n\nYou received this message because you purchased credit hours for {course_title}, ' 'an {platform_name} course.\n'.format( course_title=order.lines.first().product.title, full_name=self.user.get_full_name(), credit_hours=2, credit_provider_name=credit_provider_name, platform_name=self.site.name, receipt_url=get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration ) ) )
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(self, request): """Handle an incoming user returned to us by PayPal after approving payment.""" payment_id = request.GET.get('paymentId') payer_id = request.GET.get('PayerID') logger.info(u"Payment [%s] approved by payer [%s]", payment_id, payer_id) paypal_response = request.GET.dict() basket = self._get_basket(payment_id) if not basket: return redirect(self.payment_processor.error_url) receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration, disable_back_button=True, ) try: with transaction.atomic(): try: self.handle_payment(paypal_response, basket) except PaymentError: return redirect(self.payment_processor.error_url) except: # pylint: disable=bare-except logger.exception( 'Attempts to handle payment for basket [%d] failed.', basket.id) return redirect(receipt_url) try: order = self.create_order(request, basket) except Exception: # pylint: disable=broad-except # any errors here will be logged in the create_order method. If we wanted any # Paypal specific logging for this error, we would do that here. return redirect(receipt_url) try: self.handle_post_order(order) except Exception: # pylint: disable=broad-except self.log_order_placement_exception(basket.order_number, basket.id) return redirect(receipt_url)
def test_execution_redirect_to_lms(self): """ Verify redirection to LMS receipt page after attempted payment execution if the Otto receipt page is disabled. """ self.mock_oauth2_response() # 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) 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), fetch_redirect_response=False)
def send_confirm_purchase_email(sender, order=None, request=None, **kwargs): product = order.lines.first().product recipient = request.POST.get('req_bill_to_email', order.user.email) if request else order.user.email receipt_page_url = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration ) send_notification( order.user, 'CREDIT_RECEIPT', { 'course_title': "Test", 'receipt_page_url': receipt_page_url, 'credit_hours': 20, 'credit_provider': "test", }, order.site, recipient )
def get(self, request): """Handle an incoming user returned to us by Alipay after approving payment.""" payment_id = request.GET.get('out_trade_no') logger.info(u"Payment [%s] approved", payment_id) alipay_response = request.GET.dict() basket = self._get_basket(payment_id) if not basket: return redirect(self.payment_processor.error_url) receipt_url = get_receipt_page_url( order_number=basket.order_number, site_configuration=basket.site.siteconfiguration ) try: shipping_method = NoShippingRequired() shipping_charge = shipping_method.calculate(basket) order_total = OrderTotalCalculator().calculate(basket, shipping_charge) user = basket.owner # Given a basket, order number generation is idempotent. Although we've already # generated this order number once before, it's faster to generate it again # than to retrieve an invoice number from Alipay. order_number = basket.order_number self.handle_order_placement( order_number=order_number, user=user, basket=basket, shipping_address=None, shipping_method=shipping_method, shipping_charge=shipping_charge, billing_address=None, order_total=order_total, request=request ) return redirect(receipt_url) except: # pylint: disable=bare-except logger.exception(self.order_placement_failure_msg, basket.id) return redirect(receipt_url)
def test_execution_redirect_to_lms(self): """ Verify redirection to LMS receipt page after attempted payment execution if Otto receipt page waffle switch is disabled. """ self.site.siteconfiguration.enable_otto_receipt_page = False self.mock_oauth2_response() # 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) 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 ), fetch_redirect_response=False )
def send_course_purchase_email(sender, order=None, **kwargs): # pylint: disable=unused-argument """Send course purchase notification email when a course is purchased.""" if waffle.switch_is_active('ENABLE_NOTIFICATIONS'): # We do not currently support email sending for orders with more than one item. if len(order.lines.all()) == ORDER_LINE_COUNT: product = order.lines.first().product credit_provider_id = getattr(product.attr, 'credit_provider', None) if not credit_provider_id: logger.error( 'Failed to send credit receipt notification. Credit seat product [%s] has no provider.', product.id ) return elif product.get_product_class().name == 'Seat': provider_data = get_credit_provider_details( access_token=order.site.siteconfiguration.access_token, credit_provider_id=credit_provider_id, site_configuration=order.site.siteconfiguration ) receipt_page_url = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration ) if provider_data: send_notification( order.user, 'CREDIT_RECEIPT', { 'course_title': product.title, 'receipt_page_url': receipt_page_url, 'credit_hours': product.attr.credit_hours, 'credit_provider': provider_data['display_name'], }, order.site ) else: logger.info('Currently support receipt emails for order with one item.')
def _assert_execution_redirect(self, payer_info=None, url_redirect=None): """Verify redirection to Otto receipt page after attempted payment execution.""" self.mock_oauth2_response() # 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) creation_response = self.mock_payment_creation_response(self.basket, find=True) execution_response = self.mock_payment_execution_response(self.basket, payer_info=payer_info) response = self.client.get(reverse('paypal_execute'), self.RETURN_DATA) self.assertRedirects( response, url_redirect or get_receipt_page_url( order_number=self.basket.order_number, site_configuration=self.basket.site.siteconfiguration ), fetch_redirect_response=False ) return creation_response, execution_response
def get_redirect_url(self, *args, **kwargs): basket = Basket.get_basket(self.request.user, self.request.site) if not basket.is_empty: # Need to re-apply the voucher to the basket. Applicator().apply(basket, self.request.user, self.request) if basket.total_incl_tax != Decimal(0): raise BasketNotFreeError( "Basket [{}] is not free. User affected [{}]".format( basket.id, basket.owner.id ) ) order = self.place_free_order(basket) receipt_path = get_receipt_page_url( order_number=order.number, site_configuration=order.site.siteconfiguration ) url = get_lms_url(receipt_path) else: # If a user's basket is empty redirect the user to the basket summary # page which displays the appropriate message for empty baskets. url = reverse('basket:summary') return url
def _get_receipt_url(self): """DRY helper for getting receipt page URL.""" return get_receipt_page_url(site_configuration=self.site.siteconfiguration)
def _generate_parameters(self, basket, use_sop_profile, **kwargs): """ Generates the parameters dict. A signature is NOT included in the parameters. Arguments: basket (Basket): Basket from which the pricing and item details are pulled. use_sop_profile (bool, optional): Indicates if the Silent Order POST profile should be used. **kwargs: Additional parameters to add to the generated dict. Returns: dict: Dictionary containing the payment parameters that should be sent to CyberSource. """ site = basket.site access_key = self.access_key profile_id = self.profile_id if use_sop_profile: access_key = self.sop_access_key profile_id = self.sop_profile_id parameters = { 'access_key': access_key, 'profile_id': profile_id, 'transaction_uuid': uuid.uuid4().hex, 'signed_field_names': '', 'unsigned_field_names': '', 'signed_date_time': datetime.datetime.utcnow().strftime(ISO_8601_FORMAT), 'locale': self.language_code, 'transaction_type': 'sale', 'reference_number': basket.order_number, 'amount': str(basket.total_incl_tax), 'currency': basket.currency, 'override_custom_receipt_page': get_receipt_page_url( site_configuration=site.siteconfiguration, order_number=basket.order_number, override_url=site.siteconfiguration.build_ecommerce_url( reverse('cybersource_redirect') ) ), 'override_custom_cancel_page': self.cancel_page_url, } # Level 2/3 details if self.send_level_2_3_details: parameters['amex_data_taa1'] = site.name parameters['purchasing_level'] = '3' parameters['line_item_count'] = basket.lines.count() # Note (CCB): This field (purchase order) is required for Visa; # but, is not actually used by us/exposed on the order form. parameters['user_po'] = 'BLANK' for index, line in enumerate(basket.lines.all()): parameters['item_{}_code'.format(index)] = line.product.get_product_class().slug parameters['item_{}_discount_amount '.format(index)] = str(line.discount_value) # Note (CCB): This indicates that the total_amount field below includes tax. parameters['item_{}_gross_net_indicator'.format(index)] = 'Y' parameters['item_{}_name'.format(index)] = clean_field_value(line.product.title) parameters['item_{}_quantity'.format(index)] = line.quantity parameters['item_{}_sku'.format(index)] = line.stockrecord.partner_sku parameters['item_{}_tax_amount'.format(index)] = str(line.line_tax) parameters['item_{}_tax_rate'.format(index)] = '0' parameters['item_{}_total_amount '.format(index)] = str(line.line_price_incl_tax_incl_discounts) # Note (CCB): Course seat is not a unit of measure. Use item (ITM). parameters['item_{}_unit_of_measure'.format(index)] = 'ITM' parameters['item_{}_unit_price'.format(index)] = str(line.unit_price_incl_tax) # Only send consumer_id for hosted payment page if not use_sop_profile: parameters['consumer_id'] = basket.owner.username # Add the extra parameters parameters.update(kwargs.get('extra_parameters', {})) # Mitigate PCI compliance issues signed_field_names = parameters.keys() if any(pci_field in signed_field_names for pci_field in self.PCI_FIELDS): raise PCIViolation('One or more PCI-related fields is contained in the payment parameters. ' 'This service is NOT PCI-compliant! Deactivate this service immediately!') return parameters