def test_validate_coupon_type(self): """Coupon.coupon_type must be one of Coupon.COUPON_TYPES""" with self.assertRaises(ValidationError) as ex: CouponFactory.create(coupon_type='xyz') assert ex.exception.args[0]['__all__'][0].args[0] == ( 'coupon_type must be one of {}'.format(", ".join(Coupon.COUPON_TYPES)) )
def test_coupon_run(self): """ Test coupon serializer """ course_run = CourseRunFactory.create(course__program__financial_aid_availability=True) with self.assertRaises(ValidationError) as ex: CouponFactory.create(content_object=course_run) assert ex.exception.args[0]['__all__'][0].args[0] == 'content_object must be of type Course or Program'
def basket_and_coupons(): """ Sample basket and coupon """ basket_item = BasketItemFactory() # Some prices for the basket item product ProductVersionFactory(product=basket_item.product, price=Decimal("15.00")) product_version = ProductVersionFactory( product=basket_item.product, price=Decimal("25.00") ) run = basket_item.product.content_object CourseRunSelection.objects.create(run=run, basket=basket_item.basket) payment_worst = CouponPaymentFactory() payment_best = CouponPaymentFactory() coupon_worst = CouponFactory(payment=payment_worst, coupon_code="WORST") coupon_best = CouponFactory(payment=payment_best, coupon_code="BEST") # Coupon payment for worst coupon, with lowest discount civ_worst = CouponPaymentVersionFactory( payment=payment_worst, amount=Decimal("0.10000"), automatic=True ) # Coupon payment for best coupon, with highest discount civ_best_old = CouponPaymentVersionFactory( payment=payment_best, amount=Decimal("0.50000") ) # Coupon payment for best coupon, more recent than previous so takes precedence civ_best = CouponPaymentVersionFactory( payment=payment_best, amount=Decimal("1.00000") ) # Coupon version for worst coupon cv_worst = CouponVersionFactory(payment_version=civ_worst, coupon=coupon_worst) # Coupon version for best coupon CouponVersionFactory(payment_version=civ_best_old, coupon=coupon_best) # Most recent coupon version for best coupon cv_best = CouponVersionFactory(payment_version=civ_best, coupon=coupon_best) # Both best and worst coupons eligible for the product CouponEligibilityFactory(coupon=coupon_best, product=basket_item.product) CouponEligibilityFactory(coupon=coupon_worst, product=basket_item.product) # Apply one of the coupons to the basket CouponSelectionFactory.create(basket=basket_item.basket, coupon=coupon_best) coupongroup_worst = CouponGroup(coupon_worst, cv_worst, payment_worst, civ_worst) coupongroup_best = CouponGroup(coupon_best, cv_best, payment_best, civ_best) return SimpleNamespace( basket=basket_item.basket, basket_item=basket_item, product_version=product_version, coupongroup_best=coupongroup_best, coupongroup_worst=coupongroup_worst, run=run, )
def test_validate_amount_type(self): """ Coupon.amount_type should be one of Coupon.AMOUNT_TYPES """ with self.assertRaises(ValidationError) as ex: CouponFactory.create(amount_type='xyz') assert ex.exception.args[0]['__all__'][0].args[0] == ( 'amount_type must be one of percent-discount, fixed-discount, fixed-price' )
def test_validate_amount(self): """ Coupon.amount should be between 0 and 1 if amount_type is percent-discount """ with self.assertRaises(ValidationError) as ex: CouponFactory.create(amount=3, amount_type=Coupon.PERCENT_DISCOUNT) assert ex.exception.args[0]['__all__'][0].args[0] == ( 'amount must be between 0 and 1 if amount_type is percent-discount' )
def test_validate_discount_prev_run_coupon_type(self): """Coupon must be for a course if Coupon.coupon_type is DISCOUNTED_PREVIOUS_RUN""" run = CourseRunFactory.create() with self.assertRaises(ValidationError) as ex: CouponFactory.create( coupon_type=Coupon.DISCOUNTED_PREVIOUS_COURSE, content_object=run.course.program, ) assert ex.exception.args[0]['__all__'][0].args[0] == ( 'coupon must be for a course if coupon_type is discounted-previous-course' )
def test_is_automatic(self): """ Coupon.is_automatic_qset should be true if the coupon type is DISCOUNTED_PREVIOUS_COURSE """ coupon_not_automatic = CouponFactory.create(coupon_type=Coupon.STANDARD) assert Coupon.is_automatic_qset().filter(id=coupon_not_automatic.id).exists() is False run = CourseRunFactory.create(course__program__financial_aid_availability=True) coupon_is_automatic = CouponFactory.create( coupon_type=Coupon.DISCOUNTED_PREVIOUS_COURSE, content_object=run.course, ) assert Coupon.is_automatic_qset().filter(id=coupon_is_automatic.id).exists() is True
def test_validate_coupon_program(self): """Coupons should fail to validate for non-financial aid programs""" run = CourseRunFactory.create( course__program__financial_aid_availability=False, ) with self.assertRaises(ValidationError) as ex: CouponFactory.create( coupon_type=Coupon.STANDARD, content_object=run.course, ) assert ex.exception.args[0]['__all__'][0].args[0] == ( "coupons are only allowed for programs with financial aid" )
def test_validate_content_object(self): """ Coupon.content_object should only accept Course, CourseRun, or Program """ course_run = CourseRunFactory.create(course__program__financial_aid_availability=True) coupons = [] for obj in (course_run.course, course_run.course.program): coupons.append(CouponFactory.create(content_object=obj)) with self.assertRaises(ValidationError) as ex: CouponFactory.create(content_object=course_run) assert ex.exception.args[0]['__all__'][0].args[0] == ( 'content_object must be of type Course or Program' )
def test_prev_course_user_not_verified(self): """If a user is not verified, they should not get a coupon for the course""" coupon = CouponFactory.create( coupon_type=Coupon.DISCOUNTED_PREVIOUS_COURSE, content_object=self.run1.course, ) assert is_coupon_redeemable(coupon, self.user) is False
def test_zero_price_purchase(browser, base_test_data, logged_in_student, mocker): """ Test that a course can be purchased with a 100%-off coupon """ mocker.patch('ecommerce.views.enroll_user_on_success') # Make a 100% off coupon. By setting the price to $0 we can avoid dealing with Cybersource coupon = CouponFactory.create( amount=1, amount_type=Coupon.PERCENT_DISCOUNT, coupon_type=Coupon.STANDARD, content_object=base_test_data.program ) UserCoupon.objects.create(coupon=coupon, user=logged_in_student) browser.get("/") # Click the dashboard link on the upper right of the homepage browser.click_when_loaded(By.CLASS_NAME, "header-dashboard-link") browser.assert_no_console_errors() browser.click_when_loaded(By.CLASS_NAME, "enroll-button") browser.wait_until_loaded(By.CLASS_NAME, "continue-payment") # Click back then click the enroll now button again to assert back button behavior browser.driver.back() browser.click_when_loaded(By.CLASS_NAME, "enroll-button") browser.assert_no_console_errors() # Click 'Continue' on the order summary page browser.click_when_loaded(By.CLASS_NAME, "continue-payment") browser.assert_no_console_errors() browser.wait_until_loaded(By.CLASS_NAME, "toast-message") # No status message is shown here since this is FA program browser.assert_no_console_errors()
def test_zero_price_purchase(browser, base_test_data, logged_in_student, mocker): """ Test that a course can be purchased with a 100%-off coupon """ mocker.patch('ecommerce.views.enroll_user_on_success') # Make a 100% off coupon. By setting the price to $0 we can avoid dealing with Cybersource coupon = CouponFactory.create(amount=1, amount_type=Coupon.PERCENT_DISCOUNT, coupon_type=Coupon.STANDARD, content_object=base_test_data.program) UserCoupon.objects.create(coupon=coupon, user=logged_in_student) browser.get("/") # Click the dashboard link on the upper right of the homepage browser.click_when_loaded(By.CLASS_NAME, "header-dashboard-link") browser.assert_no_console_errors() browser.click_when_loaded(By.CLASS_NAME, "enroll-button") browser.wait_until_loaded(By.CLASS_NAME, "continue-payment") # Click back then click the enroll now button again to assert back button behavior browser.driver.back() browser.click_when_loaded(By.CLASS_NAME, "enroll-button") browser.assert_no_console_errors() # Click 'Continue' on the order summary page browser.click_when_loaded(By.CLASS_NAME, "continue-payment") browser.assert_no_console_errors() browser.wait_until_loaded(By.CLASS_NAME, "toast-message") # No status message is shown here since this is FA program browser.assert_no_console_errors()
def test_course_keys(self): """ Coupon.course_keys should return a list of all course run keys in a program, course, or course run """ run1 = CourseRunFactory.create(course__program__financial_aid_availability=True) run2 = CourseRunFactory.create(course=run1.course) run3 = CourseRunFactory.create(course__program=run1.course.program) run4 = CourseRunFactory.create(course=run3.course) coupon_program = CouponFactory.create( content_object=run1.course.program, ) assert sorted(coupon_program.course_keys) == sorted([run.edx_course_key for run in [run1, run2, run3, run4]]) coupon_course = CouponFactory.create(content_object=run1.course) assert sorted(coupon_course.course_keys) == sorted([run.edx_course_key for run in [run1, run2]])
def test_is_not_valid(self): """If a Coupon is not valid it should not be redeemable""" coupon = CouponFactory.create(content_object=self.program) with patch('ecommerce.api.Coupon.is_valid', new_callable=PropertyMock) as is_valid: is_valid.return_value = False assert is_coupon_redeemable(coupon, self.user) is False
def test_program(self): """ Coupon.course_keys should return a list of all course run keys in a program, course, or course run """ run1 = CourseRunFactory.create(course__program__financial_aid_availability=True) CourseRunFactory.create(course=run1.course) run3 = CourseRunFactory.create(course__program=run1.course.program) CourseRunFactory.create(course=run3.course) coupon_program = CouponFactory.create( content_object=run1.course.program, ) assert coupon_program.program == run1.course.program coupon_course = CouponFactory.create(content_object=run1.course) assert coupon_course.program == run1.course.program
def test_attached_to_other_user(self): """ Coupons only attached to another user should not be shown """ UserCoupon.objects.all().delete() UserCoupon.objects.create(user=UserFactory.create(), coupon=CouponFactory.create()) assert pick_coupons(self.user) == []
def test_is_coupon_redeemable_for_run(self): """Happy case for is_coupon_redeemable_for_run""" coupon = CouponFactory.create(content_object=self.run1.course) with patch('ecommerce.api.is_coupon_redeemable', autospec=True) as _is_coupon_redeemable: _is_coupon_redeemable.return_value = True assert is_coupon_redeemable_for_run(coupon, self.user, self.run1.edx_course_key) is True assert _is_coupon_redeemable.call_count == 1 _is_coupon_redeemable.assert_called_with(coupon, self.user)
def test_is_not_redeemable(self): """If is_coupon_redeemable returns False, is_coupon_redeemable_for_run should also return False""" coupon = CouponFactory.create(content_object=self.run1.course) with patch('ecommerce.api.is_coupon_redeemable', autospec=True) as _is_coupon_redeemable: _is_coupon_redeemable.return_value = False assert is_coupon_redeemable_for_run(coupon, self.user, self.run1.edx_course_key) is False assert _is_coupon_redeemable.call_count == 1 _is_coupon_redeemable.assert_called_with(coupon, self.user)
def test_no_more_coupons(self): """If user has no redemptions left the coupon should not be redeemable""" coupon = CouponFactory.create(content_object=self.program) with patch('ecommerce.api.Coupon.user_has_redemptions_left', autospec=True) as _user_has_redemptions: _user_has_redemptions.return_value = False assert is_coupon_redeemable(coupon, self.user) is False assert _user_has_redemptions.call_count == 1 _user_has_redemptions.assert_called_with(coupon, self.user)
def test_coupon_allowed_program(self): """ Assert that the price is not adjusted if the coupon is for a different program """ course_run, _ = create_purchasable_course_run() price = Decimal('0.3') coupon = CouponFactory.create() assert coupon.content_object != course_run assert calculate_coupon_price(coupon, price, course_run.edx_course_key) == price
def setUpTestData(cls): """ Create user, run, and coupons for testing """ super().setUpTestData() cls.user = SocialProfileFactory.create().user UserSocialAuthFactory.create(user=cls.user, provider='not_edx') run = CourseRunFactory.create(course__program__financial_aid_availability=True) cls.coupon = CouponFactory.create(content_object=run.course.program) UserCoupon.objects.create(coupon=cls.coupon, user=cls.user)
def test_capped_coupon_price_above_full(self): """ Assert that the adjusted price cannot go above the full price """ course_run, _ = create_purchasable_course_run() price = course_run.course.program.price coupon = CouponFactory.create( content_object=course_run.course, amount_type=Coupon.FIXED_DISCOUNT, amount=-(price + 50), ) assert calculate_coupon_price(coupon, price, course_run.edx_course_key) == price
def test_fixed_discount(self): """ Assert the price with a fixed discount """ course_run, _ = create_purchasable_course_run() price = Decimal(5) coupon = CouponFactory.create( content_object=course_run.course, amount_type=Coupon.FIXED_DISCOUNT, amount=Decimal("1.5") ) assert calculate_coupon_price(coupon, price, course_run.edx_course_key) == price - coupon.amount
def redeemed_voucher_and_user_client(voucher_and_user, client): """ Returns a voucher, user, and authenticated client """ user = voucher_and_user.user voucher = voucher_and_user.voucher client.force_login(user) voucher.coupon = CouponFactory() voucher.save() CouponRedemptionFactory(coupon_version__coupon=voucher.coupon) return SimpleNamespace(**vars(voucher_and_user), client=client)
def test_fixed_price(self): """ Assert a fixed price coupon """ course_run, _ = create_purchasable_course_run() price = Decimal(5) amount = Decimal("1.5") coupon = CouponFactory.create( content_object=course_run.course, amount_type=Coupon.FIXED_PRICE, amount=amount ) assert calculate_coupon_price(coupon, price, course_run.edx_course_key) == amount
def test_calculate_coupon_price(self): """ Assert that the price is not adjusted if the amount type is unknown """ course_run, _ = create_purchasable_course_run() price = Decimal('0.3') coupon = CouponFactory.create(content_object=course_run.course) # Use manager to skip validation, which usually prevents setting content_object to an arbitrary object Coupon.objects.filter(id=coupon.id).update(amount_type='xyz') coupon.refresh_from_db() assert calculate_coupon_price(coupon, price, course_run.edx_course_key) == price
def test_prev_course(self): """ A coupon for a previously purchased course should be redeemable if it applies to the course which is being purchased """ coupon = CouponFactory.create( coupon_type=Coupon.DISCOUNTED_PREVIOUS_COURSE, content_object=self.run1.course, ) CachedEnrollmentFactory.create(user=self.user, course_run=self.run1) assert is_coupon_redeemable(coupon, self.user) is True
def setUpTestData(cls): """ Create user, run, and coupons for testing """ super().setUpTestData() cls.user = SocialProfileFactory.create().user UserSocialAuthFactory.create(user=cls.user, provider='not_edx') run = CourseRunFactory.create( course__program__financial_aid_availability=True) cls.coupon = CouponFactory.create(content_object=run.course.program) UserCoupon.objects.create(coupon=cls.coupon, user=cls.user)
def test_course_key_not_in_list(self): """run is not in the course keys listed by Coupon""" coupon = CouponFactory.create(content_object=self.program) with patch('ecommerce.api.is_coupon_redeemable', autospec=True) as _is_coupon_redeemable, patch( 'ecommerce.api.Coupon.course_keys', new_callable=PropertyMock ) as _course_keys: _is_coupon_redeemable.return_value = True _course_keys.return_value = ['missing'] assert is_coupon_redeemable_for_run(coupon, self.user, self.run1.edx_course_key) is False assert _is_coupon_redeemable.call_count == 1 _is_coupon_redeemable.assert_called_with(coupon, self.user) assert _course_keys.call_count == 1
def test_percent_discount(self): """ Assert the price with a percent discount """ course_run, _ = create_purchasable_course_run() price = Decimal(5) coupon = CouponFactory.create( content_object=course_run.course, amount_type=Coupon.PERCENT_DISCOUNT, amount=Decimal("0.3"), ) assert calculate_coupon_price(coupon, price, course_run.edx_course_key) == price * (1 - coupon.amount)
def test_program_invalid_content_object(self): """ program should error if we set content_object to an invalid value """ coupon = CouponFactory.create() profile_content_type = ContentType.objects.get_for_model(Profile) # bypass clean() Coupon.objects.filter(id=coupon.id).update(content_type=profile_content_type) coupon.refresh_from_db() with self.assertRaises(ImproperlyConfigured) as ex: _ = coupon.program assert ex.exception.args[0] == "content_object expected to be one of Program, Course, CourseRun"
def _create_coupons(cls, user): """Create some coupons""" course_run = CourseRunFactory.create( course__program__financial_aid_availability=True, course__program__live=True, ) course = course_run.course ProgramEnrollment.objects.create(program=course.program, user=user) coupon1_auto = CouponFactory.create( coupon_type=Coupon.DISCOUNTED_PREVIOUS_COURSE, content_object=course, ) coupon2_auto = CouponFactory.create( coupon_type=Coupon.DISCOUNTED_PREVIOUS_COURSE, content_object=course, ) coupon1_attached = CouponFactory.create(content_object=course) UserCoupon.objects.create(user=user, coupon=coupon1_attached) coupon2_attached = CouponFactory.create(content_object=course) UserCoupon.objects.create(user=user, coupon=coupon2_attached) return coupon1_attached, coupon2_attached, coupon1_auto, coupon2_auto
def test_list_coupons(self): """ Test that we use pick_coupon to get the list of coupons """ # Despite enabled=False, the API returns this coupon because we patched pick_coupons coupon = CouponFactory.create(enabled=False) with patch('ecommerce.views.pick_coupons', autospec=True) as _pick_coupons: _pick_coupons.return_value = [coupon] resp = self.client.get(reverse('coupon-list')) assert resp.status_code == status.HTTP_200_OK assert resp.json() == [CouponSerializer(coupon).data] assert _pick_coupons.call_count == 1 _pick_coupons.assert_called_with(self.user)
def test_calculate_run_price_coupon(self): """ If there is a coupon calculate_run_price should use calculate_coupon_price to get the discounted price """ course_run, user = create_purchasable_course_run() coupon = CouponFactory.create(content_object=course_run.course) UserCoupon.objects.create(coupon=coupon, user=user) discounted_price = 5 with patch('ecommerce.api.calculate_coupon_price', autospec=True) as _calculate_coupon_price: _calculate_coupon_price.return_value = discounted_price assert calculate_run_price(course_run, user) == (discounted_price, coupon) program_enrollment = course_run.course.program.programenrollment_set.first() fa_price = get_formatted_course_price(program_enrollment)['price'] _calculate_coupon_price.assert_called_with(coupon, fa_price, course_run.edx_course_key)
def test_calculate_run_price_no_coupons(self): """ If there are no coupons for this program the price should be what get_formatted_course_price returned """ course_run, user = create_purchasable_course_run() # This coupon is for a different program coupon = CouponFactory.create() UserCoupon.objects.create(coupon=coupon, user=user) discounted_price = 5 program_enrollment = course_run.course.program.programenrollment_set.first() fa_price = get_formatted_course_price(program_enrollment)['price'] with patch('ecommerce.api.calculate_coupon_price', autospec=True) as _calculate_coupon_price: _calculate_coupon_price.return_value = discounted_price assert calculate_run_price(course_run, user) == (fa_price, None) assert _calculate_coupon_price.called is False
def test_calculate_run_price_other_run(self): """ If the coupon is for another course in this program it should not be returned here """ course_run, user = create_purchasable_course_run() other_course = CourseRunFactory.create(course__program=course_run.course.program).course coupon = CouponFactory.create(content_object=other_course) UserCoupon.objects.create(coupon=coupon, user=user) discounted_price = 5 program_enrollment = course_run.course.program.programenrollment_set.first() fa_price = get_formatted_course_price(program_enrollment)['price'] with patch('ecommerce.api.calculate_coupon_price', autospec=True) as _calculate_coupon_price: _calculate_coupon_price.return_value = discounted_price assert calculate_run_price(course_run, user) == (fa_price, None) assert _calculate_coupon_price.called is False