diff --git a/engine/apps/alerts/incident_appearance/renderers/base_renderer.py b/engine/apps/alerts/incident_appearance/renderers/base_renderer.py index d316123900..2207c354d5 100644 --- a/engine/apps/alerts/incident_appearance/renderers/base_renderer.py +++ b/engine/apps/alerts/incident_appearance/renderers/base_renderer.py @@ -1,10 +1,11 @@ import typing from abc import ABC, abstractmethod +from django.db.models import QuerySet from django.utils.functional import cached_property if typing.TYPE_CHECKING: - from apps.alerts.models import Alert, AlertGroup + from apps.alerts.models import Alert, AlertGroup, BundledNotification class AlertBaseRenderer(ABC): @@ -33,3 +34,11 @@ def __init__(self, alert_group: "AlertGroup", alert: typing.Optional["Alert"] = @abstractmethod def alert_renderer_class(self): raise NotImplementedError + + +class AlertGroupBundleBaseRenderer: + MAX_ALERT_GROUPS_TO_RENDER = 3 + MAX_CHANNELS_TO_RENDER = 1 + + def __init__(self, notifications: "QuerySet[BundledNotification]"): + self.notifications = notifications diff --git a/engine/apps/alerts/incident_appearance/renderers/sms_renderer.py b/engine/apps/alerts/incident_appearance/renderers/sms_renderer.py index fe533fc4e1..8044bb0ac8 100644 --- a/engine/apps/alerts/incident_appearance/renderers/sms_renderer.py +++ b/engine/apps/alerts/incident_appearance/renderers/sms_renderer.py @@ -1,4 +1,10 @@ -from apps.alerts.incident_appearance.renderers.base_renderer import AlertBaseRenderer, AlertGroupBaseRenderer +from django.db.models import Count + +from apps.alerts.incident_appearance.renderers.base_renderer import ( + AlertBaseRenderer, + AlertGroupBaseRenderer, + AlertGroupBundleBaseRenderer, +) from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE from apps.alerts.incident_appearance.templaters import AlertSmsTemplater from common.utils import str_or_backup @@ -24,3 +30,53 @@ def render(self): f"integration: {self.alert_group.channel.short_name}, " f"alerts registered: {self.alert_group.alerts.count()}." ) + + +class AlertGroupSMSBundleRenderer(AlertGroupBundleBaseRenderer): + def render(self) -> str: + """ + Renders SMS message for notification bundle: gets total count of unique alert groups and alert receive channels + in the bundle, renders text with `inside_organization_number` of 3 alert groups (MAX_ALERT_GROUPS_TO_RENDER) and + `short_name` of 1 alert receive channel (MAX_CHANNELS_TO_RENDER). If there are more unique alert groups / alert + receive channels to notify about, adds "and X more" to the SMS message + """ + + channels_and_alert_groups_count = self.notifications.aggregate( + channels_count=Count("alert_receive_channel", distinct=True), + alert_groups_count=Count("alert_group", distinct=True), + ) + alert_groups_count = channels_and_alert_groups_count["alert_groups_count"] + channels_count = channels_and_alert_groups_count["channels_count"] + + # get 3 unique alert groups from notifications + alert_groups_to_render = [] + for notification in self.notifications: + if notification.alert_group not in alert_groups_to_render: + alert_groups_to_render.append(notification.alert_group) + if len(alert_groups_to_render) == self.MAX_ALERT_GROUPS_TO_RENDER: + break + # render text for alert groups + alert_group_inside_organization_numbers = [ + alert_group.inside_organization_number for alert_group in alert_groups_to_render + ] + numbers_str = ", ".join(f"#{x}" for x in alert_group_inside_organization_numbers) + alert_groups_text = "Alert groups " if alert_groups_count > 1 else "Alert group " + alert_groups_text += numbers_str + + if alert_groups_count > self.MAX_ALERT_GROUPS_TO_RENDER: + alert_groups_text += f" and {alert_groups_count - self.MAX_ALERT_GROUPS_TO_RENDER} more" + + # render text for alert receive channels + channels_to_render = [alert_groups_to_render[i].channel for i in range(self.MAX_CHANNELS_TO_RENDER)] + channel_names = ", ".join([channel.short_name for channel in channels_to_render]) + channels_text = "integrations: " if channels_count > 1 else "integration: " + channels_text += channel_names + + if channels_count > self.MAX_CHANNELS_TO_RENDER: + channels_text += f" and {channels_count - self.MAX_CHANNELS_TO_RENDER} more" + + return ( + f"Grafana OnCall: {alert_groups_text} " + f"from stack: {channels_to_render[0].organization.stack_slug}, " + f"{channels_text}." + ) diff --git a/engine/apps/alerts/tests/test_alert_group_renderer.py b/engine/apps/alerts/tests/test_alert_group_renderer.py index 911cd61fb1..8cc1249e9e 100644 --- a/engine/apps/alerts/tests/test_alert_group_renderer.py +++ b/engine/apps/alerts/tests/test_alert_group_renderer.py @@ -1,7 +1,9 @@ import pytest +from apps.alerts.incident_appearance.renderers.sms_renderer import AlertGroupSMSBundleRenderer from apps.alerts.incident_appearance.templaters import AlertSlackTemplater, AlertWebTemplater from apps.alerts.models import AlertGroup +from apps.base.models import UserNotificationPolicy from config_integrations import grafana @@ -163,3 +165,58 @@ def test_get_resolved_text( alert_group.resolve(resolved_by=source, resolved_by_user=user) assert alert_group.get_resolve_text() == expected_text.format(username=user.get_username_with_slack_verbal()) + + +@pytest.mark.django_db +def test_alert_group_sms_bundle_renderer( + make_organization_and_user, + make_alert_receive_channel, + make_alert_group, + make_user_notification_bundle, +): + organization, user = make_organization_and_user() + alert_receive_channel_1 = make_alert_receive_channel( + organization, + ) + alert_receive_channel_2 = make_alert_receive_channel( + organization, + ) + alert_group_1 = make_alert_group(alert_receive_channel_1) + alert_group_2 = make_alert_group(alert_receive_channel_1) + alert_group_3 = make_alert_group(alert_receive_channel_1) + alert_group_4 = make_alert_group(alert_receive_channel_2) + + notification_bundle = make_user_notification_bundle(user, UserNotificationPolicy.NotificationChannel.SMS) + + # render 1 alert group and 1 channel + notification_bundle.append_notification(alert_group_1, None) + renderer = AlertGroupSMSBundleRenderer(notification_bundle.notifications.all()) + message = renderer.render() + assert message == ( + f"Grafana OnCall: Alert group #{alert_group_1.inside_organization_number} " + f"from stack: {organization.stack_slug}, " + f"integration: {alert_receive_channel_1.short_name}." + ) + + # render 3 alert groups and 1 channel + notification_bundle.append_notification(alert_group_2, None) + notification_bundle.append_notification(alert_group_3, None) + renderer = AlertGroupSMSBundleRenderer(notification_bundle.notifications.all()) + message = renderer.render() + assert message == ( + f"Grafana OnCall: Alert groups #{alert_group_1.inside_organization_number}, " + f"#{alert_group_2.inside_organization_number}, #{alert_group_3.inside_organization_number} " + f"from stack: {organization.stack_slug}, " + f"integration: {alert_receive_channel_1.short_name}." + ) + + # render 4 alert groups and 2 channels + notification_bundle.append_notification(alert_group_4, None) + renderer = AlertGroupSMSBundleRenderer(notification_bundle.notifications.all()) + message = renderer.render() + assert message == ( + f"Grafana OnCall: Alert groups #{alert_group_1.inside_organization_number}, " + f"#{alert_group_2.inside_organization_number}, #{alert_group_3.inside_organization_number} and 1 more " + f"from stack: {organization.stack_slug}, " + f"integrations: {alert_receive_channel_1.short_name} and 1 more." + ) diff --git a/engine/apps/exotel/status_callback.py b/engine/apps/exotel/status_callback.py index 4132075cde..90411f4fee 100644 --- a/engine/apps/exotel/status_callback.py +++ b/engine/apps/exotel/status_callback.py @@ -15,7 +15,7 @@ def get_call_status_callback_url(): def update_exotel_call_status(call_id: str, call_status: str, user_choice: Optional[str] = None): - from apps.base.models import UserNotificationPolicyLogRecord + from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord status_code = ExotelCallStatuses.DETERMINANT.get(call_status) if status_code is None: @@ -62,12 +62,8 @@ def update_exotel_call_status(call_id: str, call_status: str, user_choice: Optio author=phone_call_record.receiver, notification_policy=phone_call_record.notification_policy, alert_group=phone_call_record.represents_alert_group, - notification_step=phone_call_record.notification_policy.step - if phone_call_record.notification_policy - else None, - notification_channel=phone_call_record.notification_policy.notify_by - if phone_call_record.notification_policy - else None, + notification_step=UserNotificationPolicy.Step.NOTIFY, + notification_channel=UserNotificationPolicy.NotificationChannel.PHONE_CALL, ) log_record.save() logger.info( diff --git a/engine/apps/phone_notifications/migrations/0003_smsrecord_represents_bundle_uuid.py b/engine/apps/phone_notifications/migrations/0003_smsrecord_represents_bundle_uuid.py new file mode 100644 index 0000000000..cf5c89ea27 --- /dev/null +++ b/engine/apps/phone_notifications/migrations/0003_smsrecord_represents_bundle_uuid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-07-03 08:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('phone_notifications', '0002_bannedphonenumber'), + ] + + operations = [ + migrations.AddField( + model_name='smsrecord', + name='represents_bundle_uuid', + field=models.CharField(db_index=True, default=None, max_length=100, null=True), + ), + ] diff --git a/engine/apps/phone_notifications/models/sms.py b/engine/apps/phone_notifications/models/sms.py index 9763d706f9..2eabc9eec9 100644 --- a/engine/apps/phone_notifications/models/sms.py +++ b/engine/apps/phone_notifications/models/sms.py @@ -45,7 +45,7 @@ class Meta: notification_policy = models.ForeignKey( "base.UserNotificationPolicy", on_delete=models.SET_NULL, null=True, default=None ) - + represents_bundle_uuid = models.CharField(max_length=100, null=True, default=None, db_index=True) receiver = models.ForeignKey("user_management.User", on_delete=models.CASCADE, null=True, default=None) grafana_cloud_notification = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) diff --git a/engine/apps/phone_notifications/phone_backend.py b/engine/apps/phone_notifications/phone_backend.py index 71ccf626ea..19d6bc88df 100644 --- a/engine/apps/phone_notifications/phone_backend.py +++ b/engine/apps/phone_notifications/phone_backend.py @@ -1,14 +1,15 @@ import logging -from typing import Optional +from typing import Optional, Tuple import requests from django.conf import settings from apps.alerts.incident_appearance.renderers.phone_call_renderer import AlertGroupPhoneCallRenderer -from apps.alerts.incident_appearance.renderers.sms_renderer import AlertGroupSmsRenderer +from apps.alerts.incident_appearance.renderers.sms_renderer import AlertGroupSMSBundleRenderer, AlertGroupSmsRenderer from apps.alerts.signals import user_notification_action_triggered_signal from apps.base.utils import live_settings from common.api_helpers.utils import create_engine_url +from common.custom_celery_tasks import shared_dedicated_queue_retry_task from common.utils import clean_markup from .exceptions import ( @@ -27,6 +28,19 @@ logger = logging.getLogger(__name__) +@shared_dedicated_queue_retry_task( + autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None +) +def notify_by_sms_bundle_async_task(user_id, bundle_uuid): + from apps.user_management.models import User + + user = User.objects.filter(id=user_id).first() + if not user: + return + phone_backend = PhoneBackend() + phone_backend.notify_by_sms_bundle(user, bundle_uuid) + + class PhoneBackend: def __init__(self): self.phone_provider: PhoneProvider = self._get_phone_provider() @@ -148,16 +162,90 @@ def notify_by_sms(self, user, alert_group, notification_policy): from apps.base.models import UserNotificationPolicyLogRecord - log_record_error_code = None - renderer = AlertGroupSmsRenderer(alert_group) message = renderer.render() + _, log_record_error_code = self._send_sms( + user=user, + alert_group=alert_group, + notification_policy=notification_policy, + message=message, + ) + + if log_record_error_code is not None: + log_record = UserNotificationPolicyLogRecord( + author=user, + type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, + notification_policy=notification_policy, + alert_group=alert_group, + notification_error_code=log_record_error_code, + notification_step=notification_policy.step if notification_policy else None, + notification_channel=notification_policy.notify_by if notification_policy else None, + ) + log_record.save() + user_notification_action_triggered_signal.send(sender=PhoneBackend.notify_by_sms, log_record=log_record) + + @staticmethod + def notify_by_sms_bundle_async(user, bundle_uuid): + notify_by_sms_bundle_async_task.apply_async((user.id, bundle_uuid)) + + def notify_by_sms_bundle(self, user, bundle_uuid): + """ + notify_by_sms_bundle sends an sms notification bundle to a user using configured phone provider. + It handles business logic - limits, cloud notifications and UserNotificationPolicyLogRecord creation. + It creates UserNotificationPolicyLogRecord for every notification in bundle, but only one SMSRecord. + SMS itself is handled by phone provider. + """ + + from apps.alerts.models import BundledNotification + from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord + + notifications = BundledNotification.objects.filter(bundle_uuid=bundle_uuid).select_related("alert_group") + + if not notifications: + logger.info("Notification bundle is empty, related alert groups might have been deleted") + return + renderer = AlertGroupSMSBundleRenderer(notifications) + message = renderer.render() + + _, log_record_error_code = self._send_sms(user=user, message=message, bundle_uuid=bundle_uuid) + if log_record_error_code is not None: + log_records_to_create = [] + for notification in notifications: + log_record = UserNotificationPolicyLogRecord( + author=user, + type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, + notification_policy=notification.notification_policy, + alert_group=notification.alert_group, + notification_error_code=log_record_error_code, + notification_step=UserNotificationPolicy.Step.NOTIFY, + notification_channel=UserNotificationPolicy.NotificationChannel.SMS, + ) + log_records_to_create.append(log_record) + if log_records_to_create: + if log_record_error_code in UserNotificationPolicyLogRecord.ERRORS_TO_SEND_IN_SLACK_CHANNEL: + # create last log record outside of the bulk_create to get it as an object to send + # the user_notification_action_triggered_signal + log_record = log_records_to_create.pop() + log_record.save() + user_notification_action_triggered_signal.send( + sender=PhoneBackend.notify_by_sms_bundle, log_record=log_record + ) + + UserNotificationPolicyLogRecord.objects.bulk_create(log_records_to_create, batch_size=5000) + + def _send_sms( + self, user, message, alert_group=None, notification_policy=None, bundle_uuid=None + ) -> Tuple[bool, Optional[int]]: + from apps.base.models import UserNotificationPolicyLogRecord + + log_record_error_code = None record = SMSRecord( represents_alert_group=alert_group, receiver=user, notification_policy=notification_policy, exceeded_limit=False, + represents_bundle_uuid=bundle_uuid, ) try: @@ -180,22 +268,7 @@ def notify_by_sms(self, user, alert_group, notification_policy): except NumberNotVerified: log_record_error_code = UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_PHONE_NUMBER_IS_NOT_VERIFIED - if log_record_error_code is not None: - log_record = UserNotificationPolicyLogRecord( - author=user, - type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, - notification_policy=notification_policy, - alert_group=alert_group, - notification_error_code=log_record_error_code, - notification_step=notification_policy.step if notification_policy else None, - notification_channel=notification_policy.notify_by if notification_policy else None, - ) - log_record.save() - user_notification_action_triggered_signal.send(sender=PhoneBackend.notify_by_sms, log_record=log_record) - - @staticmethod - def notify_by_sms_bundle_async(user, bundle_uuid): - pass # todo: will be added in a separate PR + return log_record_error_code is None, log_record_error_code def _notify_by_provider_sms(self, user, message) -> Optional[ProviderSMS]: """ diff --git a/engine/apps/phone_notifications/tests/test_phone_backend_sms.py b/engine/apps/phone_notifications/tests/test_phone_backend_sms.py index ff9b07aa99..f8b52006f6 100644 --- a/engine/apps/phone_notifications/tests/test_phone_backend_sms.py +++ b/engine/apps/phone_notifications/tests/test_phone_backend_sms.py @@ -1,7 +1,9 @@ from unittest import mock +from unittest.mock import patch import pytest from django.test import override_settings +from django.utils import timezone from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord from apps.phone_notifications.exceptions import ( @@ -234,3 +236,62 @@ def test_notify_by_cloud_sms_handles_exceptions_from_cloud( ).count() == 1 ) + + +@pytest.mark.django_db +def test_notify_by_sms_bundle( + make_organization_and_user, + make_alert_receive_channel, + make_alert_group, + make_user_notification_bundle, + make_user_notification_policy, +): + organization, user = make_organization_and_user() + alert_receive_channel = make_alert_receive_channel(organization) + alert_group_1 = make_alert_group(alert_receive_channel) + alert_group_2 = make_alert_group(alert_receive_channel) + notification_policy = make_user_notification_policy( + user=user, + step=UserNotificationPolicy.Step.NOTIFY, + notify_by=UserNotificationPolicy.NotificationChannel.SMS, + ) + notification_bundle = make_user_notification_bundle( + user, UserNotificationPolicy.NotificationChannel.SMS, notification_task_id="test_task_id", eta=timezone.now() + ) + notification_bundle.append_notification(alert_group_1, notification_policy) + notification_bundle.append_notification(alert_group_2, notification_policy) + + bundle_uuid = "test_notifications_bundle" + + notification_bundle.notifications.update(bundle_uuid=bundle_uuid) + + assert not user.personal_log_records.exists() + assert not user.smsrecord_set.exists() + + with patch( + "apps.phone_notifications.phone_backend.PhoneBackend._notify_by_cloud_sms", side_effect=SMSLimitExceeded + ): + phone_backend = PhoneBackend() + phone_backend.notify_by_sms_bundle(user, bundle_uuid) + + # check that 2 error log records (1 for each bundled notification) and 1 sms record have been created + assert ( + user.personal_log_records.filter( + notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_SMS_LIMIT_EXCEEDED + ).count() + == notification_bundle.notifications.count() + == 2 + ) + assert user.smsrecord_set.count() == 1 + + with patch("apps.phone_notifications.phone_backend.PhoneBackend._notify_by_cloud_sms"): + phone_backend = PhoneBackend() + phone_backend.notify_by_sms_bundle(user, bundle_uuid) + + # check that 0 new error log records and 1 new sms record have been created + assert ( + user.personal_log_records.filter(notification_error_code__isnull=False).count() + == notification_bundle.notifications.count() + == 2 + ) + assert user.smsrecord_set.count() == 2 diff --git a/engine/apps/twilioapp/status_callback.py b/engine/apps/twilioapp/status_callback.py index 55de098af6..fcd16a5c1c 100644 --- a/engine/apps/twilioapp/status_callback.py +++ b/engine/apps/twilioapp/status_callback.py @@ -2,7 +2,8 @@ from django.urls import reverse -from apps.alerts.signals import user_notification_action_triggered_signal +from apps.alerts.models import BundledNotification +from apps.alerts.tasks import send_update_log_report_signal from apps.twilioapp.models import TwilioCallStatuses, TwilioPhoneCall, TwilioSMS, TwilioSMSstatuses from common.api_helpers.utils import create_engine_url @@ -20,7 +21,7 @@ def update_twilio_call_status(call_sid, call_status): Returns: """ - from apps.base.models import UserNotificationPolicyLogRecord + from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord if call_sid and call_status: logger.info(f"twilioapp.update_twilio_call_status: processing sid={call_sid} status={call_status}") @@ -68,19 +69,14 @@ def update_twilio_call_status(call_sid, call_status): author=phone_call_record.receiver, notification_policy=phone_call_record.notification_policy, alert_group=phone_call_record.represents_alert_group, - notification_step=phone_call_record.notification_policy.step - if phone_call_record.notification_policy - else None, - notification_channel=phone_call_record.notification_policy.notify_by - if phone_call_record.notification_policy - else None, + notification_step=UserNotificationPolicy.Step.NOTIFY, + notification_channel=UserNotificationPolicy.NotificationChannel.PHONE_CALL, ) log_record.save() logger.info( f"twilioapp.update_twilio_call_status: created log_record log_record_id={log_record.id} " f"type={log_record_type}" ) - user_notification_action_triggered_signal.send(sender=update_twilio_call_status, log_record=log_record) def get_error_code_by_twilio_status(status): @@ -106,7 +102,7 @@ def update_twilio_sms_status(message_sid, message_status): Returns: """ - from apps.base.models import UserNotificationPolicyLogRecord + from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord if message_sid and message_status: logger.info(f"twilioapp.update_twilio_message_status: processing sid={message_sid} status={message_status}") @@ -143,23 +139,44 @@ def update_twilio_sms_status(message_sid, message_status): log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED log_record_error_code = get_sms_error_code_by_twilio_status(status_code) if log_record_type is not None: - log_record = UserNotificationPolicyLogRecord( - type=log_record_type, - notification_error_code=log_record_error_code, - author=sms_record.receiver, - notification_policy=sms_record.notification_policy, - alert_group=sms_record.represents_alert_group, - notification_step=sms_record.notification_policy.step if sms_record.notification_policy else None, - notification_channel=sms_record.notification_policy.notify_by - if sms_record.notification_policy - else None, - ) - log_record.save() - logger.info( - f"twilioapp.update_twilio_sms_status: created log_record log_record_id={log_record.id} " - f"type={log_record_type}" - ) - user_notification_action_triggered_signal.send(sender=update_twilio_sms_status, log_record=log_record) + if sms_record.represents_bundle_uuid: + notifications = BundledNotification.objects.filter(bundle_uuid=sms_record.represents_bundle_uuid) + log_records_to_create = [] + for notification in notifications: + log_record = UserNotificationPolicyLogRecord( + type=log_record_type, + notification_error_code=log_record_error_code, + author=sms_record.receiver, + notification_policy=notification.notification_policy, + alert_group=notification.alert_group, + notification_step=UserNotificationPolicy.Step.NOTIFY, + notification_channel=UserNotificationPolicy.NotificationChannel.SMS, + ) + log_records_to_create.append(log_record) + # send send_update_log_report_signal with 10 seconds delay + send_update_log_report_signal.apply_async( + kwargs={"alert_group_pk": notification.alert_group_id}, countdown=10 + ) + UserNotificationPolicyLogRecord.objects.bulk_create(log_records_to_create, batch_size=5000) + logger.info( + f"twilioapp.update_twilio_sms_status: created log_records for sms bundle " + f"{sms_record.represents_bundle_uuid} type={log_record_type}" + ) + else: + log_record = UserNotificationPolicyLogRecord( + type=log_record_type, + notification_error_code=log_record_error_code, + author=sms_record.receiver, + notification_policy=sms_record.notification_policy, + alert_group=sms_record.represents_alert_group, + notification_step=UserNotificationPolicy.Step.NOTIFY, + notification_channel=UserNotificationPolicy.NotificationChannel.SMS, + ) + log_record.save() + logger.info( + f"twilioapp.update_twilio_sms_status: created log_record log_record_id={log_record.id} " + f"type={log_record_type}" + ) def get_sms_error_code_by_twilio_status(status): diff --git a/engine/apps/twilioapp/tests/test_sms_message.py b/engine/apps/twilioapp/tests/test_sms_message.py index bba035b5bb..9406dd8cf2 100644 --- a/engine/apps/twilioapp/tests/test_sms_message.py +++ b/engine/apps/twilioapp/tests/test_sms_message.py @@ -2,6 +2,7 @@ import pytest from django.urls import reverse +from django.utils import timezone from django.utils.datastructures import MultiValueDict from django.utils.http import urlencode from rest_framework.test import APIClient @@ -101,3 +102,64 @@ def test_update_status(mock_has_permission, mock_slack_api_call, make_twilio_sms assert response.data == "" twilio_sms.refresh_from_db() assert twilio_sms.status == TwilioSMSstatuses.DETERMINANT[status] + + +@mock.patch("apps.twilioapp.views.AllowOnlyTwilio.has_permission") +@pytest.mark.django_db +def test_update_status_for_bundled_notifications( + mock_has_permission, + mock_slack_api_call, + make_organization_and_user, + make_alert_receive_channel, + make_user_notification_policy, + make_user_notification_bundle, + make_alert_group, + make_sms_record, +): + """The test for SMSMessage status update via api for notification bundle""" + organization, user = make_organization_and_user() + alert_receive_channel = make_alert_receive_channel(organization) + alert_group_1 = make_alert_group(alert_receive_channel) + alert_group_2 = make_alert_group(alert_receive_channel) + notification_policy = make_user_notification_policy( + user=user, + step=UserNotificationPolicy.Step.NOTIFY, + notify_by=UserNotificationPolicy.NotificationChannel.SMS, + ) + + notification_bundle = make_user_notification_bundle( + user, UserNotificationPolicy.NotificationChannel.SMS, notification_task_id="test_task_id", eta=timezone.now() + ) + notification_bundle.append_notification(alert_group_1, notification_policy) + notification_bundle.append_notification(alert_group_2, notification_policy) + bundle_uuid = "test_notifications_bundle" + + notification_bundle.notifications.update(bundle_uuid=bundle_uuid) + sms_record = make_sms_record( + receiver=user, + represents_bundle_uuid=bundle_uuid, + notification_policy=notification_policy, + ) + twilio_sms = TwilioSMS.objects.create(sid="SMa12312312a123a123123c6dd2f1aee77", sms_record=sms_record) + + mock_has_permission.return_value = True + status = "delivered" + data = { + "MessageSid": twilio_sms.sid, + "MessageStatus": status, + "AccountSid": "Because of mock_has_permission there are may be any value", + } + assert user.personal_log_records.count() == 0 + + client = APIClient() + response = client.post( + path=reverse("twilioapp:sms_status_events"), + data=urlencode(MultiValueDict(data), doseq=True), + content_type="application/x-www-form-urlencoded", + ) + assert response.status_code == 204 + assert response.data == "" + twilio_sms.refresh_from_db() + assert twilio_sms.status == TwilioSMSstatuses.DETERMINANT[status] + + assert user.personal_log_records.count() == 2 diff --git a/engine/apps/zvonok/status_callback.py b/engine/apps/zvonok/status_callback.py index 3a265cb549..61e505aadb 100644 --- a/engine/apps/zvonok/status_callback.py +++ b/engine/apps/zvonok/status_callback.py @@ -10,7 +10,7 @@ def update_zvonok_call_status(call_id: str, call_status: str, user_choice: Optional[str] = None): - from apps.base.models import UserNotificationPolicyLogRecord + from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord status_code = ZvonokCallStatuses.DETERMINANT.get(call_status) if status_code is None: @@ -57,12 +57,8 @@ def update_zvonok_call_status(call_id: str, call_status: str, user_choice: Optio author=phone_call_record.receiver, notification_policy=phone_call_record.notification_policy, alert_group=phone_call_record.represents_alert_group, - notification_step=phone_call_record.notification_policy.step - if phone_call_record.notification_policy - else None, - notification_channel=phone_call_record.notification_policy.notify_by - if phone_call_record.notification_policy - else None, + notification_step=UserNotificationPolicy.Step.NOTIFY, + notification_channel=UserNotificationPolicy.NotificationChannel.PHONE_CALL, ) log_record.save() logger.info( diff --git a/engine/settings/celery_task_routes.py b/engine/settings/celery_task_routes.py index 4c9412c303..ea866c1a7a 100644 --- a/engine/settings/celery_task_routes.py +++ b/engine/settings/celery_task_routes.py @@ -130,6 +130,7 @@ "queue": "critical" }, "apps.mobile_app.fcm_relay.fcm_relay_async": {"queue": "critical"}, + "apps.phone_notifications.phone_backend.notify_by_sms_bundle_async_task": {"queue": "critical"}, "apps.schedules.tasks.drop_cached_ical.drop_cached_ical_for_custom_events_for_organization": {"queue": "critical"}, "apps.schedules.tasks.drop_cached_ical.drop_cached_ical_task": {"queue": "critical"}, # GRAFANA