Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
24da3bb
Create endpoint for resending an EhrExtract
adrianclay Nov 12, 2024
b8e0173
marking EhrExtractStatus as failed in a test scenario
ORybak5 Nov 12, 2024
49dd7e8
the endpoint returns operationOutcome populated with more information
ORybak5 Nov 12, 2024
8b42391
adding a test for GpcStructureTask scheduler
ORybak5 Nov 12, 2024
8a1a8f5
Merge branch 'main' into NIAD-3234-create-endpoint
ORybak5 Nov 13, 2024
7a4ab0e
using a refactored code instead of a stub implementation
ORybak5 Nov 13, 2024
830622d
small refactoring and adding logging to indicate a resend
ORybak5 Nov 14, 2024
9cd5301
Covering a scenario where GP2GP transfer hasn't failed but another re…
ORybak5 Nov 15, 2024
1346972
converting integration tests into unit tests
ORybak5 Nov 18, 2024
b417275
converting all Ehr resend integration tests into unit tests
ORybak5 Nov 18, 2024
28600ed
adjusting tests for pitest to pass
ORybak5 Nov 18, 2024
5838168
making EhrExtractStatus GpcAccessDocument null before resending
ORybak5 Nov 20, 2024
af7598e
converting operationOutcome into json before sending it in ResponseEn…
ORybak5 Nov 21, 2024
02bfa50
test refactoring
ORybak5 Nov 21, 2024
c86cf77
addressing sonarQube improvement suggestion
ORybak5 Nov 21, 2024
d07b5c7
tests refactoring
ORybak5 Nov 21, 2024
be42315
addressing PR comments
ORybak5 Nov 22, 2024
06bdba3
Merge branch 'main' into NIAD-3234-create-endpoint
ORybak5 Nov 25, 2024
eb762db
error type change
ORybak5 Nov 25, 2024
e851152
tests fix for the new error code
ORybak5 Nov 25, 2024
3fa64f7
using a more appropriate error code
ORybak5 Nov 25, 2024
09bdec6
Rename test to be clearer
adrianclay Nov 26, 2024
ba1f846
Rename test for clarity
adrianclay Nov 26, 2024
7cb3adc
Add assertion that the save method gets called
adrianclay Nov 26, 2024
0de4853
Use assertAll
adrianclay Nov 26, 2024
fbc1f17
No longer update the createdAt time
adrianclay Nov 26, 2024
27457f5
Replace invalid ValueSet with valid value
adrianclay Nov 26, 2024
73c5eac
Move log entry below action, and be specific about task + conversationID
adrianclay Nov 26, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.stereotype.Service;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.StrictErrorHandler;
import uk.nhs.adaptors.gp2gp.common.exception.FhirValidationException;

@Service
public class FhirParseService {

private final IParser jsonParser = prepareParser();

public <T extends IBaseResource> T parseResource(String body, Class<T> fhirClass) {
Expand All @@ -20,6 +20,10 @@
}
}

public String encodeToJson(IBaseResource resource) {
return jsonParser.setPrettyPrint(true).encodeResourceToString(resource);

Check warning on line 24 in service/src/main/java/uk/nhs/adaptors/gp2gp/common/service/FhirParseService.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 24 without causing a test to fail

removed call to setPrettyPrint (covered by 4 tests RemoveChainedCallsMutator)
}

private IParser prepareParser() {
FhirContext ctx = FhirContext.forDstu3();
ctx.newJsonParser();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package uk.nhs.adaptors.gp2gp.ehr;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Meta;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.hl7.fhir.dstu3.model.UriType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import uk.nhs.adaptors.gp2gp.common.service.FhirParseService;
import uk.nhs.adaptors.gp2gp.common.service.RandomIdGeneratorService;
import uk.nhs.adaptors.gp2gp.common.service.TimestampService;
import uk.nhs.adaptors.gp2gp.common.task.TaskDispatcher;
import uk.nhs.adaptors.gp2gp.ehr.model.EhrExtractStatus;
import uk.nhs.adaptors.gp2gp.gpc.GetGpcStructuredTaskDefinition;

import java.util.Collections;

@Slf4j
@RestController
@AllArgsConstructor(onConstructor = @__(@Autowired))
@RequestMapping(path = "/ehr-resend")
public class EhrResendController {

private static final String OPERATION_OUTCOME_URL = "https://fhir.nhs.uk/STU3/StructureDefinition/GPConnect-OperationOutcome-1";
private static final String PRECONDITION_FAILED = "PRECONDITION_FAILED";
private static final String INVALID_IDENTIFIER_VALUE = "INVALID_IDENTIFIER_VALUE";

private EhrExtractStatusRepository ehrExtractStatusRepository;
private TaskDispatcher taskDispatcher;
private RandomIdGeneratorService randomIdGeneratorService;
private final TimestampService timestampService;
private final FhirParseService fhirParseService;

@PostMapping("/{conversationId}")
public ResponseEntity<String> scheduleEhrExtractResend(@PathVariable String conversationId) {
EhrExtractStatus ehrExtractStatus = ehrExtractStatusRepository.findByConversationId(conversationId).orElseGet(() -> null);

if (ehrExtractStatus == null) {
var details = getCodeableConcept(INVALID_IDENTIFIER_VALUE);
var diagnostics = "Provide a conversationId that exists and retry the operation";

var operationOutcome = createOperationOutcome(OperationOutcome.IssueType.VALUE,
OperationOutcome.IssueSeverity.ERROR,
details,
diagnostics);
var errorBody = fhirParseService.encodeToJson(operationOutcome);

return new ResponseEntity<>(errorBody, HttpStatus.NOT_FOUND);
}

if (hasNoErrorsInEhrReceivedAcknowledgement(ehrExtractStatus) && ehrExtractStatus.getError() == null) {

var details = getCodeableConcept(PRECONDITION_FAILED);
var diagnostics = "The current resend operation is still in progress. Please wait for it to complete before retrying";
var operationOutcome = createOperationOutcome(OperationOutcome.IssueType.BUSINESSRULE,
OperationOutcome.IssueSeverity.ERROR,
details,
diagnostics);
var errorBody = fhirParseService.encodeToJson(operationOutcome);
return new ResponseEntity<>(errorBody, HttpStatus.CONFLICT);
}

var updatedEhrExtractStatus = prepareEhrExtractStatusForNewResend(ehrExtractStatus);
ehrExtractStatusRepository.save(updatedEhrExtractStatus);
createGetGpcStructuredTask(updatedEhrExtractStatus);
LOGGER.info("Scheduled GetGpcStructuredTask for resend of ConversationId: {}", conversationId);

return new ResponseEntity<>(HttpStatus.ACCEPTED);
}

private static CodeableConcept getCodeableConcept(String codeableConceptCode) {
return new CodeableConcept().addCoding(
new Coding("https://fhir.nhs.uk/STU3/ValueSet/Spine-ErrorOrWarningCode-1", codeableConceptCode, null));
}

private static boolean hasNoErrorsInEhrReceivedAcknowledgement(EhrExtractStatus ehrExtractStatus) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment, but this doesn't really feel like it belongs in the Controller. Can refactor this in a later PR though.

var ehrReceivedAcknowledgement = ehrExtractStatus.getEhrReceivedAcknowledgement();
if (ehrReceivedAcknowledgement == null) {
return true;
}

var errors = ehrReceivedAcknowledgement.getErrors();
if (errors == null || errors.isEmpty()) {
return true;
}
return false;
}

private EhrExtractStatus prepareEhrExtractStatusForNewResend(EhrExtractStatus ehrExtractStatus) {

var now = timestampService.now();
ehrExtractStatus.setUpdatedAt(now);
ehrExtractStatus.setMessageTimestamp(now);
ehrExtractStatus.setEhrExtractCorePending(null);
ehrExtractStatus.setGpcAccessDocument(null);
ehrExtractStatus.setEhrContinue(null);
ehrExtractStatus.setEhrReceivedAcknowledgement(null);

return ehrExtractStatus;
}

private void createGetGpcStructuredTask(EhrExtractStatus ehrExtractStatus) {
var getGpcStructuredTaskDefinition = GetGpcStructuredTaskDefinition.getGetGpcStructuredTaskDefinition(randomIdGeneratorService,
ehrExtractStatus);
taskDispatcher.createTask(getGpcStructuredTaskDefinition);
}

public static OperationOutcome createOperationOutcome(
OperationOutcome.IssueType type, OperationOutcome.IssueSeverity severity, CodeableConcept details, String diagnostics) {
var operationOutcome = new OperationOutcome();
Meta meta = new Meta();
meta.setProfile(Collections.singletonList(new UriType(OPERATION_OUTCOME_URL)));
operationOutcome.setMeta(meta);
operationOutcome.addIssue()
.setCode(type)
.setSeverity(severity)
.setDetails(details)
.setDiagnostics(diagnostics);
return operationOutcome;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package uk.nhs.adaptors.gp2gp.common.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import uk.nhs.adaptors.gp2gp.common.configuration.ObjectMapperBean;
import uk.nhs.adaptors.gp2gp.ehr.EhrResendController;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;


@ExtendWith(MockitoExtension.class)
class FhirParseServiceTest {

public static final String INVALID_IDENTIFIER_VALUE = "INVALID_IDENTIFIER_VALUE";
private static final String OPERATION_OUTCOME_URL = "https://fhir.nhs.uk/STU3/StructureDefinition/GPConnect-OperationOutcome-1";
private OperationOutcome operationOutcome;
private ObjectMapper objectMapper;

@BeforeEach
void setUp() {
ObjectMapperBean objectMapperBean = new ObjectMapperBean();
objectMapper = objectMapperBean.objectMapper(new Jackson2ObjectMapperBuilder());

var details = getCodeableConcept();
var diagnostics = "Provide a conversationId that exists and retry the operation";
operationOutcome = EhrResendController.createOperationOutcome(OperationOutcome.IssueType.VALUE,
OperationOutcome.IssueSeverity.ERROR,
details,
diagnostics);
}

@Test
void ableToEncodeOperationOutcomeToJson() throws JsonProcessingException {
FhirParseService fhirParseService = new FhirParseService();

String convertedToJsonOperationOutcome = fhirParseService.encodeToJson(operationOutcome);

JsonNode rootNode = objectMapper.readTree(convertedToJsonOperationOutcome);
String code =
rootNode.path("issue").get(0).path("details").path("coding").get(0).path("code").asText();
String operationOutcomeUrl = rootNode.path("meta").path("profile").get(0).asText();

assertEquals(INVALID_IDENTIFIER_VALUE, code);
assertEquals(OPERATION_OUTCOME_URL, operationOutcomeUrl);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really we should be using assertJ here for readability (assertThat(code).isEqualTo(INVALID_IDENTIFIER_VALUE))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it is the other way around, I find "assertEquals" to be short, clean and readable.

}

private static CodeableConcept getCodeableConcept() {
var details = new CodeableConcept();
var codeableConceptCoding = new Coding();
codeableConceptCoding.setSystem("http://fhir.nhs.net/ValueSet/gpconnect-error-or-warning-code-1");
codeableConceptCoding.setCode(FhirParseServiceTest.INVALID_IDENTIFIER_VALUE);
details.setCoding(List.of(codeableConceptCoding));
return details;
}
}
Loading