From 404ab86b405c2e6a7b53c9566b6d9fd3d945ec5d Mon Sep 17 00:00:00 2001 From: Alex Dusenbery Date: Thu, 14 Mar 2024 15:47:12 -0400 Subject: [PATCH] feat: batch reminder email sends to the Braze API campaign messages endpoint. --- license_manager/apps/api/tasks.py | 54 ++++++++++--------- license_manager/apps/api/tests/test_tasks.py | 27 ++++++---- .../apps/api/v1/tests/test_views.py | 2 +- license_manager/apps/api/v1/views.py | 2 +- .../apps/subscriptions/constants.py | 1 + 5 files changed, 49 insertions(+), 37 deletions(-) diff --git a/license_manager/apps/api/tasks.py b/license_manager/apps/api/tasks.py index 67691551..5342c62f 100644 --- a/license_manager/apps/api/tasks.py +++ b/license_manager/apps/api/tasks.py @@ -211,14 +211,14 @@ def send_reminder_email_task(custom_template_text, email_recipient_list, subscri enterprise_contact_email = enterprise_customer.get('contact_email') pending_license_by_email = {} - # We need to send these emails individually, because each email's text must be - # generated for every single user/activation_key + emails_for_aliasing = [] + recipients = [] for pending_license in pending_licenses: user_email = pending_license.user_email + emails_for_aliasing.append(user_email) pending_license_by_email[user_email] = pending_license license_activation_key = str(pending_license.activation_key) - braze_campaign_id = settings.BRAZE_REMIND_EMAIL_CAMPAIGN - braze_trigger_properties = { + trigger_properties = { 'TEMPLATE_GREETING': custom_template_text['greeting'], 'TEMPLATE_CLOSING': custom_template_text['closing'], 'license_activation_key': license_activation_key, @@ -229,29 +229,31 @@ def send_reminder_email_task(custom_template_text, email_recipient_list, subscri } recipient = _aliased_recipient_object_from_email(user_email) recipient['attributes'].update(get_license_tracking_properties(pending_license)) + recipient['trigger_properties'] = trigger_properties + recipients.append(recipient) - try: - braze_client_instance = BrazeApiClient() - braze_client_instance.create_braze_alias( - [user_email], - ENTERPRISE_BRAZE_ALIAS_LABEL, - ) - braze_client_instance.send_campaign_message( - braze_campaign_id, - recipients=[recipient], - trigger_properties=braze_trigger_properties, - ) - logger.info( - f'{LICENSE_DEBUG_PREFIX} Sent license reminder email ' - f'braze campaign {braze_campaign_id} to {recipient}' - ) - except BrazeClientError as exc: - message = ( - 'Error hitting Braze API. ' - f'reminder email to {user_email} for license failed.' - ) - logger.exception(message) - raise exc + # Batch at the Braze API layer + try: + braze_client_instance = BrazeApiClient() + braze_client_instance.create_braze_alias( + emails_for_aliasing, + ENTERPRISE_BRAZE_ALIAS_LABEL, + ) + braze_client_instance.send_campaign_message( + settings.BRAZE_REMIND_EMAIL_CAMPAIGN, + recipients=recipients, + ) + logger.info( + f'{LICENSE_DEBUG_PREFIX} Sent license reminder emails ' + f'braze campaign {settings.BRAZE_REMIND_EMAIL_CAMPAIGN} to {email_recipient_list}' + ) + except BrazeClientError as exc: + message = ( + 'Error hitting Braze API. ' + f'reminder email to {settings.BRAZE_REMIND_EMAIL_CAMPAIGN} for license failed.' + ) + logger.exception(message) + raise exc License.set_date_fields_to_now(pending_licenses, ['last_remind_date']) diff --git a/license_manager/apps/api/tests/test_tasks.py b/license_manager/apps/api/tests/test_tasks.py index d3a10d52..accf3737 100644 --- a/license_manager/apps/api/tests/test_tasks.py +++ b/license_manager/apps/api/tests/test_tasks.py @@ -204,6 +204,8 @@ def test_send_reminder_email_task(self, mock_enterprise_client, mock_braze_clien ] self.assertNotIn('no-license@foo.com', called_emails) + expected_recipients = [] + expected_alias_emails = [] for user_email in self.email_recipient_list: expected_license = self.subscription_plan.licenses.get( user_email=user_email @@ -212,10 +214,7 @@ def test_send_reminder_email_task(self, mock_enterprise_client, mock_braze_clien mock_enterprise_client().get_enterprise_customer_data.assert_any_call( self.subscription_plan.enterprise_customer_uuid ) - mock_braze_client().create_braze_alias.assert_any_call( - [user_email], - ENTERPRISE_BRAZE_ALIAS_LABEL, - ) + expected_alias_emails.append(user_email) expected_trigger_properties = { 'TEMPLATE_GREETING': 'Hello', @@ -234,11 +233,21 @@ def test_send_reminder_email_task(self, mock_enterprise_client, mock_braze_clien }, } expected_recipient['attributes'].update(get_license_tracking_properties(expected_license)) - mock_braze_client().send_campaign_message.assert_any_call( - settings.BRAZE_ASSIGNMENT_EMAIL_CAMPAIGN, - recipients=[expected_recipient], - trigger_properties=expected_trigger_properties, - ) + expected_recipient['trigger_properties'] = expected_trigger_properties + expected_recipients.append(expected_recipient) + + # assert all emails sent for aliasing in a single call + actual_alias_call_emails = mock_braze_client.return_value.create_braze_alias.call_args_list[0][0][0] + assert sorted(expected_alias_emails) == sorted(actual_alias_call_emails) + + # assert all recipients sent a campaign message in a single call + mock_send_message = mock_braze_client.return_value.send_campaign_message + actual_recipients_list = mock_send_message.call_args_list[0][1]['recipients'] + + def sort_key(x): + return x['attributes']['email'] + + assert sorted(expected_recipients, key=sort_key) == sorted(actual_recipients_list, key=sort_key) # Verify the 'last_remind_date' of all licenses have been updated assert_date_fields_correct( diff --git a/license_manager/apps/api/v1/tests/test_views.py b/license_manager/apps/api/v1/tests/test_views.py index 961b6763..f7a70ad0 100644 --- a/license_manager/apps/api/v1/tests/test_views.py +++ b/license_manager/apps/api/v1/tests/test_views.py @@ -1986,7 +1986,7 @@ def test_remind_all(self, mock_send_reminder_emails_task): pending_licenses = LicenseFactory.create_batch(3, status=constants.ASSIGNED) self.subscription_plan.licenses.set(unassigned_licenses + pending_licenses) - with mock.patch('license_manager.apps.subscriptions.constants.LICENSE_BULK_OPERATION_BATCH_SIZE', new=2): + with mock.patch('license_manager.apps.subscriptions.constants.REMINDER_EMAIL_BATCH_SIZE', new=2): response = self.api_client.post(self.remind_all_url, {'greeting': self.greeting, 'closing': self.closing}) assert response.status_code == status.HTTP_204_NO_CONTENT diff --git a/license_manager/apps/api/v1/views.py b/license_manager/apps/api/v1/views.py index 747c17e5..f134e213 100644 --- a/license_manager/apps/api/v1/views.py +++ b/license_manager/apps/api/v1/views.py @@ -1006,7 +1006,7 @@ def remind_all(self, request, subscription_uuid=None): return Response('Could not find any licenses pending activation', status=status.HTTP_404_NOT_FOUND) # Send reminder emails in batches. - chunked_lists = chunks(assigned_license_emails, constants.LICENSE_BULK_OPERATION_BATCH_SIZE) + chunked_lists = chunks(assigned_license_emails, constants.REMINDER_EMAIL_BATCH_SIZE) for assigned_license_email_chunk in chunked_lists: send_reminder_email_task.delay( utils.get_custom_text(request.data), diff --git a/license_manager/apps/subscriptions/constants.py b/license_manager/apps/subscriptions/constants.py index 35eda682..03dd91d0 100644 --- a/license_manager/apps/subscriptions/constants.py +++ b/license_manager/apps/subscriptions/constants.py @@ -106,6 +106,7 @@ class SegmentEvents: PENDING_ACCOUNT_CREATION_BATCH_SIZE = 100 LICENSE_SOURCE_BULK_OPERATION_BATCH_SIZE = 100 TRACK_LICENSE_CHANGES_BATCH_SIZE = 25 +REMINDER_EMAIL_BATCH_SIZE = 50 # Num distinct catalog query validation batch size VALIDATE_NUM_CATALOG_QUERIES_BATCH_SIZE = 100