def test_commerce_configuration(self): """ Test that commerce configuration is created properly. """ call_command("configure_commerce", ) # Verify commerce configuration is enabled with appropriate values commerce_configuration = CommerceConfiguration.current() self.assertTrue(commerce_configuration.enabled) self.assertTrue(commerce_configuration.checkout_on_ecommerce_service) self.assertEqual(commerce_configuration.single_course_checkout_page, "/basket/single-item/") self.assertEqual(commerce_configuration.cache_ttl, 0) # Verify commerce configuration can be disabled from command call_command( "configure_commerce", '--disable', ) commerce_configuration = CommerceConfiguration.current() self.assertFalse(commerce_configuration.enabled) # Verify commerce configuration can be disabled from command call_command( "configure_commerce", '--disable-checkout-on-ecommerce', ) commerce_configuration = CommerceConfiguration.current() self.assertFalse(commerce_configuration.checkout_on_ecommerce_service)
def test_commerce_configuration(self): """ Test that commerce configuration is created properly. """ call_command( "configure_commerce", ) # Verify commerce configuration is enabled with appropriate values commerce_configuration = CommerceConfiguration.current() self.assertTrue(commerce_configuration.enabled) self.assertTrue(commerce_configuration.checkout_on_ecommerce_service) self.assertEqual(commerce_configuration.single_course_checkout_page, "/basket/single-item/") self.assertEqual(commerce_configuration.cache_ttl, 0) # Verify commerce configuration can be disabled from command call_command( "configure_commerce", '--disable', ) commerce_configuration = CommerceConfiguration.current() self.assertFalse(commerce_configuration.enabled) # Verify commerce configuration can be disabled from command call_command( "configure_commerce", '--disable-checkout-on-ecommerce', ) commerce_configuration = CommerceConfiguration.current() self.assertFalse(commerce_configuration.checkout_on_ecommerce_service)
def get_user_orders(user): """Given a user, get the detail of all the orders from the Ecommerce service. Args: user (User): The user to authenticate as when requesting ecommerce. Returns: list of dict, representing orders returned by the Ecommerce service. """ no_data = [] user_orders = [] commerce_configuration = CommerceConfiguration.current() user_query = {'username': user.username} use_cache = commerce_configuration.is_cache_enabled cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None api = ecommerce_api_client(user) commerce_user_orders = get_edx_api_data( commerce_configuration, 'orders', api=api, querystring=user_query, cache_key=cache_key ) for order in commerce_user_orders: if order['status'].lower() == 'complete': date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ") order_data = { 'number': order['number'], 'price': order['total_excl_tax'], 'order_date': strftime_localized(date_placed, 'SHORT_DATE'), 'receipt_url': EcommerceService().get_receipt_page_url(order['number']), 'lines': order['lines'], } user_orders.append(order_data) return user_orders
def get_user_orders(user): """Given a user, get the detail of all the orders from the Ecommerce service. Args: user (User): The user to authenticate as when requesting ecommerce. Returns: list of dict, representing orders returned by the Ecommerce service. """ no_data = [] user_orders = [] allowed_course_modes = ['professional', 'verified', 'credit'] commerce_configuration = CommerceConfiguration.current() user_query = {'username': user.username} use_cache = commerce_configuration.is_cache_enabled cache_key = commerce_configuration.CACHE_KEY + '.' + str( user.id) if use_cache else None api = ecommerce_api_client(user) commerce_user_orders = get_edx_api_data(commerce_configuration, user, 'orders', api=api, querystring=user_query, cache_key=cache_key) for order in commerce_user_orders: if order['status'].lower() == 'complete': for line in order['lines']: product = line.get('product') if product: for attribute in product['attribute_values']: if attribute[ 'name'] == 'certificate_type' and attribute[ 'value'] in allowed_course_modes: try: date_placed = datetime.strptime( order['date_placed'], "%Y-%m-%dT%H:%M:%SZ") order_data = { 'number': order['number'], 'price': order['total_excl_tax'], 'title': order['lines'][0]['title'], 'order_date': strftime_localized( date_placed.replace(tzinfo=pytz.UTC), 'SHORT_DATE'), 'receipt_url': EcommerceService().get_receipt_page_url( order['number']) } user_orders.append(order_data) except KeyError: log.exception('Invalid order structure: %r', order) return no_data return user_orders
def test_get_checkout_page_url(self, skus): """ Verify the checkout page URL is properly constructed and returned. """ url = EcommerceService().get_checkout_page_url(*skus) config = CommerceConfiguration.current() expected_url = '{root}{basket_url}?{skus}'.format( basket_url=config.MULTIPLE_ITEMS_BASKET_PAGE_URL, root=settings.ECOMMERCE_PUBLIC_URL_ROOT, skus=urlencode({'sku': skus}, doseq=True), ) self.assertEqual(url, expected_url)
def test_is_enabled(self): """Verify that is_enabled() returns True when ecomm checkout is enabled. """ is_enabled = EcommerceService().is_enabled(self.user) self.assertTrue(is_enabled) config = CommerceConfiguration.current() config.checkout_on_ecommerce_service = False config.save() is_not_enabled = EcommerceService().is_enabled(self.user) self.assertFalse(is_not_enabled)
def test_is_enabled(self): """Verify that is_enabled() returns True when ecomm checkout is enabled. """ is_enabled = EcommerceService().is_enabled(self.request) self.assertTrue(is_enabled) config = CommerceConfiguration.current() config.checkout_on_ecommerce_service = False config.save() is_not_enabled = EcommerceService().is_enabled(self.request) self.assertFalse(is_not_enabled)
def checkout_receipt(request): """ Receipt view. """ page_title = _('Receipt') is_payment_complete = True payment_support_email = configuration_helpers.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL) payment_support_link = '<a href=\"mailto:{email}\">{email}</a>'.format(email=payment_support_email) is_cybersource = all(k in request.POST for k in ('signed_field_names', 'decision', 'reason_code')) if is_cybersource and request.POST['decision'] != 'ACCEPT': # Cybersource may redirect users to this view if it couldn't recover # from an error while capturing payment info. is_payment_complete = False page_title = _('Payment Failed') reason_code = request.POST['reason_code'] # if the problem was with the info submitted by the user, we present more detailed messages. if is_user_payment_error(reason_code): error_summary = _("There was a problem with this transaction. You have not been charged.") error_text = _( "Make sure your information is correct, or try again with a different card or another form of payment." ) else: error_summary = _("A system error occurred while processing your payment. You have not been charged.") error_text = _("Please wait a few minutes and then try again.") for_help_text = _("For help, contact {payment_support_link}.").format(payment_support_link=payment_support_link) else: # if anything goes wrong rendering the receipt, it indicates a problem fetching order data. error_summary = _("An error occurred while creating your receipt.") error_text = None # nothing particularly helpful to say if this happens. for_help_text = _( "If your course does not appear on your dashboard, contact {payment_support_link}." ).format(payment_support_link=payment_support_link) commerce_configuration = CommerceConfiguration.current() # user order cache should be cleared when a new order is placed # so user can see new order in their order history. if is_payment_complete and commerce_configuration.enabled and commerce_configuration.is_cache_enabled: cache_key = commerce_configuration.CACHE_KEY + '.' + str(request.user.id) cache.delete(cache_key) context = { 'page_title': page_title, 'is_payment_complete': is_payment_complete, 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), 'verified': SoftwareSecurePhotoVerification.verification_valid_or_pending(request.user).exists(), 'error_summary': error_summary, 'error_text': error_text, 'for_help_text': for_help_text, 'payment_support_email': payment_support_email, 'username': request.user.username, 'nav_hidden': True, 'is_request_in_themed_site': is_request_in_themed_site() } return render_to_response('commerce/checkout_receipt.html', context)
def get_user_orders(user): """Given a user, get the detail of all the orders from the Ecommerce service. Args: user (User): The user to authenticate as when requesting ecommerce. Returns: list of dict, representing orders returned by the Ecommerce service. """ no_data = [] user_orders = [] commerce_configuration = CommerceConfiguration.current() user_query = {'username': user.username} use_cache = commerce_configuration.is_cache_enabled cache_key = commerce_configuration.CACHE_KEY + '.' + str( user.id) if use_cache else None api = ecommerce_api_client(user) commerce_user_orders = get_edx_api_data(commerce_configuration, user, 'orders', api=api, querystring=user_query, cache_key=cache_key) for order in commerce_user_orders: if order['status'].lower() == 'complete': date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ") order_data = { 'number': order['number'], 'price': order['total_excl_tax'], 'order_date': strftime_localized(date_placed, 'SHORT_DATE'), 'receipt_url': EcommerceService().get_receipt_page_url(order['number']), 'lines': order['lines'], } # If the order lines contain a product that is not a Seat # we do not want to display the Order Details button. It # will break the receipt page if used. for order_line in order['lines']: product = order_line.get('product') if product and product.get('product_class') != 'Seat': order_data['receipt_url'] = '' break user_orders.append(order_data) return user_orders
def test_get_receipt_page_url_with_site_configuration(self): order_number = 'ORDER1' config = CommerceConfiguration.current() config.use_ecommerce_receipt_page = True config.save() receipt_page_url = EcommerceService().get_receipt_page_url(order_number) expected_url = '{ecommerce_root}{receipt_page_url}{order_number}'.format( ecommerce_root=settings.ECOMMERCE_PUBLIC_URL_ROOT, order_number=order_number, receipt_page_url=TEST_SITE_CONFIGURATION['ECOMMERCE_RECEIPT_PAGE_URL'] ) self.assertEqual(receipt_page_url, expected_url)
def test_get_receipt_page_url_with_site_configuration(self): order_number = 'ORDER1' config = CommerceConfiguration.current() config.use_ecommerce_receipt_page = True config.save() receipt_page_url = EcommerceService().get_receipt_page_url( order_number) expected_url = '{ecommerce_root}{receipt_page_url}{order_number}'.format( ecommerce_root=settings.ECOMMERCE_PUBLIC_URL_ROOT, order_number=order_number, receipt_page_url=TEST_SITE_CONFIGURATION[ 'ECOMMERCE_RECEIPT_PAGE_URL']) self.assertEqual(receipt_page_url, expected_url)
def get_user_orders(user): """Given a user, get the detail of all the orders from the Ecommerce service. Arguments: user (User): The user to authenticate as when requesting ecommerce. Returns: list of dict, representing orders returned by the Ecommerce service. """ no_data = [] user_orders = [] allowed_course_modes = ['professional', 'verified', 'credit'] commerce_configuration = CommerceConfiguration.current() user_query = {'username': user.username} use_cache = commerce_configuration.is_cache_enabled cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None api = ecommerce_api_client(user) commerce_user_orders = get_edx_api_data( commerce_configuration, user, 'orders', api=api, querystring=user_query, cache_key=cache_key ) for order in commerce_user_orders: if order['status'].lower() == 'complete': for line in order['lines']: product = line.get('product') if product: for attribute in product['attribute_values']: if attribute['name'] == 'certificate_type' and attribute['value'] in allowed_course_modes: try: date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ") order_data = { 'number': order['number'], 'price': order['total_excl_tax'], 'title': order['lines'][0]['title'], 'order_date': strftime_localized( date_placed.replace(tzinfo=pytz.UTC), 'SHORT_DATE' ), 'receipt_url': commerce_configuration.receipt_page + order['number'] } user_orders.append(order_data) except KeyError: log.exception('Invalid order structure: %r', order) return no_data return user_orders
def get_user_orders(user): """Given a user, get the detail of all the orders from the Ecommerce service. Arguments: user (User): The user to authenticate as when requesting ecommerce. Returns: list of dict, representing orders returned by the Ecommerce service. """ no_data = [] user_orders = [] allowed_course_modes = ["professional", "verified", "credit"] commerce_configuration = CommerceConfiguration.current() user_query = {"username": user.username} use_cache = commerce_configuration.is_cache_enabled cache_key = commerce_configuration.CACHE_KEY + "." + str(user.id) if use_cache else None api = ecommerce_api_client(user) commerce_user_orders = get_edx_api_data( commerce_configuration, user, "orders", api=api, querystring=user_query, cache_key=cache_key ) for order in commerce_user_orders: if order["status"].lower() == "complete": for line in order["lines"]: product = line.get("product") if product: for attribute in product["attribute_values"]: if attribute["name"] == "certificate_type" and attribute["value"] in allowed_course_modes: try: date_placed = datetime.strptime(order["date_placed"], "%Y-%m-%dT%H:%M:%SZ") order_data = { "number": order["number"], "price": order["total_excl_tax"], "title": order["lines"][0]["title"], "order_date": strftime_localized( date_placed.replace(tzinfo=pytz.UTC), "SHORT_DATE" ), "receipt_url": commerce_configuration.receipt_page + order["number"], } user_orders.append(order_data) except KeyError: log.exception("Invalid order structure: %r", order) return no_data return user_orders
def setUp(self): super(TestRefundSignal, self).setUp() # Ensure the E-Commerce service user exists UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True) self.requester = UserFactory(username="******") self.student = UserFactory( username="******", email="*****@*****.**", ) self.course_enrollment = CourseEnrollmentFactory( user=self.student, course_id=CourseKey.from_string('course-v1:org+course+run'), mode=CourseMode.VERIFIED, ) self.course_enrollment.refundable = mock.Mock(return_value=True) self.config = CommerceConfiguration.current() self.config.enable_automatic_refund_approval = True self.config.save()
def __init__(self): self.config = CommerceConfiguration.current()
def checkout_receipt(request): """ Receipt view. """ page_title = _('Receipt') is_payment_complete = True payment_support_email = configuration_helpers.get_value( 'payment_support_email', settings.PAYMENT_SUPPORT_EMAIL) payment_support_link = '<a href=\"mailto:{email}\">{email}</a>'.format( email=payment_support_email) is_cybersource = all(k in request.POST for k in ('signed_field_names', 'decision', 'reason_code')) if is_cybersource and request.POST['decision'] != 'ACCEPT': # Cybersource may redirect users to this view if it couldn't recover # from an error while capturing payment info. is_payment_complete = False page_title = _('Payment Failed') reason_code = request.POST['reason_code'] # if the problem was with the info submitted by the user, we present more detailed messages. if is_user_payment_error(reason_code): error_summary = _( "There was a problem with this transaction. You have not been charged." ) error_text = _( "Make sure your information is correct, or try again with a different card or another form of payment." ) else: error_summary = _( "A system error occurred while processing your payment. You have not been charged." ) error_text = _("Please wait a few minutes and then try again.") for_help_text = _("For help, contact {payment_support_link}.").format( payment_support_link=payment_support_link) else: # if anything goes wrong rendering the receipt, it indicates a problem fetching order data. error_summary = _("An error occurred while creating your receipt.") error_text = None # nothing particularly helpful to say if this happens. for_help_text = _( "If your course does not appear on your dashboard, contact {payment_support_link}." ).format(payment_support_link=payment_support_link) commerce_configuration = CommerceConfiguration.current() # user order cache should be cleared when a new order is placed # so user can see new order in their order history. if is_payment_complete and commerce_configuration.enabled and commerce_configuration.is_cache_enabled: cache_key = commerce_configuration.CACHE_KEY + '.' + str( request.user.id) cache.delete(cache_key) context = { 'page_title': page_title, 'is_payment_complete': is_payment_complete, 'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), 'verified': SoftwareSecurePhotoVerification.verification_valid_or_pending( request.user).exists(), 'error_summary': error_summary, 'error_text': error_text, 'for_help_text': for_help_text, 'payment_support_email': payment_support_email, 'username': request.user.username, 'nav_hidden': True, 'is_request_in_themed_site': is_request_in_themed_site() } return render_to_response('commerce/checkout_receipt.html', context)
def refund_seat(course_enrollment): """ 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 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 E-Commerce Service. exceptions.Timeout: if the attempt to reach the commerce service timed out. """ User = get_user_model() # pylint:disable=invalid-name course_key_str = unicode(course_enrollment.course_id) enrollee = course_enrollment.user service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME) api_client = ecommerce_api_client(service_user) log.info('Attempting to create a refund for user [%s], course [%s]...', enrollee.id, course_key_str) refund_ids = api_client.refunds.post({'course_id': course_key_str, 'username': enrollee.username}) if refund_ids: log.info('Refund successfully opened for user [%s], course [%s]: %r', enrollee.id, course_key_str, refund_ids) config = CommerceConfiguration.current() if config.enable_automatic_refund_approval: refunds_requiring_approval = [] for refund_id in refund_ids: try: # NOTE: Approve payment only because the user has already been unenrolled. Additionally, this # ensures we don't tie up an additional web worker when the E-Commerce Service tries to unenroll # the learner api_client.refunds(refund_id).process.put({'action': 'approve_payment_only'}) log.info('Refund [%d] successfully approved.', refund_id) except: # pylint: disable=bare-except log.exception('Failed to automatically approve refund [%d]!', refund_id) refunds_requiring_approval.append(refund_id) else: refunds_requiring_approval = refund_ids if refunds_requiring_approval: # XCOM-371: this is a temporary measure to suppress refund-related email # notifications to students and support for free enrollments. This # condition should be removed when the CourseEnrollment.refundable() logic # is updated to be more correct, or when we implement better handling (and # notifications) in Otto for handling reversal of $0 transactions. if course_enrollment.mode != 'verified': # 'verified' is the only enrollment mode that should presently # result in opening a refund request. log.info( 'Skipping refund email notification for non-verified mode for user [%s], course [%s], mode: [%s]', course_enrollment.user.id, course_enrollment.course_id, course_enrollment.mode, ) else: try: send_refund_notification(course_enrollment, refunds_requiring_approval) except: # pylint: disable=bare-except # don't break, just log a warning log.warning('Could not send email notification for refund.', exc_info=True) else: log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str) return refund_ids
def refund_seat(course_enrollment): """ 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 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 E-Commerce Service. exceptions.Timeout: if the attempt to reach the commerce service timed out. """ User = get_user_model() # pylint:disable=invalid-name course_key_str = unicode(course_enrollment.course_id) enrollee = course_enrollment.user service_user = User.objects.get( username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME) api_client = ecommerce_api_client(service_user) log.info('Attempting to create a refund for user [%s], course [%s]...', enrollee.id, course_key_str) refund_ids = api_client.refunds.post({ 'course_id': course_key_str, 'username': enrollee.username }) if refund_ids: log.info('Refund successfully opened for user [%s], course [%s]: %r', enrollee.id, course_key_str, refund_ids) config = CommerceConfiguration.current() if config.enable_automatic_refund_approval: refunds_requiring_approval = [] for refund_id in refund_ids: try: # NOTE: Approve payment only because the user has already been unenrolled. Additionally, this # ensures we don't tie up an additional web worker when the E-Commerce Service tries to unenroll # the learner api_client.refunds(refund_id).process.put( {'action': 'approve_payment_only'}) log.info('Refund [%d] successfully approved.', refund_id) except: # pylint: disable=bare-except log.exception( 'Failed to automatically approve refund [%d]!', refund_id) refunds_requiring_approval.append(refund_id) else: refunds_requiring_approval = refund_ids if refunds_requiring_approval: # XCOM-371: this is a temporary measure to suppress refund-related email # notifications to students and support for free enrollments. This # condition should be removed when the CourseEnrollment.refundable() logic # is updated to be more correct, or when we implement better handling (and # notifications) in Otto for handling reversal of $0 transactions. if course_enrollment.mode != 'verified': # 'verified' is the only enrollment mode that should presently # result in opening a refund request. log.info( 'Skipping refund email notification for non-verified mode for user [%s], course [%s], mode: [%s]', course_enrollment.user.id, course_enrollment.course_id, course_enrollment.mode, ) else: try: send_refund_notification(course_enrollment, refunds_requiring_approval) except: # pylint: disable=bare-except # don't break, just log a warning log.warning( 'Could not send email notification for refund.', exc_info=True) else: log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str) return refund_ids