check if discount expired
This commit is contained in:
@@ -412,19 +412,24 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
url = reverse('create_mode', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(url, parameters)
|
||||
|
||||
CourseEnrollmentFactory(
|
||||
is_active=True,
|
||||
course_id=self.course.id,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse('course_modes_choose', args=[six.text_type(self.course.id)]),
|
||||
follow=False,
|
||||
)
|
||||
|
||||
bannerText = u'''<div class="first-purchase-offer-banner"><span class="first-purchase-offer-banner-bold">
|
||||
15% off your first upgrade.</span> Discount automatically applied.</div>'''
|
||||
banner = u'''<div class="first-purchase-offer-banner">'''
|
||||
button = u'''<button type="submit" name="verified_mode">
|
||||
<span>Pursue a Verified Certificate</span>
|
||||
(<span class="upgrade-price-string">$8.50 USD</span>
|
||||
<del> <span class="upgrade-price-string">$10 USD</span></del>)
|
||||
</button>'''
|
||||
self.assertContains(response, bannerText, html=True)
|
||||
self.assertContains(response, banner)
|
||||
self.assertContains(response, button, html=True)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
color: #23419F;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00496f;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
// Course call to action message
|
||||
|
||||
@@ -2261,11 +2261,19 @@
|
||||
font-size: 16px;
|
||||
border-radius: 7px;
|
||||
padding: 20px;
|
||||
line-height: 1.5;
|
||||
|
||||
.first-purchase-offer-banner-bold {
|
||||
font-weight: bold;
|
||||
color: #23419f;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00496f;
|
||||
text-decoration: underline !important;
|
||||
font-weight: bold;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from experiments.models import ExperimentData
|
||||
from lms.djangoapps.commerce.models import CommerceConfiguration
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal
|
||||
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
|
||||
from lms.djangoapps.courseware.tests.factories import (
|
||||
BetaTesterFactory,
|
||||
GlobalStaffFactory,
|
||||
@@ -34,6 +35,8 @@ from lms.djangoapps.courseware.tests.factories import (
|
||||
StaffFactory
|
||||
)
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
|
||||
from openedx.features.discounts.applicability import get_discount_expiration_date
|
||||
from openedx.features.discounts.utils import format_strikeout_price
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
|
||||
from openedx.core.djangoapps.django_comment_common.models import (
|
||||
@@ -217,7 +220,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
|
||||
# Fetch the view and verify the query counts
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
with self.assertNumQueries(95, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with self.assertNumQueries(97, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
|
||||
with check_mongo_calls(4):
|
||||
url = course_home_url(self.course)
|
||||
self.client.get(url)
|
||||
@@ -432,11 +435,19 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
self.client.login(username=user.username, password=self.TEST_PASSWORD)
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
discount_expiration_date = get_discount_expiration_date(user, self.course).strftime(u'%B %d')
|
||||
upgrade_link = verified_upgrade_deadline_link(user=user, course=self.course)
|
||||
bannerText = u'''<div class="first-purchase-offer-banner">
|
||||
<span class="first-purchase-offer-banner-bold">
|
||||
{}% off your first upgrade.
|
||||
</span> Discount automatically applied.
|
||||
</div>'''.format(percentage)
|
||||
<span class="first-purchase-offer-banner-bold">
|
||||
Upgrade by {discount_expiration_date} and save {percentage}% [{strikeout_price}]</span>
|
||||
<br>Discount will be automatically applied at checkout. <a href="{upgrade_link}">Upgrade Now</a>
|
||||
</div>'''.format(
|
||||
discount_expiration_date=discount_expiration_date,
|
||||
percentage=percentage,
|
||||
strikeout_price=HTML(format_strikeout_price(user, self.course, check_for_discount=False)[0]),
|
||||
upgrade_link=upgrade_link
|
||||
)
|
||||
|
||||
if applicability:
|
||||
self.assertContains(response, bannerText, html=True)
|
||||
else:
|
||||
|
||||
@@ -11,9 +11,15 @@ from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.course_api.blocks.api import get_blocks
|
||||
from lms.djangoapps.course_blocks.utils import get_student_module_as_dict
|
||||
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from openedx.features.discounts.applicability import can_receive_discount, discount_percentage
|
||||
from openedx.features.discounts.applicability import (
|
||||
can_receive_discount,
|
||||
get_discount_expiration_date,
|
||||
discount_percentage
|
||||
)
|
||||
from openedx.features.discounts.utils import format_strikeout_price
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@@ -192,16 +198,27 @@ def get_resume_block(block):
|
||||
|
||||
|
||||
def get_first_purchase_offer_banner_fragment(user, course):
|
||||
if user and course and can_receive_discount(user=user, course=course):
|
||||
# Translator: xgettext:no-python-format
|
||||
offer_message = _(u'{banner_open}{percentage}% off your first upgrade.{span_close}'
|
||||
u' Discount automatically applied.{div_close}')
|
||||
return Fragment(HTML(offer_message).format(
|
||||
banner_open=HTML(
|
||||
'<div class="first-purchase-offer-banner"><span class="first-purchase-offer-banner-bold">'
|
||||
),
|
||||
percentage=discount_percentage(),
|
||||
span_close=HTML('</span>'),
|
||||
div_close=HTML('</div>')
|
||||
))
|
||||
if user and course:
|
||||
discount_expiration_date = get_discount_expiration_date(user, course)
|
||||
if (discount_expiration_date and
|
||||
can_receive_discount(user=user, course=course, discount_expiration_date=discount_expiration_date)):
|
||||
# Translator: xgettext:no-python-format
|
||||
offer_message = _(u'{banner_open} Upgrade by {discount_expiration_date} and save {percentage}% '
|
||||
u'[{strikeout_price}]{span_close}{br}Discount will be automatically applied at checkout. '
|
||||
u'{a_open}Upgrade Now{a_close}{div_close}')
|
||||
return Fragment(HTML(offer_message).format(
|
||||
a_open=HTML(u'<a href="{upgrade_link}">').format(
|
||||
upgrade_link=verified_upgrade_deadline_link(user=user, course=course)
|
||||
),
|
||||
a_close=HTML('</a>'),
|
||||
br=HTML('<br>'),
|
||||
banner_open=HTML(
|
||||
'<div class="first-purchase-offer-banner"><span class="first-purchase-offer-banner-bold">'
|
||||
),
|
||||
discount_expiration_date=discount_expiration_date.strftime(u'%B %d'),
|
||||
percentage=discount_percentage(),
|
||||
span_close=HTML('</span>'),
|
||||
div_close=HTML('</div>'),
|
||||
strikeout_price=HTML(format_strikeout_price(user, course, check_for_discount=False)[0])
|
||||
))
|
||||
return None
|
||||
|
||||
@@ -10,9 +10,10 @@ not other discounts like coupons or enterprise/program offers configured in ecom
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from crum import get_current_request, impersonate
|
||||
from django.utils import timezone
|
||||
import pytz
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
@@ -43,7 +44,40 @@ DISCOUNT_APPLICABILITY_FLAG = WaffleFlag(
|
||||
DISCOUNT_APPLICABILITY_HOLDBACK = 'first_purchase_discount_holdback'
|
||||
|
||||
|
||||
def can_receive_discount(user, course): # pylint: disable=unused-argument
|
||||
def get_discount_expiration_date(user, course):
|
||||
"""
|
||||
Returns the date when the discount expires for the user.
|
||||
Returns none if the user is not enrolled.
|
||||
"""
|
||||
course_enrollment = CourseEnrollment.objects.filter(
|
||||
user=user,
|
||||
course=course.id,
|
||||
mode__in=CourseMode.UPSELL_TO_VERIFIED_MODES
|
||||
)
|
||||
if len(course_enrollment) != 1:
|
||||
return None
|
||||
|
||||
enrollment = course_enrollment.first()
|
||||
try:
|
||||
# Content availability date is equivalent to max(enrollment date, course start date)
|
||||
# for most people. Using the schedule date will provide flexibility to deal with
|
||||
# more complex business rules in the future.
|
||||
content_availability_date = enrollment.schedule.start
|
||||
# We have anecdotally observed a case where the schedule.start was
|
||||
# equal to the course start, but should have been equal to the enrollment start
|
||||
# https://openedx.atlassian.net/browse/PROD-58
|
||||
# This section is meant to address that case
|
||||
if enrollment.created and course.start:
|
||||
if (content_availability_date.date() == course.start.date() and
|
||||
course.start < enrollment.created < timezone.now()):
|
||||
content_availability_date = enrollment.created
|
||||
except CourseEnrollment.schedule.RelatedObjectDoesNotExist:
|
||||
content_availability_date = max(enrollment.created, course.start)
|
||||
|
||||
return content_availability_date + timedelta(weeks=1)
|
||||
|
||||
|
||||
def can_receive_discount(user, course, discount_expiration_date=None):
|
||||
"""
|
||||
Check all the business logic about whether this combination of user and course
|
||||
can receive a discount.
|
||||
@@ -55,6 +89,16 @@ def can_receive_discount(user, course): # pylint: disable=unused-argument
|
||||
|
||||
# TODO: Add additional conditions to return False here
|
||||
|
||||
# Check if discount has expired
|
||||
if not discount_expiration_date:
|
||||
discount_expiration_date = get_discount_expiration_date(user, course)
|
||||
|
||||
if discount_expiration_date is None:
|
||||
return False
|
||||
|
||||
if discount_expiration_date < timezone.now():
|
||||
return False
|
||||
|
||||
# Course end date needs to be in the future
|
||||
if course.has_ended():
|
||||
return False
|
||||
|
||||
@@ -53,6 +53,12 @@ class TestApplicability(ModuleStoreTestCase):
|
||||
"""
|
||||
Ensure first purchase offer banner only displays for courses with a non-expired verified mode
|
||||
"""
|
||||
CourseEnrollmentFactory(
|
||||
is_active=True,
|
||||
course_id=self.course.id,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
applicability = can_receive_discount(user=self.user, course=self.course)
|
||||
self.assertEqual(applicability, True)
|
||||
|
||||
@@ -86,6 +92,12 @@ class TestApplicability(ModuleStoreTestCase):
|
||||
"""
|
||||
Ensure that only users who have not already purchased courses receive the discount.
|
||||
"""
|
||||
CourseEnrollmentFactory(
|
||||
is_active=True,
|
||||
course_id=self.course.id,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
for mode in existing_enrollments:
|
||||
CourseEnrollmentFactory.create(mode=mode, user=self.user)
|
||||
|
||||
@@ -102,6 +114,12 @@ class TestApplicability(ModuleStoreTestCase):
|
||||
"""
|
||||
Ensure that only users who have not already purchased courses receive the discount.
|
||||
"""
|
||||
CourseEnrollmentFactory(
|
||||
is_active=True,
|
||||
course_id=self.course.id,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
if entitlement_mode is not None:
|
||||
CourseEntitlementFactory.create(mode=entitlement_mode, user=self.user)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from openedx.core.djangolib.markup import HTML
|
||||
from .applicability import can_receive_discount, discount_percentage
|
||||
|
||||
|
||||
def format_strikeout_price(user, course, base_price=None):
|
||||
def format_strikeout_price(user, course, base_price=None, check_for_discount=True):
|
||||
"""
|
||||
Return a formatted price, including a struck-out original price if a discount applies, and also
|
||||
whether a discount was applied, as the tuple (formatted_price, has_discount).
|
||||
@@ -19,7 +19,7 @@ def format_strikeout_price(user, course, base_price=None):
|
||||
|
||||
original_price = format_course_price(base_price)
|
||||
|
||||
if can_receive_discount(user, course):
|
||||
if not check_for_discount or can_receive_discount(user, course):
|
||||
discount_price = base_price * ((100.0 - discount_percentage()) / 100)
|
||||
if discount_price == int(discount_price):
|
||||
discount_price = format_course_price("{:0.0f}".format(discount_price))
|
||||
|
||||
Reference in New Issue
Block a user