Adds available_discounts, which allows enumeration of the discounts that are available for a given set of products and categories
This commit is contained in:
		
							parent
							
								
									8d66ed5715
								
							
						
					
					
						commit
						fb3878ce2e
					
				
					 2 changed files with 249 additions and 3 deletions
				
			
		
							
								
								
									
										83
									
								
								registrasion/controllers/discount.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								registrasion/controllers/discount.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | |||
| import itertools | ||||
| 
 | ||||
| from conditions import ConditionController | ||||
| from registrasion import models as rego | ||||
| 
 | ||||
| from django.db.models import Sum | ||||
| 
 | ||||
| 
 | ||||
| class DiscountAndQuantity(object): | ||||
|     def __init__(self, discount, clause, quantity): | ||||
|         self.discount = discount | ||||
|         self.clause = clause | ||||
|         self.quantity = quantity | ||||
| 
 | ||||
| 
 | ||||
| def available_discounts(user, categories, products): | ||||
|     ''' Returns all discounts available to this user for the given categories | ||||
|     and products. The discounts also list the available quantity for this user, | ||||
|     not including products that are pending purchase. ''' | ||||
| 
 | ||||
|     # discounts that match provided categories | ||||
|     category_discounts = rego.DiscountForCategory.objects.filter( | ||||
|         category__in=categories | ||||
|     ) | ||||
|     # discounts that match provided products | ||||
|     product_discounts = rego.DiscountForProduct.objects.filter( | ||||
|         product__in=products | ||||
|     ) | ||||
|     # discounts that match categories for provided products | ||||
|     product_category_discounts = rego.DiscountForCategory.objects.filter( | ||||
|         category__in=(product.category for product in products) | ||||
|     ) | ||||
|     # (Not relevant: discounts that match products in provided categories) | ||||
| 
 | ||||
|     # The set of all potential discounts | ||||
|     potential_discounts = set(itertools.chain( | ||||
|         product_discounts, | ||||
|         category_discounts, | ||||
|         product_category_discounts, | ||||
|     )) | ||||
| 
 | ||||
|     discounts = [] | ||||
| 
 | ||||
|     # Markers so that we don't need to evaluate given conditions more than once | ||||
|     accepted_discounts = set() | ||||
|     failed_discounts = set() | ||||
| 
 | ||||
|     for discount in potential_discounts: | ||||
|         real_discount = rego.DiscountBase.objects.get_subclass( | ||||
|             pk=discount.discount.pk, | ||||
|         ) | ||||
|         cond = ConditionController.for_condition(real_discount) | ||||
| 
 | ||||
|         # Count the past uses of the given discount item. | ||||
|         # If this user has exceeded the limit for the clause, this clause | ||||
|         # is not available any more. | ||||
|         past_uses = rego.DiscountItem.objects.filter( | ||||
|             cart__user=user, | ||||
|             cart__active=False,  # Only past carts count | ||||
|             discount=discount.discount, | ||||
|         ) | ||||
|         agg = past_uses.aggregate(Sum("quantity")) | ||||
|         past_use_count = agg["quantity__sum"] | ||||
|         if past_use_count is None: | ||||
|             past_use_count = 0 | ||||
| 
 | ||||
|         if past_use_count >= discount.quantity: | ||||
|             # This clause has exceeded its use count | ||||
|             pass | ||||
|         elif real_discount not in failed_discounts: | ||||
|             # This clause is still available | ||||
|             if real_discount in accepted_discounts or cond.is_met(user, 0): | ||||
|                 # This clause is valid for this user | ||||
|                 discounts.append(DiscountAndQuantity( | ||||
|                     discount=real_discount, | ||||
|                     clause=discount, | ||||
|                     quantity=discount.quantity - past_use_count, | ||||
|                 )) | ||||
|                 accepted_discounts.add(real_discount) | ||||
|             else: | ||||
|                 # This clause is not valid for this user | ||||
|                 failed_discounts.add(real_discount) | ||||
|     return discounts | ||||
|  | @ -3,7 +3,9 @@ import pytz | |||
| from decimal import Decimal | ||||
| 
 | ||||
| from registrasion import models as rego | ||||
| from registrasion.controllers import discount | ||||
| from registrasion.controllers.cart import CartController | ||||
| from registrasion.controllers.invoice import InvoiceController | ||||
| 
 | ||||
| from test_cart import RegistrationCartTestCase | ||||
| 
 | ||||
|  | @ -13,7 +15,11 @@ UTC = pytz.timezone('UTC') | |||
| class DiscountTestCase(RegistrationCartTestCase): | ||||
| 
 | ||||
|     @classmethod | ||||
|     def add_discount_prod_1_includes_prod_2(cls, amount=Decimal(100)): | ||||
|     def add_discount_prod_1_includes_prod_2( | ||||
|             cls, | ||||
|             amount=Decimal(100), | ||||
|             quantity=2, | ||||
|             ): | ||||
|         discount = rego.IncludedProductDiscount.objects.create( | ||||
|             description="PROD_1 includes PROD_2 " + str(amount) + "%", | ||||
|         ) | ||||
|  | @ -24,7 +30,7 @@ class DiscountTestCase(RegistrationCartTestCase): | |||
|             discount=discount, | ||||
|             product=cls.PROD_2, | ||||
|             percentage=amount, | ||||
|             quantity=2 | ||||
|             quantity=quantity, | ||||
|         ).save() | ||||
|         return discount | ||||
| 
 | ||||
|  | @ -32,7 +38,8 @@ class DiscountTestCase(RegistrationCartTestCase): | |||
|     def add_discount_prod_1_includes_cat_2( | ||||
|             cls, | ||||
|             amount=Decimal(100), | ||||
|             quantity=2): | ||||
|             quantity=2, | ||||
|             ): | ||||
|         discount = rego.IncludedProductDiscount.objects.create( | ||||
|             description="PROD_1 includes CAT_2 " + str(amount) + "%", | ||||
|         ) | ||||
|  | @ -47,6 +54,33 @@ class DiscountTestCase(RegistrationCartTestCase): | |||
|         ).save() | ||||
|         return discount | ||||
| 
 | ||||
|     @classmethod | ||||
|     def add_discount_prod_1_includes_prod_3_and_prod_4( | ||||
|             cls, | ||||
|             amount=Decimal(100), | ||||
|             quantity=2, | ||||
|             ): | ||||
|         discount = rego.IncludedProductDiscount.objects.create( | ||||
|             description="PROD_1 includes PROD_3 and PROD_4 " + | ||||
|                         str(amount) + "%", | ||||
|         ) | ||||
|         discount.save() | ||||
|         discount.enabling_products.add(cls.PROD_1) | ||||
|         discount.save() | ||||
|         rego.DiscountForProduct.objects.create( | ||||
|             discount=discount, | ||||
|             product=cls.PROD_3, | ||||
|             percentage=amount, | ||||
|             quantity=quantity, | ||||
|         ).save() | ||||
|         rego.DiscountForProduct.objects.create( | ||||
|             discount=discount, | ||||
|             product=cls.PROD_4, | ||||
|             percentage=amount, | ||||
|             quantity=quantity, | ||||
|         ).save() | ||||
|         return discount | ||||
| 
 | ||||
|     def test_discount_is_applied(self): | ||||
|         self.add_discount_prod_1_includes_prod_2() | ||||
| 
 | ||||
|  | @ -214,3 +248,132 @@ class DiscountTestCase(RegistrationCartTestCase): | |||
|             discount_items = list(cart.cart.discountitem_set.all()) | ||||
|             # The discount is applied. | ||||
|             self.assertEqual(1, len(discount_items)) | ||||
| 
 | ||||
|     # Tests for the discount.available_discounts enumerator | ||||
|     def test_enumerate_no_discounts_for_no_input(self): | ||||
|         discounts = discount.available_discounts(self.USER_1, [], []) | ||||
|         self.assertEqual(0, len(discounts)) | ||||
| 
 | ||||
|     def test_enumerate_no_discounts_if_condition_not_met(self): | ||||
|         self.add_discount_prod_1_includes_cat_2(quantity=1) | ||||
| 
 | ||||
|         discounts = discount.available_discounts( | ||||
|             self.USER_1, | ||||
|             [], | ||||
|             [self.PROD_3], | ||||
|         ) | ||||
|         self.assertEqual(0, len(discounts)) | ||||
| 
 | ||||
|         discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) | ||||
|         self.assertEqual(0, len(discounts)) | ||||
| 
 | ||||
|     def test_category_discount_appears_once_if_met_twice(self): | ||||
|         self.add_discount_prod_1_includes_cat_2(quantity=1) | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
| 
 | ||||
|         discounts = discount.available_discounts( | ||||
|             self.USER_1, | ||||
|             [self.CAT_2], | ||||
|             [self.PROD_3], | ||||
|         ) | ||||
|         self.assertEqual(1, len(discounts)) | ||||
| 
 | ||||
|     def test_category_discount_appears_with_category(self): | ||||
|         self.add_discount_prod_1_includes_cat_2(quantity=1) | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
| 
 | ||||
|         discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) | ||||
|         self.assertEqual(1, len(discounts)) | ||||
| 
 | ||||
|     def test_category_discount_appears_with_product(self): | ||||
|         self.add_discount_prod_1_includes_cat_2(quantity=1) | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
| 
 | ||||
|         discounts = discount.available_discounts( | ||||
|             self.USER_1, | ||||
|             [], | ||||
|             [self.PROD_3], | ||||
|         ) | ||||
|         self.assertEqual(1, len(discounts)) | ||||
| 
 | ||||
|     def test_category_discount_appears_once_with_two_valid_product(self): | ||||
|         self.add_discount_prod_1_includes_cat_2(quantity=1) | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
| 
 | ||||
|         discounts = discount.available_discounts( | ||||
|             self.USER_1, | ||||
|             [], | ||||
|             [self.PROD_3, self.PROD_4] | ||||
|         ) | ||||
|         self.assertEqual(1, len(discounts)) | ||||
| 
 | ||||
|     def test_product_discount_appears_with_product(self): | ||||
|         self.add_discount_prod_1_includes_prod_2(quantity=1) | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
| 
 | ||||
|         discounts = discount.available_discounts( | ||||
|             self.USER_1, | ||||
|             [], | ||||
|             [self.PROD_2], | ||||
|         ) | ||||
|         self.assertEqual(1, len(discounts)) | ||||
| 
 | ||||
|     def test_product_discount_does_not_appear_with_category(self): | ||||
|         self.add_discount_prod_1_includes_prod_2(quantity=1) | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
| 
 | ||||
|         discounts = discount.available_discounts(self.USER_1, [self.CAT_1], []) | ||||
|         self.assertEqual(0, len(discounts)) | ||||
| 
 | ||||
|     def test_discount_quantity_is_correct_before_first_purchase(self): | ||||
|         self.add_discount_prod_1_includes_cat_2(quantity=2) | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
|         cart.add_to_cart(self.PROD_3, 1)  # Exhaust the quantity | ||||
| 
 | ||||
|         discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) | ||||
|         self.assertEqual(2, discounts[0].quantity) | ||||
|         inv = InvoiceController.for_cart(cart.cart) | ||||
|         inv.pay("Dummy reference", inv.invoice.value) | ||||
|         self.assertTrue(inv.invoice.paid) | ||||
| 
 | ||||
|     def test_discount_quantity_is_correct_after_first_purchase(self): | ||||
|         self.test_discount_quantity_is_correct_before_first_purchase() | ||||
| 
 | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_3, 1)  # Exhaust the quantity | ||||
| 
 | ||||
|         discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) | ||||
|         self.assertEqual(1, discounts[0].quantity) | ||||
|         inv = InvoiceController.for_cart(cart.cart) | ||||
|         inv.pay("Dummy reference", inv.invoice.value) | ||||
|         self.assertTrue(inv.invoice.paid) | ||||
| 
 | ||||
|     def test_discount_is_gone_after_quantity_exhausted(self): | ||||
|         self.test_discount_quantity_is_correct_after_first_purchase() | ||||
|         discounts = discount.available_discounts(self.USER_1, [self.CAT_2], []) | ||||
|         self.assertEqual(0, len(discounts)) | ||||
| 
 | ||||
|     def test_product_discount_enabled_twice_appears_twice(self): | ||||
|         self.add_discount_prod_1_includes_prod_3_and_prod_4(quantity=2) | ||||
|         cart = CartController.for_user(self.USER_1) | ||||
|         cart.add_to_cart(self.PROD_1, 1)  # Enable the discount | ||||
|         discounts = discount.available_discounts( | ||||
|             self.USER_1, | ||||
|             [], | ||||
|             [self.PROD_3, self.PROD_4], | ||||
|         ) | ||||
|         self.assertEqual(2, len(discounts)) | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer