From d337ac481f09c57f4cde4ce87b8ee5194853bcac Mon Sep 17 00:00:00 2001 From: James Herr Date: Mon, 25 Mar 2024 10:16:17 -0500 Subject: [PATCH 01/10] Added message type property --- .../external/hapi/HapiOrderConverter.java | 24 +++++++------------ .../hapi/HapiOrderConverterTest.groovy | 2 +- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java index 47c17fb61..ed132c57a 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java @@ -227,24 +227,10 @@ public FhirMetadata extractPublicMetadataToOperationOutcome( delivered = metadata.timeDelivered().toString(); } - operation - .getIssue() - .add( - createInformationIssueComponent( - String.format( - "%s ingestion", - metadata.messageType().toString().toLowerCase()), - ingestion)); + operation.getIssue().add(createInformationIssueComponent("ingestion", ingestion)); operation.getIssue().add(createInformationIssueComponent("payload hash", metadata.hash())); - operation - .getIssue() - .add( - createInformationIssueComponent( - String.format( - "%s delivered", - metadata.messageType().toString().toLowerCase()), - delivered)); + operation.getIssue().add(createInformationIssueComponent("delivered", delivered)); operation .getIssue() .add( @@ -255,6 +241,12 @@ public FhirMetadata extractPublicMetadataToOperationOutcome( .getIssue() .add(createInformationIssueComponent("status message", metadata.failureReason())); + operation + .getIssue() + .add( + createInformationIssueComponent( + "message type", metadata.messageType().toString())); + return new HapiFhirMetadata(operation); } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy index b6db07157..a974eaf6f 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy @@ -351,6 +351,6 @@ class HapiOrderConverterTest extends Specification { result.getIssue().get(4).diagnostics == time.toString() result.getIssue().get(5).diagnostics == PartnerMetadataStatus.DELIVERED.toString() result.getIssue().get(6).diagnostics == failureReason - result.getIssue().get(4).details.text.contains(messageType.toString().toLowerCase()) + result.getIssue().get(7).diagnostics == messageType.toString() } } From f9edb830133cee51e34a8ac4ebe1f6f154cf7296 Mon Sep 17 00:00:00 2001 From: saquino0827 Date: Mon, 25 Mar 2024 10:24:09 -0500 Subject: [PATCH 02/10] Update MockRSEndpoint to use random uuids --- .../gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy | 2 ++ .../external/localfile/MockRSEndpointClient.java | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy index 9e064d5b2..226316c85 100644 --- a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy +++ b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy @@ -1,5 +1,6 @@ package gov.hhs.cdc.trustedintermediary.e2e +import org.apache.hc.core5.http.io.entity.EntityUtils import spock.lang.Specification import java.nio.file.Files @@ -29,6 +30,7 @@ class MetadataTest extends Specification { when: def inboundMetadataResponse = metadataClient.get(inboundSubmissionId, true) + def body = JsonParsing.parseContent(inboundMetadataResponse) def outboundMetadataResponse = metadataClient.get(outboundSubmissionId, true) def inboundParsedJsonBody = JsonParsing.parseContent(inboundMetadataResponse) def outboundParsedJsonBody = JsonParsing.parseContent(outboundMetadataResponse) diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/MockRSEndpointClient.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/MockRSEndpointClient.java index 92205613d..d16bc7077 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/MockRSEndpointClient.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/MockRSEndpointClient.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.UUID; /** * A mock implementation of the RSEndpointClient interface that doesn't require a connection to @@ -37,7 +38,7 @@ public String requestWatersEndpoint(String body, String bearerToken) } catch (IOException e) { throw new ReportStreamEndpointClientException("Error writing the lab order", e); } - return "{ \"submissionId\": \"1234567890\" }"; + return "{ \"submissionId\": \"" + UUID.randomUUID() + "\" }"; } @Override From ac3246de19d2d31f83fc80f97bf5e23863918f6b Mon Sep 17 00:00:00 2001 From: saquino0827 Date: Mon, 25 Mar 2024 11:16:09 -0500 Subject: [PATCH 03/10] Update MetadataTest to cover the inclusion of the MessageType field in the OperationalOutcome Metadata --- .../cdc/trustedintermediary/e2e/MetadataTest.groovy | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy index 9e064d5b2..ef9fe713a 100644 --- a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy +++ b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy @@ -42,11 +42,13 @@ class MetadataTest extends Specification { [ "sender name", "receiver name", - "order ingestion", + "ingestion", "payload hash", "delivery status", - "status message" + "status message", + "message type" ].each { String metadataKey -> + println(metadataKey) def issue = (inboundParsedJsonBody.issue as List).find( {issue -> issue.details.text == metadataKey }) assert issue != null assert issue.diagnostics != null @@ -83,10 +85,11 @@ class MetadataTest extends Specification { [ "sender name", "receiver name", - "result ingestion", + "ingestion", "payload hash", "delivery status", - "status message" + "status message", + "message type" ].each { String metadataKey -> def issue = (inboundParsedJsonBody.issue as List).find( {issue -> issue.details.text == metadataKey }) assert issue != null From c6d38e2b121cabda2f75d7285ff1ce49f0b2c511 Mon Sep 17 00:00:00 2001 From: saquino0827 Date: Mon, 25 Mar 2024 16:02:00 -0500 Subject: [PATCH 04/10] Update PartnerMetadata Operational Outcome - Updates PartnerMetadata to use it's own converter - Adds the sent submission id as it's own field - Updates unit tests and fixes e2e tests --- .../e2e/MetadataTest.groovy | 16 +--- .../etor/EtorDomainRegistration.java | 9 ++- .../partner/PartnerMetadataConverter.java | 17 ++++ .../hapi/HapiPartnerMetadataConverter.java | 79 +++++++++++++++++++ .../etor/EtorDomainRegistrationTest.groovy | 7 +- .../hapi/HapiMetadataConverterTest.groovy | 49 ++++++++++++ 6 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataConverter.java create mode 100644 etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java create mode 100644 etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy diff --git a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy index ef9fe713a..ab31ae492 100644 --- a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy +++ b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy @@ -17,7 +17,6 @@ class MetadataTest extends Specification { given: def expectedStatusCode = 200 def inboundSubmissionId = UUID.randomUUID().toString() - def outboundSubmissionId = "1234567890" def orderClient = new EndpointClient("/v1/etor/orders") def labOrderJsonFileString = Files.readString(Path.of("../examples/Test/Orders/002_ORM_O01.fhir")) @@ -29,15 +28,11 @@ class MetadataTest extends Specification { when: def inboundMetadataResponse = metadataClient.get(inboundSubmissionId, true) - def outboundMetadataResponse = metadataClient.get(outboundSubmissionId, true) def inboundParsedJsonBody = JsonParsing.parseContent(inboundMetadataResponse) - def outboundParsedJsonBody = JsonParsing.parseContent(outboundMetadataResponse) then: inboundMetadataResponse.getCode() == expectedStatusCode - outboundMetadataResponse.getCode() == expectedStatusCode inboundParsedJsonBody.get("id") == inboundSubmissionId - outboundParsedJsonBody.get("id") == outboundSubmissionId [ "sender name", @@ -46,7 +41,8 @@ class MetadataTest extends Specification { "payload hash", "delivery status", "status message", - "message type" + "message type", + "sent submission id" ].each { String metadataKey -> println(metadataKey) def issue = (inboundParsedJsonBody.issue as List).find( {issue -> issue.details.text == metadataKey }) @@ -60,7 +56,6 @@ class MetadataTest extends Specification { given: def expectedStatusCode = 200 def inboundSubmissionId = UUID.randomUUID().toString() - def outboundSubmissionId = "1234567890" def resultClient = new EndpointClient("/v1/etor/results") def labResult = Files.readString(Path.of("../examples/Test/Results/001_ORU_R01.fhir")) @@ -72,15 +67,11 @@ class MetadataTest extends Specification { when: def inboundMetadataResponse = metadataClient.get(inboundSubmissionId, true) - def outboundMetadataResponse = metadataClient.get(outboundSubmissionId, true) def inboundParsedJsonBody = JsonParsing.parseContent(inboundMetadataResponse) - def outboundParsedJsonBody = JsonParsing.parseContent(outboundMetadataResponse) then: inboundMetadataResponse.getCode() == expectedStatusCode - outboundMetadataResponse.getCode() == expectedStatusCode inboundParsedJsonBody.get("id") == inboundSubmissionId - outboundParsedJsonBody.get("id") == outboundSubmissionId [ "sender name", @@ -89,7 +80,8 @@ class MetadataTest extends Specification { "payload hash", "delivery status", "status message", - "message type" + "message type", + "sent submission id" ].each { String metadataKey -> def issue = (inboundParsedJsonBody.issue as List).find( {issue -> issue.details.text == metadataKey }) assert issue != null diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java index 057e7fd5b..4872a9c8b 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java @@ -15,6 +15,7 @@ import gov.hhs.cdc.trustedintermediary.etor.messages.SendMessageHelper; import gov.hhs.cdc.trustedintermediary.etor.messages.UnableToSendMessageException; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata; +import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataConverter; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataException; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataOrchestrator; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataStorage; @@ -36,6 +37,7 @@ import gov.hhs.cdc.trustedintermediary.external.database.PostgresDao; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiMessageConverterHelper; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrderConverter; +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiPartnerMetadataConverter; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiResultConverter; import gov.hhs.cdc.trustedintermediary.external.localfile.FilePartnerMetadataStorage; import gov.hhs.cdc.trustedintermediary.external.localfile.MockRSEndpointClient; @@ -79,8 +81,7 @@ public class EtorDomainRegistration implements DomainConnector { @Inject Logger logger; @Inject DomainResponseHelper domainResponseHelper; @Inject PartnerMetadataOrchestrator partnerMetadataOrchestrator; - - @Inject OrderConverter orderConverter; + @Inject PartnerMetadataConverter partnerMetadataConverter; @Inject HapiFhir fhir; @@ -120,6 +121,8 @@ public Map> domainRegistra // Metadata ApplicationContext.register( PartnerMetadataOrchestrator.class, PartnerMetadataOrchestrator.getInstance()); + ApplicationContext.register( + PartnerMetadataConverter.class, HapiPartnerMetadataConverter.getInstance()); ApplicationContext.register(SendMessageHelper.class, SendMessageHelper.getInstance()); @@ -209,7 +212,7 @@ DomainResponse handleMetadata(DomainRequest request) { } FhirMetadata responseObject = - orderConverter.extractPublicMetadataToOperationOutcome( + partnerMetadataConverter.extractPublicMetadataToOperationOutcome( metadata.get(), metadataId); return domainResponseHelper.constructOkResponseFromString( diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataConverter.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataConverter.java new file mode 100644 index 000000000..04531e94c --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataConverter.java @@ -0,0 +1,17 @@ +package gov.hhs.cdc.trustedintermediary.etor.metadata.partner; + +import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata; + +public interface PartnerMetadataConverter { + + /** + * This method will convert {@link PartnerMetadata} and convert it into an {@link + * org.hl7.fhir.r4.model.OperationOutcome} + * + * @param metadata + * @param requestedId + * @return + */ + FhirMetadata extractPublicMetadataToOperationOutcome( + PartnerMetadata metadata, String requestedId); +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java new file mode 100644 index 000000000..5815897c2 --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java @@ -0,0 +1,79 @@ +package gov.hhs.cdc.trustedintermediary.external.hapi; + +import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata; +import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataConverter; +import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata; +import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.HapiFhirMetadata; +import org.hl7.fhir.r4.model.OperationOutcome; + +public class HapiPartnerMetadataConverter implements PartnerMetadataConverter { + + private static final HapiPartnerMetadataConverter INSTANCE = new HapiPartnerMetadataConverter(); + + public static HapiPartnerMetadataConverter getInstance() { + return INSTANCE; + } + + private HapiPartnerMetadataConverter() {} + + public FhirMetadata extractPublicMetadataToOperationOutcome( + PartnerMetadata metadata, String requestedId) { + var operation = new OperationOutcome(); + + operation.setId(requestedId); + operation.getIssue().add(createInformationIssueComponent("sender name", metadata.sender())); + operation + .getIssue() + .add(createInformationIssueComponent("receiver name", metadata.receiver())); + + String ingestion = null; + String delivered = null; + if (metadata.timeReceived() != null) { + ingestion = metadata.timeReceived().toString(); + } + if (metadata.timeDelivered() != null) { + delivered = metadata.timeDelivered().toString(); + } + + operation.getIssue().add(createInformationIssueComponent("ingestion", ingestion)); + + operation.getIssue().add(createInformationIssueComponent("payload hash", metadata.hash())); + operation.getIssue().add(createInformationIssueComponent("delivered", delivered)); + operation + .getIssue() + .add( + createInformationIssueComponent( + "delivery status", metadata.deliveryStatus().toString())); + + operation + .getIssue() + .add(createInformationIssueComponent("status message", metadata.failureReason())); + + operation + .getIssue() + .add( + createInformationIssueComponent( + "message type", metadata.messageType().toString())); + + operation + .getIssue() + .add( + createInformationIssueComponent( + "sent submission id", metadata.sentSubmissionId())); + + return new HapiFhirMetadata(operation); + } + + protected OperationOutcome.OperationOutcomeIssueComponent createInformationIssueComponent( + String details, String diagnostics) { + OperationOutcome.OperationOutcomeIssueComponent issue = + new OperationOutcome.OperationOutcomeIssueComponent(); + + issue.setSeverity(OperationOutcome.IssueSeverity.INFORMATION); + issue.setCode(OperationOutcome.IssueType.INFORMATIONAL); + issue.getDetails().setText(details); + issue.setDiagnostics(diagnostics); + + return issue; + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy index e8fb4d76b..d8992342a 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy @@ -15,6 +15,7 @@ import gov.hhs.cdc.trustedintermediary.etor.demographics.PatientDemographicsResp import gov.hhs.cdc.trustedintermediary.etor.messages.MessageRequestHandler import gov.hhs.cdc.trustedintermediary.etor.messages.UnableToSendMessageException import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata +import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataConverter import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataException import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataMessageType import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataOrchestrator @@ -240,9 +241,9 @@ class EtorDomainRegistrationTest extends Specification { mockResponseHelper.constructOkResponseFromString(_ as String) >> new DomainResponse(expectedStatusCode) TestApplicationContext.register(DomainResponseHelper, mockResponseHelper) - def mockOrderConverter = Mock(OrderConverter) - mockOrderConverter.extractPublicMetadataToOperationOutcome(_ as PartnerMetadata, _ as String) >> Mock(FhirMetadata) - TestApplicationContext.register(OrderConverter, mockOrderConverter) + def mockPartnerMetadataConverter = Mock(PartnerMetadataConverter) + mockPartnerMetadataConverter.extractPublicMetadataToOperationOutcome(_ as PartnerMetadata, _ as String) >> Mock(FhirMetadata) + TestApplicationContext.register(PartnerMetadataConverter, mockPartnerMetadataConverter) def mockFhir = Mock(HapiFhir) mockFhir.encodeResourceToJson(_) >> "" diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy new file mode 100644 index 000000000..5b05ba9d1 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy @@ -0,0 +1,49 @@ +package gov.hhs.cdc.trustedintermediary.external.hapi + +import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata +import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataMessageType +import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataStatus +import org.hl7.fhir.r4.model.OperationOutcome +import spock.lang.Specification + +import java.time.Instant + +class HapiMetadataConverterTest extends Specification { + def "creating an issue returns a valid OperationOutcomeIssueComponent with Information level severity and code" () { + when: + def output = HapiPartnerMetadataConverter.getInstance().createInformationIssueComponent("test_details", "test_diagnostics") + then: + output.getSeverity() == OperationOutcome.IssueSeverity.INFORMATION + output.getCode() == OperationOutcome.IssueType.INFORMATIONAL + output.getDetails().getText() == "test_details" + output.getDiagnostics() == "test_diagnostics" + } + + def "ExtractPublicMetadata to OperationOutcome returns FHIR metadata"() { + given: + + def sender = "sender" + def receiver = "receiver" + def time = Instant.now() + def hash = "hash" + def failureReason = "timed_out" + def messageType = PartnerMetadataMessageType.ORDER + PartnerMetadata metadata = new PartnerMetadata( + "receivedSubmissionId", "sentSubmissionId", sender, receiver, time, time, hash, PartnerMetadataStatus.DELIVERED, failureReason, messageType) + + when: + def result = HapiPartnerMetadataConverter.getInstance().extractPublicMetadataToOperationOutcome(metadata, "receivedSubmissionId").getUnderlyingOutcome() as OperationOutcome + + then: + result.getId() == "receivedSubmissionId" + result.getIssue().get(0).diagnostics == sender + result.getIssue().get(1).diagnostics == receiver + result.getIssue().get(2).diagnostics == time.toString() + result.getIssue().get(3).diagnostics == hash + result.getIssue().get(4).diagnostics == time.toString() + result.getIssue().get(5).diagnostics == PartnerMetadataStatus.DELIVERED.toString() + result.getIssue().get(6).diagnostics == failureReason + result.getIssue().get(7).diagnostics == messageType.toString() + result.getIssue().get(8).diagnostics == "sentSubmissionId" + } +} From 1c7fa4e7c9cd938f517166b77658b345630c2d2c Mon Sep 17 00:00:00 2001 From: saquino0827 Date: Tue, 26 Mar 2024 10:02:04 -0500 Subject: [PATCH 05/10] PR comment fixes - update tests to include outbound submission testing and outbound submission id/inbound submission id - remove metadata methods from OrderConverter and its implementation --- .../e2e/MetadataTest.groovy | 19 ++++-- .../etor/orders/OrderConverter.java | 5 -- .../external/hapi/HapiOrderConverter.java | 60 ------------------- .../hapi/HapiPartnerMetadataConverter.java | 9 ++- .../hapi/HapiOrderConverterTest.groovy | 38 ------------ 5 files changed, 23 insertions(+), 108 deletions(-) diff --git a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy index 208e06af9..cd2147db2 100644 --- a/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy +++ b/e2e/src/test/groovy/gov/hhs/cdc/trustedintermediary/e2e/MetadataTest.groovy @@ -1,6 +1,6 @@ package gov.hhs.cdc.trustedintermediary.e2e -import org.apache.hc.core5.http.io.entity.EntityUtils + import spock.lang.Specification import java.nio.file.Files @@ -30,10 +30,15 @@ class MetadataTest extends Specification { when: def inboundMetadataResponse = metadataClient.get(inboundSubmissionId, true) def inboundParsedJsonBody = JsonParsing.parseContent(inboundMetadataResponse) + def outboundSubmissionId = inboundParsedJsonBody.issue[8].diagnostics + def outboundMetadataResponse = metadataClient.get(outboundSubmissionId, true) + def outboundParsedJsonBody = JsonParsing.parseContent(outboundMetadataResponse) then: inboundMetadataResponse.getCode() == expectedStatusCode + outboundMetadataResponse.getCode() == expectedStatusCode inboundParsedJsonBody.get("id") == inboundSubmissionId + outboundParsedJsonBody.get("id") == outboundSubmissionId [ "sender name", @@ -43,9 +48,9 @@ class MetadataTest extends Specification { "delivery status", "status message", "message type", - "sent submission id" + "outbound submission id", + "inbound submission id" ].each { String metadataKey -> - println(metadataKey) def issue = (inboundParsedJsonBody.issue as List).find( {issue -> issue.details.text == metadataKey }) assert issue != null assert issue.diagnostics != null @@ -69,10 +74,15 @@ class MetadataTest extends Specification { when: def inboundMetadataResponse = metadataClient.get(inboundSubmissionId, true) def inboundParsedJsonBody = JsonParsing.parseContent(inboundMetadataResponse) + def outboundSubmissionId = inboundParsedJsonBody.issue[8].diagnostics + def outboundMetadataResponse = metadataClient.get(outboundSubmissionId, true) + def outboundParsedJsonBody = JsonParsing.parseContent(outboundMetadataResponse) then: inboundMetadataResponse.getCode() == expectedStatusCode + outboundMetadataResponse.getCode() == expectedStatusCode inboundParsedJsonBody.get("id") == inboundSubmissionId + outboundParsedJsonBody.get("id") == outboundSubmissionId [ "sender name", @@ -82,7 +92,8 @@ class MetadataTest extends Specification { "delivery status", "status message", "message type", - "sent submission id" + "outbound submission id", + "inbound submission id" ].each { String metadataKey -> def issue = (inboundParsedJsonBody.issue as List).find( {issue -> issue.details.text == metadataKey }) assert issue != null diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/OrderConverter.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/OrderConverter.java index 8ab155978..684652130 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/OrderConverter.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/OrderConverter.java @@ -1,8 +1,6 @@ package gov.hhs.cdc.trustedintermediary.etor.orders; import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics; -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata; -import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata; /** Interface for converting things to orders and things in orders. */ public interface OrderConverter { @@ -13,7 +11,4 @@ public interface OrderConverter { Order addContactSectionToPatientResource(Order order); Order addEtorProcessingTag(Order message); - - FhirMetadata extractPublicMetadataToOperationOutcome( - PartnerMetadata metadata, String requestedId); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java index ed132c57a..fdb0ff7d9 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverter.java @@ -1,9 +1,6 @@ package gov.hhs.cdc.trustedintermediary.external.hapi; import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics; -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata; -import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.FhirMetadata; -import gov.hhs.cdc.trustedintermediary.etor.operationoutcomes.HapiFhirMetadata; import gov.hhs.cdc.trustedintermediary.etor.orders.Order; import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; @@ -18,7 +15,6 @@ import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.MessageHeader; import org.hl7.fhir.r4.model.Meta; -import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Provenance; import org.hl7.fhir.r4.model.Reference; @@ -206,60 +202,4 @@ private Provenance createProvenanceResource(Date orderDate) { return provenance; } - - @Override - public FhirMetadata extractPublicMetadataToOperationOutcome( - PartnerMetadata metadata, String requestedId) { - var operation = new OperationOutcome(); - - operation.setId(requestedId); - operation.getIssue().add(createInformationIssueComponent("sender name", metadata.sender())); - operation - .getIssue() - .add(createInformationIssueComponent("receiver name", metadata.receiver())); - - String ingestion = null; - String delivered = null; - if (metadata.timeReceived() != null) { - ingestion = metadata.timeReceived().toString(); - } - if (metadata.timeDelivered() != null) { - delivered = metadata.timeDelivered().toString(); - } - - operation.getIssue().add(createInformationIssueComponent("ingestion", ingestion)); - - operation.getIssue().add(createInformationIssueComponent("payload hash", metadata.hash())); - operation.getIssue().add(createInformationIssueComponent("delivered", delivered)); - operation - .getIssue() - .add( - createInformationIssueComponent( - "delivery status", metadata.deliveryStatus().toString())); - - operation - .getIssue() - .add(createInformationIssueComponent("status message", metadata.failureReason())); - - operation - .getIssue() - .add( - createInformationIssueComponent( - "message type", metadata.messageType().toString())); - - return new HapiFhirMetadata(operation); - } - - protected OperationOutcome.OperationOutcomeIssueComponent createInformationIssueComponent( - String details, String diagnostics) { - OperationOutcome.OperationOutcomeIssueComponent issue = - new OperationOutcome.OperationOutcomeIssueComponent(); - - issue.setSeverity(OperationOutcome.IssueSeverity.INFORMATION); - issue.setCode(OperationOutcome.IssueType.INFORMATIONAL); - issue.getDetails().setText(details); - issue.setDiagnostics(diagnostics); - - return issue; - } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java index 5815897c2..63b10ba95 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiPartnerMetadataConverter.java @@ -16,6 +16,7 @@ public static HapiPartnerMetadataConverter getInstance() { private HapiPartnerMetadataConverter() {} + @Override public FhirMetadata extractPublicMetadataToOperationOutcome( PartnerMetadata metadata, String requestedId) { var operation = new OperationOutcome(); @@ -59,7 +60,13 @@ public FhirMetadata extractPublicMetadataToOperationOutcome( .getIssue() .add( createInformationIssueComponent( - "sent submission id", metadata.sentSubmissionId())); + "outbound submission id", metadata.sentSubmissionId())); + + operation + .getIssue() + .add( + createInformationIssueComponent( + "inbound submission id", metadata.receivedSubmissionId())); return new HapiFhirMetadata(operation); } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy index a974eaf6f..3c2a8d3a2 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiOrderConverterTest.groovy @@ -273,17 +273,6 @@ class HapiOrderConverterTest extends Specification { !contactSection.hasName() } - - def "creating an issue returns a valid OperationOutcomeIssueComponent with Information level severity and code" () { - when: - def output = HapiOrderConverter.getInstance().createInformationIssueComponent("test_details", "test_diagnostics") - then: - output.getSeverity() == OperationOutcome.IssueSeverity.INFORMATION - output.getCode() == OperationOutcome.IssueType.INFORMATIONAL - output.getDetails().getText() == "test_details" - output.getDiagnostics() == "test_diagnostics" - } - Patient fakePatientResource(boolean addHumanName) { def patient = new Patient() @@ -326,31 +315,4 @@ class HapiOrderConverterTest extends Specification { return patient } - - def "ExtractPublicMetadata to OperationOutcome returns FHIR metadata"() { - given: - - def sender = "sender" - def receiver = "receiver" - def time = Instant.now() - def hash = "hash" - def failureReason = "timed_out" - def messageType = PartnerMetadataMessageType.ORDER - PartnerMetadata metadata = new PartnerMetadata( - "receivedSubmissionId", "sentSubmissionId", sender, receiver, time, time, hash, PartnerMetadataStatus.DELIVERED, failureReason, messageType) - - when: - def result = HapiOrderConverter.getInstance().extractPublicMetadataToOperationOutcome(metadata, "receivedSubmissionId").getUnderlyingOutcome() as OperationOutcome - - then: - result.getId() == "receivedSubmissionId" - result.getIssue().get(0).diagnostics == sender - result.getIssue().get(1).diagnostics == receiver - result.getIssue().get(2).diagnostics == time.toString() - result.getIssue().get(3).diagnostics == hash - result.getIssue().get(4).diagnostics == time.toString() - result.getIssue().get(5).diagnostics == PartnerMetadataStatus.DELIVERED.toString() - result.getIssue().get(6).diagnostics == failureReason - result.getIssue().get(7).diagnostics == messageType.toString() - } } From 06dfa8780f511dad66c4e6d132e6a6e73d95d5ee Mon Sep 17 00:00:00 2001 From: halprin Date: Tue, 26 Mar 2024 12:11:37 -0600 Subject: [PATCH 06/10] Delete the PR environment only when the PR is closed --- .github/workflows/terraform-ci-deploy.yml | 34 +------------ .github/workflows/terraform-ci-destroy.yml | 57 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/terraform-ci-destroy.yml diff --git a/.github/workflows/terraform-ci-deploy.yml b/.github/workflows/terraform-ci-deploy.yml index ae5a19a89..e02f6963e 100644 --- a/.github/workflows/terraform-ci-deploy.yml +++ b/.github/workflows/terraform-ci-deploy.yml @@ -17,6 +17,7 @@ jobs: - uses: actions/checkout@v4 + # keep in sync with terraform-ci-destroy.yml - uses: dorny/paths-filter@v3 id: filter with: @@ -74,36 +75,3 @@ jobs: - paths-filter uses: ./.github/workflows/deploy_reusable-skip.yml if: needs.paths-filter.outputs.operations != 'true' - - - destroy-environment: - name: Destroy PR Environment - environment: - name: pr - needs: - - pr-deploy - - paths-filter - if: needs.paths-filter.outputs.operations == 'true' && always() - runs-on: ubuntu-latest - env: - ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - ARM_USE_OIDC: true - permissions: - id-token: write - contents: read - defaults: - run: - working-directory: operations/environments/pr - - steps: - - - uses: actions/checkout@v4 - - - name: Terraform Init - id: init - run: terraform init -backend-config="key=pr_${{ github.event.number }}.tfstate" - - - name: Terraform Destroy - run: terraform destroy -auto-approve -input=false -var="pr_number=${{ github.event.number }}" diff --git a/.github/workflows/terraform-ci-destroy.yml b/.github/workflows/terraform-ci-destroy.yml new file mode 100644 index 000000000..6b9420b69 --- /dev/null +++ b/.github/workflows/terraform-ci-destroy.yml @@ -0,0 +1,57 @@ +name: Terraform CI Deploy + +on: + pull_request: + types: + - closed + +jobs: + + paths-filter: + runs-on: ubuntu-latest + outputs: + operations: ${{ steps.filter.outputs.operations }} + + steps: + + - uses: actions/checkout@v4 + + # keep in sync with terraform-ci-deploy.yml + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + operations: + - 'operations/environments/pr/**' + - 'operations/template/**' + + destroy-environment: + name: Destroy PR Environment + environment: + name: pr + needs: + - paths-filter + if: needs.paths-filter.outputs.operations == 'true' + runs-on: ubuntu-latest + env: + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + ARM_USE_OIDC: true + permissions: + id-token: write + contents: read + defaults: + run: + working-directory: operations/environments/pr + + steps: + + - uses: actions/checkout@v4 + + - name: Terraform Init + id: init + run: terraform init -backend-config="key=pr_${{ github.event.number }}.tfstate" + + - name: Terraform Destroy + run: terraform destroy -auto-approve -input=false -var="pr_number=${{ github.event.number }}" From 0796ece2279a8d8549e83a1c5f62acaf42e16783 Mon Sep 17 00:00:00 2001 From: halprin Date: Tue, 26 Mar 2024 12:12:28 -0600 Subject: [PATCH 07/10] Rename the destroy workflow --- .github/workflows/terraform-ci-destroy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/terraform-ci-destroy.yml b/.github/workflows/terraform-ci-destroy.yml index 6b9420b69..c6c3ff9b1 100644 --- a/.github/workflows/terraform-ci-destroy.yml +++ b/.github/workflows/terraform-ci-destroy.yml @@ -1,4 +1,4 @@ -name: Terraform CI Deploy +name: Terraform CI Destroy on: pull_request: From a19f790bfeb7a90b16fabbd5f9c4a1fe4bfdffc6 Mon Sep 17 00:00:00 2001 From: halprin Date: Tue, 26 Mar 2024 12:14:09 -0600 Subject: [PATCH 08/10] Remove unused GitHub action jjob id --- .github/workflows/terraform-ci-destroy.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/terraform-ci-destroy.yml b/.github/workflows/terraform-ci-destroy.yml index c6c3ff9b1..9f9c928f0 100644 --- a/.github/workflows/terraform-ci-destroy.yml +++ b/.github/workflows/terraform-ci-destroy.yml @@ -50,7 +50,6 @@ jobs: - uses: actions/checkout@v4 - name: Terraform Init - id: init run: terraform init -backend-config="key=pr_${{ github.event.number }}.tfstate" - name: Terraform Destroy From d557068b91a8e6cc90916b782d1250e8fdeea39f Mon Sep 17 00:00:00 2001 From: saquino0827 Date: Tue, 26 Mar 2024 13:33:29 -0500 Subject: [PATCH 09/10] Add missing receivedSubmissionId check in HapiMetadataConverterTest --- .../external/hapi/HapiMetadataConverterTest.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy index 5b05ba9d1..937ef38eb 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy @@ -45,5 +45,6 @@ class HapiMetadataConverterTest extends Specification { result.getIssue().get(6).diagnostics == failureReason result.getIssue().get(7).diagnostics == messageType.toString() result.getIssue().get(8).diagnostics == "sentSubmissionId" + result.getIssue().get(9).diagnostics == "receivedSubmissionId" } } From 714190fb09546112dcacd671f54aaa0f937dd3d4 Mon Sep 17 00:00:00 2001 From: Basilio Bogado <541149+basiliskus@users.noreply.github.com> Date: Wed, 27 Mar 2024 07:23:42 -0700 Subject: [PATCH 10/10] Updated instructions to set up RS locally (#979) --- .secrets.baseline | 14 +++++++++++--- README.md | 24 ++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 2bcc73f34..bc6710c5f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -138,7 +138,7 @@ "filename": "README.md", "hashed_secret": "367e3228ed16bf72b36de9a4134ee8b825cafacb", "is_verified": false, - "line_number": 316, + "line_number": 314, "is_secret": false }, { @@ -146,7 +146,7 @@ "filename": "README.md", "hashed_secret": "40bd7d88eae0468b048e62e1056ac390970b2b51", "is_verified": false, - "line_number": 321, + "line_number": 319, "is_secret": false }, { @@ -154,7 +154,15 @@ "filename": "README.md", "hashed_secret": "0d46754ae17642645ca041edaac9a1c1569f5edc", "is_verified": false, - "line_number": 326, + "line_number": 324, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "README.md", + "hashed_secret": "3c4da28c9bc45d01e4950ee6f8e67a8d1a1ec19b", + "is_verified": false, + "line_number": 334, "is_secret": false } ], diff --git a/README.md b/README.md index 3e5984b4d..03a051827 100644 --- a/README.md +++ b/README.md @@ -281,13 +281,11 @@ For database documentation [go here](/docs/database.md) ApplicationContext.register(RSEndpointClient.class, MockRSEndpointClient.getInstance()); } else { ApplicationContext.register(RSEndpointClient.class, ReportStreamEndpointClient.getInstance()); - ApplicationContext.register(AzureClient.class, AzureClient.getInstance()); } ``` with: ```Java ApplicationContext.register(RSEndpointClient.class, ReportStreamEndpointClient.getInstance()); - ApplicationContext.register(AzureClient.class, AzureClient.getInstance()); ``` 3. Run TI with `./gradlew clean app:run` @@ -331,9 +329,31 @@ with this option enabled. "apiKey": "Contents of file at trusted-intermediary/mock_credentials/organization-report-stream-private-key-local.pem", "user": "flexion" } + ``` + 3. Create secret for `DEFAULT-SFTP` + 1. Path for this secret: `DEFAULT-SFTP` + 2. JSON data: + ``` + { + "@type": "UserPass", + "user": "user", + "pass": "pass" + } + ``` #### Submit request to ReportStream +In order to submit a request, you'll need to authenticate with ReportStream using JWT auth: +1. Create a JWT for the sender (e.g. `flexion.simulated-hospital`) using the sender's private key. You may use [this CLI tool](https://github.com/mike-engel/jwt-cli) to create the JWT: + ``` + jwt encode --exp='+5min' --jti $(uuidgen) --alg RS256 -k -i -s -a staging.prime.cdc.gov --no-iat -S @/path/to/sender_private.pem + ``` +2. Use the generated JWT to authenticate with ReportStream and get the token, which will be in the `access_token` response + ``` + curl --header 'Content-Type: application/x-www-form-urlencoded' --data 'scope=flexion.*.report' --data 'client_assertion=' --data 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' --data 'grant_type=client_credentials' 'http://localhost:7071/api/token' + ``` +3. Submit an Order or Result using the returned token + ##### Orders To test sending from a simulated hospital: