Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2163 HOTFIX - Status Reason Updates #2196

Merged
merged 3 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 18 additions & 23 deletions app/celery/process_ses_receipts_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
NOTIFICATION_PERMANENT_FAILURE,
NOTIFICATION_TEMPORARY_FAILURE,
STATUS_REASON_RETRYABLE,
STATUS_REASON_UNDELIVERABLE,
STATUS_REASON_UNREACHABLE,
)
from app.clients.email.aws_ses import get_aws_responses
from app.dao import notifications_dao, services_dao, templates_dao
Expand Down Expand Up @@ -177,7 +177,7 @@ def process_ses_results( # noqa: C901 (too complex 14 > 10)
aws_response_dict = get_aws_responses(notification_type)

# This is the prospective, updated status.
notification_status = aws_response_dict['notification_status']
incoming_status = aws_response_dict['notification_status']
reference = ses_message['mail']['messageId']

try:
Expand All @@ -188,44 +188,39 @@ def process_ses_results( # noqa: C901 (too complex 14 > 10)
self.retry(queue=QueueNames.RETRY)
else:
current_app.logger.warning(
'notification not found for reference: %s (update to %s)', reference, notification_status
'notification not found for reference: %s (update to %s)', reference, incoming_status
)
return

# Prevent regressing bounce status. Note that this is a test of the existing status; not the new status.
if (
notification.status_reason
and 'bounce' in notification.status_reason
and notification.status
in {
NOTIFICATION_TEMPORARY_FAILURE,
NOTIFICATION_PERMANENT_FAILURE,
}
):
if notification.status_reason and notification.status in {
NOTIFICATION_TEMPORARY_FAILURE,
NOTIFICATION_PERMANENT_FAILURE,
}:
# async from AWS means we may get a delivered status after a bounce, in rare cases
current_app.logger.warning(
'Notification: %s was marked as a bounce, cannot be updated to: %s',
notification.id,
notification_status,
incoming_status,
)
return

# This is a test of the new status. Is it a bounce?
if notification_status in (NOTIFICATION_TEMPORARY_FAILURE, NOTIFICATION_PERMANENT_FAILURE):
if incoming_status in (NOTIFICATION_TEMPORARY_FAILURE, NOTIFICATION_PERMANENT_FAILURE):
# Add the failure status reason to the notification.
if notification_status == NOTIFICATION_PERMANENT_FAILURE:
if incoming_status == NOTIFICATION_PERMANENT_FAILURE:
failure_reason = 'Failed to deliver email due to hard bounce'
status_reason = STATUS_REASON_UNDELIVERABLE
status_reason = STATUS_REASON_UNREACHABLE
else:
failure_reason = 'Temporarily failed to deliver email due to soft bounce'
status_reason = STATUS_REASON_RETRYABLE

notification.status_reason = status_reason
notification.status = notification_status
notification.status = incoming_status

current_app.logger.warning(
'%s - %s - in process_ses_results for notification %s',
notification_status,
incoming_status,
failure_reason,
notification.id,
)
Expand All @@ -235,15 +230,15 @@ def process_ses_results( # noqa: C901 (too complex 14 > 10)
check_and_queue_va_profile_notification_status_callback(notification)

return
elif notification_status == NOTIFICATION_DELIVERED:
elif incoming_status == NOTIFICATION_DELIVERED:
# Delivered messages should never have a status reason.
notification.status_reason = None

if notification.status not in (NOTIFICATION_SENDING, NOTIFICATION_PENDING):
notifications_dao.duplicate_update_warning(notification, notification_status)
notifications_dao.duplicate_update_warning(notification, incoming_status)
return

notifications_dao._update_notification_status(notification=notification, status=notification_status)
notifications_dao._update_notification_status(notification=notification, status=incoming_status)

if not aws_response_dict['success']:
current_app.logger.info(
Expand All @@ -255,14 +250,14 @@ def process_ses_results( # noqa: C901 (too complex 14 > 10)
else:
current_app.logger.info(
'SES callback return status of %s for notification: %s',
notification_status,
incoming_status,
notification.id,
)

log_notification_total_time(
notification.id,
notification.created_at,
notification_status,
incoming_status,
'ses',
)

Expand Down
10 changes: 6 additions & 4 deletions app/clients/email/aws_ses.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,31 @@
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication

from app.constants import NOTIFICATION_DELIVERED, NOTIFICATION_PERMANENT_FAILURE, NOTIFICATION_TEMPORARY_FAILURE

ses_response_map = {
'Permanent': {
'message': 'Hard bounced',
'success': False,
'notification_status': 'permanent-failure',
'notification_status': NOTIFICATION_PERMANENT_FAILURE,
'notification_statistics_status': STATISTICS_FAILURE,
},
'Temporary': {
'message': 'Soft bounced',
'success': False,
'notification_status': 'temporary-failure',
'notification_status': NOTIFICATION_TEMPORARY_FAILURE,
'notification_statistics_status': STATISTICS_FAILURE,
},
'Delivery': {
'message': 'Delivered',
'success': True,
'notification_status': 'delivered',
'notification_status': NOTIFICATION_DELIVERED,
'notification_statistics_status': STATISTICS_DELIVERED,
},
'Complaint': {
'message': 'Complaint',
'success': True,
'notification_status': 'delivered',
'notification_status': NOTIFICATION_DELIVERED,
'notification_statistics_status': STATISTICS_DELIVERED,
},
}
Expand Down
3 changes: 2 additions & 1 deletion app/clients/sms/aws_pinpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
NOTIFICATION_PERMANENT_FAILURE,
PINPOINT_PROVIDER,
STATUS_REASON_BLOCKED,
STATUS_REASON_INVALID_NUMBER,
STATUS_REASON_RETRYABLE,
STATUS_REASON_UNDELIVERABLE,
)
Expand All @@ -41,7 +42,7 @@ class AwsPinpointClient(SmsClient):
'SUCCESSFUL': (NOTIFICATION_DELIVERED, None),
'DELIVERED': (NOTIFICATION_DELIVERED, None),
'PENDING': (NOTIFICATION_SENDING, None),
'INVALID': (NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNDELIVERABLE),
'INVALID': (NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_INVALID_NUMBER),
'UNREACHABLE': (NOTIFICATION_TEMPORARY_FAILURE, STATUS_REASON_RETRYABLE),
'UNKNOWN': (NOTIFICATION_TEMPORARY_FAILURE, STATUS_REASON_RETRYABLE),
'BLOCKED': (NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_BLOCKED),
Expand Down
5 changes: 3 additions & 2 deletions app/clients/sms/twilio.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
NOTIFICATION_SENT,
NOTIFICATION_TEMPORARY_FAILURE,
STATUS_REASON_BLOCKED,
STATUS_REASON_INVALID_NUMBER,
STATUS_REASON_RETRYABLE,
STATUS_REASON_UNDELIVERABLE,
STATUS_REASON_UNREACHABLE,
Expand Down Expand Up @@ -62,9 +63,9 @@ class TwilioSMSClient(SmsClient):
'21610': TwilioStatus(21610, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_BLOCKED),
# 21612: 'Invalid to/from combo'
'21612': TwilioStatus(21612, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNREACHABLE),
'21614': TwilioStatus(21614, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNREACHABLE),
'21614': TwilioStatus(21614, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_INVALID_NUMBER),
'21617': TwilioStatus(21617, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNDELIVERABLE),
'21635': TwilioStatus(21635, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNREACHABLE),
'21635': TwilioStatus(21635, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_INVALID_NUMBER),
'30001': TwilioStatus(30001, NOTIFICATION_TEMPORARY_FAILURE, STATUS_REASON_RETRYABLE),
# 30002: 'Account suspended'
'30002': TwilioStatus(30002, NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNDELIVERABLE),
Expand Down
3 changes: 2 additions & 1 deletion tests/app/celery/test_process_pinpoint_receipt_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
NOTIFICATION_PERMANENT_FAILURE,
PINPOINT_PROVIDER,
STATUS_REASON_BLOCKED,
STATUS_REASON_INVALID_NUMBER,
STATUS_REASON_RETRYABLE,
STATUS_REASON_UNDELIVERABLE,
)
Expand All @@ -27,7 +28,7 @@
[
('_SMS.BUFFERED', 'SUCCESSFUL', NOTIFICATION_DELIVERED, None),
('_SMS.SUCCESS', 'DELIVERED', NOTIFICATION_DELIVERED, None),
('_SMS.FAILURE', 'INVALID', NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNDELIVERABLE),
('_SMS.FAILURE', 'INVALID', NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_INVALID_NUMBER),
('_SMS.FAILURE', 'UNREACHABLE', NOTIFICATION_TEMPORARY_FAILURE, STATUS_REASON_RETRYABLE),
('_SMS.FAILURE', 'UNKNOWN', NOTIFICATION_TEMPORARY_FAILURE, STATUS_REASON_RETRYABLE),
('_SMS.FAILURE', 'BLOCKED', NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_BLOCKED),
Expand Down
16 changes: 12 additions & 4 deletions tests/app/celery/test_process_ses_receipts_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
NOTIFICATION_TEMPORARY_FAILURE,
STATUS_REASON_RETRYABLE,
STATUS_REASON_UNDELIVERABLE,
STATUS_REASON_UNREACHABLE,
)
from app.dao.notifications_dao import get_notification_by_id
from app.models import Complaint, Notification, Service, Template
Expand Down Expand Up @@ -454,7 +455,7 @@ def test_ses_callback_should_set_status_to_permanent_failure(
assert process_ses_receipts_tasks.process_ses_results(ses_hard_bounce_callback(reference=ref)) is None
db_notification = notify_db_session.session.get(Notification, notification_id)
assert db_notification.status == NOTIFICATION_PERMANENT_FAILURE
assert db_notification.status_reason == STATUS_REASON_UNDELIVERABLE
assert db_notification.status_reason == STATUS_REASON_UNREACHABLE
assert send_mock.called


Expand Down Expand Up @@ -618,12 +619,19 @@ def get_complaint_notification_and_email(mocker):
return complaint, notification, recipient_email


@pytest.mark.parametrize('status', (NOTIFICATION_PERMANENT_FAILURE, NOTIFICATION_TEMPORARY_FAILURE))
@pytest.mark.parametrize(
'status, status_reason',
(
(NOTIFICATION_PERMANENT_FAILURE, STATUS_REASON_UNREACHABLE),
(NOTIFICATION_TEMPORARY_FAILURE, STATUS_REASON_RETRYABLE),
),
)
def test_process_ses_results_no_bounce_regression(
notify_db_session,
sample_template,
sample_notification,
status,
status_reason,
):
"""
If a bounce status has been persisted for a notificaiton, no further status updates should occur.
Expand All @@ -633,7 +641,7 @@ def test_process_ses_results_no_bounce_regression(
notification = sample_notification(
template=sample_template(template_type=EMAIL_TYPE),
status=status,
status_reason='bounce',
status_reason=status_reason,
reference=str(uuid4()),
)

Expand All @@ -644,4 +652,4 @@ def test_process_ses_results_no_bounce_regression(

notify_db_session.session.refresh(notification)
assert notification.status == status, 'The status should not have changed.'
assert notification.status_reason == 'bounce'
assert notification.status_reason == status_reason
Loading