[LEARNER-437] Reflect discount on the Program About Page (WL)
This commit is contained in:
@@ -130,7 +130,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
# Configure whether we're upgrading or not
|
||||
url = reverse('course_modes_choose', args=[unicode(prof_course.id)])
|
||||
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)
|
||||
|
||||
def _generate_enterprise_learner_context(self, enable_audit_enrollment=False):
|
||||
|
||||
@@ -15,6 +15,7 @@ class CommerceConfiguration(ConfigurationModel):
|
||||
API_NAME = 'commerce'
|
||||
CACHE_KEY = 'commerce.api.data'
|
||||
DEFAULT_RECEIPT_PAGE_URL = '/checkout/receipt/?order_number='
|
||||
MULTIPLE_ITEMS_BASKET_PAGE_URL = '/basket/add/'
|
||||
|
||||
checkout_on_ecommerce_service = models.BooleanField(
|
||||
default=False,
|
||||
|
||||
@@ -97,7 +97,9 @@ class EcommerceServiceTests(TestCase):
|
||||
def test_get_checkout_page_url(self, skus):
|
||||
""" Verify the checkout page URL is properly constructed and returned. """
|
||||
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,
|
||||
skus=urlencode({'sku': skus}, doseq=True),
|
||||
)
|
||||
|
||||
@@ -87,9 +87,9 @@ class EcommerceService(object):
|
||||
Absolute path to the ecommerce checkout page showing basket that contains specified products.
|
||||
|
||||
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(
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -313,14 +313,10 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
|
||||
def test_ecommerce_checkout_redirect(self):
|
||||
"""Verify the block link redirects to ecommerce checkout if it's enabled."""
|
||||
sku = 'TESTSKU'
|
||||
checkout_page = '/test_basket/'
|
||||
CommerceConfiguration.objects.create(
|
||||
checkout_on_ecommerce_service=True,
|
||||
single_course_checkout_page=checkout_page
|
||||
)
|
||||
configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
|
||||
self.setup_course_and_user(sku=sku)
|
||||
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
|
||||
def test_no_verification_deadline(self):
|
||||
|
||||
@@ -469,12 +469,8 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
_id(bool): Tell the method to either expect an id in the href or not.
|
||||
|
||||
"""
|
||||
checkout_page = '/test_basket/'
|
||||
sku = 'TEST123'
|
||||
CommerceConfiguration.objects.create(
|
||||
checkout_on_ecommerce_service=True,
|
||||
single_course_checkout_page=checkout_page
|
||||
)
|
||||
configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
|
||||
course = CourseFactory.create()
|
||||
CourseModeFactory(mode_slug=CourseMode.PROFESSIONAL, course_id=course.id, sku=sku, min_price=1)
|
||||
|
||||
@@ -486,7 +482,10 @@ class ViewsTestCase(ModuleStoreTestCase):
|
||||
# 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
|
||||
# (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
|
||||
response = self.client.get(reverse('about_course', args=[unicode(course.id)]))
|
||||
|
||||
@@ -131,7 +131,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
|
||||
)
|
||||
def test_start_flow_with_ecommerce(self):
|
||||
"""Verify user gets redirected to ecommerce checkout when ecommerce checkout is enabled."""
|
||||
checkout_page = '/test_basket/'
|
||||
sku = 'TESTSKU'
|
||||
# When passing a SKU ecommerce api gets called.
|
||||
httpretty.register_uri(
|
||||
@@ -140,11 +139,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
|
||||
body=json.dumps(['foo', 'bar']),
|
||||
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))
|
||||
CommerceConfiguration.objects.create(
|
||||
checkout_on_ecommerce_service=True,
|
||||
single_course_checkout_page=checkout_page
|
||||
)
|
||||
|
||||
course = self._create_course('verified', sku=sku)
|
||||
self._enroll(course.id)
|
||||
response = self._get_page('verify_student_start_flow', course.id, expected_status_code=302)
|
||||
|
||||
@@ -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
|
||||
<a href="${buy_button_href}" class="btn-brand btn-large hidden-sm btn-start">
|
||||
${Text(_('Buy the {program_name} (${price} USD)')).format(
|
||||
program_name=title,
|
||||
price=program['price_ranges'][0]['total']
|
||||
price=full_program_price
|
||||
)}
|
||||
</a>
|
||||
% else:
|
||||
@@ -99,7 +99,7 @@ description_max_length = 250
|
||||
<div class="col col-12 md-col-7 lg-col-8 left-col">
|
||||
<ul class="list-divided program-desc-tbl">
|
||||
<li class="item">
|
||||
<span>
|
||||
<span class="description">
|
||||
<span class="fa fa-book fa-lg" aria-hidden="true"></span>
|
||||
${_('Number of Courses')}
|
||||
</span>
|
||||
@@ -110,7 +110,7 @@ description_max_length = 250
|
||||
</a>
|
||||
</li>
|
||||
<li class="item">
|
||||
<span>
|
||||
<span class="description">
|
||||
<span class="fa fa-clock-o fa-lg" aria-hidden="true"></span>
|
||||
${_('Average Length')}
|
||||
</span>
|
||||
@@ -121,7 +121,7 @@ description_max_length = 250
|
||||
</span>
|
||||
</li>
|
||||
<li class="item">
|
||||
<span>
|
||||
<span class="description">
|
||||
<span class="fa fa-tachometer fa-lg" aria-hidden="true"></span>
|
||||
${_('Effort')}
|
||||
</span>
|
||||
@@ -133,15 +133,36 @@ description_max_length = 250
|
||||
</span>
|
||||
</li>
|
||||
<li class="item">
|
||||
<span>
|
||||
<span class="description">
|
||||
<span class="fa fa-tag fa-lg" aria-hidden="true"></span>
|
||||
${_('Price (USD)')}
|
||||
</span>
|
||||
<span>
|
||||
${Text(_('${full_program_price} for entire program')).format(
|
||||
full_program_price=full_program_price
|
||||
)}
|
||||
</span>
|
||||
% if program.get('discount_data') and program['discount_data']['is_discounted']:
|
||||
<span class="price">
|
||||
<span role="group" aria-label="${_('Original Price')}" class="original-price">
|
||||
${Text(_('${oldPrice}')).format(
|
||||
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>
|
||||
</ul>
|
||||
<div id="accordion-group">
|
||||
|
||||
@@ -872,6 +872,19 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
self.program['applicable_seat_types'] = [seat['type']]
|
||||
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):
|
||||
data = ProgramMarketingDataExtender(self.program, self.user).extend()
|
||||
|
||||
@@ -887,10 +900,13 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
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):
|
||||
course = ModuleStoreCourseFactory()
|
||||
course = self.update_course(course, self.user.id)
|
||||
course_run = CourseRunFactory(key=unicode(course.id), seats=[])
|
||||
program = ProgramFactory(courses=[CourseFactory(course_runs=[course_run])])
|
||||
# Create three seatless course runs and add them to the program
|
||||
course_runs = []
|
||||
for __ in range(3):
|
||||
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()
|
||||
|
||||
@@ -988,7 +1004,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
self._prepare_program_for_discounted_price_calculation_endpoint()
|
||||
mock_discount_data = {
|
||||
'total_incl_tax_excl_discounts': 200.0,
|
||||
'currency': "USD",
|
||||
'currency': 'USD',
|
||||
'total_incl_tax': 50.0
|
||||
}
|
||||
httpretty.register_uri(
|
||||
@@ -999,6 +1015,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
data = ProgramMarketingDataExtender(self.program, self.user).extend()
|
||||
self._update_discount_data(mock_discount_data)
|
||||
|
||||
self.assertEqual(
|
||||
data['skus'],
|
||||
@@ -1015,7 +1032,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
self._prepare_program_for_discounted_price_calculation_endpoint()
|
||||
mock_discount_data = {
|
||||
'total_incl_tax_excl_discounts': 200.0,
|
||||
'currency': "USD",
|
||||
'currency': 'USD',
|
||||
'total_incl_tax': 50.0
|
||||
}
|
||||
httpretty.register_uri(
|
||||
@@ -1026,6 +1043,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
data = ProgramMarketingDataExtender(self.program, AnonymousUserFactory()).extend()
|
||||
self._update_discount_data(mock_discount_data)
|
||||
|
||||
self.assertEqual(
|
||||
data['skus'],
|
||||
|
||||
@@ -605,7 +605,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
|
||||
published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs'])
|
||||
if len(published_course_runs) == 1:
|
||||
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'])
|
||||
else:
|
||||
# If a course in the program has more than 1 published course run
|
||||
@@ -626,6 +626,11 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
|
||||
# Make an API call to calculate the discounted price
|
||||
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({
|
||||
'discount_data': discount_data,
|
||||
'full_program_price': discount_data['total_incl_tax']
|
||||
|
||||
Reference in New Issue
Block a user