You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_condition.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. from decimal import Decimal as D
  2. from unittest import mock
  3. import pytest
  4. from django.test import TestCase
  5. from django.utils.timezone import now
  6. from oscar.apps.basket.models import Basket
  7. from oscar.apps.offer import applicator, custom, models
  8. from oscar.core.loading import get_class
  9. from oscar.test import factories
  10. from oscar.test.basket import add_product
  11. from tests._site.model_tests_app.models import BasketOwnerCalledBarry
  12. Selector = get_class('partner.strategy', 'Selector')
  13. @pytest.fixture
  14. def products_some():
  15. return [factories.create_product(), factories.create_product()]
  16. @pytest.fixture()
  17. def range():
  18. return factories.RangeFactory()
  19. @pytest.fixture
  20. def range_all():
  21. return factories.RangeFactory(
  22. name="All products range", includes_all_products=True
  23. )
  24. @pytest.fixture
  25. def range_some(products_some):
  26. return factories.RangeFactory(
  27. name="Some products", products=products_some
  28. )
  29. @pytest.fixture
  30. def count_condition(range_all):
  31. return models.CountCondition(range=range_all, type="Count", value=2)
  32. @pytest.fixture
  33. def value_condition(range_all):
  34. return models.ValueCondition(range=range_all, type="Value", value=D("10.00"))
  35. @pytest.fixture
  36. def coverage_condition(range_some):
  37. return models.CoverageCondition(range=range_some, type="Coverage", value=2)
  38. @pytest.fixture
  39. def empty_basket():
  40. return factories.create_basket(empty=True)
  41. @pytest.fixture
  42. def partial_basket(empty_basket):
  43. basket = empty_basket
  44. add_product(basket)
  45. return basket
  46. @pytest.fixture
  47. def mock_offer():
  48. return mock.Mock()
  49. @pytest.mark.django_db
  50. class TestCountCondition:
  51. @pytest.fixture(autouse=True)
  52. def setUp(self, mock_offer):
  53. self.offer = mock_offer
  54. def test_description(self, count_condition):
  55. assert count_condition.description
  56. def test_is_not_satisfied_by_empty_basket(self, count_condition, empty_basket):
  57. assert count_condition.is_satisfied(self.offer, empty_basket) is False
  58. def test_not_discountable_product_fails_condition(
  59. self, count_condition, empty_basket
  60. ):
  61. basket = empty_basket
  62. prod1, prod2 = factories.create_product(), factories.create_product()
  63. prod2.is_discountable = False
  64. prod2.save()
  65. add_product(basket, product=prod1)
  66. add_product(basket, product=prod2)
  67. assert count_condition.is_satisfied(self.offer, basket) is False
  68. def test_empty_basket_fails_partial_condition(self, count_condition, empty_basket):
  69. assert count_condition.is_partially_satisfied(self.offer, empty_basket) is False
  70. def test_smaller_quantity_basket_passes_partial_condition(
  71. self, count_condition, empty_basket
  72. ):
  73. basket = empty_basket
  74. add_product(basket)
  75. assert count_condition.is_partially_satisfied(self.offer, basket)
  76. assert count_condition._num_matches == 1
  77. def test_smaller_quantity_basket_upsell_message(
  78. self, count_condition, empty_basket
  79. ):
  80. basket = empty_basket
  81. add_product(basket)
  82. assert "Buy 1 more product from " in count_condition.get_upsell_message(
  83. self.offer, basket
  84. )
  85. def test_matching_quantity_basket_fails_partial_condition(
  86. self, count_condition, empty_basket
  87. ):
  88. basket = empty_basket
  89. add_product(basket, quantity=2)
  90. assert count_condition.is_partially_satisfied(self.offer, basket) is False
  91. def test_matching_quantity_basket_passes_condition(
  92. self, count_condition, empty_basket
  93. ):
  94. basket = empty_basket
  95. add_product(basket, quantity=2)
  96. assert count_condition.is_satisfied(self.offer, basket)
  97. def test_greater_quantity_basket_passes_condition(
  98. self, count_condition, empty_basket
  99. ):
  100. basket = empty_basket
  101. add_product(basket, quantity=3)
  102. assert count_condition.is_satisfied(self.offer, basket)
  103. def test_consumption(self, count_condition, empty_basket):
  104. basket = empty_basket
  105. add_product(basket, quantity=3)
  106. count_condition.consume_items(self.offer, basket, [])
  107. assert 1 == basket.all_lines()[0].quantity_without_discount
  108. def test_is_satisfied_accounts_for_consumed_items(
  109. self, count_condition, empty_basket
  110. ):
  111. basket = empty_basket
  112. add_product(basket, quantity=3)
  113. count_condition.consume_items(self.offer, basket, [])
  114. assert count_condition.is_satisfied(self.offer, basket) is False
  115. @pytest.mark.django_db
  116. class TestValueCondition:
  117. @pytest.fixture(autouse=True)
  118. def setUp(self, empty_basket, value_condition, mock_offer):
  119. self.basket = empty_basket
  120. self.condition = value_condition
  121. self.offer = mock_offer
  122. self.item = factories.create_product(price=D("5.00"))
  123. self.expensive_item = factories.create_product(price=D("15.00"))
  124. def test_description(self, value_condition):
  125. assert value_condition.description
  126. def test_empty_basket_fails_condition(self):
  127. assert self.condition.is_satisfied(self.offer, self.basket) is False
  128. def test_empty_basket_fails_partial_condition(self):
  129. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  130. def test_less_value_basket_fails_condition(self):
  131. add_product(self.basket, D("5"))
  132. assert self.condition.is_satisfied(self.offer, self.basket) is False
  133. def test_not_discountable_item_fails_condition(self):
  134. product = factories.create_product(is_discountable=False)
  135. add_product(self.basket, D("15"), product=product)
  136. assert self.condition.is_satisfied(self.offer, self.basket) is False
  137. def test_upsell_message(self):
  138. add_product(self.basket, D("5"))
  139. assert "Spend" in self.condition.get_upsell_message(self.offer, self.basket)
  140. def test_matching_basket_fails_partial_condition(self):
  141. add_product(self.basket, D("5"), 2)
  142. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  143. def test_less_value_basket_passes_partial_condition(self):
  144. add_product(self.basket, D("5"), 1)
  145. assert self.condition.is_partially_satisfied(self.offer, self.basket)
  146. def test_matching_basket_passes_condition(self):
  147. add_product(self.basket, D("5"), 2)
  148. assert self.condition.is_satisfied(self.offer, self.basket)
  149. def test_greater_than_basket_passes_condition(self):
  150. add_product(self.basket, D("5"), 3)
  151. assert self.condition.is_satisfied(self.offer, self.basket)
  152. def test_consumption(self):
  153. add_product(self.basket, D("5"), 3)
  154. self.condition.consume_items(self.offer, self.basket, [])
  155. assert 1 == self.basket.all_lines()[0].quantity_without_discount
  156. def test_consumption_with_high_value_product(self):
  157. add_product(self.basket, D("15"), 1)
  158. self.condition.consume_items(self.offer, self.basket, [])
  159. assert 0 == self.basket.all_lines()[0].quantity_without_discount
  160. def test_is_consumed_respects_quantity_consumed(self):
  161. add_product(self.basket, D("15"), 1)
  162. assert self.condition.is_satisfied(self.offer, self.basket)
  163. self.condition.consume_items(self.offer, self.basket, [])
  164. assert self.condition.is_satisfied(self.offer, self.basket) is False
  165. @pytest.mark.django_db
  166. class TestCoverageCondition:
  167. @pytest.fixture(autouse=True)
  168. def setUp(self, range_some, products_some, empty_basket, coverage_condition):
  169. self.products = products_some
  170. self.range = range_some
  171. self.basket = empty_basket
  172. self.condition = coverage_condition
  173. self.offer = mock.Mock()
  174. def test_empty_basket_fails(self):
  175. assert self.condition.is_satisfied(self.offer, self.basket) is False
  176. def test_empty_basket_fails_partial_condition(self):
  177. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  178. def test_single_item_fails(self):
  179. add_product(self.basket, product=self.products[0])
  180. assert self.condition.is_satisfied(self.offer, self.basket) is False
  181. def test_not_discountable_item_fails(self):
  182. self.products[0].is_discountable = False
  183. self.products[0].save()
  184. add_product(self.basket, product=self.products[0])
  185. add_product(self.basket, product=self.products[1])
  186. assert self.condition.is_satisfied(self.offer, self.basket) is False
  187. def test_single_item_passes_partial_condition(self):
  188. add_product(self.basket, product=self.products[0])
  189. assert self.condition.is_partially_satisfied(self.offer, self.basket)
  190. def test_upsell_message(self):
  191. add_product(self.basket, product=self.products[0])
  192. assert "Buy 1 more" in self.condition.get_upsell_message(
  193. self.offer, self.basket
  194. )
  195. def test_duplicate_item_fails(self):
  196. add_product(self.basket, quantity=2, product=self.products[0])
  197. assert self.condition.is_satisfied(self.offer, self.basket) is False
  198. def test_duplicate_item_passes_partial_condition(self):
  199. add_product(self.basket, quantity=2, product=self.products[0])
  200. assert self.condition.is_partially_satisfied(self.offer, self.basket)
  201. def test_covering_items_pass(self):
  202. add_product(self.basket, product=self.products[0])
  203. add_product(self.basket, product=self.products[1])
  204. assert self.condition.is_satisfied(self.offer, self.basket)
  205. def test_covering_items_fail_partial_condition(self):
  206. add_product(self.basket, product=self.products[0])
  207. add_product(self.basket, product=self.products[1])
  208. assert self.condition.is_partially_satisfied(self.offer, self.basket) is False
  209. def test_covering_items_are_consumed(self):
  210. add_product(self.basket, product=self.products[0])
  211. add_product(self.basket, product=self.products[1])
  212. self.condition.consume_items(self.offer, self.basket, [])
  213. assert 0 == self.basket.num_items_without_discount
  214. def test_consumed_items_checks_affected_items(self):
  215. # Create new offer
  216. range = models.Range.objects.create(
  217. name="All products", includes_all_products=True
  218. )
  219. cond = models.CoverageCondition(range=range, type="Coverage", value=2)
  220. # Get 4 distinct products in the basket
  221. self.products.extend([factories.create_product(), factories.create_product()])
  222. for product in self.products:
  223. add_product(self.basket, product=product)
  224. assert cond.is_satisfied(self.offer, self.basket)
  225. cond.consume_items(self.offer, self.basket, [])
  226. assert 2 == self.basket.num_items_without_discount
  227. assert cond.is_satisfied(self.offer, self.basket)
  228. cond.consume_items(self.offer, self.basket, [])
  229. assert 0 == self.basket.num_items_without_discount
  230. @pytest.mark.django_db
  231. class TestConditionProxyModels(object):
  232. def test_name_and_description(self, range):
  233. """
  234. Tests that the condition proxy classes all return a name and
  235. description. Unfortunately, the current implementations means
  236. a valid range and value are required.
  237. """
  238. for type, __ in models.Condition.TYPE_CHOICES:
  239. condition = models.Condition(type=type, range=range, value=5)
  240. assert all([condition.name, condition.description, str(condition)])
  241. def test_proxy(self, range):
  242. for type, __ in models.Condition.TYPE_CHOICES:
  243. condition = models.Condition(type=type, range=range, value=5)
  244. proxy = condition.proxy()
  245. assert condition.type == proxy.type
  246. assert condition.range == proxy.range
  247. assert condition.value == proxy.value
  248. class TestCustomCondition(TestCase):
  249. def setUp(self):
  250. self.condition = custom.create_condition(BasketOwnerCalledBarry)
  251. self.offer = models.ConditionalOffer(condition=self.condition)
  252. self.basket = Basket()
  253. def test_is_not_satisfied_by_non_match(self):
  254. self.basket.owner = factories.UserFactory(first_name="Alan")
  255. assert self.offer.is_condition_satisfied(self.basket) is False
  256. def test_is_satisfied_by_match(self):
  257. self.basket.owner = factories.UserFactory(first_name="Barry")
  258. assert self.offer.is_condition_satisfied(self.basket)
  259. class TestOffersWithCountCondition(TestCase):
  260. def setUp(self):
  261. super().setUp()
  262. self.basket = factories.create_basket(empty=True)
  263. # Create range and add one product to it.
  264. rng = factories.RangeFactory(name='All products', includes_all_products=True)
  265. self.product = factories.ProductFactory()
  266. rng.add_product(self.product)
  267. # Create a non-exclusive offer #1.
  268. condition1 = factories.ConditionFactory(range=rng, value=D('1'))
  269. benefit1 = factories.BenefitFactory(range=rng, value=D('10'))
  270. self.offer1 = factories.ConditionalOfferFactory(
  271. condition=condition1, benefit=benefit1, start_datetime=now(),
  272. name='Test offer #1', exclusive=False,
  273. )
  274. # Create a non-exclusive offer #2.
  275. condition2 = factories.ConditionFactory(range=rng, value=D('1'))
  276. benefit2 = factories.BenefitFactory(range=rng, value=D('5'))
  277. self.offer2 = factories.ConditionalOfferFactory(
  278. condition=condition2, benefit=benefit2, start_datetime=now(),
  279. name='Test offer #2', exclusive=False,
  280. )
  281. def add_product(self):
  282. self.basket.add_product(self.product)
  283. self.basket.strategy = Selector().strategy()
  284. applicator.Applicator().apply(self.basket)
  285. def assertOffersApplied(self, offers):
  286. applied_offers = self.basket.applied_offers()
  287. self.assertEqual(len(offers), len(applied_offers))
  288. for offer in offers:
  289. self.assertIn(offer.id, applied_offers, msg=offer)
  290. def test_both_non_exclusive_offers_are_applied(self):
  291. self.add_product()
  292. self.assertOffersApplied([self.offer1, self.offer2])