Skip to content
This repository has been archived by the owner on Feb 7, 2025. It is now read-only.

Story/672/marshal results into fhir #755

Merged
merged 21 commits into from
Jan 9, 2024
Merged
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
53a0d0b
Initial commit with the needed FHIR resources created in the HapiOrde…
jcrichlake Jan 4, 2024
8abaf38
Merge branch 'main' of https://github.com/CDCgov/trusted-intermediary…
jcrichlake Jan 4, 2024
eb091c2
Updating Orchestrator with WIP of converter call
jcrichlake Jan 4, 2024
65a1606
Fixed postgres DAO issues and added marshaling to metadata handler
jcrichlake Jan 5, 2024
8894567
Reverting Azure client registration
jcrichlake Jan 5, 2024
a4eb334
Fixing other test
jcrichlake Jan 8, 2024
77f7f61
Merge branch 'main' into story/672/marshal-results-into-fhir
jcrichlake Jan 8, 2024
fb2e2f9
Fixing test
jcrichlake Jan 8, 2024
9c18f78
Fixing formatting
jcrichlake Jan 8, 2024
e3a0712
Adding test for Domain Response helper methods
jcrichlake Jan 8, 2024
6f34e1e
Re-adding constructor temporarily
jcrichlake Jan 8, 2024
757fbbd
Fixing e2e test and linting
jcrichlake Jan 8, 2024
fbe883a
Merge branch 'main' into story/672/marshal-results-into-fhir
jcrichlake Jan 8, 2024
4b47b2d
Removing commented code
jcrichlake Jan 8, 2024
2ef1be6
Merge branch 'story/672/marshal-results-into-fhir' of https://github.…
jcrichlake Jan 8, 2024
a81f471
Updated openapi docs for metadata response
basiliskus Jan 8, 2024
8b8434e
Merge branch 'main' into story/672/marshal-results-into-fhir
halprin Jan 8, 2024
d363e8f
Merge branch 'main' into story/672/marshal-results-into-fhir
jcrichlake Jan 9, 2024
1d412cc
Adding wrapper class
jcrichlake Jan 9, 2024
03c54ce
Merge branch 'story/672/marshal-results-into-fhir' of https://github.…
jcrichlake Jan 9, 2024
269dd5c
Adding java docs
jcrichlake Jan 9, 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
2 changes: 1 addition & 1 deletion .github/workflows/terraform-deploy_reusable.yml
Original file line number Diff line number Diff line change
@@ -74,7 +74,7 @@ jobs:
- name: Run Db migration
run: |
export PGPASSWORD=$(az account get-access-token --resource-type oss-rdbms --query "[accessToken]" -o tsv)
psql "host=$(terraform output -raw database_hostname) port=5432 dbname=postgres user=cdcti-github sslmode=require" -c "CREATE TABLE IF NOT EXISTS metadata (message_id varchar(30), sender varchar(30), receiver varchar(30), hash_of_order varchar(1000), time_received timestamptz); GRANT ALL ON metadata TO azure_pg_admin; ALTER TABLE metadata OWNER TO azure_pg_admin;"
psql "host=$(terraform output -raw database_hostname) port=5432 dbname=postgres user=cdcti-github sslmode=require" -c "CREATE TABLE IF NOT EXISTS metadata (message_id varchar(30) PRIMARY KEY, sender varchar(30), receiver varchar(30), hash_of_order varchar(1000), time_received timestamptz); GRANT ALL ON metadata TO azure_pg_admin; ALTER TABLE metadata OWNER TO azure_pg_admin;"

- id: export-terraform-output
name: Export Terraform Output
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ class MetadataTest extends Specification {

then:
metadataResponse.getCode() == expectedStatusCode
parsedJsonBody.receivedSubmissionId == submissionId
parsedJsonBody.get("id") == submissionId
}

def "a 404 is returned when there is no metadata for a given ID"() {
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@
import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamOrderSender;
import gov.hhs.cdc.trustedintermediary.wrappers.DbDao;
import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException;
import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir;
import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
import gov.hhs.cdc.trustedintermediary.wrappers.SqlDriverManager;
import java.io.IOException;
@@ -43,6 +44,7 @@
import java.util.Optional;
import java.util.function.Function;
import javax.inject.Inject;
import org.hl7.fhir.r4.model.OperationOutcome;

/**
* The domain connector for the ETOR domain. It connects it with the larger trusted intermediary. It
@@ -62,6 +64,10 @@ public class EtorDomainRegistration implements DomainConnector {
@Inject DomainResponseHelper domainResponseHelper;
@Inject PartnerMetadataOrchestrator partnerMetadataOrchestrator;

@Inject OrderConverter orderConverter;

@Inject HapiFhir fhir;

private final Map<HttpEndpoint, Function<DomainRequest, DomainResponse>> endpoints =
Map.of(
new HttpEndpoint("POST", DEMOGRAPHICS_API_ENDPOINT, true),
@@ -175,7 +181,11 @@ DomainResponse handleMetadata(DomainRequest request) {
404, "Metadata not found for ID: " + metadataId);
}

return domainResponseHelper.constructOkResponse(metadata.get());
OperationOutcome responseObject =
orderConverter.extractPublicMetadataToOperationOutcome(metadata.get());

return domainResponseHelper.constructOkResponseFromString(
fhir.encodeResourceToJson(responseObject));
} catch (PartnerMetadataException e) {
String errorMessage = "Unable to retrieve requested metadata";
logger.logError(errorMessage, e);
Original file line number Diff line number Diff line change
@@ -25,6 +25,15 @@ public PartnerMetadata(
this(receivedSubmissionId, null, sender, null, timeReceived, hash);
}

public PartnerMetadata(
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes in the PR to add Submission received id

Copy link
Contributor

Choose a reason for hiding this comment

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

So it will be added in this PR, and then removed in the other one? Is that necessary?

String receivedSubmissionId,
String sender,
String receiver,
Instant timeReceived,
String hash) {
this(receivedSubmissionId, null, sender, receiver, timeReceived, hash);
}

public PartnerMetadata(String receivedSubmissionId, String hash) {
this(receivedSubmissionId, null, null, null, null, hash);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package gov.hhs.cdc.trustedintermediary.etor.orders;

import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics;
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata;
import org.hl7.fhir.r4.model.OperationOutcome;

/** Interface for converting things to orders and things in orders. */
public interface OrderConverter {
@@ -9,4 +11,6 @@ public interface OrderConverter {
Order<?> convertMetadataToOmlOrder(Order<?> order);

Order<?> addContactSectionToPatientResource(Order<?> order);

OperationOutcome extractPublicMetadataToOperationOutcome(PartnerMetadata metadata);
jcrichlake marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ protected Connection connect() throws SQLException {

// If the below prop isn't set to require and we just set ssl=true it will expect a CA cert
// in azure which breaks it
props.setProperty("sslmode", ssl);
props.setProperty("ssl", ssl);
conn = driverManager.getConnection(url, props);
logger.logInfo("DB Connected Successfully");
return conn;
@@ -90,9 +90,12 @@ public synchronized void upsertMetadata(

try (Connection conn = connect();
PreparedStatement statement =
conn.prepareStatement("INSERT INTO metadata VALUES (?, ?, ?, ?, ?)")) {
// TODO: Update the below statement to handle on conflict, after we figure out what that
// behavior should be
conn.prepareStatement(
"""
INSERT INTO metadata VALUES (?, ?, ?, ?, ?)
ON CONFLICT (message_id) DO UPDATE SET receiver = EXCLUDED.receiver
""")) {

statement.setString(1, receivedSubmissionId);
statement.setString(2, sender);
statement.setString(3, receiver);
@@ -120,6 +123,7 @@ public synchronized PartnerMetadata fetchMetadata(String receivedSubmissionId)

return new PartnerMetadata(
result.getString("message_id"),
result.getString("sender"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change going to be in the other PR that will add the sentSubmissionId handling?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes

result.getString("receiver"),
result.getTimestamp("time_received").toInstant(),
result.getString("hash_of_order"));
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gov.hhs.cdc.trustedintermediary.external.hapi;

import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics;
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata;
import gov.hhs.cdc.trustedintermediary.etor.orders.Order;
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter;
import gov.hhs.cdc.trustedintermediary.wrappers.Logger;
@@ -15,6 +16,7 @@
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;
@@ -200,4 +202,36 @@ private Provenance createProvenanceResource(Date orderDate) {

return provenance;
}

@Override
public OperationOutcome extractPublicMetadataToOperationOutcome(PartnerMetadata metadata) {
Fixed Show fixed Hide fixed
var operation = new OperationOutcome();

operation.setId(metadata.receivedSubmissionId());
operation.getIssue().add(createInformationIssueComponent("sender name", metadata.sender()));
operation
.getIssue()
.add(createInformationIssueComponent("receiver name", metadata.receiver()));
operation
.getIssue()
.add(
createInformationIssueComponent(
"order ingestion", metadata.timeReceived().toString()));
operation.getIssue().add(createInformationIssueComponent("payload hash", metadata.hash()));

return 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;
}
}
20 changes: 1 addition & 19 deletions etor/src/main/resources/openapi_etor.yaml
Original file line number Diff line number Diff line change
@@ -92,7 +92,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/MetadataResponse'
$ref: 'https://github.com/LinuxForHealth/FHIR/blob/main/fhir-openapi/src/main/webapp/META-INF/openapi.json?raw=true#/components/schemas/OperationOutcome'
'401':
description: Authentication failed due to invalid token or unknown organization
content:
@@ -125,24 +125,6 @@ components:
patientId:
type: string
example: MRN7465737865
MetadataResponse:
type: object
properties:
uniqueId:
type: string
example: abc123
sender:
type: string
example: simulated-hospital
receiver:
type: string
example: simulated-lab
timeReceived:
type: string
example: 2023-12-01T12:00:00.000000Z
hash:
type: string
example: abc123
BadRequestError:
description: Bad Request
type: object
Original file line number Diff line number Diff line change
@@ -16,13 +16,15 @@ import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataException
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataOrchestrator
import gov.hhs.cdc.trustedintermediary.etor.orders.Order
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderController
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderResponse
import gov.hhs.cdc.trustedintermediary.etor.orders.SendOrderUseCase
import gov.hhs.cdc.trustedintermediary.etor.orders.UnableToSendOrderException
import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson
import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException
import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir
import gov.hhs.cdc.trustedintermediary.wrappers.Logger
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter
import org.hl7.fhir.r4.model.OperationOutcome

import java.time.Instant
import spock.lang.Specification

@@ -325,10 +327,16 @@ class EtorDomainRegistrationTest extends Specification {
TestApplicationContext.register(PartnerMetadataOrchestrator, mockPartnerMetadataOrchestrator)

def mockResponseHelper = Mock(DomainResponseHelper)
mockResponseHelper.constructOkResponse(_ as PartnerMetadata) >> new DomainResponse(expectedStatusCode)
mockResponseHelper.constructOkResponseFromString(_ as String) >> new DomainResponse(expectedStatusCode)
TestApplicationContext.register(DomainResponseHelper, mockResponseHelper)

TestApplicationContext.register(Formatter, Jackson.getInstance())
def mockOrderConverter = Mock(OrderConverter)
mockOrderConverter.extractPublicMetadataToOperationOutcome(_ as PartnerMetadata) >> Mock(OperationOutcome)
TestApplicationContext.register(OrderConverter, mockOrderConverter)

def mockFhir = Mock(HapiFhir)
mockFhir.encodeResourceToJson(_ as OperationOutcome) >> ""
TestApplicationContext.register(HapiFhir, mockFhir)
TestApplicationContext.injectRegisteredImplementations()

when:
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@ package gov.hhs.cdc.trustedintermediary.etor.metadata

import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext
import gov.hhs.cdc.trustedintermediary.etor.RSEndpointClient
import gov.hhs.cdc.trustedintermediary.etor.orders.Order
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter
import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrderConverter
import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson
import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamEndpointClientException
import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter
@@ -25,9 +28,12 @@ class PartnerMetadataOrchestratorTest extends Specification {
mockClient = Mock(RSEndpointClient)

TestApplicationContext.register(PartnerMetadataOrchestrator, PartnerMetadataOrchestrator.getInstance())
TestApplicationContext.register(OrderConverter, HapiOrderConverter.getInstance())
TestApplicationContext.register(PartnerMetadataStorage, mockPartnerMetadataStorage)

TestApplicationContext.register(RSEndpointClient, mockClient)
TestApplicationContext.register(Formatter, mockFormatter)

TestApplicationContext.injectRegisteredImplementations()
}

Original file line number Diff line number Diff line change
@@ -148,17 +148,18 @@ class PostgresDaoTest extends Specification {
def "fetchMetadata returns partnermetadata when rows exist"() {
given:
def messageId = "12345"
def receiver = "DogCow"
def sender = "DogCow"
Timestamp timestampForMock = Timestamp.from(Instant.parse("2024-01-03T15:45:33.30Z"))
Instant timeReceived = timestampForMock.toInstant()
def hash = receiver.hashCode().toString()
def expected = new PartnerMetadata(messageId, receiver, timeReceived, hash)
def hash = sender.hashCode().toString()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the changes to the DB implementation in this PR be in the other PR given is not directly related to the marshaling of the response?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The above changes are fixing a bug I had from previous PRs, because of how I was retrieving results, one column from the db was never being populated. So I updated the above test after fixing the results retrieval as the test started to fail.

def expected = new PartnerMetadata(messageId, sender, null, timeReceived, hash)

mockDriver.getConnection(_ as String, _ as Properties) >> mockConn
mockConn.prepareStatement(_ as String) >> mockPreparedStatement
mockResultSet.next() >> true
mockResultSet.getString("message_id") >> messageId
mockResultSet.getString("receiver") >> receiver
mockResultSet.getString("sender") >> sender
mockResultSet.getString("receiver") >> null
mockResultSet.getTimestamp("time_received") >> timestampForMock
mockResultSet.getString("hash_of_order") >> hash
mockPreparedStatement.executeQuery() >> mockResultSet
Original file line number Diff line number Diff line change
@@ -3,11 +3,13 @@ package gov.hhs.cdc.trustedintermediary.external.hapi
import gov.hhs.cdc.trustedintermediary.DemographicsMock
import gov.hhs.cdc.trustedintermediary.OrderMock
import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext
import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata
import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter
import org.hl7.fhir.r4.model.Address
import org.hl7.fhir.r4.model.ContactPoint
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.HumanName
import org.hl7.fhir.r4.model.OperationOutcome
import org.hl7.fhir.r4.model.StringType

import java.time.Instant
@@ -242,6 +244,17 @@ 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()
Original file line number Diff line number Diff line change
@@ -40,10 +40,24 @@ public DomainResponse constructResponse(int httpStatus, Object objectResponseBod
return response;
}

public DomainResponse constructResponseFromString(int httpStatus, String jsonResponseBody) {
logger.logInfo("Constructing the response");
var response = new DomainResponse(httpStatus);
response.setBody(jsonResponseBody);

response.setHeaders(Map.of(CONTENT_TYPE_LITERAL, APPLICATION_JSON_LITERAL));

return response;
}

public DomainResponse constructOkResponse(Object objectResponseBody) {
return constructResponse(200, objectResponseBody);
}

public DomainResponse constructOkResponseFromString(String jsonBody) {
return constructResponseFromString(200, jsonBody);
}

public DomainResponse constructErrorResponse(int httpStatus, String errorString) {
return constructResponse(httpStatus, Map.of("error", errorString));
}
Original file line number Diff line number Diff line change
@@ -53,6 +53,20 @@ class DomainResponseHelperTest extends Specification {
actual.getHeaders().get(DomainResponseHelper.CONTENT_TYPE_LITERAL) == DomainResponseHelper.APPLICATION_JSON_LITERAL
}

def "constructOkResponseFromString returns expected response"() {
given:
def expectedResponseStatus = 200
def expectedResponseBody = "{json: DogCow goes Moof}"

when:
def actual = DomainResponseHelper.getInstance().constructOkResponseFromString(expectedResponseBody)

then:
actual.getBody() == expectedResponseBody
actual.getStatusCode() == expectedResponseStatus
actual.getHeaders().get(DomainResponseHelper.CONTENT_TYPE_LITERAL) == DomainResponseHelper.APPLICATION_JSON_LITERAL
}

def "constructErrorResponse with error message returns expected response"() {
given:
def expectedResponseStatus = 500