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:
Christopher Neugebauer 2016-03-25 18:09:24 +11:00
parent 8d66ed5715
commit fb3878ce2e
2 changed files with 249 additions and 3 deletions

View 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

View file

@ -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))