Example #1
0
    def test_tracking_context(self):
        """
        Ensure the tracking context is set up in the api client correctly and
        automatically.
        """
        # fake an ecommerce api request.
        httpretty.register_uri(
            httpretty.POST,
            '{}/baskets/1/'.format(TEST_API_URL),
            status=200, body='{}',
            adding_headers={'Content-Type': 'application/json'}
        )
        mock_tracker = mock.Mock()
        mock_tracker.resolve_context = mock.Mock(return_value={'client_id': self.TEST_CLIENT_ID})
        with mock.patch('commerce.tracker.get_tracker', return_value=mock_tracker):
            ecommerce_api_client(self.user).baskets(1).post()

        # make sure the request's JWT token payload included correct tracking context values.
        actual_header = httpretty.last_request().headers['Authorization']
        expected_payload = {
            'username': self.user.username,
            'full_name': self.user.profile.name,
            'email': self.user.email,
            'tracking_context': {
                'lms_user_id': self.user.id,  # pylint: disable=no-member
                'lms_client_id': self.TEST_CLIENT_ID,
            },
        }
        expected_header = 'JWT {}'.format(jwt.encode(expected_payload, TEST_API_SIGNING_KEY))
        self.assertEqual(actual_header, expected_header)
Example #2
0
def checkout_with_ecommerce_service(user, course_key, course_mode, processor):     # pylint: disable=invalid-name
    """ Create a new basket and trigger immediate checkout, using the E-Commerce API. """
    course_id = unicode(course_key)
    try:
        api = ecommerce_api_client(user)
        # Make an API call to create the order and retrieve the results
        result = api.baskets.post({
            'products': [{'sku': course_mode.sku}],
            'checkout': True,
            'payment_processor_name': processor
        })

        # Pass the payment parameters directly from the API response.
        return result.get('payment_data')
    except SlumberBaseException:
        params = {'username': user.username, 'mode': course_mode.slug, 'course_id': course_id}
        log.exception('Failed to create order for %(username)s %(mode)s mode of %(course_id)s', params)
        raise
    finally:
        audit_log(
            'checkout_requested',
            course_id=course_id,
            mode=course_mode.slug,
            processor_name=processor,
            user_id=user.id
        )
Example #3
0
 def get(self, request, *_args, **kwargs):
     """ HTTP handler. """
     try:
         order = ecommerce_api_client(request.user).baskets(kwargs['basket_id']).order.get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
Example #4
0
 def get(self, request, number):  # pylint:disable=unused-argument
     """ HTTP handler. """
     try:
         order = ecommerce_api_client(request.user).orders(number).get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
Example #5
0
    def test_client_unicode(self):
        """
        The client should handle json responses properly when they contain
        unicode character data.

        Regression test for ECOM-1606.
        """
        expected_content = '{"result": "Préparatoire"}'
        httpretty.register_uri(
            httpretty.GET,
            '{}/baskets/1/order/'.format(TEST_API_URL),
            status=200, body=expected_content,
            adding_headers={'Content-Type': 'application/json'},
        )
        actual_object = ecommerce_api_client(self.user).baskets(1).order.get()
        self.assertEqual(actual_object, {u"result": u"Préparatoire"})
Example #6
0
def refund_seat(course_enrollment, request_user):
    """
    Attempt to initiate a refund for any orders associated with the seat being
    unenrolled, using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        request_user: the user as whom to authenticate to the commerce service
            when attempting to initiate the refund.

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during
            communication with the commerce service.
        exceptions.Timeout: if the attempt to reach the commerce service timed
            out.

    """
    course_key_str = unicode(course_enrollment.course_id)
    unenrolled_user = course_enrollment.user

    try:
        refund_ids = ecommerce_api_client(request_user
                                          or unenrolled_user).refunds.post({
                                              'course_id':
                                              course_key_str,
                                              'username':
                                              unenrolled_user.username
                                          })
    except HttpClientError, exc:
        if exc.response.status_code == 403 and request_user != unenrolled_user:
            # this is a known limitation; commerce service does not presently
            # support the case of a non-superusers initiating a refund on
            # behalf of another user.
            log.warning(
                "User [%s] was not authorized to initiate a refund for user [%s] "
                "upon unenrollment from course [%s]", request_user.id,
                unenrolled_user.id, course_key_str)
            return []
        else:
            # no other error is anticipated, so re-raise the Exception
            raise exc
Example #7
0
def refund_seat(course_enrollment, request_user):
    """
    Attempt to initiate a refund for any orders associated with the seat being
    unenrolled, using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        request_user: the user as whom to authenticate to the commerce service
            when attempting to initiate the refund.

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during
            communication with the commerce service.
        exceptions.Timeout: if the attempt to reach the commerce service timed
            out.

    """
    course_key_str = unicode(course_enrollment.course_id)
    unenrolled_user = course_enrollment.user

    try:
        refund_ids = ecommerce_api_client(request_user or unenrolled_user).refunds.post(
            {"course_id": course_key_str, "username": unenrolled_user.username}
        )
    except HttpClientError, exc:
        if exc.response.status_code == 403 and request_user != unenrolled_user:
            # this is a known limitation; commerce service does not presently
            # support the case of a non-superusers initiating a refund on
            # behalf of another user.
            log.warning(
                "User [%s] was not authorized to initiate a refund for user [%s] " "upon unenrollment from course [%s]",
                request_user.id,
                unenrolled_user.id,
                course_key_str,
            )
            return []
        else:
            # no other error is anticipated, so re-raise the Exception
            raise exc
Example #8
0
    def post(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """
        Attempt to create the basket and enroll the user.
        """
        user = request.user
        valid, course_key, error = self._is_data_valid(request)
        if not valid:
            return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)

        embargo_response = embargo_api.get_embargo_response(request, course_key, user)

        if embargo_response:
            return embargo_response

        # Don't do anything if an enrollment already exists
        course_id = unicode(course_key)
        enrollment = CourseEnrollment.get_enrollment(user, course_key)
        if enrollment and enrollment.is_active:
            msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
            return DetailResponse(msg, status=HTTP_409_CONFLICT)

        # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS
        # redirects to track selection.
        honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)

        if not honor_mode:
            msg = Messages.NO_HONOR_MODE.format(course_id=course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
        elif not honor_mode.sku:
            # If there are no course modes with SKUs, enroll the user without contacting the external API.
            msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id,
                                                  username=user.username)
            log.info(msg)
            self._enroll(course_key, user)
            self._handle_marketing_opt_in(request, course_key, user)
            return DetailResponse(msg)

        # Setup the API

        try:
            api = ecommerce_api_client(user)
        except ValueError:
            self._enroll(course_key, user)
            msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key))
            log.debug(msg)
            return DetailResponse(msg)

        response = None

        # Make the API call
        try:
            response_data = api.baskets.post({
                'products': [{'sku': honor_mode.sku}],
                'checkout': True,
            })

            payment_data = response_data["payment_data"]
            if payment_data:
                # Pass data to the client to begin the payment flow.
                response = JsonResponse(payment_data)
            elif response_data['order']:
                # The order was completed immediately because there is no charge.
                msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
                log.debug(msg)
                response = DetailResponse(msg)
            else:
                msg = u'Unexpected response from basket endpoint.'
                log.error(
                    msg + u' Could not enroll user %(username)s in course %(course_id)s.',
                    {'username': user.id, 'course_id': course_id},
                )
                raise InvalidResponseError(msg)
        except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
            log.exception(ex.message)
            return InternalRequestErrorResponse(ex.message)
        finally:
            audit_log(
                'checkout_requested',
                course_id=course_id,
                mode=honor_mode.slug,
                processor_name=None,
                user_id=user.id
            )

        self._handle_marketing_opt_in(request, course_key, user)
        return response
Example #9
0
    def get(
        self, request, course_id,
        always_show_payment=False,
        current_step=None,
        message=FIRST_TIME_VERIFY_MSG
    ):
        """Render the pay/verify requirements page.

        Arguments:
            request (HttpRequest): The request object.
            course_id (unicode): The ID of the course the user is trying
                to enroll in.

        Keyword Arguments:
            always_show_payment (bool): If True, show the payment steps
                even if the user has already paid.  This is useful
                for users returning to the flow after paying.
            current_step (string): The current step in the flow.
            message (string): The messaging to display.

        Returns:
            HttpResponse

        Raises:
            Http404: The course does not exist or does not
                have a verified mode.

        """
        # Parse the course key
        # The URL regex should guarantee that the key format is valid.
        course_key = CourseKey.from_string(course_id)
        course = modulestore().get_course(course_key)

        # Verify that the course exists and has a verified mode
        if course is None:
            log.warn(u"No course specified for verification flow request.")
            raise Http404

        # Check whether the user has access to this course
        # based on country access rules.
        redirect_url = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path
        )
        if redirect_url:
            return redirect(redirect_url)

        expired_verified_course_mode, unexpired_paid_course_mode = self._get_expired_verified_and_paid_mode(course_key)

        # Check that the course has an unexpired paid mode
        if unexpired_paid_course_mode is not None:
            if CourseMode.is_verified_mode(unexpired_paid_course_mode):
                log.info(
                    u"Entering verified workflow for user '%s', course '%s', with current step '%s'.",
                    request.user.id, course_id, current_step
                )
        elif expired_verified_course_mode is not None:
            # Check if there is an *expired* verified course mode;
            # if so, we should show a message explaining that the verification
            # deadline has passed.
            log.info(u"Verification deadline for '%s' has passed.", course_id)
            context = {
                'course': course,
                'deadline': (
                    get_default_time_display(expired_verified_course_mode.expiration_datetime)
                    if expired_verified_course_mode.expiration_datetime else ""
                )
            }
            return render_to_response("verify_student/missed_verification_deadline.html", context)
        else:
            # Otherwise, there has never been a verified/paid mode,
            # so return a page not found response.
            log.warn(
                u"No paid/verified course mode found for course '%s' for verification/payment flow request",
                course_id
            )
            raise Http404

        # Check whether the user has verified, paid, and enrolled.
        # A user is considered "paid" if he or she has an enrollment
        # with a paid course mode (such as "verified").
        # For this reason, every paid user is enrolled, but not
        # every enrolled user is paid.
        # If the course mode is not verified(i.e only paid) then already_verified is always True
        already_verified = self._check_already_verified(request.user) \
            if CourseMode.is_verified_mode(unexpired_paid_course_mode) else True
        already_paid, is_enrolled = self._check_enrollment(request.user, course_key)

        # Redirect the user to a more appropriate page if the
        # messaging won't make sense based on the user's
        # enrollment / payment / verification status.
        redirect_response = self._redirect_if_necessary(
            message,
            already_verified,
            already_paid,
            is_enrolled,
            course_key
        )
        if redirect_response is not None:
            return redirect_response

        display_steps = self._display_steps(
            always_show_payment,
            already_verified,
            already_paid,
            unexpired_paid_course_mode
        )
        requirements = self._requirements(display_steps, request.user.is_active)

        if current_step is None:
            current_step = display_steps[0]['name']

        # Allow the caller to skip the first page
        # This is useful if we want the user to be able to
        # use the "back" button to return to the previous step.
        # This parameter should only work for known skip-able steps
        if request.GET.get('skip-first-step') and current_step in self.SKIP_STEPS:
            display_step_names = [step['name'] for step in display_steps]
            current_step_idx = display_step_names.index(current_step)
            if (current_step_idx + 1) < len(display_steps):
                current_step = display_steps[current_step_idx + 1]['name']

        courseware_url = ""
        if not course.start or course.start < datetime.datetime.today().replace(tzinfo=UTC):
            courseware_url = reverse(
                'course_root',
                kwargs={'course_id': unicode(course_key)}
            )

        full_name = (
            request.user.profile.name
            if request.user.profile.name
            else ""
        )

        # If the user set a contribution amount on another page,
        # use that amount to pre-fill the price selection form.
        contribution_amount = request.session.get(
            'donation_for_course', {}
        ).get(unicode(course_key), '')

        # Remember whether the user is upgrading
        # so we can fire an analytics event upon payment.
        request.session['attempting_upgrade'] = (message == self.UPGRADE_MSG)

        # Determine the photo verification status
        verification_good_until = self._verification_valid_until(request.user)

        # get available payment processors
        if unexpired_paid_course_mode.sku:
            # transaction will be conducted via ecommerce service
            processors = ecommerce_api_client(request.user).payment.processors.get()
        else:
            # transaction will be conducted using legacy shopping cart
            processors = [settings.CC_PROCESSOR_NAME]

        # Render the top-level page
        context = {
            'contribution_amount': contribution_amount,
            'course': course,
            'course_key': unicode(course_key),
            'course_mode': unexpired_paid_course_mode,
            'courseware_url': courseware_url,
            'current_step': current_step,
            'disable_courseware_js': True,
            'display_steps': display_steps,
            'is_active': json.dumps(request.user.is_active),
            'message_key': message,
            'platform_name': settings.PLATFORM_NAME,
            'processors': processors,
            'requirements': requirements,
            'user_full_name': full_name,
            'verification_deadline': (
                get_default_time_display(unexpired_paid_course_mode.expiration_datetime)
                if unexpired_paid_course_mode.expiration_datetime else ""
            ),
            'already_verified': already_verified,
            'verification_good_until': verification_good_until,
            'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"),
        }
        return render_to_response("verify_student/pay_and_verify.html", context)
Example #10
0
    def post(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """
        Attempt to create the basket and enroll the user.
        """
        user = request.user
        valid, course_key, error = self._is_data_valid(request)
        if not valid:
            return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)

        # Don't do anything if an enrollment already exists
        course_id = unicode(course_key)
        enrollment = CourseEnrollment.get_enrollment(user, course_key)
        if enrollment and enrollment.is_active:
            msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
            return DetailResponse(msg, status=HTTP_409_CONFLICT)

        # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS
        # redirects to track selection.
        honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)

        if not honor_mode:
            msg = Messages.NO_HONOR_MODE.format(course_id=course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
        elif not honor_mode.sku:
            # If there are no course modes with SKUs, enroll the user without contacting the external API.
            msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id,
                                                  username=user.username)
            log.debug(msg)
            self._enroll(course_key, user)
            return DetailResponse(msg)

        # Setup the API

        try:
            api = ecommerce_api_client(user)
        except ValueError:
            self._enroll(course_key, user)
            msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key))
            log.debug(msg)
            return DetailResponse(msg)

        # Make the API call
        try:
            response_data = api.baskets.post({
                'products': [{'sku': honor_mode.sku}],
                'checkout': True,
                'payment_processor_name': 'cybersource'
            })

            payment_data = response_data["payment_data"]
            if payment_data:
                # Pass data to the client to begin the payment flow.
                return JsonResponse(payment_data)
            elif response_data['order']:
                # The order was completed immediately because there isno charge.
                msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
                log.debug(msg)
                return DetailResponse(msg)
            else:
                msg = u'Unexpected response from basket endpoint.'
                log.error(
                    msg + u' Could not enroll user %(username)s in course %(course_id)s.',
                    {'username': user.id, 'course_id': course_id},
                )
                raise InvalidResponseError(msg)
        except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
            log.exception(ex.message)
            return InternalRequestErrorResponse(ex.message)