Skip to content

Commit abc8f76

Browse files
Adrian ClayAlex-NitaMartinWheelerMTORybak5
authored
NIAD-2394: Use placeholders when mapping DocumentReferences, so that any MigrateDocument failures are represented as AbsentAttachments in XML (#718)
* Added placeholders for Filename, MediaType, and error message * Update the MongoDB field with actual filename (#722) If the attachment ended up being absent after being fetched, we'd like to give it a new filename prefixed with 'AbsentAttachment'. This prefix is part of the GP2GP spec for absent attachments Co-authored-by: Adrian Clay <[email protected]> * Generate ExternalAttachments with placeholders for Filename and ContentType * Add changelog entry * Add tests for AttachmentDescription * Add space to error message placeholder The template expects that a space is added to the end of the string when provided, and a blank string when no error occurs. --------- Co-authored-by: Alex-Nita <[email protected]> Co-authored-by: MartinWheelerMT <[email protected]> Co-authored-by: Ole <[email protected]>
1 parent e9b1566 commit abc8f76

27 files changed

+322
-246
lines changed

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Correctly send documents which can't be fetched over GP Connect as absent attachments.
12+
Previously these documents wouldn't have the correct "Content Type", or "Filename" sent according to GP2GP specification.
13+
The adaptor also now sends the GP Connect error detail to the requesting practice to help diagnose the issue.
14+
915
## [2.0.2] - 2024-04-10
1016

1117
### Fixed
12-
- Updated dependencies to keep adaptor secure.
1318

19+
- Updated dependencies to keep adaptor secure.
1420

1521
## [2.0.1] - 2024-02-22
1622

1723
### Fixed
18-
- When mapping an `AllergyIntollerance` to an `ObservationStatement`, both the `availabilityTime` and `effectiveTime`
24+
- When mapping an `AllergyIntolerance` to an `ObservationStatement`, both the `availabilityTime` and `effectiveTime`
1925
fields were previously mapped from the `onset` field and the `assertedDate` field was ignored.
2026
Now, the `effectiveTime` is populated with the `onset` field, and the `availabilityTime` is populated with the
2127
`assertedDate` field.

service/src/intTest/java/uk/nhs/adaptors/gp2gp/ehr/EhrExtractStatusServiceTest.java

Lines changed: 77 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
import java.time.temporal.ChronoUnit;
2525
import java.util.ArrayList;
2626
import java.util.List;
27+
import java.util.Map;
2728
import java.util.UUID;
2829
import java.util.stream.Collectors;
2930

31+
import org.jetbrains.annotations.NotNull;
3032
import org.junit.jupiter.api.BeforeEach;
3133
import org.junit.jupiter.api.Test;
3234
import org.junit.jupiter.api.extension.ExtendWith;
@@ -48,6 +50,7 @@ public class EhrExtractStatusServiceTest {
4850

4951
public static final Instant NOW = Instant.now();
5052
private static final Instant FIVE_DAYS_AGO = NOW.minus(Duration.ofDays(5));
53+
private static final int DEFAULT_CONTENT_LENGTH = 244;
5154

5255
@Autowired
5356
private EhrExtractStatusService ehrExtractStatusService;
@@ -63,6 +66,58 @@ public void emptyDatabase() {
6366
ehrExtractStatusRepository.deleteAll();
6467
}
6568

69+
@Test
70+
public void When_FetchDocumentObjectNameAndSize_With_OneMissingAttachment_Expect_Returned() {
71+
var inProgressConversationId = generateRandomUppercaseUUID();
72+
73+
addInProgressTransfer(
74+
inProgressConversationId, List.of(
75+
EhrExtractStatus.GpcDocument.builder()
76+
.fileName("AbsentAttachment4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60.rtx")
77+
.documentId("4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60")
78+
.contentLength(DEFAULT_CONTENT_LENGTH)
79+
.gpConnectErrorMessage("404 Not Found")
80+
.contentType("application/msword")
81+
.build()
82+
)
83+
);
84+
85+
final var results = ehrExtractStatusService.fetchDocumentObjectNameAndSize(inProgressConversationId);
86+
87+
assertThat(results).isEqualTo(Map.of(
88+
"FILENAME_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "AbsentAttachment4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60.rtx",
89+
"LENGTH_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "244",
90+
"ERROR_MESSAGE_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "Absent Attachment: 404 Not Found ",
91+
"CONTENT_TYPE_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "text/plain"
92+
));
93+
}
94+
95+
@Test
96+
public void When_FetchDocumentObjectNameAndSize_With_OnePresentAttachment_Expect_Returned() {
97+
var inProgressConversationId = generateRandomUppercaseUUID();
98+
99+
addInProgressTransfer(
100+
inProgressConversationId, List.of(
101+
EhrExtractStatus.GpcDocument.builder()
102+
.fileName("4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60.rtx")
103+
.documentId("4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60")
104+
.contentLength(DEFAULT_CONTENT_LENGTH)
105+
.gpConnectErrorMessage(null)
106+
.contentType("application/msword")
107+
.build()
108+
)
109+
);
110+
111+
final var results = ehrExtractStatusService.fetchDocumentObjectNameAndSize(inProgressConversationId);
112+
113+
assertThat(results).isEqualTo(Map.of(
114+
"FILENAME_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60.rtx",
115+
"LENGTH_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "244",
116+
"ERROR_MESSAGE_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "",
117+
"CONTENT_TYPE_PLACEHOLDER_ID=4E0C8345-A9AB-48EA-8882-DC9E9F3F5F60", "application/msword"
118+
));
119+
}
120+
66121
@Test
67122
public void When_FindInProgressTransfers_With_OneInProgress_Expect_Returned() {
68123
var inProgressConversationId = generateRandomUppercaseUUID();
@@ -132,7 +187,7 @@ public void When_FindInProgressTransfers_With_MultipleInProgress_Expect_AllRetur
132187
@Test
133188
public void When_UpdateEhrExtractStatusAccessDocument_Expect_DocumentRecordUpdated() {
134189
when(timestampService.now()).thenReturn(NOW);
135-
var ehrStatus = addCompleteTransferWithDocuments();
190+
var ehrStatus = addCompleteTransferWithDocument();
136191

137192
updateEhrExtractStatusAccessDocument(ehrStatus.getConversationId(), DOCUMENT_ID);
138193

@@ -144,7 +199,8 @@ public void When_UpdateEhrExtractStatusAccessDocument_Expect_DocumentRecordUpdat
144199
() -> assertThat(actual.getObjectName()).isEqualTo("this is a storage path.path"),
145200
() -> assertThat(actual.getMessageId()).isEqualTo("988290"),
146201
() -> assertThat(actual.getContentLength()).isEqualTo(1),
147-
() -> assertThat(actual.getGpConnectErrorMessage()).isEqualTo("This is a fantastic error message")
202+
() -> assertThat(actual.getGpConnectErrorMessage()).isEqualTo("This is a fantastic error message"),
203+
() -> assertThat(actual.getFileName()).isEqualTo("NewUpdatedFileName.txt")
148204
);
149205
}
150206

@@ -158,13 +214,14 @@ private EhrExtractStatus updateEhrExtractStatusAccessDocument(String conversatio
158214
.build(),
159215
"this is a storage path.path",
160216
1,
161-
"This is a fantastic error message"
217+
"This is a fantastic error message",
218+
"NewUpdatedFileName.txt"
162219
);
163220
}
164221

165222
@Test
166223
public void When_UpdateEhrExtractStatusAccessDocument_With_InvalidConversationId_Expect_ThrowsException() {
167-
addCompleteTransferWithDocuments();
224+
addCompleteTransferWithDocument();
168225

169226
assertThrows(
170227
EhrExtractException.class,
@@ -174,7 +231,7 @@ public void When_UpdateEhrExtractStatusAccessDocument_With_InvalidConversationId
174231

175232
@Test
176233
public void When_UpdateEhrExtractStatusAccessDocument_With_InvalidDocumentId_Expect_ThrowsException() {
177-
final var ehrStatus = addCompleteTransferWithDocuments();
234+
final var ehrStatus = addCompleteTransferWithDocument();
178235

179236
assertThrows(
180237
EhrExtractException.class,
@@ -186,7 +243,7 @@ public void When_UpdateEhrExtractStatusAccessDocument_With_InvalidDocumentId_Exp
186243
@Test
187244
public void When_UpdateEhrExtractStatusAccessDocument_Expect_ReturnsUpdatedEhrStatusRecord() {
188245
when(timestampService.now()).thenReturn(NOW);
189-
var ehrStatus = addCompleteTransferWithDocuments();
246+
var ehrStatus = addCompleteTransferWithDocument();
190247

191248
final var returnedRecord = updateEhrExtractStatusAccessDocument(ehrStatus.getConversationId(), DOCUMENT_ID);
192249

@@ -217,6 +274,10 @@ public void When_UpdateEhrExtractStatusAccessDocumentDocumentReferences_Expect_D
217274
}
218275

219276
private void addInProgressTransfer(String conversationId) {
277+
addInProgressTransfer(conversationId, List.of());
278+
}
279+
280+
private void addInProgressTransfer(String conversationId, List<EhrExtractStatus.GpcDocument> documents) {
220281
EhrExtractStatus extractStatus = EhrExtractStatus.builder()
221282
.ackPending(buildPositiveAckPending())
222283
.ackToRequester(buildPositiveAckToRequester())
@@ -232,7 +293,7 @@ private void addInProgressTransfer(String conversationId) {
232293
.ehrExtractMessageId(generateRandomUppercaseUUID())
233294
.ehrRequest(buildEhrRequest())
234295
.gpcAccessDocument(EhrExtractStatus.GpcAccessDocument.builder()
235-
.documents(new ArrayList<>())
296+
.documents(documents)
236297
.build())
237298
.gpcAccessStructured(EhrExtractStatus.GpcAccessStructured.builder()
238299
.accessedAt(FIVE_DAYS_AGO)
@@ -334,59 +395,17 @@ private void addFailedIncumbentTransfer() {
334395
ehrExtractStatusRepository.save(extractStatus);
335396
}
336397

337-
public EhrExtractStatus addCompleteTransfer() {
338-
String ehrMessageRef = generateRandomUppercaseUUID();
339-
340-
EhrExtractStatus extractStatus = EhrExtractStatus.builder()
341-
.ackHistory(EhrExtractStatus.AckHistory.builder()
342-
.acks(List.of(
343-
EhrExtractStatus.EhrReceivedAcknowledgement.builder()
344-
.rootId(generateRandomUppercaseUUID())
345-
.received(FIVE_DAYS_AGO)
346-
.conversationClosed(FIVE_DAYS_AGO)
347-
.messageRef(ehrMessageRef)
348-
.build()))
349-
.build())
350-
.ackPending(EhrExtractStatus.AckPending.builder()
351-
.messageId(generateRandomUppercaseUUID())
352-
.taskId(generateRandomUppercaseUUID())
353-
.typeCode(ACK_TYPE)
354-
.updatedAt(FIVE_DAYS_AGO.toString())
355-
.build())
356-
.ackToRequester(buildPositiveAckToRequester())
357-
.conversationId(generateRandomUppercaseUUID())
358-
.created(FIVE_DAYS_AGO)
359-
.ehrExtractCore(EhrExtractStatus.EhrExtractCore.builder()
360-
.sentAt(FIVE_DAYS_AGO)
361-
.build())
362-
.ehrExtractCorePending(EhrExtractStatus.EhrExtractCorePending.builder()
363-
.sentAt(FIVE_DAYS_AGO)
364-
.taskId(generateRandomUppercaseUUID())
365-
.build())
366-
.ehrReceivedAcknowledgement(EhrExtractStatus.EhrReceivedAcknowledgement.builder()
367-
.conversationClosed(FIVE_DAYS_AGO)
368-
.messageRef(ehrMessageRef)
369-
.received(FIVE_DAYS_AGO)
370-
.rootId(generateRandomUppercaseUUID())
371-
.build())
372-
.ehrRequest(buildEhrRequest())
373-
.gpcAccessDocument(EhrExtractStatus.GpcAccessDocument.builder()
374-
.documents(new ArrayList<>())
375-
.build())
376-
.gpcAccessStructured(EhrExtractStatus.GpcAccessStructured.builder()
377-
.accessedAt(FIVE_DAYS_AGO)
378-
.objectName(generateRandomUppercaseUUID() + ".json")
379-
.taskId(generateRandomUppercaseUUID())
380-
.build())
381-
.messageTimestamp(FIVE_DAYS_AGO)
382-
.updatedAt(FIVE_DAYS_AGO)
383-
.build();
384-
385-
return ehrExtractStatusRepository.save(extractStatus);
398+
private EhrExtractStatus addCompleteTransfer() {
399+
return addCompleteTransferWithDocuments(List.of());
386400
}
387401

402+
private EhrExtractStatus addCompleteTransferWithDocument() {
403+
return addCompleteTransferWithDocuments(List.of(
404+
EhrExtractStatus.GpcDocument.builder().documentId(DOCUMENT_ID).build()
405+
));
406+
}
388407

389-
public EhrExtractStatus addCompleteTransferWithDocuments() {
408+
private @NotNull EhrExtractStatus addCompleteTransferWithDocuments(List<EhrExtractStatus.GpcDocument> documents) {
390409
String ehrMessageRef = generateRandomUppercaseUUID();
391410

392411
EhrExtractStatus extractStatus = EhrExtractStatus.builder()
@@ -423,9 +442,7 @@ public EhrExtractStatus addCompleteTransferWithDocuments() {
423442
.build())
424443
.ehrRequest(buildEhrRequest())
425444
.gpcAccessDocument(EhrExtractStatus.GpcAccessDocument.builder()
426-
.documents(List.of(
427-
EhrExtractStatus.GpcDocument.builder().documentId(DOCUMENT_ID).build()
428-
))
445+
.documents(documents)
429446
.build())
430447
.gpcAccessStructured(EhrExtractStatus.GpcAccessStructured.builder()
431448
.accessedAt(FIVE_DAYS_AGO)

service/src/main/java/uk/nhs/adaptors/gp2gp/ehr/EhrExtractStatusService.java

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import static java.lang.String.format;
44

55
import static org.springframework.util.CollectionUtils.isEmpty;
6+
import static org.springframework.util.CollectionUtils.newHashMap;
67

78
import java.time.Instant;
89
import java.util.List;
910
import java.util.Map;
1011
import java.util.Optional;
11-
import java.util.stream.Collectors;
1212

13+
import org.apache.commons.lang3.StringUtils;
1314
import org.springframework.beans.factory.annotation.Autowired;
1415
import org.springframework.data.mongodb.core.FindAndModifyOptions;
1516
import org.springframework.data.mongodb.core.MongoTemplate;
@@ -108,6 +109,9 @@ public class EhrExtractStatusService {
108109
private static final String ERROR_MESSAGE_PATH = ERROR + DOT + MESSAGE;
109110
private static final String ERROR_TASK_TYPE_PATH = ERROR + DOT + TASK_TYPE;
110111
private static final String LENGTH_PLACEHOLDER = "LENGTH_PLACEHOLDER_ID=";
112+
private static final String ERROR_MESSAGE_PLACEHOLDER = "ERROR_MESSAGE_PLACEHOLDER_ID=";
113+
private static final String CONTENT_TYPE_PLACEHOLDER = "CONTENT_TYPE_PLACEHOLDER_ID=";
114+
private static final String FILENAME_TYPE_PLACEHOLDER = "FILENAME_PLACEHOLDER_ID=";
111115
private static final String ACKS_SET = ACK_HISTORY + DOT + ACKS;
112116

113117
private final MongoTemplate mongoTemplate;
@@ -138,10 +142,27 @@ public Map<String, String> fetchDocumentObjectNameAndSize(String conversationId)
138142
if (ehrExtractStatusSearch.isPresent()) {
139143
var ehrExtractStatus = ehrExtractStatusSearch.get();
140144
var ehrDocuments = ehrExtractStatus.getGpcAccessDocument().getDocuments();
141-
return ehrDocuments.stream()
142-
.collect(Collectors.toMap(
143-
(document) -> LENGTH_PLACEHOLDER + document.getDocumentId(),
144-
(document) -> document.getContentLength() + ""));
145+
146+
Map<String, String> replacementMap = newHashMap(ehrDocuments.size());
147+
148+
for (var document:ehrDocuments) {
149+
String error = document.getGpConnectErrorMessage() == null ? ""
150+
: "Absent Attachment: " + document.getGpConnectErrorMessage() + StringUtils.SPACE;
151+
152+
replacementMap.put(ERROR_MESSAGE_PLACEHOLDER + document.getDocumentId(),
153+
error);
154+
replacementMap.put(LENGTH_PLACEHOLDER + document.getDocumentId(),
155+
String.valueOf(document.getContentLength()));
156+
157+
if (document.getGpConnectErrorMessage() != null) {
158+
replacementMap.put(CONTENT_TYPE_PLACEHOLDER + document.getDocumentId(), "text/plain");
159+
} else {
160+
replacementMap.put(CONTENT_TYPE_PLACEHOLDER + document.getDocumentId(), document.getContentType());
161+
}
162+
replacementMap.put(FILENAME_TYPE_PLACEHOLDER + document.getDocumentId(), document.getFileName());
163+
}
164+
165+
return replacementMap;
145166
}
146167
return null;
147168
}
@@ -173,7 +194,8 @@ public EhrExtractStatus updateEhrExtractStatusAccessDocument(
173194
DocumentTaskDefinition documentTaskDefinition,
174195
String storagePath,
175196
int base64ContentLength,
176-
String errorMessage
197+
String errorMessage,
198+
String filename
177199
) {
178200
Query query = new Query();
179201
query.addCriteria(Criteria
@@ -189,6 +211,7 @@ public EhrExtractStatus updateEhrExtractStatusAccessDocument(
189211
update.set(DOCUMENT_MESSAGE_ID_PATH, documentTaskDefinition.getMessageId());
190212
update.set(DOCUMENT_BASE64_CONTENT_LENGTH, base64ContentLength);
191213
update.set(GPC_DOCUMENTS + ARRAY_REFERENCE + "gpConnectErrorMessage", errorMessage);
214+
update.set(GPC_DOCUMENTS + ARRAY_REFERENCE + "fileName", filename);
192215
FindAndModifyOptions returningUpdatedRecordOption = getReturningUpdatedRecordOption();
193216

194217
EhrExtractStatus ehrExtractStatus = mongoTemplate.findAndModify(query, update, returningUpdatedRecordOption,

service/src/main/java/uk/nhs/adaptors/gp2gp/ehr/GetAbsentAttachmentTaskExecutor.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public EhrExtractStatus handleAbsentAttachment(DocumentTaskDefinition taskDefini
5050
taskDefinition.getConversationId()
5151
));
5252

53-
var storagePath = buildAbsentAttachmentFileName(taskDefinition.getDocumentId());
53+
final var storagePath = buildAbsentAttachmentFileName(taskDefinition.getDocumentId());
54+
final var fileName = buildAbsentAttachmentFileName(taskDefinition.getDocumentId());
5455

5556
var mhsOutboundRequestData = documentToMHSTranslator.translateFileContentToMhsOutboundRequestData(taskDefinition, fileContent);
5657

@@ -63,7 +64,8 @@ public EhrExtractStatus handleAbsentAttachment(DocumentTaskDefinition taskDefini
6364
taskDefinition,
6465
storagePath,
6566
fileContent.length(),
66-
getErrorMessage(taskDefinition, gpcResponseError)
67+
getErrorMessage(taskDefinition, gpcResponseError),
68+
fileName
6769
);
6870
}
6971

0 commit comments

Comments
 (0)