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)
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 )
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)
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)
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"})
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
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
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
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)
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)