| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- from decimal import Decimal as D
- from unittest import mock
-
- import pytest
- from django.test import TestCase
- from django.utils.timezone import now
-
- from oscar.apps.basket.models import Basket
- from oscar.apps.offer import applicator, custom, models
- from oscar.core.loading import get_class
- from oscar.test import factories
- from oscar.test.basket import add_product
- from tests._site.model_tests_app.models import BasketOwnerCalledBarry
-
- Selector = get_class('partner.strategy', 'Selector')
-
-
- @pytest.fixture
- def products_some():
- return [factories.create_product(), factories.create_product()]
-
-
- @pytest.fixture()
- def range():
- return factories.RangeFactory()
-
-
- @pytest.fixture
- def range_all():
- return factories.RangeFactory(
- name="All products range", includes_all_products=True
- )
-
-
- @pytest.fixture
- def range_some(products_some):
- return factories.RangeFactory(
- name="Some products", products=products_some
- )
-
-
- @pytest.fixture
- def count_condition(range_all):
- return models.CountCondition(range=range_all, type="Count", value=2)
-
-
- @pytest.fixture
- def value_condition(range_all):
- return models.ValueCondition(range=range_all, type="Value", value=D("10.00"))
-
-
- @pytest.fixture
- def coverage_condition(range_some):
- return models.CoverageCondition(range=range_some, type="Coverage", value=2)
-
-
- @pytest.fixture
- def empty_basket():
- return factories.create_basket(empty=True)
-
-
- @pytest.fixture
- def partial_basket(empty_basket):
- basket = empty_basket
- add_product(basket)
- return basket
-
-
- @pytest.fixture
- def mock_offer():
- return mock.Mock()
-
-
- @pytest.mark.django_db
- class TestCountCondition:
-
- @pytest.fixture(autouse=True)
- def setUp(self, mock_offer):
- self.offer = mock_offer
-
- def test_description(self, count_condition):
- assert count_condition.description
-
- def test_is_not_satisfied_by_empty_basket(self, count_condition, empty_basket):
- assert count_condition.is_satisfied(self.offer, empty_basket) is False
-
- def test_not_discountable_product_fails_condition(
- self, count_condition, empty_basket
- ):
- basket = empty_basket
- prod1, prod2 = factories.create_product(), factories.create_product()
- prod2.is_discountable = False
- prod2.save()
- add_product(basket, product=prod1)
- add_product(basket, product=prod2)
- assert count_condition.is_satisfied(self.offer, basket) is False
-
- def test_empty_basket_fails_partial_condition(self, count_condition, empty_basket):
- assert count_condition.is_partially_satisfied(self.offer, empty_basket) is False
-
- def test_smaller_quantity_basket_passes_partial_condition(
- self, count_condition, empty_basket
- ):
- basket = empty_basket
- add_product(basket)
- assert count_condition.is_partially_satisfied(self.offer, basket)
- assert count_condition._num_matches == 1
-
- def test_smaller_quantity_basket_upsell_message(
- self, count_condition, empty_basket
- ):
- basket = empty_basket
- add_product(basket)
- assert "Buy 1 more product from " in count_condition.get_upsell_message(
- self.offer, basket
- )
-
- def test_matching_quantity_basket_fails_partial_condition(
- self, count_condition, empty_basket
- ):
- basket = empty_basket
- add_product(basket, quantity=2)
- assert count_condition.is_partially_satisfied(self.offer, basket) is False
-
- def test_matching_quantity_basket_passes_condition(
- self, count_condition, empty_basket
- ):
- basket = empty_basket
- add_product(basket, quantity=2)
- assert count_condition.is_satisfied(self.offer, basket)
-
- def test_greater_quantity_basket_passes_condition(
- self, count_condition, empty_basket
- ):
- basket = empty_basket
- add_product(basket, quantity=3)
- assert count_condition.is_satisfied(self.offer, basket)
-
- def test_consumption(self, count_condition, empty_basket):
- basket = empty_basket
- add_product(basket, quantity=3)
- count_condition.consume_items(self.offer, basket, [])
- assert 1 == basket.all_lines()[0].quantity_without_discount
-
- def test_is_satisfied_accounts_for_consumed_items(
- self, count_condition, empty_basket
- ):
- basket = empty_basket
- add_product(basket, quantity=3)
- count_condition.consume_items(self.offer, basket, [])
- assert count_condition.is_satisfied(self.offer, basket) is False
-
-
- @pytest.mark.django_db
- class TestValueCondition:
- @pytest.fixture(autouse=True)
- def setUp(self, empty_basket, value_condition, mock_offer):
- self.basket = empty_basket
- self.condition = value_condition
- self.offer = mock_offer
- self.item = factories.create_product(price=D("5.00"))
- self.expensive_item = factories.create_product(price=D("15.00"))
-
- def test_description(self, value_condition):
- assert value_condition.description
-
- def test_empty_basket_fails_condition(self):
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
- def test_empty_basket_fails_partial_condition(self):
- assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
-
- def test_less_value_basket_fails_condition(self):
- add_product(self.basket, D("5"))
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
- def test_not_discountable_item_fails_condition(self):
- product = factories.create_product(is_discountable=False)
- add_product(self.basket, D("15"), product=product)
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
- def test_upsell_message(self):
- add_product(self.basket, D("5"))
- assert "Spend" in self.condition.get_upsell_message(self.offer, self.basket)
-
- def test_matching_basket_fails_partial_condition(self):
- add_product(self.basket, D("5"), 2)
- assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
-
- def test_less_value_basket_passes_partial_condition(self):
- add_product(self.basket, D("5"), 1)
- assert self.condition.is_partially_satisfied(self.offer, self.basket)
-
- def test_matching_basket_passes_condition(self):
- add_product(self.basket, D("5"), 2)
- assert self.condition.is_satisfied(self.offer, self.basket)
-
- def test_greater_than_basket_passes_condition(self):
- add_product(self.basket, D("5"), 3)
- assert self.condition.is_satisfied(self.offer, self.basket)
-
- def test_consumption(self):
- add_product(self.basket, D("5"), 3)
- self.condition.consume_items(self.offer, self.basket, [])
- assert 1 == self.basket.all_lines()[0].quantity_without_discount
-
- def test_consumption_with_high_value_product(self):
- add_product(self.basket, D("15"), 1)
- self.condition.consume_items(self.offer, self.basket, [])
- assert 0 == self.basket.all_lines()[0].quantity_without_discount
-
- def test_is_consumed_respects_quantity_consumed(self):
- add_product(self.basket, D("15"), 1)
- assert self.condition.is_satisfied(self.offer, self.basket)
- self.condition.consume_items(self.offer, self.basket, [])
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
-
- @pytest.mark.django_db
- class TestCoverageCondition:
-
- @pytest.fixture(autouse=True)
- def setUp(self, range_some, products_some, empty_basket, coverage_condition):
- self.products = products_some
- self.range = range_some
- self.basket = empty_basket
- self.condition = coverage_condition
- self.offer = mock.Mock()
-
- def test_empty_basket_fails(self):
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
- def test_empty_basket_fails_partial_condition(self):
- assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
-
- def test_single_item_fails(self):
- add_product(self.basket, product=self.products[0])
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
- def test_not_discountable_item_fails(self):
- self.products[0].is_discountable = False
- self.products[0].save()
- add_product(self.basket, product=self.products[0])
- add_product(self.basket, product=self.products[1])
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
- def test_single_item_passes_partial_condition(self):
- add_product(self.basket, product=self.products[0])
- assert self.condition.is_partially_satisfied(self.offer, self.basket)
-
- def test_upsell_message(self):
- add_product(self.basket, product=self.products[0])
- assert "Buy 1 more" in self.condition.get_upsell_message(
- self.offer, self.basket
- )
-
- def test_duplicate_item_fails(self):
- add_product(self.basket, quantity=2, product=self.products[0])
- assert self.condition.is_satisfied(self.offer, self.basket) is False
-
- def test_duplicate_item_passes_partial_condition(self):
- add_product(self.basket, quantity=2, product=self.products[0])
- assert self.condition.is_partially_satisfied(self.offer, self.basket)
-
- def test_covering_items_pass(self):
- add_product(self.basket, product=self.products[0])
- add_product(self.basket, product=self.products[1])
- assert self.condition.is_satisfied(self.offer, self.basket)
-
- def test_covering_items_fail_partial_condition(self):
- add_product(self.basket, product=self.products[0])
- add_product(self.basket, product=self.products[1])
- assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
-
- def test_covering_items_are_consumed(self):
- add_product(self.basket, product=self.products[0])
- add_product(self.basket, product=self.products[1])
- self.condition.consume_items(self.offer, self.basket, [])
- assert 0 == self.basket.num_items_without_discount
-
- def test_consumed_items_checks_affected_items(self):
- # Create new offer
- range = models.Range.objects.create(
- name="All products", includes_all_products=True
- )
- cond = models.CoverageCondition(range=range, type="Coverage", value=2)
-
- # Get 4 distinct products in the basket
- self.products.extend([factories.create_product(), factories.create_product()])
- for product in self.products:
- add_product(self.basket, product=product)
-
- assert cond.is_satisfied(self.offer, self.basket)
- cond.consume_items(self.offer, self.basket, [])
- assert 2 == self.basket.num_items_without_discount
-
- assert cond.is_satisfied(self.offer, self.basket)
- cond.consume_items(self.offer, self.basket, [])
- assert 0 == self.basket.num_items_without_discount
-
-
- @pytest.mark.django_db
- class TestConditionProxyModels(object):
- def test_name_and_description(self, range):
- """
- Tests that the condition proxy classes all return a name and
- description. Unfortunately, the current implementations means
- a valid range and value are required.
- """
- for type, __ in models.Condition.TYPE_CHOICES:
- condition = models.Condition(type=type, range=range, value=5)
- assert all([condition.name, condition.description, str(condition)])
-
- def test_proxy(self, range):
- for type, __ in models.Condition.TYPE_CHOICES:
- condition = models.Condition(type=type, range=range, value=5)
- proxy = condition.proxy()
- assert condition.type == proxy.type
- assert condition.range == proxy.range
- assert condition.value == proxy.value
-
-
- class TestCustomCondition(TestCase):
- def setUp(self):
- self.condition = custom.create_condition(BasketOwnerCalledBarry)
- self.offer = models.ConditionalOffer(condition=self.condition)
- self.basket = Basket()
-
- def test_is_not_satisfied_by_non_match(self):
- self.basket.owner = factories.UserFactory(first_name="Alan")
- assert self.offer.is_condition_satisfied(self.basket) is False
-
- def test_is_satisfied_by_match(self):
- self.basket.owner = factories.UserFactory(first_name="Barry")
- assert self.offer.is_condition_satisfied(self.basket)
-
-
- class TestOffersWithCountCondition(TestCase):
-
- def setUp(self):
- super().setUp()
-
- self.basket = factories.create_basket(empty=True)
-
- # Create range and add one product to it.
- rng = factories.RangeFactory(name='All products', includes_all_products=True)
- self.product = factories.ProductFactory()
- rng.add_product(self.product)
-
- # Create a non-exclusive offer #1.
- condition1 = factories.ConditionFactory(range=rng, value=D('1'))
- benefit1 = factories.BenefitFactory(range=rng, value=D('10'))
- self.offer1 = factories.ConditionalOfferFactory(
- condition=condition1, benefit=benefit1, start_datetime=now(),
- name='Test offer #1', exclusive=False,
- )
-
- # Create a non-exclusive offer #2.
- condition2 = factories.ConditionFactory(range=rng, value=D('1'))
- benefit2 = factories.BenefitFactory(range=rng, value=D('5'))
- self.offer2 = factories.ConditionalOfferFactory(
- condition=condition2, benefit=benefit2, start_datetime=now(),
- name='Test offer #2', exclusive=False,
- )
-
- def add_product(self):
- self.basket.add_product(self.product)
- self.basket.strategy = Selector().strategy()
- applicator.Applicator().apply(self.basket)
-
- def assertOffersApplied(self, offers):
- applied_offers = self.basket.applied_offers()
- self.assertEqual(len(offers), len(applied_offers))
- for offer in offers:
- self.assertIn(offer.id, applied_offers, msg=offer)
-
- def test_both_non_exclusive_offers_are_applied(self):
- self.add_product()
- self.assertOffersApplied([self.offer1, self.offer2])
|