def test_less_or_equal_to_zero(self): """ An order may not have a negative or zero price """ course_run, user = create_purchasable_course_run() program = course_run.course.program for invalid_price in ( 0, -1.23, ): program.price = invalid_price program.save() with patch('ecommerce.api.get_purchasable_course_run', autospec=True, return_value=course_run) as mocked: with self.assertRaises(ImproperlyConfigured) as ex: create_unfulfilled_order(course_run.edx_course_key, user) assert ex.exception.args[ 0] == "Price to be charged is less than zero" assert mocked.call_count == 1 assert mocked.call_args[0] == (course_run.edx_course_key, user) assert Order.objects.count() == 0
def test_no_program_enrollment(self): """ For a user to purchase a course run they must already be enrolled in the program """ course_run, user = create_purchasable_course_run() ProgramEnrollment.objects.filter(program=course_run.course.program, user=user).delete() with self.assertRaises(Http404): create_unfulfilled_order(course_run.edx_course_key, user)
def test_make_reference_id(self): """ make_reference_id should concatenate the reference prefix and the order id """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) assert "MM-{}-{}".format(CYBERSOURCE_REFERENCE_PREFIX, order.id) == make_reference_id(order)
def test_failed_enroll(self): """ If we fail to enroll in edX, the order status should be fulfilled but an error email should be sent """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) data = {} for _ in range(5): data[FAKE.text()] = FAKE.text() data['req_reference_number'] = make_reference_id(order) data['decision'] = 'ACCEPT' with patch('ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True), patch( 'ecommerce.views.enroll_user_on_success', side_effect=KeyError ), patch( 'ecommerce.views.MailgunClient.send_individual_email', ) as send_email: self.client.post(reverse('order-fulfillment'), data=data) assert Order.objects.count() == 1 # An enrollment failure should not prevent the order from being fulfilled order = Order.objects.first() assert order.status == Order.FULFILLED assert send_email.call_count == 1 assert send_email.call_args[0][0] == 'Error occurred when enrolling user during order fulfillment' assert send_email.call_args[0][1].startswith( 'Error occurred when enrolling user during order fulfillment for {order}. ' 'Exception: '.format( order=order, ) ) assert send_email.call_args[0][2] == '*****@*****.**'
def test_not_accept(self, decision, should_send_email): """ If the decision is not ACCEPT then the order should be marked as failed """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) data = { 'req_reference_number': make_reference_id(order), 'decision': decision, } with patch('ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True), patch( 'ecommerce.views.MailgunClient.send_individual_email', ) as send_email: resp = self.client.post(reverse('order-fulfillment'), data=data) assert resp.status_code == status.HTTP_200_OK assert len(resp.content) == 0 order.refresh_from_db() assert Order.objects.count() == 1 assert order.status == Order.FAILED if should_send_email: assert send_email.call_count == 1 assert send_email.call_args[0] == ( 'Order fulfillment failed, decision={decision}'.format( decision='something else'), 'Order fulfillment failed for order {order}'.format( order=order), '*****@*****.**', ) else: assert send_email.call_count == 0
def test_failed_enroll(self): """ If we fail to enroll in edX, the order status should be fulfilled but an error email should be sent """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) data = {} for _ in range(5): data[FAKE.text()] = FAKE.text() data['req_reference_number'] = make_reference_id(order) data['decision'] = 'ACCEPT' with patch( 'ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True), patch( 'ecommerce.views.enroll_user_on_success', side_effect=KeyError), patch( 'ecommerce.views.MailgunClient.send_individual_email', ) as send_email: self.client.post(reverse('order-fulfillment'), data=data) assert Order.objects.count() == 1 # An enrollment failure should not prevent the order from being fulfilled order = Order.objects.first() assert order.status == Order.FULFILLED assert send_email.call_count == 1 assert send_email.call_args[0][ 0] == 'Error occurred when enrolling user during order fulfillment' assert send_email.call_args[0][1].startswith( 'Error occurred when enrolling user during order fulfillment for {order}. ' 'Exception: '.format(order=order, )) assert send_email.call_args[0][2] == '*****@*****.**'
def test_not_accept(self, decision, should_send_email): """ If the decision is not ACCEPT then the order should be marked as failed """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) data = { 'req_reference_number': make_reference_id(order), 'decision': decision, } with patch( 'ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True ), patch( 'ecommerce.views.MailgunClient.send_individual_email', ) as send_email: resp = self.client.post(reverse('order-fulfillment'), data=data) assert resp.status_code == status.HTTP_200_OK assert len(resp.content) == 0 order.refresh_from_db() assert Order.objects.count() == 1 assert order.status == Order.FAILED if should_send_email: assert send_email.call_count == 1 assert send_email.call_args[0] == ( 'Order fulfillment failed, decision={decision}'.format(decision='something else'), 'Order fulfillment failed for order {order}'.format(order=order), '*****@*****.**', ) else: assert send_email.call_count == 0
def test_get_new_order_by_reference_number(self): """ get_new_order_by_reference_number returns an Order with status created """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) same_order = get_new_order_by_reference_number(make_reference_id(order)) assert same_order.id == order.id
def test_less_or_equal_to_zero(self): """ An order may not have a negative or zero price """ course_run, user = create_purchasable_course_run() program = course_run.course.program for invalid_price in (0, -1.23,): program.price = invalid_price program.save() with patch('ecommerce.api.get_purchasable_course_run', autospec=True, return_value=course_run) as mocked: with self.assertRaises(ImproperlyConfigured) as ex: create_unfulfilled_order(course_run.edx_course_key, user) assert ex.exception.args[0] == "Price to be charged is less than zero" assert mocked.call_count == 1 assert mocked.call_args[0] == (course_run.edx_course_key, user) assert Order.objects.count() == 0
def post( self, request, *args, **kwargs ): # pylint: disable=too-many-locals,unused-argument """ Create a new unfulfilled Order from the user's basket and return information used to submit to CyberSource. """ validated_basket = validate_basket_for_checkout(request.user) affiliate_id = get_affiliate_id_from_request(request) order = create_unfulfilled_order(validated_basket, affiliate_id=affiliate_id) base_url = request.build_absolute_uri("/") text_id = validated_basket.product_version.product.content_object.text_id receipt_url = make_receipt_url(base_url=base_url, readable_id=text_id) user_ip, _ = get_client_ip(request) if order.total_price_paid == 0: # If price is $0, don't bother going to CyberSource, just mark as fulfilled order.status = Order.FULFILLED order.save() sync_hubspot_deal(order) complete_order(order) order.save_and_log(request.user) product = validated_basket.product_version.product # $0 orders do not go to CyberSource so we need to build a payload # for GTM in order to track these purchases as well. Actual tracking # call is sent from the frontend. payload = { "transaction_id": "T-{}".format(order.id), "transaction_total": 0.00, "product_type": product.type_string, "courseware_id": text_id, "reference_number": "REF-{}".format(order.id), } # This redirects the user to our order success page url = receipt_url if settings.ENABLE_ORDER_RECEIPTS: send_ecommerce_order_receipt(order) method = "GET" else: # This generates a signed payload which is submitted as an HTML form to CyberSource cancel_url = urljoin(base_url, "checkout/") payload = generate_cybersource_sa_payload( order=order, receipt_url=receipt_url, cancel_url=cancel_url, ip_address=user_ip, ) url = settings.CYBERSOURCE_SECURE_ACCEPTANCE_URL method = "POST" return Response({"payload": payload, "url": url, "method": method})
def test_already_purchased_but_need_exam_attempts(self, order_status, has_to_pay): """ Can purchase a course run again if failed all exam attempts """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) order.status = order_status order.save() assert get_purchasable_course_run(course_run.edx_course_key, user) == course_run assert has_to_pay.call_count == 1
def test_signed_payload(self): """ A valid payload should be signed appropriately """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) username = '******' transaction_uuid = 'hex' fake_user_ip = "194.100.0.1" now = now_in_utc() with patch('ecommerce.api.get_social_username', autospec=True, return_value=username): with patch('ecommerce.api.now_in_utc', autospec=True, return_value=now) as now_mock: with patch('ecommerce.api.uuid.uuid4', autospec=True, return_value=MagicMock(hex=transaction_uuid)): payload = generate_cybersource_sa_payload(order, 'dashboard_url', fake_user_ip) signature = payload.pop('signature') assert generate_cybersource_sa_signature(payload) == signature signed_field_names = payload['signed_field_names'].split(',') assert signed_field_names == sorted(payload.keys()) quoted_course_key = quote_plus(course_run.edx_course_key) assert payload == { 'access_key': CYBERSOURCE_ACCESS_KEY, 'amount': str(order.total_price_paid), 'consumer_id': username, 'currency': 'USD', 'item_0_code': 'course', 'item_0_name': '{}'.format(course_run.title), 'item_0_quantity': 1, 'item_0_sku': '{}'.format(course_run.edx_course_key), 'item_0_tax_amount': '0', 'item_0_unit_price': str(order.total_price_paid), 'line_item_count': 1, 'locale': 'en-us', 'override_custom_cancel_page': 'dashboard_url?status=cancel&course_key={}'.format(quoted_course_key), 'override_custom_receipt_page': "dashboard_url?status=receipt&course_key={}".format(quoted_course_key), 'reference_number': make_reference_id(order), 'profile_id': CYBERSOURCE_PROFILE_ID, 'signed_date_time': now.strftime(ISO_8601_FORMAT), 'signed_field_names': ','.join(signed_field_names), 'transaction_type': 'sale', 'transaction_uuid': transaction_uuid, 'unsigned_field_names': '', 'merchant_defined_data1': 'course', 'merchant_defined_data2': '{}'.format(course_run.title), 'merchant_defined_data3': '{}'.format(course_run.edx_course_key), "customer_ip_address": fake_user_ip if fake_user_ip else None, } assert now_mock.called
def test_signed_payload(self): """ A valid payload should be signed appropriately """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) username = '******' transaction_uuid = 'hex' now = now_in_utc() with patch('ecommerce.api.get_social_username', autospec=True, return_value=username): with patch('ecommerce.api.now_in_utc', autospec=True, return_value=now) as now_mock: with patch('ecommerce.api.uuid.uuid4', autospec=True, return_value=MagicMock(hex=transaction_uuid)): payload = generate_cybersource_sa_payload(order, 'dashboard_url') signature = payload.pop('signature') assert generate_cybersource_sa_signature(payload) == signature signed_field_names = payload['signed_field_names'].split(',') assert signed_field_names == sorted(payload.keys()) quoted_course_key = quote_plus(course_run.edx_course_key) assert payload == { 'access_key': CYBERSOURCE_ACCESS_KEY, 'amount': str(order.total_price_paid), 'consumer_id': username, 'currency': 'USD', 'item_0_code': 'course', 'item_0_name': '{}'.format(course_run.title), 'item_0_quantity': 1, 'item_0_sku': '{}'.format(course_run.edx_course_key), 'item_0_tax_amount': '0', 'item_0_unit_price': str(order.total_price_paid), 'line_item_count': 1, 'locale': 'en-us', 'override_custom_cancel_page': 'dashboard_url?status=cancel&course_key={}'.format(quoted_course_key), 'override_custom_receipt_page': "dashboard_url?status=receipt&course_key={}".format(quoted_course_key), 'reference_number': make_reference_id(order), 'profile_id': CYBERSOURCE_PROFILE_ID, 'signed_date_time': now.strftime(ISO_8601_FORMAT), 'signed_field_names': ','.join(signed_field_names), 'transaction_type': 'sale', 'transaction_uuid': transaction_uuid, 'unsigned_field_names': '', 'merchant_defined_data1': 'course', 'merchant_defined_data2': '{}'.format(course_run.title), 'merchant_defined_data3': '{}'.format(course_run.edx_course_key), } assert now_mock.called
def test_already_purchased(self): """ Purchasable course runs must not be already purchased """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) # succeeds because order is unfulfilled assert course_run == get_purchasable_course_run(course_run.edx_course_key, user) order.status = Order.FULFILLED order.save() with self.assertRaises(ValidationError) as ex: get_purchasable_course_run(course_run.edx_course_key, user) assert ex.exception.args[0] == 'Course run {} is already purchased'.format(course_run.edx_course_key)
def test_status(self): """ get_order_by_reference_number should only get orders with status=CREATED """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) order.status = Order.FAILED order.save() with self.assertRaises(EcommerceException) as ex: # change order number to something not likely to already exist in database order.id = 98765432 assert not Order.objects.filter(id=order.id).exists() get_new_order_by_reference_number(make_reference_id(order)) assert ex.exception.args[0] == "Unable to find order {}".format(order.id)
def post(self, request, *args, **kwargs): """ Creates a new unfulfilled Order and returns information used to submit to CyberSource """ try: course_id = request.data['course_id'] except KeyError: raise ValidationError("Missing course_id") order = create_unfulfilled_order(course_id, request.user) payload = generate_cybersource_sa_payload(order) return Response({ 'payload': payload, 'url': settings.CYBERSOURCE_SECURE_ACCEPTANCE_URL, })
def test_already_purchased(self, order_status, has_to_pay): """ Purchasable course runs must not be already purchased """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) # succeeds because order is unfulfilled assert course_run == get_purchasable_course_run(course_run.edx_course_key, user) order.status = order_status order.save() with self.assertRaises(ValidationError) as ex: get_purchasable_course_run(course_run.edx_course_key, user) assert ex.exception.args[0] == 'Course run {} is already purchased'.format(course_run.edx_course_key) assert has_to_pay.call_count == 1
def test_ignore_duplicate_cancel(self): """ If the decision is CANCEL and we already have a duplicate failed order, don't change anything. """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) order.status = Order.FAILED order.save() data = { 'req_reference_number': make_reference_id(order), 'decision': 'CANCEL', } with patch('ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True): resp = self.client.post(reverse('order-fulfillment'), data=data) assert resp.status_code == status.HTTP_200_OK assert Order.objects.count() == 1 assert Order.objects.get(id=order.id).status == Order.FAILED
def test_order_fulfilled(self): """ Test the happy case """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) data_before = order.to_dict() data = {} for _ in range(5): data[FAKE.text()] = FAKE.text() data['req_reference_number'] = make_reference_id(order) data['decision'] = 'ACCEPT' with patch('ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True), patch( 'ecommerce.views.enroll_user_on_success', autospec=True, ) as enroll_user, patch( 'ecommerce.views.MailgunClient.send_individual_email', ) as send_email: resp = self.client.post(reverse('order-fulfillment'), data=data) assert len(resp.content) == 0 assert resp.status_code == status.HTTP_200_OK order.refresh_from_db() assert order.status == Order.FULFILLED assert order.receipt_set.count() == 1 assert order.receipt_set.first().data == data enroll_user.assert_called_with(order) assert send_email.call_count == 0 assert OrderAudit.objects.count() == 2 order_audit = OrderAudit.objects.last() assert order_audit.order == order assert order_audit.data_before == data_before assert order_audit.data_after == order.to_dict()
def test_error_on_duplicate_order(self, order_status, decision): """If there is a duplicate message (except for CANCEL), raise an exception""" course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) order.status = order_status order.save() data = { 'req_reference_number': make_reference_id(order), 'decision': decision, } with patch('ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True), self.assertRaises( EcommerceException) as ex: self.client.post(reverse('order-fulfillment'), data=data) assert Order.objects.count() == 1 assert Order.objects.get(id=order.id).status == order_status assert ex.exception.args[ 0] == "Order {id} is expected to have status 'created'".format( id=order.id, )
def test_ignore_duplicate_cancel(self): """ If the decision is CANCEL and we already have a duplicate failed order, don't change anything. """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) order.status = Order.FAILED order.save() data = { 'req_reference_number': make_reference_id(order), 'decision': 'CANCEL', } with patch( 'ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True ): resp = self.client.post(reverse('order-fulfillment'), data=data) assert resp.status_code == status.HTTP_200_OK assert Order.objects.count() == 1 assert Order.objects.get(id=order.id).status == Order.FAILED
def test_create_order(self): """ Create Order from a purchasable course """ course_run, user = create_purchasable_course_run() price = course_run.courseprice_set.get(is_valid=True).price with patch('ecommerce.api.get_purchasable_course_run', autospec=True, return_value=course_run) as mocked: order = create_unfulfilled_order(course_run.edx_course_key, user) assert mocked.call_count == 1 assert mocked.call_args[0] == (course_run.edx_course_key, user) assert Order.objects.count() == 1 assert order.status == Order.CREATED assert order.total_price_paid == price assert order.user == user assert order.line_set.count() == 1 line = order.line_set.first() assert line.course_key == course_run.edx_course_key assert line.description == 'Seat for {}'.format(course_run.title) assert line.price == price
def test_error_on_duplicate_order(self, order_status, decision): """If there is a duplicate message (except for CANCEL), raise an exception""" course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) order.status = order_status order.save() data = { 'req_reference_number': make_reference_id(order), 'decision': decision, } with patch( 'ecommerce.views.IsSignedByCyberSource.has_permission', return_value=True ), self.assertRaises(EcommerceException) as ex: self.client.post(reverse('order-fulfillment'), data=data) assert Order.objects.count() == 1 assert Order.objects.get(id=order.id).status == order_status assert ex.exception.args[0] == "Order {id} is expected to have status 'created'".format( id=order.id, )
def test_signed_payload(self): """ A valid payload should be signed appropriately """ course_run, user = create_purchasable_course_run() order = create_unfulfilled_order(course_run.edx_course_key, user) username = '******' transaction_uuid = 'hex' now = datetime.utcnow() with patch('ecommerce.api.get_social_username', autospec=True, return_value=username): with patch('ecommerce.api.datetime', autospec=True, utcnow=MagicMock(return_value=now)): with patch('ecommerce.api.uuid.uuid4', autospec=True, return_value=MagicMock(hex=transaction_uuid)): payload = generate_cybersource_sa_payload(order) signature = payload.pop('signature') assert generate_cybersource_sa_signature(payload) == signature signed_field_names = payload['signed_field_names'].split(',') assert signed_field_names == sorted(payload.keys()) assert payload == { 'access_key': CYBERSOURCE_ACCESS_KEY, 'amount': str(order.total_price_paid), 'consumer_id': username, 'currency': 'USD', 'locale': 'en-us', 'override_custom_cancel_page': 'https://micromasters.mit.edu?cancel', 'override_custom_receipt_page': "https://micromasters.mit.edu?receipt", 'reference_number': make_reference_id(order), 'profile_id': CYBERSOURCE_PROFILE_ID, 'signed_date_time': now.strftime(ISO_8601_FORMAT), 'signed_field_names': ','.join(signed_field_names), 'transaction_type': 'sale', 'transaction_uuid': transaction_uuid, 'unsigned_field_names': '', }
def test_create_order(self, has_coupon): # pylint: disable=too-many-locals """ Create Order from a purchasable course """ course_run, user = create_purchasable_course_run() discounted_price = round(course_run.course.program.price/2, 2) coupon = None if has_coupon: coupon = CouponFactory.create(content_object=course_run.course) price_tuple = (discounted_price, coupon) with patch( 'ecommerce.api.get_purchasable_course_run', autospec=True, return_value=course_run, ) as get_purchasable, patch( 'ecommerce.api.calculate_run_price', autospec=True, return_value=price_tuple, ) as _calculate_run_price: order = create_unfulfilled_order(course_run.edx_course_key, user) assert get_purchasable.call_count == 1 assert get_purchasable.call_args[0] == (course_run.edx_course_key, user) assert _calculate_run_price.call_count == 1 assert _calculate_run_price.call_args[0] == (course_run, user) assert Order.objects.count() == 1 assert order.status == Order.CREATED assert order.total_price_paid == discounted_price assert order.user == user assert order.line_set.count() == 1 line = order.line_set.first() assert line.course_key == course_run.edx_course_key assert line.description == 'Seat for {}'.format(course_run.title) assert line.price == discounted_price assert OrderAudit.objects.count() == 1 order_audit = OrderAudit.objects.first() assert order_audit.order == order assert order_audit.data_after == order.to_dict() # data_before only has modified_at different, since we only call save_and_log # after Order is already created data_before = order_audit.data_before dict_before = order.to_dict() del data_before['modified_at'] del dict_before['modified_at'] assert data_before == dict_before if has_coupon: assert RedeemedCoupon.objects.count() == 1 redeemed_coupon = RedeemedCoupon.objects.get(order=order, coupon=coupon) assert RedeemedCouponAudit.objects.count() == 1 audit = RedeemedCouponAudit.objects.first() assert audit.redeemed_coupon == redeemed_coupon assert audit.data_after == serialize_model_object(redeemed_coupon) else: assert RedeemedCoupon.objects.count() == 0 assert RedeemedCouponAudit.objects.count() == 0
def post(self, request, *args, **kwargs): """ If the course run is part of a financial aid program, create a new unfulfilled Order and return information used to submit to CyberSource. If the program does not have financial aid, this will return a URL to let the user pay for the course on edX. """ try: course_id = request.data['course_id'] except KeyError: raise ValidationError("Missing course_id") course_run = get_object_or_404( CourseRun, course__program__live=True, edx_course_key=course_id, ) if course_run.course.program.financial_aid_availability: order = create_unfulfilled_order(course_id, request.user) dashboard_url = request.build_absolute_uri('/dashboard/') if order.total_price_paid == 0: # If price is $0, don't bother going to CyberSource, just mark as fulfilled order.status = Order.FULFILLED order.save_and_log(request.user) try: enroll_user_on_success(order) except: # pylint: disable=bare-except log.exception( "Error occurred when enrolling user in one or more courses for order %s. " "See other errors above for more info.", order ) try: MailgunClient().send_individual_email( "Error occurred when enrolling user during $0 checkout", "Error occurred when enrolling user during $0 checkout for {order}. " "Exception: {exception}".format( order=order, exception=traceback.format_exc() ), settings.ECOMMERCE_EMAIL, ) except: # pylint: disable=bare-except log.exception( "Error occurred when sending the email to notify support " "of user enrollment error during order %s $0 checkout", order, ) # This redirects the user to our order success page payload = {} url = make_dashboard_receipt_url(dashboard_url, course_id, 'receipt') method = 'GET' else: # This generates a signed payload which is submitted as an HTML form to CyberSource payload = generate_cybersource_sa_payload(order, dashboard_url) url = settings.CYBERSOURCE_SECURE_ACCEPTANCE_URL method = 'POST' else: # This redirects the user to edX to purchase the course there payload = {} url = urljoin(settings.EDXORG_BASE_URL, '/course_modes/choose/{}/'.format(course_id)) method = 'GET' return Response({ 'payload': payload, 'url': url, 'method': method, })
def post(self, request, *args, **kwargs): """ If the course run is part of a financial aid program, create a new unfulfilled Order and return information used to submit to CyberSource. If the program does not have financial aid, this will return a URL to let the user pay for the course on edX. """ user_ip, _ = get_client_ip(request) try: course_id = request.data['course_id'] except KeyError: raise ValidationError("Missing course_id") course_run = get_object_or_404( CourseRun, course__program__live=True, edx_course_key=course_id, ) if course_run.course.program.financial_aid_availability: order = create_unfulfilled_order(course_id, request.user) dashboard_url = request.build_absolute_uri('/dashboard/') if order.total_price_paid == 0: # If price is $0, don't bother going to CyberSource, just mark as fulfilled order.status = Order.FULFILLED order.save_and_log(request.user) try: enroll_user_on_success(order) except: # pylint: disable=bare-except log.exception( "Error occurred when enrolling user in one or more courses for order %s. " "See other errors above for more info.", order) try: MailgunClient().send_individual_email( "Error occurred when enrolling user during $0 checkout", "Error occurred when enrolling user during $0 checkout for {order}. " "Exception: {exception}".format( order=order, exception=traceback.format_exc()), settings.ECOMMERCE_EMAIL, ) except: # pylint: disable=bare-except log.exception( "Error occurred when sending the email to notify support " "of user enrollment error during order %s $0 checkout", order, ) # This redirects the user to our order success page payload = {} url = make_dashboard_receipt_url(dashboard_url, course_id, 'receipt') method = 'GET' else: # This generates a signed payload which is submitted as an HTML form to CyberSource payload = generate_cybersource_sa_payload( order, dashboard_url, user_ip) url = settings.CYBERSOURCE_SECURE_ACCEPTANCE_URL method = 'POST' else: # This redirects the user to edX to purchase the course there payload = {} url = urljoin(settings.EDXORG_BASE_URL, '/course_modes/choose/{}/'.format(course_id)) method = 'GET' return Response({ 'payload': payload, 'url': url, 'method': method, })