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 decimal import Decimal
|
||||||
|
|
||||||
from registrasion import models as rego
|
from registrasion import models as rego
|
||||||
|
from registrasion.controllers import discount
|
||||||
from registrasion.controllers.cart import CartController
|
from registrasion.controllers.cart import CartController
|
||||||
|
from registrasion.controllers.invoice import InvoiceController
|
||||||
|
|
||||||
from test_cart import RegistrationCartTestCase
|
from test_cart import RegistrationCartTestCase
|
||||||
|
|
||||||
|
@ -13,7 +15,11 @@ UTC = pytz.timezone('UTC')
|
||||||
class DiscountTestCase(RegistrationCartTestCase):
|
class DiscountTestCase(RegistrationCartTestCase):
|
||||||
|
|
||||||
@classmethod
|
@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(
|
discount = rego.IncludedProductDiscount.objects.create(
|
||||||
description="PROD_1 includes PROD_2 " + str(amount) + "%",
|
description="PROD_1 includes PROD_2 " + str(amount) + "%",
|
||||||
)
|
)
|
||||||
|
@ -24,7 +30,7 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
discount=discount,
|
discount=discount,
|
||||||
product=cls.PROD_2,
|
product=cls.PROD_2,
|
||||||
percentage=amount,
|
percentage=amount,
|
||||||
quantity=2
|
quantity=quantity,
|
||||||
).save()
|
).save()
|
||||||
return discount
|
return discount
|
||||||
|
|
||||||
|
@ -32,7 +38,8 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
def add_discount_prod_1_includes_cat_2(
|
def add_discount_prod_1_includes_cat_2(
|
||||||
cls,
|
cls,
|
||||||
amount=Decimal(100),
|
amount=Decimal(100),
|
||||||
quantity=2):
|
quantity=2,
|
||||||
|
):
|
||||||
discount = rego.IncludedProductDiscount.objects.create(
|
discount = rego.IncludedProductDiscount.objects.create(
|
||||||
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
description="PROD_1 includes CAT_2 " + str(amount) + "%",
|
||||||
)
|
)
|
||||||
|
@ -47,6 +54,33 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
).save()
|
).save()
|
||||||
return discount
|
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):
|
def test_discount_is_applied(self):
|
||||||
self.add_discount_prod_1_includes_prod_2()
|
self.add_discount_prod_1_includes_prod_2()
|
||||||
|
|
||||||
|
@ -214,3 +248,132 @@ class DiscountTestCase(RegistrationCartTestCase):
|
||||||
discount_items = list(cart.cart.discountitem_set.all())
|
discount_items = list(cart.cart.discountitem_set.all())
|
||||||
# The discount is applied.
|
# The discount is applied.
|
||||||
self.assertEqual(1, len(discount_items))
|
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…
Reference in a new issue