check if discount expired

This commit is contained in:
Matthew Piatetsky
2019-10-02 16:52:59 -04:00
parent 8aa2fad4ec
commit f6d0bf77df
8 changed files with 133 additions and 25 deletions

View File

@@ -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')

View File

@@ -33,6 +33,11 @@
color: #23419F;
}
a {
color: #00496f;
text-decoration: underline;
font-weight: bold;
}
}
// Course call to action message

View File

@@ -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;
}
}
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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))