Ejemplo n.º 1
0
    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
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)
Ejemplo n.º 4
0
 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)
Ejemplo n.º 5
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] == '*****@*****.**'
Ejemplo n.º 6
0
 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)
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
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] == '*****@*****.**'
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
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
Ejemplo n.º 11
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
Ejemplo n.º 12
0
    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
Ejemplo n.º 13
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})
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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
Ejemplo n.º 17
0
    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
Ejemplo n.º 18
0
    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)
Ejemplo n.º 19
0
    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)
Ejemplo n.º 20
0
    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)
Ejemplo n.º 21
0
    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,
        })
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
    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
Ejemplo n.º 24
0
    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()
Ejemplo n.º 25
0
    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, )
Ejemplo n.º 26
0
    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
Ejemplo n.º 27
0
    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
Ejemplo n.º 28
0
    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,
        )
Ejemplo n.º 29
0
    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()
Ejemplo n.º 30
0
    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': '',
        }
Ejemplo n.º 31
0
    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
Ejemplo n.º 32
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,
        })
Ejemplo n.º 33
0
    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
Ejemplo n.º 34
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.
        """
        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,
        })