Skip to content

Commit c98a9b5

Browse files
Use the display message from OperationOutcome when GP Connect Document error occurs (#736)
This display message is then used to populate the error field which is sent to the requesting system.
1 parent 7f5f0b1 commit c98a9b5

9 files changed

Lines changed: 490 additions & 14 deletions

File tree

service/src/intTest/java/uk/nhs/adaptors/gp2gp/gpc/GetGpcDocumentComponentTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public void When_AccessDocumentNotFoundError_Expect_EhrStatusUpdatedAndAbsentAtt
158158
assertThat(gpcDocuments.get(0).getAccessedAt()).isNotNull();
159159
assertThat(gpcDocuments.get(0).getObjectName()).isEqualTo(absentAttachmentFilename);
160160
assertThat(gpcDocuments.get(0).getMessageId()).isEqualTo(documentId);
161-
assertThat(gpcDocuments.get(0).getGpConnectErrorMessage()).isEqualTo("The document could not be retrieved");
161+
assertThat(gpcDocuments.get(0).getGpConnectErrorMessage()).isEqualTo("No Record Found");
162162

163163
assertDoesNotThrow(() -> storageConnector.downloadFromStorage(absentAttachmentFilename));
164164

service/src/main/java/uk/nhs/adaptors/gp2gp/common/service/WebClientFilterService.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,26 @@
1212
import java.util.regex.Pattern;
1313
import java.util.stream.Collectors;
1414

15+
import com.fasterxml.jackson.databind.DeserializationFeature;
16+
import com.fasterxml.jackson.databind.MapperFeature;
17+
import com.fasterxml.jackson.databind.json.JsonMapper;
18+
import com.fasterxml.jackson.databind.module.SimpleModule;
19+
import org.hl7.fhir.dstu3.model.OperationOutcome;
1520
import org.slf4j.MDC;
21+
1622
import org.springframework.http.HttpStatus;
1723
import org.springframework.web.reactive.function.client.ClientResponse;
1824
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
1925

2026
import com.fasterxml.jackson.core.JsonProcessingException;
21-
import com.fasterxml.jackson.databind.ObjectMapper;
2227

2328
import lombok.extern.slf4j.Slf4j;
2429
import reactor.core.publisher.Mono;
2530
import reactor.util.retry.Retry;
2631
import uk.nhs.adaptors.gp2gp.common.configuration.WebClientConfiguration;
2732
import uk.nhs.adaptors.gp2gp.common.exception.MaximumExternalAttachmentsException;
2833
import uk.nhs.adaptors.gp2gp.common.exception.RetryLimitReachedException;
34+
import uk.nhs.adaptors.gp2gp.common.utils.OperationOutcomeIssueTypeDeserializer;
2935
import uk.nhs.adaptors.gp2gp.gpc.exception.EhrRequestException;
3036
import uk.nhs.adaptors.gp2gp.gpc.exception.GpConnectException;
3137
import uk.nhs.adaptors.gp2gp.gpc.exception.GpConnectInvalidException;
@@ -137,15 +143,27 @@ private static Mono<ClientResponse> getErrorException(ClientResponse clientRespo
137143
var exceptionMessage = String.format(REQUEST_EXCEPTION_MESSAGE, requestType, outcome);
138144

139145
try {
140-
var objectMapper = new ObjectMapper();
141-
var outcomeJson = objectMapper.readTree(outcome);
142-
var codes = outcomeJson.findValuesAsText("code");
146+
var module = new SimpleModule();
147+
module.addDeserializer(OperationOutcome.IssueType.class, new OperationOutcomeIssueTypeDeserializer());
148+
149+
var jsonMapper = JsonMapper
150+
.builder()
151+
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
152+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
153+
.build();
154+
155+
jsonMapper.registerModule(module);
156+
157+
var operationOutcome = jsonMapper.readValue(outcome, OperationOutcome.class);
158+
var codes = jsonMapper.readTree(outcome).findValuesAsText("code");
159+
143160
var statusCode = clientResponse.statusCode();
144161
var errorCode = getErrorCode(HttpStatus.resolve(statusCode.value()), codes);
145162

146-
return getMonoError(errorCode, exceptionMessage);
163+
return getMonoError(errorCode, exceptionMessage, operationOutcome);
147164
} catch (JsonProcessingException e) {
148165
return Mono.error(new GpConnectException(exceptionMessage));
166+
149167
}
150168
});
151169
}
@@ -184,7 +202,7 @@ private static int getErrorCode(HttpStatus statusCode, List<String> codes) {
184202
return NACK_ERROR_20;
185203
}
186204

187-
private static Mono<ClientResponse> getMonoError(int errorCode, String exceptionMessage) {
205+
private static Mono<ClientResponse> getMonoError(int errorCode, String exceptionMessage, OperationOutcome operationOutcome) {
188206
switch (errorCode) {
189207
case NACK_ERROR_6:
190208
return Mono.error(new GpConnectNotFoundException(exceptionMessage));
@@ -194,7 +212,7 @@ private static Mono<ClientResponse> getMonoError(int errorCode, String exception
194212
return Mono.error(new EhrRequestException(exceptionMessage));
195213
case NACK_ERROR_20:
196214
default:
197-
return Mono.error(new GpConnectException(exceptionMessage));
215+
return Mono.error(new GpConnectException(exceptionMessage, operationOutcome));
198216
}
199217
}
200218

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package uk.nhs.adaptors.gp2gp.common.utils;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.JsonDeserializer;
6+
import org.hl7.fhir.dstu3.model.OperationOutcome.IssueType;
7+
import org.hl7.fhir.exceptions.FHIRException;
8+
9+
import java.io.IOException;
10+
11+
public class OperationOutcomeIssueTypeDeserializer extends JsonDeserializer<IssueType> {
12+
13+
@Override
14+
public IssueType deserialize(JsonParser p, DeserializationContext context) throws IOException {
15+
try {
16+
return IssueType.fromCode(p.getText());
17+
} catch (FHIRException e) {
18+
throw new IOException("Failed to deserialize OperationOutcomeIssueType value: " + p.getText(), e);
19+
}
20+
}
21+
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static uk.nhs.adaptors.gp2gp.ehr.utils.AbsentAttachmentUtils.buildAbsentAttachmentFileName;
44

5+
import org.apache.commons.lang3.StringUtils;
56
import org.springframework.beans.factory.annotation.Autowired;
67
import org.springframework.stereotype.Component;
78

@@ -16,6 +17,8 @@
1617
import uk.nhs.adaptors.gp2gp.gpc.DocumentToMHSTranslator;
1718
import uk.nhs.adaptors.gp2gp.gpc.StorageDataWrapperProvider;
1819

20+
import java.util.Optional;
21+
1922
@Slf4j
2023
@Component
2124
@AllArgsConstructor(onConstructor = @__(@Autowired))
@@ -33,11 +36,12 @@ public Class<GetAbsentAttachmentTaskDefinition> getTaskType() {
3336

3437
@Override
3538
public void execute(GetAbsentAttachmentTaskDefinition taskDefinition) {
36-
var ehrExtractStatus = handleAbsentAttachment(taskDefinition);
39+
var ehrExtractStatus = handleAbsentAttachment(taskDefinition, Optional.empty());
3740
detectTranslationCompleteService.beginSendingCompleteExtract(ehrExtractStatus);
3841
}
3942

40-
public EhrExtractStatus handleAbsentAttachment(DocumentTaskDefinition taskDefinition) {
43+
public EhrExtractStatus handleAbsentAttachment(DocumentTaskDefinition taskDefinition,
44+
Optional<String> gpcResponseError) {
4145
var taskId = taskDefinition.getTaskId();
4246

4347
var fileContent = Base64Utils.toBase64String(AbsentAttachmentFileMapper.mapDataToAbsentAttachment(
@@ -56,12 +60,19 @@ public EhrExtractStatus handleAbsentAttachment(DocumentTaskDefinition taskDefini
5660
storageConnectorService.uploadFile(storageDataWrapperWithMhsOutboundRequest, storagePath);
5761

5862
return ehrExtractStatusService.updateEhrExtractStatusAccessDocument(
59-
taskDefinition, storagePath, fileContent.length(), getTitle(taskDefinition)
63+
taskDefinition,
64+
storagePath,
65+
fileContent.length(),
66+
getErrorMessage(taskDefinition, gpcResponseError)
6067
);
6168
}
6269

63-
private String getTitle(DocumentTaskDefinition taskDefinition) {
64-
if (taskDefinition.getTitle() != null) {
70+
private String getErrorMessage(DocumentTaskDefinition taskDefinition, Optional<String> exceptionDisplay) {
71+
if (exceptionDisplay.isPresent()) {
72+
return exceptionDisplay.get();
73+
}
74+
75+
if (!StringUtils.isEmpty(taskDefinition.getTitle())) {
6576
return taskDefinition.getTitle();
6677
}
6778

service/src/main/java/uk/nhs/adaptors/gp2gp/gpc/GetGpcDocumentTaskExecutor.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import lombok.SneakyThrows;
55
import lombok.extern.slf4j.Slf4j;
66

7+
import org.apache.commons.lang3.StringUtils;
78
import org.hl7.fhir.dstu3.model.Binary;
9+
import org.hl7.fhir.dstu3.model.Coding;
10+
import org.hl7.fhir.dstu3.model.OperationOutcome;
811
import org.springframework.beans.factory.annotation.Autowired;
912
import org.springframework.stereotype.Component;
1013

@@ -17,6 +20,8 @@
1720
import uk.nhs.adaptors.gp2gp.ehr.model.EhrExtractStatus;
1821
import uk.nhs.adaptors.gp2gp.gpc.exception.GpConnectException;
1922

23+
import java.util.Optional;
24+
2025
@Slf4j
2126
@Component
2227
@AllArgsConstructor(onConstructor = @__(@Autowired))
@@ -54,12 +59,27 @@ public void execute(GetGpcDocumentTaskDefinition taskDefinition) {
5459
ehrExtractStatus = handleValidGpcDocument(response, taskDefinition);
5560
} catch (GpConnectException e) {
5661
LOGGER.warn("Binary request returned an unexpected response", e);
57-
ehrExtractStatus = getAbsentAttachmentTaskExecutor.handleAbsentAttachment(taskDefinition);
62+
63+
var gpcResponseError = getDisplayFromOperationOutcome(e.getOperationOutcome());
64+
65+
ehrExtractStatus = getAbsentAttachmentTaskExecutor.handleAbsentAttachment(taskDefinition, gpcResponseError);
5866
}
5967

6068
detectTranslationCompleteService.beginSendingCompleteExtract(ehrExtractStatus);
6169
}
6270

71+
private Optional<String> getDisplayFromOperationOutcome(OperationOutcome operationOutcome) {
72+
return Optional.ofNullable(operationOutcome)
73+
.filter(oo -> oo.hasIssue() && !oo.getIssue().isEmpty())
74+
.map(oo -> oo.getIssue().get(0))
75+
.filter(issue -> issue.hasDetails()
76+
&& issue.getDetails().hasCoding()
77+
&& !issue.getDetails().getCoding().isEmpty())
78+
.map(issue -> issue.getDetails().getCoding().get(0))
79+
.filter(coding -> coding.hasDisplay() && StringUtils.isNotBlank(coding.getDisplay()))
80+
.map(Coding::getDisplay);
81+
}
82+
6383
private EhrExtractStatus handleValidGpcDocument(String response, GetGpcDocumentTaskDefinition taskDefinition) {
6484
var taskId = taskDefinition.getTaskId();
6585
var storagePath = GpcFilenameUtils.generateDocumentStoragePath(
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
package uk.nhs.adaptors.gp2gp.gpc.exception;
22

3+
import lombok.Getter;
4+
import org.hl7.fhir.dstu3.model.OperationOutcome;
5+
6+
@Getter
37
public class GpConnectException extends RuntimeException {
8+
9+
private OperationOutcome operationOutcome;
10+
411
public GpConnectException(String message) {
512
super(message);
613
}
714

815
public GpConnectException(String message, Throwable cause) {
916
super(message, cause);
1017
}
18+
19+
public GpConnectException(String message, OperationOutcome operationOutcome) {
20+
super(message);
21+
this.operationOutcome = operationOutcome == null ? null : operationOutcome.copy();
22+
}
1123
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package uk.nhs.adaptors.gp2gp.ehr;
2+
3+
import org.jetbrains.annotations.Nullable;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.extension.ExtendWith;
6+
import org.mockito.InjectMocks;
7+
import org.mockito.Mock;
8+
import org.mockito.junit.jupiter.MockitoExtension;
9+
import uk.nhs.adaptors.gp2gp.common.storage.StorageConnectorService;
10+
import uk.nhs.adaptors.gp2gp.gpc.DetectTranslationCompleteService;
11+
import uk.nhs.adaptors.gp2gp.gpc.DocumentToMHSTranslator;
12+
13+
import java.util.Optional;
14+
15+
import static org.mockito.ArgumentMatchers.any;
16+
import static org.mockito.Mockito.anyInt;
17+
import static org.mockito.Mockito.eq;
18+
import static org.mockito.Mockito.verify;
19+
20+
@ExtendWith(MockitoExtension.class)
21+
public class GetAbsentAttachmentTaskExecutorTest {
22+
@Mock private EhrExtractStatusService ehrExtractStatusService;
23+
@Mock private DocumentToMHSTranslator documentToMHSTranslator;
24+
@Mock private DetectTranslationCompleteService detectTranslationCompleteService;
25+
@Mock private StorageConnectorService storageConnectorService;
26+
27+
@InjectMocks
28+
private GetAbsentAttachmentTaskExecutor getAbsentAttachmentTaskExecutor;
29+
30+
@Test
31+
public void When_HandleAbsentAttachmentWithNoError_Expect_DefaultValueIsUsedAsError() {
32+
var taskDefinition = buildAbsentAttachment(null);
33+
34+
getAbsentAttachmentTaskExecutor.handleAbsentAttachment(
35+
taskDefinition,
36+
Optional.empty());
37+
38+
verify(ehrExtractStatusService).updateEhrExtractStatusAccessDocument(
39+
any(),
40+
any(),
41+
anyInt(),
42+
eq("The document could not be retrieved")
43+
);
44+
}
45+
46+
@Test
47+
public void When_HandleAbsentAttachmentWithJustTaskDefinitionTitle_Expect_TitleIsUsedAsError() {
48+
var taskDefinition = buildAbsentAttachment("This-is-the-task-definition-title");
49+
50+
getAbsentAttachmentTaskExecutor.handleAbsentAttachment(
51+
taskDefinition,
52+
Optional.empty());
53+
54+
verify(ehrExtractStatusService).updateEhrExtractStatusAccessDocument(
55+
any(),
56+
any(),
57+
anyInt(),
58+
eq("This-is-the-task-definition-title")
59+
);
60+
}
61+
62+
@Test
63+
public void When_HandleAbsentAttachmentWithJustGpcResponseError_Expect_GpcResponseErrorIsUsedAsError() {
64+
var taskDefinition = buildAbsentAttachment(null);
65+
66+
getAbsentAttachmentTaskExecutor.handleAbsentAttachment(
67+
taskDefinition,
68+
Optional.of("This-is-the-gpc-response-error"));
69+
70+
verify(ehrExtractStatusService).updateEhrExtractStatusAccessDocument(
71+
any(),
72+
any(),
73+
anyInt(),
74+
eq("This-is-the-gpc-response-error")
75+
);
76+
}
77+
78+
@Test
79+
public void When_HandleAbsentAttachmentWithGpcResponseErrorAndTitle_Expect_GpcResponseErrorIsUsedAsError() {
80+
var taskDefinition = buildAbsentAttachment("This-is-the-task-definition-title");
81+
82+
getAbsentAttachmentTaskExecutor.handleAbsentAttachment(
83+
taskDefinition,
84+
Optional.of("This-is-the-gpc-response-error"));
85+
86+
verify(ehrExtractStatusService).updateEhrExtractStatusAccessDocument(
87+
any(),
88+
any(),
89+
anyInt(),
90+
eq("This-is-the-gpc-response-error")
91+
);
92+
}
93+
94+
@Test
95+
public void When_HandleAbsentAttachment_Expect_AbsentAttachmentFilenameIsUsed() {
96+
var taskDefinition = buildAbsentAttachment(null);
97+
98+
getAbsentAttachmentTaskExecutor.handleAbsentAttachment(taskDefinition, Optional.empty());
99+
100+
verify(ehrExtractStatusService).updateEhrExtractStatusAccessDocument(
101+
any(),
102+
eq("AbsentAttachmentDocument-Id.txt"),
103+
anyInt(),
104+
any()
105+
);
106+
}
107+
108+
private static GetAbsentAttachmentTaskDefinition buildAbsentAttachment(@Nullable String title) {
109+
return GetAbsentAttachmentTaskDefinition.builder()
110+
.conversationId("Conversation-Id")
111+
.taskId("Task-Id")
112+
.originalDescription("This-is-the-original-description")
113+
.toOdsCode("XX1111")
114+
.documentId("Document-Id")
115+
.title(title)
116+
.build();
117+
}
118+
}

0 commit comments

Comments
 (0)