Commit d7be3278 by McKenzie Welter Committed by McKenzie Welter

consider entitlements in program one click purchase eligibility

parent d872147a
...@@ -401,6 +401,7 @@ class CourseEntitlementSerializer(serializers.ModelSerializer): ...@@ -401,6 +401,7 @@ class CourseEntitlementSerializer(serializers.ModelSerializer):
currency = serializers.SlugRelatedField(read_only=True, slug_field='code') currency = serializers.SlugRelatedField(read_only=True, slug_field='code')
sku = serializers.CharField() sku = serializers.CharField()
mode = serializers.SlugRelatedField(slug_field='name', queryset=SeatType.objects.all()) mode = serializers.SlugRelatedField(slug_field='name', queryset=SeatType.objects.all())
expires = serializers.DateTimeField()
@classmethod @classmethod
def prefetch_queryset(cls): def prefetch_queryset(cls):
...@@ -408,7 +409,7 @@ class CourseEntitlementSerializer(serializers.ModelSerializer): ...@@ -408,7 +409,7 @@ class CourseEntitlementSerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
model = CourseEntitlement model = CourseEntitlement
fields = ('mode', 'price', 'currency', 'sku',) fields = ('mode', 'price', 'currency', 'sku', 'expires')
class MinimalOrganizationSerializer(serializers.ModelSerializer): class MinimalOrganizationSerializer(serializers.ModelSerializer):
......
...@@ -394,7 +394,8 @@ class EcommerceApiDataLoader(AbstractDataLoader): ...@@ -394,7 +394,8 @@ class EcommerceApiDataLoader(AbstractDataLoader):
defaults = { defaults = {
'price': price, 'price': price,
'currency': currency, 'currency': currency,
'sku': sku 'sku': sku,
'expires': self.parse_date(body['expires'])
} }
course.entitlements.update_or_create(mode=mode, defaults=defaults) course.entitlements.update_or_create(mode=mode, defaults=defaults)
return sku return sku
......
...@@ -438,6 +438,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -438,6 +438,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
""" Assert a Course Entitlement was loaded into the database for each entry in the specified data body. """ """ Assert a Course Entitlement was loaded into the database for each entry in the specified data body. """
self.assertEqual(CourseEntitlement.objects.count(), len(body)) self.assertEqual(CourseEntitlement.objects.count(), len(body))
for datum in body: for datum in body:
expires = datum['expires']
attributes = {attribute['name']: attribute['value'] for attribute in datum['attribute_values']} attributes = {attribute['name']: attribute['value'] for attribute in datum['attribute_values']}
course = Course.objects.get(uuid=attributes['UUID']) course = Course.objects.get(uuid=attributes['UUID'])
stock_record = datum['stockrecords'][0] stock_record = datum['stockrecords'][0]
...@@ -450,6 +451,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -450,6 +451,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
entitlement = course.entitlements.get(mode=mode) entitlement = course.entitlements.get(mode=mode)
self.assertEqual(entitlement.expires, expires)
self.assertEqual(entitlement.course, course) self.assertEqual(entitlement.course, course)
self.assertEqual(entitlement.mode, mode) self.assertEqual(entitlement.mode, mode)
self.assertEqual(entitlement.price, price) self.assertEqual(entitlement.price, price)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-11-16 17:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0068_auto_20171108_1614'),
]
operations = [
migrations.AddField(
model_name='courseentitlement',
name='expires',
field=models.DateTimeField(blank=True, null=True),
),
]
...@@ -779,6 +779,7 @@ class CourseEntitlement(TimeStampedModel): ...@@ -779,6 +779,7 @@ class CourseEntitlement(TimeStampedModel):
price = models.DecimalField(**PRICE_FIELD_CONFIG) price = models.DecimalField(**PRICE_FIELD_CONFIG)
currency = models.ForeignKey(Currency) currency = models.ForeignKey(Currency)
sku = models.CharField(max_length=128, null=True, blank=True) sku = models.CharField(max_length=128, null=True, blank=True)
expires = models.DateTimeField(null=True, blank=True)
class Meta(object): class Meta(object):
unique_together = ( unique_together = (
...@@ -934,6 +935,11 @@ class Program(TimeStampedModel): ...@@ -934,6 +935,11 @@ class Program(TimeStampedModel):
applicable_seat_types = [seat_type.name.lower() for seat_type in self.type.applicable_seat_types.all()] applicable_seat_types = [seat_type.name.lower() for seat_type in self.type.applicable_seat_types.all()]
for course in self.courses.all(): for course in self.courses.all():
entitlement_products = set(course.entitlements.filter(mode__name__in=applicable_seat_types).exclude(
expires__lte=datetime.datetime.now(pytz.UTC)))
if len(entitlement_products) == 1:
continue
course_runs = set(course.course_runs.filter(status=CourseRunStatus.Published)) - excluded_course_runs course_runs = set(course.course_runs.filter(status=CourseRunStatus.Published)) - excluded_course_runs
if len(course_runs) != 1: if len(course_runs) != 1:
......
...@@ -355,6 +355,7 @@ class CourseEntitlementFactory(factory.DjangoModelFactory): ...@@ -355,6 +355,7 @@ class CourseEntitlementFactory(factory.DjangoModelFactory):
price = FuzzyDecimal(0.0, 650.0) price = FuzzyDecimal(0.0, 650.0)
currency = factory.Iterator(Currency.objects.all()) currency = factory.Iterator(Currency.objects.all())
sku = FuzzyText(length=8) sku = FuzzyText(length=8)
expires = FuzzyDateTime(datetime.datetime(2014, 1, 1, tzinfo=UTC))
course = factory.SubFactory(CourseFactory) course = factory.SubFactory(CourseFactory)
class Meta: class Meta:
......
...@@ -386,7 +386,7 @@ class ProgramEligibilityFilterTests(SiteMixin, TestCase): ...@@ -386,7 +386,7 @@ class ProgramEligibilityFilterTests(SiteMixin, TestCase):
courses=[course_run.course], courses=[course_run.course],
one_click_purchase_enabled=True, one_click_purchase_enabled=True,
) )
with self.assertNumQueries(11): with self.assertNumQueries(12):
self.assertEqual( self.assertEqual(
list(program_filter.queryset({}, Program.objects.all())), list(program_filter.queryset({}, Program.objects.all())),
[one_click_purchase_eligible_program] [one_click_purchase_eligible_program]
......
...@@ -510,6 +510,30 @@ class ProgramTests(TestCase): ...@@ -510,6 +510,30 @@ class ProgramTests(TestCase):
return factories.ProgramFactory(type=program_type, courses=[course_run.course]) return factories.ProgramFactory(type=program_type, courses=[course_run.course])
def create_program_with_entitlements_and_seats(self):
verified_seat_type, __ = SeatType.objects.get_or_create(name=Seat.VERIFIED)
program_type = factories.ProgramTypeFactory(applicable_seat_types=[verified_seat_type])
courses = []
for __ in range(3):
entitlement = factories.CourseEntitlementFactory(mode=verified_seat_type, expires=None)
for __ in range(3):
factories.SeatFactory(
course_run=factories.CourseRunFactory(
end=None,
enrollment_end=None,
course=entitlement.course
),
type=Seat.VERIFIED, upgrade_deadline=None
)
courses.append(entitlement.course)
program = factories.ProgramFactory(
courses=courses,
one_click_purchase_enabled=True,
type=program_type,
)
return program, courses
def assert_one_click_purchase_ineligible_program( def assert_one_click_purchase_ineligible_program(
self, end=None, enrollment_start=None, enrollment_end=None, seat_type=Seat.VERIFIED, self, end=None, enrollment_start=None, enrollment_end=None, seat_type=Seat.VERIFIED,
upgrade_deadline=None, one_click_purchase_enabled=True, excluded_course_runs=None, program_type=None upgrade_deadline=None, one_click_purchase_enabled=True, excluded_course_runs=None, program_type=None
...@@ -570,6 +594,69 @@ class ProgramTests(TestCase): ...@@ -570,6 +594,69 @@ class ProgramTests(TestCase):
) )
self.assertTrue(program.is_program_eligible_for_one_click_purchase) self.assertTrue(program.is_program_eligible_for_one_click_purchase)
def test_one_click_purchase_eligible_with_entitlements(self):
""" Verify that program is one click purchase eligible when its courses have unexpired entitlement products. """
# Program has one_click_purchase_enabled set to True,
# all courses have a verified mode entitlement product and multiple course runs.
program, __ = self.create_program_with_entitlements_and_seats()
self.assertTrue(program.is_program_eligible_for_one_click_purchase)
def test_one_click_purchase_ineligible_expired_entitlement(self):
""" Verify that program is not one click purchase eligible if course entitlement product is expired. """
program, courses = self.create_program_with_entitlements_and_seats()
expired_entitlement = courses[2].entitlements.first()
expired_entitlement.expires = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7)
expired_entitlement.save()
self.assertFalse(program.is_program_eligible_for_one_click_purchase)
def test_one_click_purchase_eligible_expired_entitlement_one_run(self):
"""
Verify that program is one click purchase eligible if there is only one
published course run for the course whose entitlement product is expired.
"""
program, courses = self.create_program_with_entitlements_and_seats()
expired_entitlement = courses[2].entitlements.first()
expired_entitlement.expires = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7)
expired_entitlement.save()
CourseRun.objects.filter(course=courses[2]).delete()
factories.SeatFactory(
course_run=factories.CourseRunFactory(
end=None,
enrollment_end=None,
course=courses[2]
),
type=Seat.VERIFIED, upgrade_deadline=None
)
self.assertTrue(program.is_program_eligible_for_one_click_purchase)
def test_one_click_purchase_eligible_future_expires(self):
""" Verify that program is one click purchase eligible if course entitlement product expires in the future. """
program, courses = self.create_program_with_entitlements_and_seats()
future_expiring_entitlement = courses[1].entitlements.first()
future_expiring_entitlement.expires = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=7)
future_expiring_entitlement.save()
self.assertTrue(program.is_program_eligible_for_one_click_purchase)
def test_one_click_purchase_ineligible_wrong_mode(self):
""" Verify that program is not one click purchase eligible if course entitlement product has the wrong mode. """
program, courses = self.create_program_with_entitlements_and_seats()
honor_seat_type, __ = SeatType.objects.get_or_create(name=Seat.HONOR)
honor_mode_entitlement = courses[0].entitlements.first()
honor_mode_entitlement.mode = honor_seat_type
honor_mode_entitlement.save()
self.assertFalse(program.is_program_eligible_for_one_click_purchase)
def test_one_click_purchase_ineligible_multiple_entitlements(self):
"""
Verify that program is not one click purchase eligible if course has
multiple entitlement products with correct modes.
"""
program, courses = self.create_program_with_entitlements_and_seats()
credit_seat_type, __ = SeatType.objects.get_or_create(name=Seat.CREDIT)
program.type.applicable_seat_types.add(credit_seat_type)
factories.CourseEntitlementFactory(mode=credit_seat_type, expires=None, course=courses[0])
self.assertFalse(program.is_program_eligible_for_one_click_purchase)
def test_one_click_purchase_eligible_with_unpublished_runs(self): def test_one_click_purchase_eligible_with_unpublished_runs(self):
""" Verify that program with unpublished course runs is one click purchase eligible. """ """ Verify that program with unpublished course runs is one click purchase eligible. """
......
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