Skip to content

Commit 15d0dc5

Browse files
authored
Fix v0/baskets/create_from_product so that baskets get discounts as we expect (#3290)
1 parent c2fe1c8 commit 15d0dc5

4 files changed

Lines changed: 519 additions & 22 deletions

File tree

ecommerce/api.py

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,12 @@ def get_auto_apply_discounts_for_basket(basket_id: int) -> QuerySet[Discount]:
852852
"""
853853
Get the auto-apply discounts that can be applied to a basket.
854854
855+
This includes the financial assistant discounts for the user and products,
856+
if there are any, since those are also automatically applied regardless of
857+
the flag on the discount. This does not include B2B discounts because those
858+
are applied in a different manner - either through a separate API, or by the
859+
learner via the cart interface.
860+
855861
Args:
856862
basket_id (int): The ID of the basket to get the auto-apply discounts for.
857863
@@ -861,29 +867,122 @@ def get_auto_apply_discounts_for_basket(basket_id: int) -> QuerySet[Discount]:
861867
basket = Basket.objects.get(pk=basket_id)
862868
products = basket.get_products()
863869

870+
finaid_discounts = []
871+
872+
for product in products:
873+
finaid_discount = determine_courseware_flexible_price_discount(
874+
product, basket.user
875+
)
876+
877+
if finaid_discount:
878+
finaid_discounts.append(finaid_discount.id)
879+
864880
return Discount.objects.filter(
865-
Q(products__product__in=products) | Q(products__isnull=True),
881+
Q(activation_date__lte=now_in_utc()) | Q(activation_date=None),
882+
Q(expiration_date__gt=now_in_utc()) | Q(expiration_date=None),
883+
).filter(
866884
Q(user_discount_discount__user=basket.user)
867-
| Q(user_discount_discount__isnull=True),
868-
automatic=True,
885+
| Q(pk__in=finaid_discounts)
886+
| Q(automatic=True)
869887
)
870888

871889

872-
def apply_discount_to_basket(basket: Basket, discount: Discount, *, allow_finaid=False):
890+
def apply_discount_to_basket(basket: Basket, discount: Discount, *, allow_finaid=False): # noqa: C901
873891
"""
874892
Apply a discount to a basket.
875893
894+
Discount application is subject to rules:
895+
- The discount itself must be valid on its face (not inactive, applies to products, etc.)
896+
- The discount is not a financial assistance tier discount, unless allow_finaid is set
897+
- The discount provides a better price to the learner than any other applied discount
898+
- The discount is not overriding a user discount
899+
900+
If a user discount is supplied to this function, then that discount will be
901+
applied _unless_ a financial assistance discount is also applied. User
902+
discounts take precedence over any other discount, other than financial
903+
assistance discounts.
904+
905+
This function is not for use with B2B or verified program enrollment code
906+
redemption. Those use cases have their own redemption code paths because
907+
they need different verification steps.
908+
876909
Args:
877910
discount (Discount): The Discount to apply to the basket.
878911
Keyword Args:
879912
allow_finaid (bool): Allow a financial assistance discount through.
880913
"""
881914
if discount.is_valid(basket, allow_finaid=allow_finaid):
882-
BasketDiscount.objects.create(
915+
defaults = {
916+
"redeemed_discount": discount,
917+
"redemption_date": now_in_utc(),
918+
}
919+
920+
if basket.discounts.count() > 0 and basket.basket_items.count() > 0:
921+
# Check to make sure the supplied discount can be applied. This means
922+
# that it should not override any user discounts that are applied,
923+
# and it should be better than the other discounts in the basket.
924+
925+
if discount.user_discount_discount.filter(user=basket.user).exists():
926+
# This is a user discount.
927+
# Check for an existing tier discount - user discount shouldn't override that
928+
finaid_discounts = [
929+
basket_discount
930+
for basket_discount in basket.discounts.all()
931+
if basket_discount.redeemed_discount.flexible_price_tiers.count()
932+
> 0
933+
]
934+
935+
if len(finaid_discounts) > 0:
936+
# There is a finaid discount, so don't apply this user one.
937+
return
938+
else:
939+
is_finaid_discount = discount.flexible_price_tiers.exists()
940+
has_user_discount = (
941+
basket.discounts.filter(
942+
redeemed_discount__user_discount_discount__user=basket.user
943+
).count()
944+
> 0
945+
)
946+
947+
if is_finaid_discount and not allow_finaid:
948+
# Financial assistance discount; bail unless the flag is set
949+
return
950+
951+
if has_user_discount and is_finaid_discount and allow_finaid:
952+
# Basket has a user discount applied; this is a finaid
953+
# discount (and we're allowed to apply it); apply the
954+
# discount without further evaluation.
955+
956+
BasketDiscount.objects.update_or_create(
957+
redeemed_by=basket.user,
958+
redeemed_basket=basket,
959+
defaults=defaults,
960+
create_defaults=defaults,
961+
)
962+
return
963+
964+
if has_user_discount:
965+
# This basket has a user discount applied; this isn't a
966+
# finaid discount that we're permitting to be applied; so
967+
# skip this one.
968+
return
969+
970+
found_better = False
971+
972+
for item in basket.basket_items.all():
973+
test_price = discount.discount_product(item.product, basket.user)
974+
if test_price and item.discounted_price >= test_price:
975+
found_better = True
976+
break
977+
978+
if not found_better:
979+
return
980+
981+
BasketDiscount.objects.update_or_create(
883982
redeemed_by=basket.user,
884-
redeemed_discount=discount,
885983
redeemed_basket=basket,
886-
redemption_date=now_in_utc(),
984+
defaults=defaults,
985+
create_defaults=defaults,
887986
)
888987

889988

0 commit comments

Comments
 (0)