Commit 8779027b by Douglas Hall Committed by Douglas Hall

Add Enterprise offers backend.

This commit adds the business logic implementation of Enterprise
offers. An Enterprise offer consists of a custom Condition model
which checks that the user is linked to the specified EnterpriseCustomer,
the course run(s) exist in that EnterpriseCustomer's catalogs, and the
user has granted consent to share data for the course run(s).

ENT-638
parent 7197a198
"""
Methods for fetching enterprise API data.
"""
import logging
from urllib import urlencode
from django.conf import settings
from django.core.cache import cache
from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberHttpBaseException
from ecommerce.core.utils import get_cache_key
logger = logging.getLogger(__name__)
def fetch_enterprise_learner_entitlements(site, learner_id):
"""
Fetch enterprise learner entitlements along-with data sharing consent requirement.
......@@ -155,3 +163,44 @@ def fetch_enterprise_learner_data(site, user):
cache.set(cache_key, response, settings.ENTERPRISE_API_CACHE_TIMEOUT)
return response
def catalog_contains_course_runs(site, course_run_ids, enterprise_customer_uuid, enterprise_customer_catalog_uuid=None):
"""
Determine if course runs are associated with the EnterpriseCustomer.
"""
query_params = {'course_run_ids': course_run_ids}
api_resource_name = 'enterprise-customer'
api_resource_id = enterprise_customer_uuid
if enterprise_customer_catalog_uuid:
api_resource_name = 'enterprise_catalogs'
api_resource_id = enterprise_customer_catalog_uuid
cache_key = get_cache_key(
site_domain=site.domain,
resource='{resource}-{resource_id}-contains_content_items'.format(
resource=api_resource_name,
resource_id=api_resource_id,
),
query_params=urlencode(query_params, True)
)
contains_content = cache.get(cache_key)
if contains_content is None:
api = site.siteconfiguration.enterprise_api_client
endpoint = getattr(api, api_resource_name)(api_resource_id)
try:
contains_content = endpoint.contains_content_items.get(**query_params)['contains_content_items']
cache.set(cache_key, contains_content, settings.ENTERPRISE_API_CACHE_TIMEOUT)
except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout):
logger.exception(
'Failed to check if course_runs [%s] exist in '
'EnterpriseCustomerCatalog [%s]'
'for EnterpriseCustomer [%s].',
course_run_ids,
enterprise_customer_catalog_uuid,
enterprise_customer_uuid,
)
contains_content = False
return contains_content
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
Benefit = get_model('offer', 'Benefit')
class EnterprisePercentageDiscountBenefit(BenefitWithoutRangeMixin, PercentageBenefitMixin, PercentageDiscountBenefit):
""" Enterprise-related PercentageDiscountBenefit without an attached range. """
class Meta(object):
app_label = 'enterprise'
proxy = True
@property
def name(self):
return _('{value}% enterprise discount').format(value=self.value)
class EnterpriseAbsoluteDiscountBenefit(BenefitWithoutRangeMixin, AbsoluteBenefitMixin, AbsoluteDiscountBenefit):
""" Enterprise-related AbsoluteDiscountBenefit without an attached range. """
class Meta(object):
app_label = 'enterprise'
proxy = True
@property
def name(self):
return _('{value} fixed-price enterprise discount').format(value=self.value)
from __future__ import unicode_literals
import logging
from oscar.core.loading import get_model
from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberHttpBaseException
from ecommerce.enterprise.api import catalog_contains_course_runs, fetch_enterprise_learner_data
from ecommerce.enterprise.constants import ENTERPRISE_OFFERS_SWITCH
from ecommerce.extensions.offer.decorators import check_condition_applicability
from ecommerce.extensions.offer.mixins import ConditionWithoutRangeMixin, SingleItemConsumptionConditionMixin
Condition = get_model('offer', 'Condition')
logger = logging.getLogger(__name__)
class EnterpriseCustomerCondition(ConditionWithoutRangeMixin, SingleItemConsumptionConditionMixin, Condition):
class Meta(object):
app_label = 'enterprise'
proxy = True
@property
def name(self):
return "Basket contains a seat from {}'s catalog".format(self.enterprise_customer_name)
def _get_course_runs_with_consent(self, data_sharing_consent_records):
"""
Return the course run IDs for which the learner has consented to share data.
Arguments:
data_sharing_consent_records (list of dict): The learner's existing data sharing consent records.
Returns:
list of strings: The list of course run IDs for which the learner has given data sharing consent.
"""
return [record['course_id'] for record in data_sharing_consent_records if record['consent_provided']]
@check_condition_applicability([ENTERPRISE_OFFERS_SWITCH])
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument
"""
Determines if a user is eligible for an enterprise customer offer
based on their association with the enterprise customer and whether
or not they have consented to sharing data with the enterprise customer.
Args:
basket (Basket): Contains information about order line items, the current site,
and the user attempting to make the purchase.
Returns:
bool
"""
try:
learner_data = fetch_enterprise_learner_data(basket.site, basket.owner)['results'][0]
except (ConnectionError, KeyError, SlumberHttpBaseException, Timeout):
logger.exception(
'Failed to retrieve enterprise learner data for site [%s] and user [%s].',
basket.site.domain,
basket.owner.username,
)
return False
except IndexError:
# Learner is not linked to any EnterpriseCustomer.
return False
enterprise_customer = learner_data['enterprise_customer']
if str(self.enterprise_customer_uuid) != enterprise_customer['uuid']:
# Learner is not linked to the EnterpriseCustomer associated with this condition.
return False
course_runs_with_consent = self._get_course_runs_with_consent(learner_data['data_sharing_consent_records'])
course_run_ids = []
for line in basket.all_lines():
course = line.product.course
if not course:
# Basket contains products not related to a course_run.
return False
if enterprise_customer['enable_data_sharing_consent'] and course.id not in course_runs_with_consent:
# Basket contains course_runs for which the learner has not given consent to share data.
return False
course_run_ids.append(course.id)
if not catalog_contains_course_runs(basket.site, course_run_ids, self.enterprise_customer_uuid,
enterprise_customer_catalog_uuid=self.enterprise_customer_catalog_uuid):
# Basket contains course runs that do not exist in the EnterpriseCustomerCatalogs
# associated with the EnterpriseCustomer.
return False
return True
# Waffle switch used to enable/disable Enterprise offers.
ENTERPRISE_OFFERS_SWITCH = 'enable_enterprise_offers'
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-04 14:30
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = [
('offer', '0012_condition_program_uuid'),
]
operations = [
migrations.CreateModel(
name='EnterpriseAbsoluteDiscountBenefit',
fields=[
],
options={
'proxy': True,
},
bases=('offer.absolutediscountbenefit',),
),
migrations.CreateModel(
name='EnterprisePercentageDiscountBenefit',
fields=[
],
options={
'proxy': True,
},
bases=('offer.percentagediscountbenefit',),
),
migrations.CreateModel(
name='EnterpriseCustomerCondition',
fields=[
],
options={
'proxy': True,
},
bases=('offer.condition',),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from ecommerce.enterprise.constants import ENTERPRISE_OFFERS_SWITCH
def create_switch(apps, schema_editor):
"""Create the `enable_enterprise_offers` switch if it does not already exist."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.update_or_create(name=ENTERPRISE_OFFERS_SWITCH, defaults={'active': False})
def delete_switch(apps, schema_editor):
"""Delete the `enable_enterprise_offers` switch."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.filter(name=ENTERPRISE_OFFERS_SWITCH).delete()
class Migration(migrations.Migration):
dependencies = [
('enterprise', '0001_initial'),
]
operations = [
migrations.RunPython(create_switch, delete_switch)
]
import copy
import json
from urllib import urlencode
from uuid import uuid4
import httpretty
import requests
from django.conf import settings
def raise_timeout(request, uri, headers): # pylint: disable=unused-argument
raise requests.Timeout('Connection timed out.')
class EnterpriseServiceMockMixin(object):
"""
Mocks for the Open edX service 'Enterprise Service' responses.
......@@ -128,7 +134,8 @@ class EnterpriseServiceMockMixin(object):
learner_id=1,
enterprise_customer_uuid='cf246b88-d5f6-4908-a522-fc307e0b0c59',
consent_enabled=True,
consent_provided=True
consent_provided=True,
course_run_id='course-v1:edX DemoX Demo_Course'
):
"""
Helper function to register enterprise learner API endpoint.
......@@ -179,7 +186,7 @@ class EnterpriseServiceMockMixin(object):
"exists": True,
"consent_provided": consent_provided,
"consent_required": consent_enabled and not consent_provided,
"course_id": "course-v1:edX DemoX Demo_Course",
"course_id": course_run_id,
}
]
}
......@@ -324,6 +331,17 @@ class EnterpriseServiceMockMixin(object):
content_type='application/json'
)
def mock_enterprise_learner_api_raise_exception(self):
"""
Helper function to register enterprise learner API endpoint and raise an exception.
"""
self.mock_access_token_response()
httpretty.register_uri(
method=httpretty.GET,
uri=self.ENTERPRISE_LEARNER_URL,
body=raise_timeout
)
def mock_enterprise_learner_api_for_failure(self):
"""
Helper function to register enterprise learner API endpoint for a
......@@ -468,3 +486,31 @@ class EnterpriseServiceMockMixin(object):
granted=False,
required=False,
)
def mock_catalog_contains_course_runs(self, course_run_ids, enterprise_customer_uuid,
enterprise_customer_catalog_uuid=None, contains_content=True,
raise_exception=False):
self.mock_access_token_response()
query_params = urlencode({'course_run_ids': course_run_ids}, True)
body = raise_timeout if raise_exception else json.dumps({'contains_content_items': contains_content})
httpretty.register_uri(
method=httpretty.GET,
uri='{}enterprise-customer/{}/contains_content_items/?{}'.format(
self.site.siteconfiguration.enterprise_api_url,
enterprise_customer_uuid,
query_params
),
body=body,
content_type='application/json'
)
if enterprise_customer_catalog_uuid:
httpretty.register_uri(
method=httpretty.GET,
uri='{}enterprise_catalogs/{}/contains_content_items/?{}'.format(
self.site.siteconfiguration.enterprise_api_url,
enterprise_customer_catalog_uuid,
query_params
),
body=body,
content_type='application/json'
)
import ddt
import httpretty
from django.conf import settings
from django.core.cache import cache
......@@ -5,6 +6,7 @@ from oscar.core.loading import get_model
from ecommerce.core.tests import toggle_switch
from ecommerce.core.utils import get_cache_key
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.enterprise import api as enterprise_api
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.extensions.partner.strategy import DefaultStrategy
......@@ -14,10 +16,12 @@ Catalog = get_model('catalogue', 'Catalog')
StockRecord = get_model('partner', 'StockRecord')
@ddt.ddt
@httpretty.activate
class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase):
def setUp(self):
super(EnterpriseAPITests, self).setUp()
self.course_run = CourseFactory()
self.learner = self.create_user(is_staff=True)
self.client.login(username=self.learner.username, password=self.password)
......@@ -59,6 +63,20 @@ class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase):
cached_course = cache.get(cache_key)
self.assertEqual(cached_course, response)
def _assert_contains_course_runs(self, expected, course_run_ids, enterprise_customer_uuid,
enterprise_customer_catalog_uuid):
"""
Helper method to validate the response from the method `catalog_contains_course_runs`.
"""
actual = enterprise_api.catalog_contains_course_runs(
self.site,
course_run_ids,
enterprise_customer_uuid,
enterprise_customer_catalog_uuid=enterprise_customer_catalog_uuid,
)
self.assertEqual(expected, actual)
def test_fetch_enterprise_learner_data(self):
"""
Verify that method "fetch_enterprise_learner_data" returns a proper
......@@ -106,3 +124,38 @@ class EnterpriseAPITests(EnterpriseServiceMockMixin, TestCase):
# the cache
enterprise_api.fetch_enterprise_learner_entitlements(self.request.site, enterprise_learner_id)
self._assert_num_requests(expected_number_of_requests)
@ddt.data(
(True, None),
(True, 'fake-uuid'),
(False, None),
(False, 'fake-uuid'),
)
@ddt.unpack
def test_catalog_contains_course_runs(self, expected, enterprise_customer_catalog_uuid):
"""
Verify that method `catalog_contains_course_runs` returns the appropriate response.
"""
self.mock_catalog_contains_course_runs(
[self.course_run.id],
'fake-uuid',
enterprise_customer_catalog_uuid=enterprise_customer_catalog_uuid,
contains_content=expected,
)
self._assert_contains_course_runs(expected, [self.course_run.id], 'fake-uuid', enterprise_customer_catalog_uuid)
def test_catalog_contains_course_runs_with_api_exception(self):
"""
Verify that method `catalog_contains_course_runs` returns the appropriate response
when the Enterprise API cannot be reached.
"""
self.mock_catalog_contains_course_runs(
[self.course_run.id],
'fake-uuid',
enterprise_customer_catalog_uuid='fake-uuid',
contains_content=False,
raise_exception=True,
)
self._assert_contains_course_runs(False, [self.course_run.id], 'fake-uuid', 'fake-uuid')
from ecommerce.extensions.test import factories, mixins
from ecommerce.tests.testcases import TestCase
class EnterpriseAbsoluteDiscountBenefitTests(mixins.BenefitTestMixin, TestCase):
factory_class = factories.EnterpriseAbsoluteDiscountBenefitFactory
name_format = '{value} fixed-price enterprise discount'
class EnterprisePercentageDiscountBenefitTests(mixins.BenefitTestMixin, TestCase):
factory_class = factories.EnterprisePercentageDiscountBenefitFactory
name_format = '{value}% enterprise discount'
from decimal import Decimal
import httpretty
from oscar.core.loading import get_model
from waffle.models import Switch
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.enterprise.constants import ENTERPRISE_OFFERS_SWITCH
from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin
from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin
from ecommerce.extensions.test import factories
from ecommerce.tests.factories import ProductFactory, SiteConfigurationFactory
from ecommerce.tests.testcases import TestCase
Product = get_model('catalogue', 'Product')
LOGGER_NAME = 'ecommerce.programs.conditions'
class EnterpriseCustomerConditionTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, TestCase):
def setUp(self):
super(EnterpriseCustomerConditionTests, self).setUp()
Switch.objects.update_or_create(name=ENTERPRISE_OFFERS_SWITCH, defaults={'active': True})
self.user = factories.UserFactory()
self.condition = factories.EnterpriseCustomerConditionFactory()
self.test_product = ProductFactory(stockrecords__price_excl_tax=10)
self.course_run = CourseFactory()
self.course_run.create_or_update_seat('verified', True, Decimal(100), self.partner)
def test_name(self):
""" The name should contain the EnterpriseCustomer's name. """
condition = factories.EnterpriseCustomerConditionFactory()
expected = "Basket contains a seat from {}'s catalog".format(condition.enterprise_customer_name)
self.assertEqual(condition.name, expected)
@httpretty.activate
def test_is_satisfied_true(self):
""" Ensure the condition returns true if all basket requirements are met. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.course_run.seat_products[0])
self.mock_enterprise_learner_api(
learner_id=self.user.id,
enterprise_customer_uuid=str(self.condition.enterprise_customer_uuid),
course_run_id=self.course_run.id,
)
self.mock_catalog_contains_course_runs(
[self.course_run.id],
self.condition.enterprise_customer_uuid,
enterprise_customer_catalog_uuid=self.condition.enterprise_customer_catalog_uuid,
)
self.assertTrue(self.condition.is_satisfied(offer, basket))
def test_is_satisfied_empty_basket(self):
""" Ensure the condition returns False if the basket is empty. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
self.assertTrue(basket.is_empty)
self.assertFalse(self.condition.is_satisfied(offer, basket))
def test_is_satisfied_free_basket(self):
""" Ensure the condition returns False if the basket total is zero. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
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.EnterpriseOfferFactory(site=SiteConfigurationFactory().site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.test_product)
self.assertFalse(self.condition.is_satisfied(offer, basket))
@httpretty.activate
def test_is_satisfied_enterprise_learner_error(self):
""" Ensure the condition returns false if the enterprise learner data cannot be retrieved. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.course_run.seat_products[0])
self.mock_enterprise_learner_api_raise_exception()
self.assertFalse(self.condition.is_satisfied(offer, basket))
@httpretty.activate
def test_is_satisfied_no_enterprise_learner(self):
""" Ensure the condition returns false if the learner is not linked to an EnterpriseCustomer. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.course_run.seat_products[0])
self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
self.assertFalse(self.condition.is_satisfied(offer, basket))
@httpretty.activate
def test_is_satisfied_wrong_enterprise(self):
""" Ensure the condition returns false if the learner is associated with a different EnterpriseCustomer. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.course_run.seat_products[0])
self.mock_enterprise_learner_api(
learner_id=self.user.id,
course_run_id=self.course_run.id,
)
self.assertFalse(self.condition.is_satisfied(offer, basket))
@httpretty.activate
def test_is_satisfied_no_course_product(self):
""" Ensure the condition returns false if the basket contains a product not associated with a course run. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.test_product)
self.mock_enterprise_learner_api(
learner_id=self.user.id,
enterprise_customer_uuid=str(self.condition.enterprise_customer_uuid),
course_run_id=self.course_run.id,
)
self.assertFalse(self.condition.is_satisfied(offer, basket))
@httpretty.activate
def test_is_satisfied_consent_not_granted(self):
""" Ensure the condition returns false if consent has not been granted for the given course run. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.course_run.seat_products[0])
self.mock_enterprise_learner_api(
learner_id=self.user.id,
enterprise_customer_uuid=str(self.condition.enterprise_customer_uuid),
)
self.assertFalse(self.condition.is_satisfied(offer, basket))
@httpretty.activate
def test_is_satisfied_course_run_not_in_catalog(self):
""" Ensure the condition returns false if the course run is not in the Enterprise catalog. """
offer = factories.EnterpriseOfferFactory(site=self.site, condition=self.condition)
basket = factories.BasketFactory(site=self.site, owner=self.user)
basket.add_product(self.course_run.seat_products[0])
self.mock_enterprise_learner_api(
learner_id=self.user.id,
enterprise_customer_uuid=str(self.condition.enterprise_customer_uuid),
course_run_id=self.course_run.id,
)
self.mock_catalog_contains_course_runs(
[self.course_run.id],
self.condition.enterprise_customer_uuid,
enterprise_customer_catalog_uuid=self.condition.enterprise_customer_catalog_uuid,
contains_content=False
)
self.assertFalse(self.condition.is_satisfied(offer, basket))
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-09-26 17:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('offer', '0014_conditionaloffer_site'),
]
operations = [
migrations.AddField(
model_name='condition',
name='enterprise_customer_catalog_uuid',
field=models.UUIDField(blank=True, null=True, verbose_name='EnterpriseCustomerCatalog UUID'),
),
migrations.AddField(
model_name='condition',
name='enterprise_customer_name',
field=models.CharField(max_length=255, blank=True, null=True, verbose_name='EnterpriseCustomer Name'),
),
migrations.AddField(
model_name='condition',
name='enterprise_customer_uuid',
field=models.UUIDField(blank=True, null=True, verbose_name='EnterpriseCustomer UUID'),
),
]
......@@ -23,6 +23,22 @@ class BenefitWithoutRangeMixin(object):
return sorted(line_tuples, key=operator.itemgetter(0))
class ConditionWithoutRangeMixin(object):
""" Mixin for Conditions 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 can_apply_condition(self, line):
"""
Determines whether the condition can be applied to a given basket line.
"""
if not line.stockrecord_id:
return False
return line.product.get_is_discountable()
class AbsoluteBenefitMixin(object):
""" Mixin for fixed-amount Benefits. """
benefit_class_type = Benefit.FIXED
......
......@@ -324,6 +324,23 @@ class Range(AbstractRange):
class Condition(AbstractCondition):
enterprise_customer_uuid = models.UUIDField(
null=True,
blank=True,
verbose_name=_('EnterpriseCustomer UUID')
)
# De-normalizing the EnterpriseCustomer name for optimization purposes.
enterprise_customer_name = models.CharField(
max_length=255,
null=True,
blank=True,
verbose_name=_('EnterpriseCustomer Name')
)
enterprise_customer_catalog_uuid = models.UUIDField(
null=True,
blank=True,
verbose_name=_('EnterpriseCustomerCatalog UUID')
)
program_uuid = models.UUIDField(null=True, blank=True, verbose_name=_('Program UUID'))
......
......@@ -3,6 +3,7 @@ from django.template import Context, Template
from oscar.core.loading import get_model
from oscar.test.factories import BenefitFactory
from ecommerce.enterprise.benefits import EnterpriseAbsoluteDiscountBenefit, EnterprisePercentageDiscountBenefit
from ecommerce.extensions.offer.templatetags.offer_tags import benefit_type
from ecommerce.programs.benefits import AbsoluteDiscountBenefitWithoutRange, PercentageDiscountBenefitWithoutRange
from ecommerce.programs.custom import class_path
......@@ -27,6 +28,8 @@ class OfferTests(TestCase):
({'type': ''}, None),
({'type': '', 'proxy_class': class_path(PercentageDiscountBenefitWithoutRange)}, Benefit.PERCENTAGE),
({'type': '', 'proxy_class': class_path(AbsoluteDiscountBenefitWithoutRange)}, Benefit.FIXED),
({'type': '', 'proxy_class': class_path(EnterprisePercentageDiscountBenefit)}, Benefit.PERCENTAGE),
({'type': '', 'proxy_class': class_path(EnterpriseAbsoluteDiscountBenefit)}, Benefit.FIXED),
)
@ddt.unpack
def test_benefit_type(self, factory_kwargs, expected):
......
......@@ -7,6 +7,8 @@ from oscar.test.factories import ConditionalOfferFactory as BaseConditionalOffer
from oscar.test.factories import VoucherFactory as BaseVoucherFactory
from oscar.test.factories import * # pylint:disable=wildcard-import,unused-wildcard-import
from ecommerce.enterprise.benefits import EnterpriseAbsoluteDiscountBenefit, EnterprisePercentageDiscountBenefit
from ecommerce.enterprise.conditions import EnterpriseCustomerCondition
from ecommerce.programs.benefits import AbsoluteDiscountBenefitWithoutRange, PercentageDiscountBenefitWithoutRange
from ecommerce.programs.conditions import ProgramCourseRunSeatsCondition
from ecommerce.programs.custom import class_path
......@@ -156,3 +158,39 @@ class ProgramOfferFactory(ConditionalOfferFactory):
max_basket_applications = 1
offer_type = ConditionalOffer.SITE
status = ConditionalOffer.OPEN
class EnterpriseAbsoluteDiscountBenefitFactory(BenefitFactory):
range = None
type = ''
value = 10
proxy_class = class_path(EnterpriseAbsoluteDiscountBenefit)
class EnterprisePercentageDiscountBenefitFactory(BenefitFactory):
range = None
type = ''
value = 10
proxy_class = class_path(EnterprisePercentageDiscountBenefit)
class EnterpriseCustomerConditionFactory(ConditionFactory):
range = None
type = ''
value = None
enterprise_customer_uuid = factory.LazyFunction(uuid.uuid4)
enterprise_customer_name = factory.Faker('word')
enterprise_customer_catalog_uuid = factory.LazyFunction(uuid.uuid4)
proxy_class = class_path(EnterpriseCustomerCondition)
class Meta(object):
model = EnterpriseCustomerCondition
class EnterpriseOfferFactory(ConditionalOfferFactory):
benefit = factory.SubFactory(EnterprisePercentageDiscountBenefitFactory)
condition = factory.SubFactory(EnterpriseCustomerConditionFactory)
max_basket_applications = 1
offer_type = ConditionalOffer.SITE
priority = 10
status = ConditionalOffer.OPEN
......@@ -170,7 +170,7 @@
padding-top: 20px;
margin-bottom: 1.25rem;
p.voucher {
p.offer, p.voucher {
@include float(left);
margin: 0 20px;
......
{% load i18n %}
{% load currency_filters %}
{% load crispy_forms_tags %}
{% load offer_tags %}
{% load widget_tweaks %}
{% load staticfiles %}
......@@ -52,6 +53,21 @@
<span class="price">-{{basket.total_discount|currency:basket.currency}}</span>
</div>
{% endif %}
<div id="offer-information" class="row">
{% block offers %}
<div class="offers">
{% for offer_id, offer in basket.applied_offers.items %}
{% if offer.condition.enterprise_customer_name %}
<p class="offer">
{% blocktrans with enterprise_customer_name=offer.condition.enterprise_customer_name benefit=offer.benefit|benefit_discount %}
{{ benefit }} discount provided by {{ enterprise_customer_name }}.
{% endblocktrans %}
</p>
{% endif %}
{% endfor %}
</div>
{% endblock offers %}
</div>
<div id="voucher-information" class="row">
{% if show_voucher_form %}
{% block vouchers %}
......
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