diff --git a/ecommerce/courses/publishers.py b/ecommerce/courses/publishers.py index bdc76cca78f..2347a1f87cd 100644 --- a/ecommerce/courses/publishers.py +++ b/ecommerce/courses/publishers.py @@ -4,11 +4,13 @@ import logging from urllib.parse import urljoin +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from oscar.core.loading import get_model from requests.exceptions import HTTPError from ecommerce.core.constants import ENROLLMENT_CODE_SEAT_TYPES +from ecommerce.courses.constants import CertificateType from ecommerce.courses.utils import mode_for_product logger = logging.getLogger(__name__) @@ -36,6 +38,18 @@ def serialize_seat_for_commerce_api(self, seat): if enrollment_code: bulk_sku = enrollment_code.stockrecords.first().partner_sku + android_sku = None + ios_sku = None + if getattr(seat.attr, 'certificate_type', '') == CertificateType.VERIFIED: + android_stock_record = StockRecord.objects.filter( + product__parent=seat.parent, partner_sku__contains='mobile.android').first() + ios_stock_record = StockRecord.objects.filter( + product__parent=seat.parent, partner_sku__contains='mobile.ios').first() + if android_stock_record: + android_sku = android_stock_record.partner_sku + if ios_stock_record: + ios_sku = ios_stock_record.partner_sku + return { 'name': mode_for_product(seat), 'currency': stock_record.price_currency, @@ -43,6 +57,8 @@ def serialize_seat_for_commerce_api(self, seat): 'sku': stock_record.partner_sku, 'bulk_sku': bulk_sku, 'expires': self.get_seat_expiration(seat), + 'android_sku': android_sku, + 'ios_sku': ios_sku, } def publish(self, course): @@ -63,7 +79,11 @@ def publish(self, course): name = course.name verification_deadline = self.get_course_verification_deadline(course) - modes = [self.serialize_seat_for_commerce_api(seat) for seat in course.seat_products] + + # Do not fetch mobile seats to create Course modes. Mobile skus are + # added to the verified course mode in serialize_seat_for_commerce_api() + seat_products = course.seat_products.filter(~Q(stockrecords__partner_sku__contains="mobile")) + modes = [self.serialize_seat_for_commerce_api(seat) for seat in seat_products] has_credit = 'credit' in [mode['name'] for mode in modes] if has_credit: diff --git a/ecommerce/courses/tests/test_publishers.py b/ecommerce/courses/tests/test_publishers.py index 49191303f4c..eaeee9b9585 100644 --- a/ecommerce/courses/tests/test_publishers.py +++ b/ecommerce/courses/tests/test_publishers.py @@ -7,6 +7,7 @@ import ddt import mock import responses +from django.db.models import Q from django.utils import timezone from oscar.core.loading import get_model from requests import Timeout @@ -16,6 +17,7 @@ from ecommerce.core.url_utils import get_lms_url from ecommerce.courses.publishers import LMSPublisher from ecommerce.courses.tests.factories import CourseFactory +from ecommerce.extensions.catalogue.models import Product from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin from ecommerce.tests.testcases import TestCase @@ -41,6 +43,39 @@ def setUp(self): self.publisher = LMSPublisher() self.error_message = 'Failed to publish commerce data for {course_id} to LMS.'.format(course_id=self.course.id) + def _create_mobile_seat_for_course(self, course, sku_prefix): + """ Create a mobile seat for a course given the sku_prefix """ + web_seat = Product.objects.filter( + ~Q(stockrecords__partner_sku__icontains="mobile"), + parent__isnull=False, + course=course, + attributes__name="id_verification_required", + parent__product_class__name="Seat" + ).first() + web_stock_record = web_seat.stockrecords.first() + mobile_seat = Product.objects.create( + course=course, + parent=web_seat.parent, + structure=web_seat.structure, + expires=web_seat.expires, + is_public=web_seat.is_public, + title="{} {}".format(sku_prefix.capitalize(), web_seat.title.lower()) + ) + + mobile_seat.attr.certificate_type = web_seat.attr.certificate_type + mobile_seat.attr.course_key = web_seat.attr.course_key + mobile_seat.attr.id_verification_required = web_seat.attr.id_verification_required + mobile_seat.attr.save() + + StockRecord.objects.create( + partner=web_stock_record.partner, + product=mobile_seat, + partner_sku="mobile.{}.{}".format(sku_prefix.lower(), web_stock_record.partner_sku.lower()), + price_currency=web_stock_record.price_currency, + price_excl_tax=web_stock_record.price_excl_tax, + ) + return mobile_seat + def tearDown(self): super().tearDown() responses.stop() @@ -133,6 +168,39 @@ def test_serialize_seat_for_commerce_api(self): 'sku': stock_record.partner_sku, 'bulk_sku': None, 'expires': None, + 'android_sku': None, + 'ios_sku': None, + } + self.assertDictEqual(actual, expected) + + # Try with an expiration datetime + expires = datetime.datetime.utcnow() + seat.expires = expires + expected['expires'] = expires.isoformat() + actual = self.publisher.serialize_seat_for_commerce_api(seat) + self.assertDictEqual(actual, expected) + + def test_serialize_seat_for_commerce_api_with_mobile_skus(self): + """ Verify that mobile SKUS are added to the JSON serializable key as expected. """ + # Grab the verified seat + self._create_mobile_seat_for_course(self.course, 'android') + self._create_mobile_seat_for_course(self.course, 'ios') + seat = self.course.seat_products.filter( + ~Q(stockrecords__partner_sku__contains="mobile"), + attribute_values__value_text='verified', + ).first() + stock_record = seat.stockrecords.first() + + actual = self.publisher.serialize_seat_for_commerce_api(seat) + expected = { + 'name': 'verified', + 'currency': 'USD', + 'price': int(stock_record.price_excl_tax), + 'sku': stock_record.partner_sku, + 'bulk_sku': None, + 'expires': None, + 'android_sku': 'mobile.android.{}'.format(stock_record.partner_sku.lower()), + 'ios_sku': 'mobile.ios.{}'.format(stock_record.partner_sku.lower()), } self.assertDictEqual(actual, expected) @@ -165,6 +233,8 @@ def test_serialize_seat_for_commerce_api_with_professional(self, is_verified, ex 'sku': stock_record.partner_sku, 'bulk_sku': None, 'expires': None, + 'android_sku': None, + 'ios_sku': None, } self.assertDictEqual(actual, expected) @@ -181,6 +251,8 @@ def test_serialize_seat_with_enrollment_code(self): 'sku': stock_record.partner_sku, 'bulk_sku': ec_stock_record.partner_sku, 'expires': None, + 'android_sku': None, + 'ios_sku': None, } self.assertDictEqual(actual, expected)