From b5a96f17f24e40d7e74016596944663c49d3b9ab Mon Sep 17 00:00:00 2001 From: jawad khan Date: Fri, 18 Aug 2023 00:12:38 +0500 Subject: [PATCH] feat: mail mobile team for a mobile course change in publisher (#4014) * feat: mail mobile team for a mobile course change in publisher This will fix any unknown change from publisher to a course having mobile seats. After this fix mobile team will see mail and adjust price of the course on playstore or appstore. In the longer run we want to replace this solution by changing the course price directly using mobile platform apis. LEARNER-9377 * fix: fixed coverage issue --- docs/additional_features/gate_ecommerce.rst | 3 + ecommerce/extensions/api/constatnts.py | 9 +++ ecommerce/extensions/api/serializers.py | 13 ++++ ecommerce/extensions/api/tests/test_utils.py | 62 +++++++++++++++++++ ecommerce/extensions/api/utils.py | 36 +++++++++++ ...rocessorconfiguration_mobile_team_email.py | 18 ++++++ ecommerce/extensions/iap/models.py | 6 ++ 7 files changed, 147 insertions(+) create mode 100644 ecommerce/extensions/api/constatnts.py create mode 100644 ecommerce/extensions/api/tests/test_utils.py create mode 100644 ecommerce/extensions/api/utils.py create mode 100644 ecommerce/extensions/iap/migrations/0006_iapprocessorconfiguration_mobile_team_email.py diff --git a/docs/additional_features/gate_ecommerce.rst b/docs/additional_features/gate_ecommerce.rst index 4c0cdbac2ef..d0377bbcd83 100644 --- a/docs/additional_features/gate_ecommerce.rst +++ b/docs/additional_features/gate_ecommerce.rst @@ -66,6 +66,9 @@ Waffle offers the following feature gates. * - disable_redundant_payment_check_for_mobile - Switch - Enable returning an error for duplicate transaction_id for mobile in-app purchases. + * - mail_mobile_team_for_change_in_course + - Switch + - Alert mobile team for a change in a course having mobile seats, so that they can adjust prices on mobile platforms. * - enable_stripe_payment_processor - Flag - Ignore client side payment processor setting and use Stripe. For background, see `frontend-app-payment 0005-stripe-custom-actions `_. diff --git a/ecommerce/extensions/api/constatnts.py b/ecommerce/extensions/api/constatnts.py new file mode 100644 index 00000000000..1e4a5deebad --- /dev/null +++ b/ecommerce/extensions/api/constatnts.py @@ -0,0 +1,9 @@ +# .. toggle_name: mail_mobile_team_for_change_in_course +# .. toggle_type: waffle_switch +# .. toggle_default: False +# .. toggle_description: Alert mobile team for a change in a course having mobile seats. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2023-07-25 +# .. toggle_tickets: LEARNER-9377 +# .. toggle_status: supported +MAIL_MOBILE_TEAM_FOR_CHANGE_IN_COURSE = 'mail_mobile_team_for_change_in_course' diff --git a/ecommerce/extensions/api/serializers.py b/ecommerce/extensions/api/serializers.py index 5e8b29f94d5..11825a2c891 100644 --- a/ecommerce/extensions/api/serializers.py +++ b/ecommerce/extensions/api/serializers.py @@ -46,6 +46,8 @@ get_enterprise_customer_uuid_from_voucher ) from ecommerce.entitlements.utils import create_or_update_course_entitlement +from ecommerce.extensions.api.constatnts import MAIL_MOBILE_TEAM_FOR_CHANGE_IN_COURSE +from ecommerce.extensions.api.utils import send_mail_to_mobile_team_for_change_in_course from ecommerce.extensions.api.v2.constants import ( ENABLE_HOIST_ORDER_HISTORY, REFUND_ORDER_EMAIL_CLOSING, @@ -820,6 +822,13 @@ def validate_products(self, products): return products + def _get_seats_offered_on_mobile(self, course): + certificate_type_query = Q(attributes__name='certificate_type', attribute_values__value_text='verified') + mobile_query = Q(stockrecords__partner_sku__contains='mobile') + mobile_seats = course.seat_products.filter(certificate_type_query & mobile_query) + + return mobile_seats + def get_partner(self): """Validate partner""" if not self.partner: @@ -879,6 +888,10 @@ def save(self): # pylint: disable=arguments-differ published = (resp_message is None) if published: + mobile_seats = self._get_seats_offered_on_mobile(course) + if waffle.switch_is_active(MAIL_MOBILE_TEAM_FOR_CHANGE_IN_COURSE) and mobile_seats: + send_mail_to_mobile_team_for_change_in_course(course, mobile_seats) + return created, None, None raise Exception(resp_message) diff --git a/ecommerce/extensions/api/tests/test_utils.py b/ecommerce/extensions/api/tests/test_utils.py new file mode 100644 index 00000000000..cc81c0293d1 --- /dev/null +++ b/ecommerce/extensions/api/tests/test_utils.py @@ -0,0 +1,62 @@ +import mock +from testfixtures import LogCapture + +from ecommerce.courses.tests.factories import CourseFactory +from ecommerce.extensions.api.utils import send_mail_to_mobile_team_for_change_in_course +from ecommerce.extensions.iap.models import IAPProcessorConfiguration +from ecommerce.tests.testcases import TestCase + + +class UtilTests(TestCase): + def setUp(self): + super(UtilTests, self).setUp() + self.course = CourseFactory(id='test/course/123', name='Test Course 123') + seat = self.course.create_or_update_seat('verified', True, 60) + second_seat = self.course.create_or_update_seat('verified', True, 70) + self.mock_mobile_team_mail = 'abc@example.com' + self.mock_email_body = { + 'subject': 'Course Change Alert for Test Course 123', + 'body': 'Course: Test Course 123, Sku: {}, Price: 70.00\n' + 'Course: Test Course 123, Sku: {}, Price: 60.00'.format( + second_seat.stockrecords.all()[0].partner_sku, + seat.stockrecords.all()[0].partner_sku + ) + } + + def test_send_mail_to_mobile_team_with_no_email_specified(self): + logger_name = 'ecommerce.extensions.api.utils' + email_sender = 'ecommerce.extensions.communication.utils.Dispatcher.dispatch_direct_messages' + msg_t = "Couldn't mail mobile team for change in {}. No email was specified for mobile team in configurations" + msg = msg_t.format(self.course.name) + with LogCapture(logger_name) as utils_logger,\ + mock.patch(email_sender) as mock_send_email: + + send_mail_to_mobile_team_for_change_in_course(self.course, self.course.seat_products.all()) + utils_logger.check_present( + ( + logger_name, + 'INFO', + msg + ) + ) + assert mock_send_email.call_count == 0 + + def test_send_mail_to_mobile_team(self): + logger_name = 'ecommerce.extensions.api.utils' + email_sender = 'ecommerce.extensions.communication.utils.Dispatcher.dispatch_direct_messages' + iap_configs = IAPProcessorConfiguration.get_solo() + iap_configs.mobile_team_email = self.mock_mobile_team_mail + iap_configs.save() + with LogCapture(logger_name) as utils_logger,\ + mock.patch(email_sender) as mock_send_email: + + send_mail_to_mobile_team_for_change_in_course(self.course, self.course.seat_products.all()) + utils_logger.check_present( + ( + logger_name, + 'INFO', + "Sent change in {} email to mobile team.".format(self.course.name) + ) + ) + assert mock_send_email.call_count == 1 + mock_send_email.assert_called_with(self.mock_mobile_team_mail, self.mock_email_body) diff --git a/ecommerce/extensions/api/utils.py b/ecommerce/extensions/api/utils.py new file mode 100644 index 00000000000..d3ce55b71eb --- /dev/null +++ b/ecommerce/extensions/api/utils.py @@ -0,0 +1,36 @@ +import logging + +from oscar.core.loading import get_class + +from ecommerce.extensions.iap.models import IAPProcessorConfiguration + +Dispatcher = get_class('communication.utils', 'Dispatcher') +logger = logging.getLogger(__name__) + + +def send_mail_to_mobile_team_for_change_in_course(course, seats): + recipient = IAPProcessorConfiguration.get_solo().mobile_team_email + if not recipient: + msg = "Couldn't mail mobile team for change in %s. No email was specified for mobile team in configurations" + logger.info(msg, course.name) + return + + def format_seat(seat): + seat_template = "Course: {}, Sku: {}, Price: {}" + stock_record = seat.stockrecords.all()[0] + result = seat_template.format( + course.name, + stock_record.partner_sku, + stock_record.price_excl_tax, + ) + return result + + formatted_seats = [format_seat(seat) for seat in seats if seat.stockrecords.all()] + + messages = { + 'subject': 'Course Change Alert for {}'.format(course.name), + 'body': "\n".join(formatted_seats) + } + + Dispatcher().dispatch_direct_messages(recipient, messages) + logger.info("Sent change in %s email to mobile team.", course.name) diff --git a/ecommerce/extensions/iap/migrations/0006_iapprocessorconfiguration_mobile_team_email.py b/ecommerce/extensions/iap/migrations/0006_iapprocessorconfiguration_mobile_team_email.py new file mode 100644 index 00000000000..bd39130ea1c --- /dev/null +++ b/ecommerce/extensions/iap/migrations/0006_iapprocessorconfiguration_mobile_team_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.19 on 2023-08-02 08:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('iap', '0005_paymentprocessorresponseextension_meta_data'), + ] + + operations = [ + migrations.AddField( + model_name='iapprocessorconfiguration', + name='mobile_team_email', + field=models.EmailField(default='', max_length=254, verbose_name='mobile team email'), + ), + ] diff --git a/ecommerce/extensions/iap/models.py b/ecommerce/extensions/iap/models.py index cf301084def..eea2659bba8 100644 --- a/ecommerce/extensions/iap/models.py +++ b/ecommerce/extensions/iap/models.py @@ -22,6 +22,12 @@ class IAPProcessorConfiguration(SingletonModel): ) ) + mobile_team_email = models.EmailField( + default='', + verbose_name=_('mobile team email'), + max_length=254 + ) + class Meta: verbose_name = "IAP Processor Configuration"