Commit 75531b2f by Marko Jevtic

[LEARNER-437] Reflect discount on the Program About Page (WL)

parent 5a919f2c
...@@ -130,7 +130,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest ...@@ -130,7 +130,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
# Configure whether we're upgrading or not # Configure whether we're upgrading or not
url = reverse('course_modes_choose', args=[unicode(prof_course.id)]) url = reverse('course_modes_choose', args=[unicode(prof_course.id)])
response = self.client.get(url) response = self.client.get(url)
self.assertRedirects(response, 'http://testserver/test_basket/?sku=TEST', fetch_redirect_response=False) self.assertRedirects(response, 'http://testserver/basket/add/?sku=TEST', fetch_redirect_response=False)
ecomm_test_utils.update_commerce_config(enabled=False) ecomm_test_utils.update_commerce_config(enabled=False)
def _generate_enterprise_learner_context(self, enable_audit_enrollment=False): def _generate_enterprise_learner_context(self, enable_audit_enrollment=False):
......
...@@ -15,6 +15,7 @@ class CommerceConfiguration(ConfigurationModel): ...@@ -15,6 +15,7 @@ class CommerceConfiguration(ConfigurationModel):
API_NAME = 'commerce' API_NAME = 'commerce'
CACHE_KEY = 'commerce.api.data' CACHE_KEY = 'commerce.api.data'
DEFAULT_RECEIPT_PAGE_URL = '/checkout/receipt/?order_number=' DEFAULT_RECEIPT_PAGE_URL = '/checkout/receipt/?order_number='
MULTIPLE_ITEMS_BASKET_PAGE_URL = '/basket/add/'
checkout_on_ecommerce_service = models.BooleanField( checkout_on_ecommerce_service = models.BooleanField(
default=False, default=False,
......
...@@ -97,7 +97,9 @@ class EcommerceServiceTests(TestCase): ...@@ -97,7 +97,9 @@ class EcommerceServiceTests(TestCase):
def test_get_checkout_page_url(self, skus): def test_get_checkout_page_url(self, skus):
""" Verify the checkout page URL is properly constructed and returned. """ """ Verify the checkout page URL is properly constructed and returned. """
url = EcommerceService().get_checkout_page_url(*skus) url = EcommerceService().get_checkout_page_url(*skus)
expected_url = '{root}/test_basket/?{skus}'.format( config = CommerceConfiguration.current()
expected_url = '{root}{basket_url}?{skus}'.format(
basket_url=config.MULTIPLE_ITEMS_BASKET_PAGE_URL,
root=settings.ECOMMERCE_PUBLIC_URL_ROOT, root=settings.ECOMMERCE_PUBLIC_URL_ROOT,
skus=urlencode({'sku': skus}, doseq=True), skus=urlencode({'sku': skus}, doseq=True),
) )
......
...@@ -87,9 +87,9 @@ class EcommerceService(object): ...@@ -87,9 +87,9 @@ class EcommerceService(object):
Absolute path to the ecommerce checkout page showing basket that contains specified products. Absolute path to the ecommerce checkout page showing basket that contains specified products.
Example: Example:
http://localhost:8002/basket/single_item/?sku=5H3HG5&sku=57FHHD http://localhost:8002/basket/add/?sku=5H3HG5&sku=57FHHD
""" """
return '{checkout_page_path}?{skus}'.format( return '{checkout_page_path}?{skus}'.format(
checkout_page_path=self.get_absolute_ecommerce_url(self.config.single_course_checkout_page), checkout_page_path=self.get_absolute_ecommerce_url(self.config.MULTIPLE_ITEMS_BASKET_PAGE_URL),
skus=urlencode({'sku': skus}, doseq=True), skus=urlencode({'sku': skus}, doseq=True),
) )
...@@ -313,14 +313,10 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -313,14 +313,10 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_ecommerce_checkout_redirect(self): def test_ecommerce_checkout_redirect(self):
"""Verify the block link redirects to ecommerce checkout if it's enabled.""" """Verify the block link redirects to ecommerce checkout if it's enabled."""
sku = 'TESTSKU' sku = 'TESTSKU'
checkout_page = '/test_basket/' configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
CommerceConfiguration.objects.create(
checkout_on_ecommerce_service=True,
single_course_checkout_page=checkout_page
)
self.setup_course_and_user(sku=sku) self.setup_course_and_user(sku=sku)
block = VerifiedUpgradeDeadlineDate(self.course, self.user) block = VerifiedUpgradeDeadlineDate(self.course, self.user)
self.assertEqual(block.link, '{}?sku={}'.format(checkout_page, sku)) self.assertEqual(block.link, '{}?sku={}'.format(configuration.MULTIPLE_ITEMS_BASKET_PAGE_URL, sku))
## VerificationDeadlineDate ## VerificationDeadlineDate
def test_no_verification_deadline(self): def test_no_verification_deadline(self):
......
...@@ -469,12 +469,8 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -469,12 +469,8 @@ class ViewsTestCase(ModuleStoreTestCase):
_id(bool): Tell the method to either expect an id in the href or not. _id(bool): Tell the method to either expect an id in the href or not.
""" """
checkout_page = '/test_basket/'
sku = 'TEST123' sku = 'TEST123'
CommerceConfiguration.objects.create( configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
checkout_on_ecommerce_service=True,
single_course_checkout_page=checkout_page
)
course = CourseFactory.create() course = CourseFactory.create()
CourseModeFactory(mode_slug=CourseMode.PROFESSIONAL, course_id=course.id, sku=sku, min_price=1) CourseModeFactory(mode_slug=CourseMode.PROFESSIONAL, course_id=course.id, sku=sku, min_price=1)
...@@ -486,7 +482,10 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -486,7 +482,10 @@ class ViewsTestCase(ModuleStoreTestCase):
# Construct the link according the following scenarios and verify its presence in the response: # Construct the link according the following scenarios and verify its presence in the response:
# (1) shopping cart is enabled and the user is not logged in # (1) shopping cart is enabled and the user is not logged in
# (2) shopping cart is enabled and the user is logged in # (2) shopping cart is enabled and the user is logged in
href = '<a href="{uri_stem}?sku={sku}" class="add-to-cart">'.format(uri_stem=checkout_page, sku=sku) href = '<a href="{uri_stem}?sku={sku}" class="add-to-cart">'.format(
uri_stem=configuration.MULTIPLE_ITEMS_BASKET_PAGE_URL,
sku=sku,
)
# Generate the course about page content # Generate the course about page content
response = self.client.get(reverse('about_course', args=[unicode(course.id)])) response = self.client.get(reverse('about_course', args=[unicode(course.id)]))
......
...@@ -131,7 +131,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -131,7 +131,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
) )
def test_start_flow_with_ecommerce(self): def test_start_flow_with_ecommerce(self):
"""Verify user gets redirected to ecommerce checkout when ecommerce checkout is enabled.""" """Verify user gets redirected to ecommerce checkout when ecommerce checkout is enabled."""
checkout_page = '/test_basket/'
sku = 'TESTSKU' sku = 'TESTSKU'
# When passing a SKU ecommerce api gets called. # When passing a SKU ecommerce api gets called.
httpretty.register_uri( httpretty.register_uri(
...@@ -140,11 +139,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ...@@ -140,11 +139,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
body=json.dumps(['foo', 'bar']), body=json.dumps(['foo', 'bar']),
content_type="application/json", content_type="application/json",
) )
configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
checkout_page = configuration.MULTIPLE_ITEMS_BASKET_PAGE_URL
httpretty.register_uri(httpretty.GET, "{}{}".format(TEST_PUBLIC_URL_ROOT, checkout_page)) httpretty.register_uri(httpretty.GET, "{}{}".format(TEST_PUBLIC_URL_ROOT, checkout_page))
CommerceConfiguration.objects.create(
checkout_on_ecommerce_service=True,
single_course_checkout_page=checkout_page
)
course = self._create_course('verified', sku=sku) course = self._create_course('verified', sku=sku)
self._enroll(course.id) self._enroll(course.id)
response = self._get_page('verify_student_start_flow', course.id, expected_status_code=302) response = self._get_page('verify_student_start_flow', course.id, expected_status_code=302)
......
...@@ -1085,6 +1085,26 @@ ...@@ -1085,6 +1085,26 @@
} }
} }
} }
.description {
display: block;
float: left;
}
.price {
.green-highlight {
font-weight: 700;
color: palette(success, text);
}
.original-price {
text-decoration: line-through;
}
.savings {
display: block;
}
}
} }
} }
......
...@@ -69,7 +69,7 @@ description_max_length = 250 ...@@ -69,7 +69,7 @@ description_max_length = 250
<a href="${buy_button_href}" class="btn-brand btn-large hidden-sm btn-start"> <a href="${buy_button_href}" class="btn-brand btn-large hidden-sm btn-start">
${Text(_('Buy the {program_name} (${price} USD)')).format( ${Text(_('Buy the {program_name} (${price} USD)')).format(
program_name=title, program_name=title,
price=program['price_ranges'][0]['total'] price=full_program_price
)} )}
</a> </a>
% else: % else:
...@@ -99,7 +99,7 @@ description_max_length = 250 ...@@ -99,7 +99,7 @@ description_max_length = 250
<div class="col col-12 md-col-7 lg-col-8 left-col"> <div class="col col-12 md-col-7 lg-col-8 left-col">
<ul class="list-divided program-desc-tbl"> <ul class="list-divided program-desc-tbl">
<li class="item"> <li class="item">
<span> <span class="description">
<span class="fa fa-book fa-lg" aria-hidden="true"></span> <span class="fa fa-book fa-lg" aria-hidden="true"></span>
${_('Number of Courses')} ${_('Number of Courses')}
</span> </span>
...@@ -110,7 +110,7 @@ description_max_length = 250 ...@@ -110,7 +110,7 @@ description_max_length = 250
</a> </a>
</li> </li>
<li class="item"> <li class="item">
<span> <span class="description">
<span class="fa fa-clock-o fa-lg" aria-hidden="true"></span> <span class="fa fa-clock-o fa-lg" aria-hidden="true"></span>
${_('Average Length')} ${_('Average Length')}
</span> </span>
...@@ -121,7 +121,7 @@ description_max_length = 250 ...@@ -121,7 +121,7 @@ description_max_length = 250
</span> </span>
</li> </li>
<li class="item"> <li class="item">
<span> <span class="description">
<span class="fa fa-tachometer fa-lg" aria-hidden="true"></span> <span class="fa fa-tachometer fa-lg" aria-hidden="true"></span>
${_('Effort')} ${_('Effort')}
</span> </span>
...@@ -133,15 +133,36 @@ description_max_length = 250 ...@@ -133,15 +133,36 @@ description_max_length = 250
</span> </span>
</li> </li>
<li class="item"> <li class="item">
<span> <span class="description">
<span class="fa fa-tag fa-lg" aria-hidden="true"></span> <span class="fa fa-tag fa-lg" aria-hidden="true"></span>
${_('Price (USD)')} ${_('Price (USD)')}
</span> </span>
<span> % if program.get('discount_data') and program['discount_data']['is_discounted']:
${Text(_('${full_program_price} for entire program')).format( <span class="price">
full_program_price=full_program_price <span role="group" aria-label="${_('Original Price')}" class="original-price">
)} ${Text(_('${oldPrice}')).format(
</span> oldPrice=full_program_price_format.format(program['discount_data']['total_incl_tax_excl_discounts'])
)}
</span>
<span role="group" aria-label="${_('Discounted Price')}" class="discount green-highlight">
${Text(_('${newPrice}{htmlEnd} for entire program')).format(
newPrice=full_program_price,
htmlEnd=HTML('</span>')
)}
<span class="savings green-highlight">
${Text(_('You save ${discount_value} {currency}')).format(
discount_value=full_program_price_format.format(program['discount_data']['discount_value']),
currency=program['discount_data']['currency']
)}
</span>
</span>
% else:
<span>
${Text(_('${full_program_price} for entire program')).format(
full_program_price=full_program_price
)}
</span>
% endif
</li> </li>
</ul> </ul>
<div id="accordion-group"> <div id="accordion-group">
......
...@@ -872,6 +872,19 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ...@@ -872,6 +872,19 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
self.program['applicable_seat_types'] = [seat['type']] self.program['applicable_seat_types'] = [seat['type']]
return seat return seat
def _update_discount_data(self, mock_discount_data):
"""
Helper method that updates mocked discount data with
- a flag indicating whether the program price is discounted
- the amount of the discount (0 in case there's no discount)
"""
program_discounted_price = mock_discount_data['total_incl_tax']
program_full_price = mock_discount_data['total_incl_tax_excl_discounts']
mock_discount_data.update({
'is_discounted': program_discounted_price < program_full_price,
'discount_value': program_full_price - program_discounted_price
})
def test_instructors(self): def test_instructors(self):
data = ProgramMarketingDataExtender(self.program, self.user).extend() data = ProgramMarketingDataExtender(self.program, self.user).extend()
...@@ -887,10 +900,13 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ...@@ -887,10 +900,13 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses) self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses)
def test_course_pricing_when_all_course_runs_have_no_seats(self): def test_course_pricing_when_all_course_runs_have_no_seats(self):
course = ModuleStoreCourseFactory() # Create three seatless course runs and add them to the program
course = self.update_course(course, self.user.id) course_runs = []
course_run = CourseRunFactory(key=unicode(course.id), seats=[]) for __ in range(3):
program = ProgramFactory(courses=[CourseFactory(course_runs=[course_run])]) course = ModuleStoreCourseFactory()
course = self.update_course(course, self.user.id)
course_runs.append(CourseRunFactory(key=unicode(course.id), seats=[]))
program = ProgramFactory(courses=[CourseFactory(course_runs=course_runs)])
data = ProgramMarketingDataExtender(program, self.user).extend() data = ProgramMarketingDataExtender(program, self.user).extend()
...@@ -988,7 +1004,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ...@@ -988,7 +1004,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
self._prepare_program_for_discounted_price_calculation_endpoint() self._prepare_program_for_discounted_price_calculation_endpoint()
mock_discount_data = { mock_discount_data = {
'total_incl_tax_excl_discounts': 200.0, 'total_incl_tax_excl_discounts': 200.0,
'currency': "USD", 'currency': 'USD',
'total_incl_tax': 50.0 'total_incl_tax': 50.0
} }
httpretty.register_uri( httpretty.register_uri(
...@@ -999,6 +1015,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ...@@ -999,6 +1015,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
) )
data = ProgramMarketingDataExtender(self.program, self.user).extend() data = ProgramMarketingDataExtender(self.program, self.user).extend()
self._update_discount_data(mock_discount_data)
self.assertEqual( self.assertEqual(
data['skus'], data['skus'],
...@@ -1015,7 +1032,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ...@@ -1015,7 +1032,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
self._prepare_program_for_discounted_price_calculation_endpoint() self._prepare_program_for_discounted_price_calculation_endpoint()
mock_discount_data = { mock_discount_data = {
'total_incl_tax_excl_discounts': 200.0, 'total_incl_tax_excl_discounts': 200.0,
'currency': "USD", 'currency': 'USD',
'total_incl_tax': 50.0 'total_incl_tax': 50.0
} }
httpretty.register_uri( httpretty.register_uri(
...@@ -1026,6 +1043,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ...@@ -1026,6 +1043,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
) )
data = ProgramMarketingDataExtender(self.program, AnonymousUserFactory()).extend() data = ProgramMarketingDataExtender(self.program, AnonymousUserFactory()).extend()
self._update_discount_data(mock_discount_data)
self.assertEqual( self.assertEqual(
data['skus'], data['skus'],
......
...@@ -605,7 +605,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender): ...@@ -605,7 +605,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs']) published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs'])
if len(published_course_runs) == 1: if len(published_course_runs) == 1:
for seat in published_course_runs[0]['seats']: for seat in published_course_runs[0]['seats']:
if seat['type'] in applicable_seat_types: if seat['type'] in applicable_seat_types and seat['sku']:
skus.append(seat['sku']) skus.append(seat['sku'])
else: else:
# If a course in the program has more than 1 published course run # If a course in the program has more than 1 published course run
...@@ -626,6 +626,11 @@ class ProgramMarketingDataExtender(ProgramDataExtender): ...@@ -626,6 +626,11 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
# Make an API call to calculate the discounted price # Make an API call to calculate the discounted price
discount_data = api.baskets.calculate.get(sku=skus) discount_data = api.baskets.calculate.get(sku=skus)
program_discounted_price = discount_data['total_incl_tax']
program_full_price = discount_data['total_incl_tax_excl_discounts']
discount_data['is_discounted'] = program_discounted_price < program_full_price
discount_data['discount_value'] = program_full_price - program_discounted_price
self.data.update({ self.data.update({
'discount_data': discount_data, 'discount_data': discount_data,
'full_program_price': discount_data['total_incl_tax'] 'full_program_price': discount_data['total_incl_tax']
......
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