Commit bc0cb796 by Douglas Hall Committed by Douglas Hall

Refactor offer code to keep it DRY in preparation for Enterprise Offers feature.

parent 110304b5
......@@ -19,7 +19,6 @@ from ecommerce.core.constants import COURSE_ID_REGEX, ENROLLMENT_CODE_SWITCH, IS
from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.courses.models import Course
from ecommerce.invoice.models import Invoice
from ecommerce.programs.constants import BENEFIT_PROXY_CLASS_MAP
logger = logging.getLogger(__name__)
......@@ -579,7 +578,7 @@ class CouponSerializer(ProductPaymentInfoMixin, serializers.ModelSerializer):
voucher_type = serializers.SerializerMethodField()
def get_benefit_type(self, obj):
return retrieve_benefit(obj).type or BENEFIT_PROXY_CLASS_MAP[retrieve_benefit(obj).proxy_class]
return retrieve_benefit(obj).type or getattr(retrieve_benefit(obj).proxy(), 'benefit_class_type', None)
def get_benefit_value(self, obj):
return retrieve_benefit(obj).value
......
......@@ -382,7 +382,10 @@ class BasketCalculateViewTests(ProgramTestMixin, TestCase):
@httpretty.activate
def test_basket_calculate_program_offer(self):
""" Verify successful basket calculation with a program offer """
offer = ProgramOfferFactory(benefit=PercentageDiscountBenefitWithoutRangeFactory(value=100))
offer = ProgramOfferFactory(
site=self.site,
benefit=PercentageDiscountBenefitWithoutRangeFactory(value=100)
)
program_uuid = offer.condition.program_uuid
self.mock_program_detail_endpoint(program_uuid, self.site_configuration.discovery_api_url)
self.mock_enrollment_api(self.user.username)
......@@ -459,6 +462,7 @@ class BasketCalculateViewTests(ProgramTestMixin, TestCase):
self.site_configuration.enable_partial_program = True
self.site_configuration.save()
offer = ProgramOfferFactory(
site=self.site,
benefit=PercentageDiscountBenefitWithoutRangeFactory(value=100),
condition=ProgramCourseRunSeatsConditionFactory()
)
......@@ -490,6 +494,7 @@ class BasketCalculateViewTests(ProgramTestMixin, TestCase):
self.site_configuration.enable_partial_program = True
self.site_configuration.save()
offer = ProgramOfferFactory(
site=self.site,
benefit=PercentageDiscountBenefitWithoutRangeFactory(value=100),
condition=ProgramCourseRunSeatsConditionFactory()
)
......
......@@ -26,7 +26,6 @@ from ecommerce.extensions.payment.processors.invoice import InvoicePayment
from ecommerce.extensions.voucher.models import CouponVouchers
from ecommerce.extensions.voucher.utils import update_voucher_offer
from ecommerce.invoice.models import Invoice
from ecommerce.programs.constants import BENEFIT_PROXY_CLASS_MAP
Basket = get_model('basket', 'Basket')
Benefit = get_model('offer', 'Benefit')
......@@ -431,7 +430,9 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
new_offer = update_voucher_offer(
offer=voucher_offer,
benefit_value=benefit_value or voucher_offer.benefit.value,
benefit_type=voucher_offer.benefit.type or BENEFIT_PROXY_CLASS_MAP[voucher_offer.benefit.proxy_class],
benefit_type=voucher_offer.benefit.type or getattr(
voucher_offer.benefit.proxy(), 'benefit_class_type', None
),
coupon=coupon,
max_uses=voucher_offer.max_global_applications,
program_uuid=program_uuid
......
from functools import wraps
import waffle
def check_condition_applicability(switches=None):
"""
Decorator for checking the applicability of a Condition.
Applies some global logic for determining the applicability
of a Condition to a Basket. This decorator expects the wrapped
function to receive a Condition, ConditionalOffer, and Basket
as parameters.
Arguments:
switches (list): List of waffle switch names which should be enabled for
the Condition to be applicable to the Basket.
"""
def outer_wrapper(func):
@wraps(func)
def _decorated(condition, offer, basket):
if offer.site != basket.site:
return False
if basket.is_empty:
return False
if basket.total_incl_tax == 0:
return False
if switches:
for switch in switches:
if not waffle.switch_is_active(switch):
return False
return func(condition, offer, basket)
return _decorated
return outer_wrapper
import operator
from oscar.core.loading import get_model
Benefit = get_model('offer', 'Benefit')
class BenefitWithoutRangeMixin(object):
""" Mixin for Benefits without an attached range.
The range is only used for the name and description. We would prefer not
to deal with ranges since we rely on the condition to fully determine if
a conditional offer is applicable to a basket.
"""
def get_applicable_lines(self, offer, basket, range=None): # pylint: disable=unused-argument,redefined-builtin
condition = offer.condition.proxy() or offer.condition
line_tuples = condition.get_applicable_lines(offer, basket, most_expensive_first=False)
# Do not allow multiple discounts per line
line_tuples = [line_tuple for line_tuple in line_tuples if line_tuple[1].quantity_without_discount > 0]
# We sort lines to be cheapest first to ensure consistent applications
return sorted(line_tuples, key=operator.itemgetter(0))
class AbsoluteBenefitMixin(object):
""" Mixin for fixed-amount Benefits. """
benefit_class_type = Benefit.FIXED
class PercentageBenefitMixin(object):
""" Mixin for percentage-based Benefits. """
benefit_class_type = Benefit.PERCENTAGE
class SingleItemConsumptionConditionMixin(object):
def consume_items(self, offer, basket, affected_lines): # pylint: disable=unused-argument
""" Marks items within the basket lines as consumed so they can't be reused in other offers.
This offer will consume only 1 unit of quantity for each affected line.
Args:
offer (AbstractConditionalOffer)
basket (AbstractBasket)
affected_lines (tuple[]): The lines that have been affected by the discount.
This should be list of tuples (line, discount, qty)
"""
for line, __, __ in affected_lines:
quantity_to_consume = min(line.quantity_without_discount, 1)
line.consume(quantity_to_consume)
......@@ -21,3 +21,13 @@ def benefit_discount(benefit):
str: String value containing formatted benefit value and type.
"""
return format_benefit_value(benefit)
@register.filter(name='benefit_type')
def benefit_type(benefit):
_type = benefit.type
if not _type:
_type = getattr(benefit.proxy(), 'benefit_class_type', None)
return _type
from mock import patch
from oscar.test.factories import ConditionalOfferFactory, ConditionFactory
from waffle.models import Switch
from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin
from ecommerce.extensions.offer.decorators import check_condition_applicability
from ecommerce.extensions.test.factories import UserFactory, create_basket
from ecommerce.tests.factories import SiteConfigurationFactory
from ecommerce.tests.testcases import TestCase
class OfferDecoratorTests(DiscoveryTestMixin, TestCase):
def setUp(self):
super(OfferDecoratorTests, self).setUp()
self.condition = ConditionFactory()
self.offer = ConditionalOfferFactory(condition=self.condition, site=self.site)
self.user = UserFactory()
@patch('ecommerce.extensions.offer.models.Condition.is_satisfied')
def test_check_condition_applicability(self, mock_is_satisfied):
"""
Validate check_condition_applicability decorator returns True if it is applicable.
"""
mock_is_satisfied.return_value = True
mock_is_satisfied.__name__ = 'is_satisfied'
basket = create_basket(self.user, self.site)
self.assertTrue(
check_condition_applicability()(self.condition.is_satisfied)(self.condition, self.offer, basket)
)
def test_check_condition_applicability_site_mismatch(self):
"""
Validate check_condition_applicability decorator returns False if the offer site and basket site do not match.
"""
basket = create_basket(self.user, SiteConfigurationFactory().site)
self.assertFalse(
check_condition_applicability()(self.condition.is_satisfied)(self.condition, self.offer, basket)
)
def test_check_condition_applicability_empty_basket(self):
"""
Validate check_condition_applicability decorator returns False if the basket is empty.
"""
basket = create_basket(self.user, self.site, empty=True)
self.assertFalse(
check_condition_applicability()(self.condition.is_satisfied)(self.condition, self.offer, basket)
)
def test_check_condition_applicability_free_basket(self):
"""
Validate check_condition_applicability decorator returns False if the basket is free.
"""
basket = create_basket(self.user, self.site, price='0.00')
self.assertFalse(
check_condition_applicability()(self.condition.is_satisfied)(self.condition, self.offer, basket)
)
@patch('ecommerce.extensions.offer.models.Condition.is_satisfied')
def test_check_condition_applicability_switch_active(self, mock_is_satisfied):
"""
Validate check_condition_applicability decorator returns False if the specified switch is active.
"""
mock_is_satisfied.return_value = True
mock_is_satisfied.__name__ = 'is_satisfied'
basket = create_basket(self.user, self.site)
switch = 'fake_switch'
Switch.objects.update_or_create(name=switch, defaults={'active': True})
self.assertTrue(
check_condition_applicability([switch])(self.condition.is_satisfied)(self.condition, self.offer, basket)
)
def test_check_condition_applicability_switch_inactive(self):
"""
Validate check_condition_applicability decorator returns False if the specified switch is inactive.
"""
basket = create_basket(self.user, self.site)
switch = 'fake_switch'
Switch.objects.update_or_create(name=switch, defaults={'active': False})
self.assertFalse(
check_condition_applicability([switch])(self.condition.is_satisfied)(self.condition, self.offer, basket)
)
import ddt
from django.template import Context, Template
from oscar.core.loading import get_model
from oscar.test.factories import BenefitFactory
from ecommerce.extensions.offer.templatetags.offer_tags import benefit_type
from ecommerce.programs.benefits import AbsoluteDiscountBenefitWithoutRange, PercentageDiscountBenefitWithoutRange
from ecommerce.programs.custom import class_path
from ecommerce.tests.testcases import TestCase
Benefit = get_model('offer', 'Benefit')
@ddt.data
class OfferTests(TestCase):
def test_benefit_discount(self):
benefit = BenefitFactory(type=Benefit.PERCENTAGE, value=35.00)
......@@ -15,3 +20,15 @@ class OfferTests(TestCase):
"{{ benefit|benefit_discount }}"
)
self.assertEqual(template.render(Context({'benefit': benefit})), '35%')
@ddt.data(
({'type': Benefit.PERCENTAGE}, Benefit.PERCENTAGE),
({'type': Benefit.FIXED}, Benefit.FIXED),
({'type': ''}, None),
({'type': '', 'proxy_class': class_path(PercentageDiscountBenefitWithoutRange)}, Benefit.PERCENTAGE),
({'type': '', 'proxy_class': class_path(AbsoluteDiscountBenefitWithoutRange)}, Benefit.FIXED),
)
@ddt.unpack
def test_benefit_type(self, factory_kwargs, expected):
benefit = BenefitFactory(**factory_kwargs)
self.assertEqual(benefit_type(benefit), expected)
......@@ -57,14 +57,8 @@ def format_benefit_value(benefit):
Returns:
benefit_value (str): String value containing formatted benefit value and type.
"""
# TODO: Find a better way to format benefit value so we can remove this import.
# Techdebt ticket: LEARNER-1317
# Import is here because of a circular dependency.
from ecommerce.programs.constants import BENEFIT_PROXY_CLASS_MAP
benefit_value = _remove_exponent_and_trailing_zeros(Decimal(str(benefit.value)))
benefit_type = benefit.type or BENEFIT_PROXY_CLASS_MAP[benefit.proxy_class]
benefit_type = benefit.type or getattr(benefit.proxy(), 'benefit_class_type', None)
if benefit_type == Benefit.PERCENTAGE:
benefit_value = _('{benefit_value}%'.format(benefit_value=benefit_value))
......
......@@ -21,7 +21,7 @@ Voucher = get_model('voucher', 'Voucher')
OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator')
def create_basket(owner=None, site=None, empty=False): # pylint:disable=function-redefined
def create_basket(owner=None, site=None, empty=False, price='10.00'): # pylint:disable=function-redefined
if site is None:
site = SiteConfigurationFactory().site
if owner is None:
......@@ -30,7 +30,7 @@ def create_basket(owner=None, site=None, empty=False): # pylint:disable=functio
basket.strategy = Default()
if not empty:
product = create_product()
create_stockrecord(product, num_in_stock=2, price_excl_tax=D('10.00'))
create_stockrecord(product, num_in_stock=2, price_excl_tax=D(price))
basket.add_product(product)
return basket
......
class BenefitTestMixin(object):
factory_class = None
name_format = ''
def setUp(self):
super(BenefitTestMixin, self).setUp()
self.benefit = self.factory_class() # pylint: disable=not-callable
def test_name(self):
self.assertEqual(self.benefit.name, self.name_format.format(value=self.benefit.value))
def test_description(self):
self.assertEqual(self.benefit.description, self.name_format.format(value=self.benefit.value))
......@@ -22,7 +22,7 @@ from ecommerce.extensions.api import exceptions
from ecommerce.extensions.offer.utils import get_discount_percentage, get_discount_value
from ecommerce.invoice.models import Invoice
from ecommerce.programs.conditions import ProgramCourseRunSeatsCondition
from ecommerce.programs.constants import BENEFIT_MAP, BENEFIT_PROXY_CLASS_MAP
from ecommerce.programs.constants import BENEFIT_MAP
from ecommerce.programs.custom import class_path, create_condition
logger = logging.getLogger(__name__)
......@@ -130,7 +130,7 @@ def _get_info_for_coupon_report(coupon, voucher):
discount_data = get_voucher_discount_info(benefit, seat_stockrecord.price_excl_tax)
coupon_type, discount_percentage, discount_amount = _get_discount_info(discount_data)
else:
benefit_type = benefit.type or BENEFIT_PROXY_CLASS_MAP[benefit.proxy_class]
benefit_type = benefit.type or getattr(benefit.proxy(), 'benefit_class_type', None)
if benefit_type == Benefit.PERCENTAGE:
coupon_type = _('Discount') if benefit.value < 100 else _('Enrollment')
......
import operator
from django.utils.translation import ugettext_lazy as _
from oscar.apps.offer.benefits import AbsoluteDiscountBenefit, PercentageDiscountBenefit
from oscar.core.loading import get_model
from ecommerce.extensions.offer.mixins import AbsoluteBenefitMixin, BenefitWithoutRangeMixin, PercentageBenefitMixin
class ConditionBasedApplicationMixin(object):
def get_applicable_lines(self, offer, basket, range=None): # pylint: disable=unused-argument,redefined-builtin
condition = offer.condition.proxy() or offer.condition
line_tuples = condition.get_applicable_lines(offer, basket, most_expensive_first=False)
# Do not allow multiple discounts per line
line_tuples = [line_tuple for line_tuple in line_tuples if line_tuple[1].quantity_without_discount > 0]
# We sort lines to be cheapest first to ensure consistent applications
return sorted(line_tuples, key=operator.itemgetter(0))
Benefit = get_model('offer', 'Benefit')
class PercentageDiscountBenefitWithoutRange(ConditionBasedApplicationMixin, PercentageDiscountBenefit):
class PercentageDiscountBenefitWithoutRange(BenefitWithoutRangeMixin, PercentageBenefitMixin,
PercentageDiscountBenefit):
""" PercentageDiscountBenefit without an attached range.
The range is only used for the name and description. We would prefer not
......@@ -33,7 +25,7 @@ class PercentageDiscountBenefitWithoutRange(ConditionBasedApplicationMixin, Perc
return _('{value}% program discount').format(value=self.value)
class AbsoluteDiscountBenefitWithoutRange(ConditionBasedApplicationMixin, AbsoluteDiscountBenefit):
class AbsoluteDiscountBenefitWithoutRange(BenefitWithoutRangeMixin, AbsoluteBenefitMixin, AbsoluteDiscountBenefit):
""" AbsoluteDiscountBenefit without an attached range.
The range is only used for the name and description. We would prefer not
......
......@@ -11,13 +11,15 @@ from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import HttpNotFoundError, SlumberBaseException
from ecommerce.core.utils import get_cache_key
from ecommerce.extensions.offer.decorators import check_condition_applicability
from ecommerce.extensions.offer.mixins import SingleItemConsumptionConditionMixin
from ecommerce.programs.utils import get_program
Condition = get_model('offer', 'Condition')
logger = logging.getLogger(__name__)
class ProgramCourseRunSeatsCondition(Condition):
class ProgramCourseRunSeatsCondition(SingleItemConsumptionConditionMixin, Condition):
class Meta(object):
app_label = 'programs'
proxy = True
......@@ -41,6 +43,7 @@ class ProgramCourseRunSeatsCondition(Condition):
return program_course_run_skus
@check_condition_applicability()
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument
"""
Determines if a user is eligible for a program offer based on products in their basket
......@@ -53,12 +56,6 @@ class ProgramCourseRunSeatsCondition(Condition):
bool
"""
if basket.is_empty:
return False
if basket.total_incl_tax == 0:
return False
basket_skus = set([line.stockrecord.partner_sku for line in basket.all_lines()])
try:
program = get_program(self.program_uuid, basket.site.siteconfiguration)
......@@ -141,18 +138,3 @@ class ProgramCourseRunSeatsCondition(Condition):
line_tuples.append((price, line))
return sorted(line_tuples, reverse=most_expensive_first, key=operator.itemgetter(0))
def consume_items(self, offer, basket, affected_lines): # pylint: disable=unused-argument
""" Marks items within the basket lines as consumed so they can't be reused in other offers.
This offer will consume only 1 unit of quantity for each affected line.
Args:
offer (AbstractConditionalOffer)
basket (AbstractBasket)
affected_lines (tuple[]): The lines that have been affected by the discount.
This should be list of tuples (line, discount, qty)
"""
for line, __, __ in affected_lines:
quantity_to_consume = min(line.quantity_without_discount, 1)
line.consume(quantity_to_consume)
import six
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model
from ecommerce.programs.benefits import AbsoluteDiscountBenefitWithoutRange, PercentageDiscountBenefitWithoutRange
from ecommerce.programs.custom import class_path
Benefit = get_model('offer', 'Benefit')
......@@ -11,9 +9,6 @@ BENEFIT_MAP = {
Benefit.FIXED: AbsoluteDiscountBenefitWithoutRange,
Benefit.PERCENTAGE: PercentageDiscountBenefitWithoutRange,
}
BENEFIT_PROXY_CLASS_MAP = dict(
(class_path(proxy_class), benefit_type) for benefit_type, proxy_class in six.iteritems(BENEFIT_MAP)
)
BENEFIT_TYPE_CHOICES = (
(Benefit.PERCENTAGE, _('Percentage')),
(Benefit.FIXED, _('Absolute')),
......
......@@ -6,7 +6,7 @@ from oscar.core.loading import get_model
from ecommerce.programs.api import ProgramsApiClient
from ecommerce.programs.conditions import ProgramCourseRunSeatsCondition
from ecommerce.programs.constants import BENEFIT_MAP, BENEFIT_PROXY_CLASS_MAP, BENEFIT_TYPE_CHOICES
from ecommerce.programs.constants import BENEFIT_MAP, BENEFIT_TYPE_CHOICES
from ecommerce.programs.custom import class_path, create_condition
Benefit = get_model('offer', 'Benefit')
......@@ -40,7 +40,7 @@ class ProgramOfferForm(forms.ModelForm):
if instance:
initial.update({
'program_uuid': instance.condition.program_uuid,
'benefit_type': BENEFIT_PROXY_CLASS_MAP[instance.benefit.proxy_class],
'benefit_type': instance.benefit.proxy().benefit_class_type,
'benefit_value': instance.benefit.value,
})
super(ProgramOfferForm, self).__init__(data, files, auto_id, prefix, initial, error_class, label_suffix,
......
{% extends 'edx/base.html' %}
{% load i18n %}
{% load programs %}
{% load offer_tags %}
{% load staticfiles %}
{% block title %}{% trans "Program Offers" %}{% endblock %}
......
from django import template
from ecommerce.programs.constants import BENEFIT_PROXY_CLASS_MAP
register = template.Library()
@register.filter
def benefit_type(benefit):
_type = benefit.type
if not _type:
_type = BENEFIT_PROXY_CLASS_MAP[benefit.proxy_class]
return _type
from ecommerce.extensions.test import factories
from ecommerce.extensions.test import factories, mixins
from ecommerce.tests.testcases import TestCase
class BenefitTestMixin(object):
factory_class = None
name_format = ''
def setUp(self):
super(BenefitTestMixin, self).setUp()
self.benefit = self.factory_class() # pylint: disable=not-callable
def test_name(self):
self.assertEqual(self.benefit.name, self.name_format.format(value=self.benefit.value))
def test_description(self):
self.assertEqual(self.benefit.description, self.name_format.format(value=self.benefit.value))
class AbsoluteDiscountBenefitWithoutRangeTests(BenefitTestMixin, TestCase):
class AbsoluteDiscountBenefitWithoutRangeTests(mixins.BenefitTestMixin, TestCase):
factory_class = factories.AbsoluteDiscountBenefitWithoutRangeFactory
name_format = '{value} fixed-price program discount'
class PercentageDiscountBenefitWithoutRangeTests(BenefitTestMixin, TestCase):
class PercentageDiscountBenefitWithoutRangeTests(mixins.BenefitTestMixin, TestCase):
factory_class = factories.PercentageDiscountBenefitWithoutRangeFactory
name_format = '{value}% program discount'
......@@ -8,7 +8,7 @@ from slumber.exceptions import HttpNotFoundError, SlumberBaseException
from ecommerce.courses.models import Course
from ecommerce.extensions.test import factories
from ecommerce.programs.tests.mixins import ProgramTestMixin
from ecommerce.tests.factories import ProductFactory
from ecommerce.tests.factories import ProductFactory, SiteConfigurationFactory
from ecommerce.tests.testcases import TestCase
Product = get_model('catalogue', 'Product')
......@@ -33,7 +33,7 @@ class ProgramCourseRunSeatsConditionTests(ProgramTestMixin, TestCase):
def test_is_satisfied_no_enrollments(self):
""" The method should return True if the basket contains one course run seat corresponding to each
course in the program. """
offer = factories.ProgramOfferFactory(condition=self.condition)
offer = factories.ProgramOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=factories.UserFactory())
program = self.mock_program_detail_endpoint(
self.condition.program_uuid, self.site_configuration.discovery_api_url
......@@ -85,7 +85,7 @@ class ProgramCourseRunSeatsConditionTests(ProgramTestMixin, TestCase):
def test_is_satisfied_with_enrollments(self):
""" The condition should be satisfied if one valid course run from each course is in either the
basket or the user's enrolled courses and the site has enabled partial program offers. """
offer = factories.ProgramOfferFactory(condition=self.condition)
offer = factories.ProgramOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=factories.UserFactory())
program = self.mock_program_detail_endpoint(
self.condition.program_uuid, self.site_configuration.discovery_api_url
......@@ -129,7 +129,7 @@ class ProgramCourseRunSeatsConditionTests(ProgramTestMixin, TestCase):
@ddt.data(HttpNotFoundError, SlumberBaseException, Timeout)
def test_is_satisfied_with_exception_for_programs(self, value):
""" The method should return False if there is an exception when trying to get program details. """
offer = factories.ProgramOfferFactory(condition=self.condition)
offer = factories.ProgramOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=factories.UserFactory())
basket.add_product(self.test_product)
......@@ -141,7 +141,7 @@ class ProgramCourseRunSeatsConditionTests(ProgramTestMixin, TestCase):
def test_is_satisfied_with_exception_for_enrollments(self):
""" The method should return True despite having an error at the enrollment check, given 1 course run seat
corresponding to each course in the program. """
offer = factories.ProgramOfferFactory(condition=self.condition)
offer = factories.ProgramOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=factories.UserFactory())
program = self.mock_program_detail_endpoint(
self.condition.program_uuid,
......@@ -159,16 +159,23 @@ class ProgramCourseRunSeatsConditionTests(ProgramTestMixin, TestCase):
def test_is_satisfied_free_basket(self):
""" Ensure the basket returns False if the basket total is zero. """
offer = factories.ProgramOfferFactory(condition=self.condition)
offer = factories.ProgramOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=factories.UserFactory())
test_product = factories.ProductFactory(stockrecords__price_excl_tax=0,
stockrecords__partner__short_code='test')
basket.add_product(test_product)
self.assertFalse(self.condition.is_satisfied(offer, basket))
def test_is_satisfied_site_mismatch(self):
""" Ensure the condition returns False if the offer site does not match the basket site. """
offer = factories.ProgramOfferFactory(site=SiteConfigurationFactory().site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=factories.UserFactory())
basket.add_product(self.test_product)
self.assertFalse(self.condition.is_satisfied(offer, basket))
def test_is_satisfied_program_retrieval_failure(self):
""" The method should return False if no program is retrieved """
offer = factories.ProgramOfferFactory(condition=self.condition)
offer = factories.ProgramOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=factories.UserFactory())
basket.add_product(self.test_product)
self.condition.program_uuid = None
......
......@@ -5,7 +5,7 @@ import httpretty
from oscar.core.loading import get_model
from ecommerce.extensions.test import factories
from ecommerce.programs.constants import BENEFIT_MAP, BENEFIT_PROXY_CLASS_MAP
from ecommerce.programs.constants import BENEFIT_MAP
from ecommerce.programs.custom import class_path
from ecommerce.programs.forms import ProgramOfferForm
from ecommerce.programs.tests.mixins import ProgramTestMixin
......@@ -48,7 +48,7 @@ class ProgramOfferFormTests(ProgramTestMixin, TestCase):
program_offer = factories.ProgramOfferFactory()
form = ProgramOfferForm(instance=program_offer)
self.assertEqual(form['program_uuid'].value(), program_offer.condition.program_uuid.hex)
self.assertEqual(form['benefit_type'].value(), BENEFIT_PROXY_CLASS_MAP[program_offer.benefit.proxy_class])
self.assertEqual(form['benefit_type'].value(), program_offer.benefit.proxy().benefit_class_type)
self.assertEqual(form['benefit_value'].value(), program_offer.benefit.value)
def test_clean_percentage(self):
......
......@@ -18,7 +18,10 @@ class ProgramOfferTests(ProgramTestMixin, TestCase):
@httpretty.activate
def test_offer(self):
# Our offer is for 100%, so all lines should end up with a price of 0.
offer = factories.ProgramOfferFactory(benefit=factories.PercentageDiscountBenefitWithoutRangeFactory(value=100))
offer = factories.ProgramOfferFactory(
site=self.site,
benefit=factories.PercentageDiscountBenefitWithoutRangeFactory(value=100)
)
basket = factories.BasketFactory(site=self.site, owner=self.create_user())
program_uuid = offer.condition.program_uuid
......
import ddt
from oscar.core.loading import get_model
from oscar.test.factories import BenefitFactory
from ecommerce.programs.benefits import AbsoluteDiscountBenefitWithoutRange, PercentageDiscountBenefitWithoutRange
from ecommerce.programs.custom import class_path
from ecommerce.programs.templatetags.programs import benefit_type
from ecommerce.tests.testcases import TestCase
Benefit = get_model('offer', 'Benefit')
@ddt.ddt
class ProgramOfferTemplateTagTests(TestCase):
@ddt.data(
({'type': Benefit.PERCENTAGE}, Benefit.PERCENTAGE),
({'type': Benefit.FIXED}, Benefit.FIXED),
({'type': '', 'proxy_class': class_path(PercentageDiscountBenefitWithoutRange)}, Benefit.PERCENTAGE),
({'type': '', 'proxy_class': class_path(AbsoluteDiscountBenefitWithoutRange)}, Benefit.FIXED),
)
@ddt.unpack
def test_benefit_type(self, factory_kwargs, expected):
benefit = BenefitFactory(**factory_kwargs)
self.assertEqual(benefit_type(benefit), expected)
......@@ -7,7 +7,6 @@ from oscar.core.loading import get_model
from ecommerce.extensions.test import factories
from ecommerce.programs.benefits import PercentageDiscountBenefitWithoutRange
from ecommerce.programs.constants import BENEFIT_PROXY_CLASS_MAP
from ecommerce.programs.custom import class_path
from ecommerce.programs.tests.mixins import ProgramTestMixin
from ecommerce.tests.testcases import CacheMixin, TestCase
......@@ -137,7 +136,7 @@ class ProgramOfferUpdateViewTests(ProgramTestMixin, ViewTestMixin, TestCase):
""" The program offer should be updated. """
data = {
'program_uuid': self.program_offer.condition.program_uuid,
'benefit_type': BENEFIT_PROXY_CLASS_MAP[self.program_offer.benefit.proxy_class],
'benefit_type': self.program_offer.benefit.proxy().benefit_class_type,
'benefit_value': self.program_offer.benefit.value,
}
response = self.client.post(self.path, data, follow=False)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment