Skip to content

Commit c95f2da

Browse files
authored
Merge pull request #336 from NHSDigital/DTOSS-10448-remove-validation-errors-and-retry
DTOSS-10448: Remove validation errors and retry
2 parents 0215df7 + 6ca3aa4 commit c95f2da

File tree

10 files changed

+421
-124
lines changed

10 files changed

+421
-124
lines changed

manage_breast_screening/notifications/management/commands/command_helpers.py renamed to manage_breast_screening/notifications/management/commands/helpers/message_batch_helpers.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import re
23
from datetime import datetime
34
from zoneinfo import ZoneInfo
45

@@ -14,8 +15,6 @@
1415

1516
TZ_INFO = ZoneInfo("Europe/London")
1617
RECOVERABLE_STATUS_CODES = [
17-
# Validation error
18-
400,
1918
# Client side issue
2019
408,
2120
# Retried too early
@@ -29,6 +28,8 @@
2928
# Issue with backend services
3029
504,
3130
]
31+
VALIDATION_ERROR_STATUS_CODE = 400
32+
MESSAGE_PATH_REGEX = r"(?<=\/data\/attributes\/messages\/)(\d*)(?=\/)"
3233

3334

3435
class MessageBatchHelpers:
@@ -61,10 +62,45 @@ def mark_batch_as_failed(
6162
}
6263
)
6364
)
65+
elif response.status_code == VALIDATION_ERROR_STATUS_CODE:
66+
MessageBatchHelpers.process_validation_errors(message_batch, retry_count)
6467
else:
6568
message_batch.status = MessageBatchStatusChoices.FAILED_UNRECOVERABLE.value
6669
message_batch.save()
6770

6871
for message in message_batch.messages.all():
6972
message.status = MessageStatusChoices.FAILED.value
73+
message.sent_at = datetime.now(tz=TZ_INFO)
7074
message.save()
75+
76+
@staticmethod
77+
def process_validation_errors(message_batch: MessageBatch, retry_count: int = 0):
78+
message_batch_errors = json.loads(message_batch.nhs_notify_errors).get("errors")
79+
80+
messages = list(Message.objects.filter(batch=message_batch).all())
81+
for error in message_batch_errors:
82+
message_index_result = re.search(
83+
MESSAGE_PATH_REGEX, error["source"]["pointer"]
84+
)
85+
if message_index_result is not None:
86+
message_index = int(message_index_result.group(0))
87+
message = messages[message_index]
88+
message.batch = None
89+
if message.nhs_notify_errors is None:
90+
message.nhs_notify_errors = json.dumps([error])
91+
else:
92+
message.nhs_notify_errors = json.dumps(
93+
json.loads(message.nhs_notify_errors) + [error]
94+
)
95+
message.save()
96+
97+
message_batch.status = MessageBatchStatusChoices.FAILED_RECOVERABLE.value
98+
message_batch.save()
99+
Queue.RetryMessageBatches().add(
100+
json.dumps(
101+
{
102+
"message_batch_id": str(message_batch.id),
103+
"retry_count": retry_count,
104+
}
105+
)
106+
)

manage_breast_screening/notifications/management/commands/retry_failed_message_batch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from django.core.management.base import BaseCommand, CommandError
77

8-
from manage_breast_screening.notifications.management.commands.command_helpers import (
8+
from manage_breast_screening.notifications.management.commands.helpers.message_batch_helpers import (
99
MessageBatchHelpers,
1010
)
1111
from manage_breast_screening.notifications.models import (

manage_breast_screening/notifications/management/commands/send_message_batch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from django.core.management.base import BaseCommand, CommandError
55

6-
from manage_breast_screening.notifications.management.commands.command_helpers import (
6+
from manage_breast_screening.notifications.management.commands.helpers.message_batch_helpers import (
77
MessageBatchHelpers,
88
)
99
from manage_breast_screening.notifications.models import (
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.5 on 2025-09-02 10:13
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('notifications', '0015_add_nbss_id_as_index_to_appointments'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='message',
15+
name='nhs_notify_errors',
16+
field=models.JSONField(blank=True, null=True),
17+
),
18+
]

manage_breast_screening/notifications/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class Message(models.Model):
9595
status = models.CharField(
9696
max_length=50, choices=MessageStatusChoices, default="pending_enrichment"
9797
)
98+
nhs_notify_errors = models.JSONField(blank=True, null=True)
9899

99100
appointment = models.ForeignKey(
100101
"notifications.Appointment", on_delete=models.PROTECT

manage_breast_screening/notifications/tests/integration/test_retry_message_batches_from_queue.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import json
22
import uuid
3-
from unittest.mock import patch
3+
from unittest.mock import MagicMock, patch
44

55
import pytest
6+
import requests
67

78
from manage_breast_screening.notifications.management.commands.retry_failed_message_batch import (
89
Command,
910
)
1011
from manage_breast_screening.notifications.models import Message, MessageBatch
12+
from manage_breast_screening.notifications.services.api_client import ApiClient
1113
from manage_breast_screening.notifications.services.queue import Queue
1214
from manage_breast_screening.notifications.tests.factories import (
1315
MessageBatchFactory,
@@ -59,3 +61,50 @@ def test_message_batch_is_retried_successfully(
5961
assert message_batch_actual[0].status == "sent"
6062
messages_actual = Message.objects.filter(id=message.id)
6163
assert messages_actual[0].status == "delivered"
64+
65+
@patch.object(
66+
ApiClient, "send_message_batch", return_value=MagicMock(spec=requests.Response)
67+
)
68+
@pytest.mark.django_db
69+
def test_invalid_messages_are_removed(
70+
self, mock_send_message_batch, mock_jwt_encode, routing_plan_id, monkeypatch
71+
):
72+
monkeypatch.setenv(
73+
"QUEUE_STORAGE_CONNECTION_STRING", Helpers().azurite_connection_string()
74+
)
75+
notify_errors = {
76+
"errors": [
77+
{
78+
"status": 400,
79+
"source": {
80+
"pointer": "/data/attributes/messages/1/recipient/nhsNumber"
81+
},
82+
}
83+
]
84+
}
85+
86+
mock_send_message_batch.return_value.status_code = 400
87+
mock_send_message_batch.return_value.json.return_value = json.dumps(
88+
notify_errors
89+
)
90+
91+
invalid_message = MessageFactory(status="failed")
92+
ok_message_1 = MessageFactory(status="failed")
93+
ok_message_2 = MessageFactory(status="failed")
94+
message_batch = MessageBatchFactory(
95+
routing_plan_id=routing_plan_id,
96+
status="failed_recoverable",
97+
messages=[ok_message_1, invalid_message, ok_message_2],
98+
)
99+
queue = Queue.RetryMessageBatches()
100+
with Helpers().queue_listener(queue, Command().handle):
101+
queue.add(
102+
json.dumps(
103+
{"message_batch_id": str(message_batch.id), "retry_count": "0"}
104+
)
105+
)
106+
107+
invalid_message.refresh_from_db()
108+
assert invalid_message.status == "failed"
109+
assert invalid_message.batch is None
110+
assert invalid_message.nhs_notify_errors == json.dumps(notify_errors["errors"])

0 commit comments

Comments
 (0)