diff --git a/ecommerce/extensions/api/serializers.py b/ecommerce/extensions/api/serializers.py index 8ca14721d26..6bf959d4472 100644 --- a/ecommerce/extensions/api/serializers.py +++ b/ecommerce/extensions/api/serializers.py @@ -57,7 +57,8 @@ ) from ecommerce.extensions.catalogue.utils import attach_vouchers_to_coupon_product from ecommerce.extensions.checkout.views import ReceiptResponseView -from ecommerce.extensions.iap.api.v1.utils import apply_price_of_inapp_purchase, get_auth_headers, create_ios_product +from ecommerce.extensions.iap.constants import CREATE_APPSTORE_PRODUCTS_FOR_INAPP +from ecommerce.extensions.iap.api.v1.utils import apply_price_of_inapp_purchase, create_ios_product, get_auth_headers from ecommerce.extensions.iap.constants import ANDROID_SKU_PREFIX, IOS_SKU_PREFIX from ecommerce.extensions.iap.processors.ios_iap import IOSIAP from ecommerce.extensions.iap.utils import create_mobile_seat @@ -839,16 +840,18 @@ def _update_or_create_mobile_seats(self, course): if mobile_seats: self._update_mobile_seats(mobile_seats, web_seat, course) else: + logger.info("Creating mobile seats for course [%s]", course.id) create_mobile_seat(ANDROID_SKU_PREFIX, web_seat) ios_seat = create_mobile_seat(IOS_SKU_PREFIX, web_seat) - partner_short_code = self.context['request'].site.siteconfiguration.partner.short_code - configuration = settings.PAYMENT_PROCESSOR_CONFIG[partner_short_code.lower()][IOSIAP.NAME.lower()] - course_data = { - 'price': ios_seat.stockrecords.first().price_excl_tax, - 'name': course.name, - 'key': course.id - } - create_ios_product(course_data, ios_seat, configuration) + if waffle.switch_is_active(CREATE_APPSTORE_PRODUCTS_FOR_INAPP): + partner_short_code = self.context['request'].site.siteconfiguration.partner.short_code + configuration = settings.PAYMENT_PROCESSOR_CONFIG[partner_short_code.lower()][IOSIAP.NAME.lower()] + course_data = { + 'price': ios_seat.price_excl_tax, + 'name': course.name, + 'key': course.id + } + create_ios_product(course_data, ios_seat, configuration) def _update_mobile_seats(self, mobile_seats, web_seat, course): failure_msg = False @@ -942,7 +945,6 @@ def save(self): # pylint: disable=arguments-differ resp_message = course.publish_to_lms() published = (resp_message is None) - if published: self._update_or_create_mobile_seats(course) diff --git a/ecommerce/extensions/api/v2/tests/views/test_publication.py b/ecommerce/extensions/api/v2/tests/views/test_publication.py index adc881ac848..a76672dc47c 100644 --- a/ecommerce/extensions/api/v2/tests/views/test_publication.py +++ b/ecommerce/extensions/api/v2/tests/views/test_publication.py @@ -24,6 +24,7 @@ from ecommerce.entitlements.utils import create_or_update_course_entitlement from ecommerce.extensions.api.v2.tests.views import JSON_CONTENT_TYPE from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin +from ecommerce.extensions.iap.constants import CREATE_APPSTORE_PRODUCTS_FOR_INAPP from ecommerce.extensions.iap.utils import create_child_products_for_mobile from ecommerce.tests.testcases import TestCase @@ -158,6 +159,7 @@ def setUp(self): self.client.login(username=self.user.username, password=self.password) self.publication_switch = toggle_switch('publish_course_modes_to_lms', True) + self.appstore_product_switch = toggle_switch(CREATE_APPSTORE_PRODUCTS_FOR_INAPP, False) def _toggle_publication(self, is_enabled): """Toggle LMS publication.""" @@ -233,7 +235,7 @@ def assert_entitlement_saved(self, course, expected): self.assertEqual(entitlement.attr.UUID, self.course_uuid) self.assertEqual(entitlement.stockrecords.get(partner=self.partner).price_excl_tax, expected['price']) - def assert_seat_saved(self, course, expected, test_mobile_seats=False): + def assert_seat_saved(self, course, expected, test_mobile_seats=True): certificate_type = '' verified_product = False @@ -258,17 +260,17 @@ def assert_seat_saved(self, course, expected, test_mobile_seats=False): self.assertEqual(seat.stockrecords.get(partner=self.partner).price_excl_tax, expected['price']) if test_mobile_seats and verified_product: - android_seat = course.seat_products.get(title='Android ' + seat_title) + android_seat = course.seat_products.get(title='Android ' + seat_title.lower()) self.assertEqual(android_seat.expires, expires) self.assertEqual(android_seat.stockrecords.get(partner=self.partner).price_excl_tax, expected['price']) - ios_seat = course.seat_products.get(title='Ios ' + seat_title) + ios_seat = course.seat_products.get(title='Ios ' + seat_title.lower()) self.assertEqual(ios_seat.expires, expires) self.assertEqual(ios_seat.stockrecords.get(partner=self.partner).price_excl_tax, expected['price']) return seat - def assert_course_saved(self, course_id, expected, enrollment_code_count=0, test_mobile_seats=False): + def assert_course_saved(self, course_id, expected, enrollment_code_count=0, test_mobile_seats=True): """Verify that the expected Course and associated products have been saved.""" # Verify that Course was saved. self.assertTrue(Course.objects.filter(id=course_id).exists()) @@ -343,7 +345,7 @@ def test_update(self): response = self.client.put(self.update_path, json.dumps(updated_data), JSON_CONTENT_TYPE) self.assertEqual(response.status_code, 500) self.assertEqual(response.data.get('error'), error_msg) - self.assert_course_saved(self.course_id, expected=self.data) + self.assert_course_saved(self.course_id, expected=self.data, test_mobile_seats=False) # If publication succeeds, the view should return a 200 and data should be saved. mock_publish.return_value = None @@ -512,8 +514,8 @@ def test_mobile_seats_update(self, _, __): # Since we are only concerned with expiry date and price # therefore we are setting title manually here. - android_seat.product.title = 'Android Seat in A New Name with verified certificate' - ios_seat.product.title = 'Ios Seat in A New Name with verified certificate' + android_seat.product.title = 'Android seat in a new name with verified certificate' + ios_seat.product.title = 'Ios seat in a new name with verified certificate' android_seat.product.save() ios_seat.product.save() with mock.patch.object(LMSPublisher, 'publish') as mock_publish: @@ -523,5 +525,21 @@ def test_mobile_seats_update(self, _, __): response = self.client.put(self.update_path, json.dumps(updated_data), JSON_CONTENT_TYPE) self.assertEqual(response.status_code, 200) - self.assert_course_saved(self.course_id, expected=updated_data, - enrollment_code_count=1, test_mobile_seats=True) + self.assert_course_saved(self.course_id, expected=updated_data, enrollment_code_count=1) + + @mock.patch('ecommerce.extensions.api.serializers.create_ios_product') + def test_ios_seat_created(self, mock_create_ios_product): + """Verify that a Course and associated mobile products can be updated and published.""" + self.create_course_and_seats() + updated_data = self.generate_update_payload() + + with mock.patch.object(LMSPublisher, 'publish') as mock_publish: + # If publication succeeds, the view should return a 200 and data should be saved. + mock_publish.return_value = None + self.appstore_product_switch.active = True + self.appstore_product_switch.save() + + response = self.client.put(self.update_path, json.dumps(updated_data), JSON_CONTENT_TYPE) + mock_create_ios_product.assert_called_once() + self.assertEqual(response.status_code, 200) + self.assert_course_saved(self.course_id, expected=updated_data, enrollment_code_count=1) diff --git a/ecommerce/extensions/iap/api/v1/views.py b/ecommerce/extensions/iap/api/v1/views.py index a896421dd22..87033d4a4a6 100644 --- a/ecommerce/extensions/iap/api/v1/views.py +++ b/ecommerce/extensions/iap/api/v1/views.py @@ -4,6 +4,7 @@ import app_store_notifications_v2_validator as asn2 import httplib2 +import waffle from django.conf import settings from django.db import transaction from django.utils.decorators import method_decorator @@ -84,6 +85,7 @@ from ecommerce.extensions.iap.api.v1.exceptions import RefundCompletionException from ecommerce.extensions.iap.api.v1.serializers import MobileOrderSerializer from ecommerce.extensions.iap.api.v1.utils import create_ios_product, products_in_basket_already_purchased +from ecommerce.extensions.iap.constants import CREATE_APPSTORE_PRODUCTS_FOR_INAPP from ecommerce.extensions.iap.models import IAPProcessorConfiguration from ecommerce.extensions.iap.processors.android_iap import AndroidIAP from ecommerce.extensions.iap.processors.ios_iap import IOSIAP @@ -487,18 +489,19 @@ def post(self, request): course.publish_to_lms() created_skus[course_run_key] = [mobile_products[0].partner_sku, mobile_products[1].partner_sku] - # create ios product on appstore - partner_short_code = request.site.siteconfiguration.partner.short_code - configuration = settings.PAYMENT_PROCESSOR_CONFIG[partner_short_code.lower()][IOSIAP.NAME.lower()] - ios_product = list((filter(lambda sku: 'ios' in sku.partner_sku, mobile_products)))[0] - course_data = { - 'price': ios_product.price_excl_tax, - 'name': course.name, - 'key': course_run_key - } - error_msg = create_ios_product(course_data, ios_product, configuration) - if error_msg: - failed_ios_products.append(error_msg) + if waffle.switch_is_active(CREATE_APPSTORE_PRODUCTS_FOR_INAPP): + # create ios product on appstore + partner_short_code = request.site.siteconfiguration.partner.short_code + configuration = settings.PAYMENT_PROCESSOR_CONFIG[partner_short_code.lower()][IOSIAP.NAME.lower()] + ios_product = list((filter(lambda sku: 'ios' in sku.partner_sku, mobile_products)))[0] + course_data = { + 'price': ios_product.price_excl_tax, + 'name': course.name, + 'key': course_run_key + } + error_msg = create_ios_product(course_data, ios_product, configuration) + if error_msg: + failed_ios_products.append(error_msg) result = { 'new_mobile_skus': created_skus, diff --git a/ecommerce/extensions/iap/constants.py b/ecommerce/extensions/iap/constants.py index 2847da95e90..1bfddff4cd8 100644 --- a/ecommerce/extensions/iap/constants.py +++ b/ecommerce/extensions/iap/constants.py @@ -2,3 +2,13 @@ ANDROID_SKU_PREFIX = 'android' IOS_SKU_PREFIX = 'ios' MISSING_WEB_SEAT_ERROR = "Couldn't find existing web seat for course [%s]" + +# .. toggle_name: create_appstore_products_for_inapp +# .. toggle_type: waffle_switch +# .. toggle_default: False +# .. toggle_description: Create ios products on appstore using Apple in-app apis. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2023-07-25 +# .. toggle_tickets: LEARNER-9951 +# .. toggle_status: supported +CREATE_APPSTORE_PRODUCTS_FOR_INAPP = 'create_appstore_products_for_inapp'