Skip to content

Commit

Permalink
feat(ihe): develop to branch
Browse files Browse the repository at this point in the history
Refs: #1667
Signed-off-by: Jonah Kaye <[email protected]>
  • Loading branch information
jonahkaye committed Jun 21, 2024
2 parents 09f0610 + f092077 commit 47b931c
Show file tree
Hide file tree
Showing 41 changed files with 881 additions and 562 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ export type SetDocQueryProgress = {

/**
* Update a single patient's consolidated query progress.
* Keeps existing sibling properties when those are not provided
* @returns the updated Patient
* Keeps existing sibling properties when those are not provided.
*/
export async function updateConsolidatedQueryProgress({
patient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ export async function createOrUpdateConsolidatedPatientData({

return transformedBundle;
} catch (error) {
const errorMsg = errorToString(error);
const msg = "Error converting and executing fhir bundle resources";
log(`${msg}: ${errorToString(error)}`);
log(`${msg}: ${errorMsg}`);
if (errorMsg.includes("ID")) throw new MetriportError(errorMsg, error, { cxId, patientId });
throw new MetriportError(msg, error, { cxId, patientId });
}
}
Expand Down
23 changes: 7 additions & 16 deletions packages/api/src/command/medical/patient/convert-fhir-to-cda.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
import { out } from "@metriport/core/util/log";
import { capture } from "@metriport/core/util/notifications";
import { makeFhirToCdaConverter } from "../../../external/fhir-to-cda-converter/converter-factory";
import { toFHIR as toFHIROrganization } from "../../../external/fhir/organization";
import { Bundle } from "../../../routes/medical/schemas/fhir";
import { getOrganizationOrFail } from "../organization/get-organization";

export async function convertFhirToCda({
cxId,
patientId,
docId,
validatedBundle,
splitCompositions = true,
}: {
cxId: string;
patientId: string;
docId: string;
validatedBundle: Bundle;
}): Promise<void> {
const { log } = out(`convertFhirToCda - cxId: ${cxId}, patientId: ${patientId}`);
splitCompositions?: boolean;
}): Promise<string[]> {
const { log } = out(`convertFhirToCda - cxId: ${cxId}`);
const cdaConverter = makeFhirToCdaConverter();
const organization = await getOrganizationOrFail({ cxId });

try {
const fhirOrganization = toFHIROrganization(organization);
await cdaConverter.requestConvert({
return cdaConverter.requestConvert({
cxId,
patientId,
docId,
bundle: validatedBundle,
organization: fhirOrganization,
orgOid: organization.oid,
splitCompositions,
});
} catch (error) {
const msg = `Error converting FHIR to CDA`;
log(`${msg} - error: ${error}`);
capture.error(msg, { extra: { error, cxId, patientId } });
capture.error(msg, { extra: { error, cxId, splitCompositions } });
throw error;
}
}
110 changes: 110 additions & 0 deletions packages/api/src/command/medical/patient/handle-data-contributions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { createUploadFilePath } from "@metriport/core/domain/document/upload";
import { Patient } from "@metriport/core/domain/patient";
import { toFHIR as toFhirPatient } from "@metriport/core/external/fhir/patient/index";
import { uploadCdaDocuments, uploadFhirBundleToS3 } from "@metriport/core/fhir-to-cda/upload";
import { uuidv7 } from "@metriport/core/util/uuid-v7";
import BadRequestError from "../../../errors/bad-request";
import { processCcdRequest } from "../../../external/cda/process-ccd-request";
import { toFHIR as toFhirOrganization } from "../../../external/fhir/organization";
import { countResources } from "../../../external/fhir/patient/count-resources";
import { hydrateBundle } from "../../../external/fhir/shared/hydrate-bundle";
import { validateFhirEntries } from "../../../external/fhir/shared/json-validator";
import { Bundle as ValidBundle } from "../../../routes/medical/schemas/fhir";
import { Config } from "../../../shared/config";
import { getOrganizationOrFail } from "../organization/get-organization";
import { createOrUpdateConsolidatedPatientData } from "./consolidated-create";
import { convertFhirToCda } from "./convert-fhir-to-cda";
import { getPatientOrFail } from "./get-patient";

const MAX_RESOURCE_COUNT_PER_REQUEST = 50;
const MAX_RESOURCE_STORED_LIMIT = 1000;

export async function handleDataContribution({
patientId,
cxId,
bundle,
}: {
patientId: string;
cxId: string;
bundle: ValidBundle;
}) {
const [organization, patient] = await Promise.all([
getOrganizationOrFail({ cxId }),
getPatientOrFail({ id: patientId, cxId }),
]);

const fhirOrganization = toFhirOrganization(organization);
const fhirPatient = toFhirPatient(patient);
const docId = uuidv7();
const fhirBundleDestinationKey = createUploadFilePath(
cxId,
patientId,
`${docId}_FHIR_BUNDLE.json`
);
const fullBundle = hydrateBundle(bundle, fhirPatient, fhirOrganization, fhirBundleDestinationKey);
const validatedBundle = validateFhirEntries(fullBundle);
const incomingAmount = validatedBundle.entry.length;

await checkResourceLimit(incomingAmount, patient);
await uploadFhirBundleToS3({
cxId,
patientId,
fhirBundle: validatedBundle,
destinationKey: fhirBundleDestinationKey,
});
const consolidatedDataUploadResults = await createOrUpdateConsolidatedPatientData({
cxId,
patientId: patient.id,
fhirBundle: validatedBundle,
});

const convertAndUploadCdaPromise = async () => {
const isValidForCdaConversion = hasCompositionResource(validatedBundle);
if (isValidForCdaConversion) {
const converted = await convertFhirToCda({
cxId,
validatedBundle,
});
await uploadCdaDocuments({
cxId,
patientId,
cdaBundles: converted,
organization: fhirOrganization,
docId,
});
}
};
const createAndUploadCcdPromise = async () => {
// TODO: To minimize generating CCDs, make it a delayed job (run it ~5min after it was initiated, only once for all requests within that time window)
await processCcdRequest(patient, fhirOrganization);
};

await Promise.all([createAndUploadCcdPromise(), convertAndUploadCdaPromise()]);
return consolidatedDataUploadResults;
}

async function checkResourceLimit(incomingAmount: number, patient: Patient) {
if (!Config.isCloudEnv() || Config.isSandbox()) {
const { total: currentAmount } = await countResources({
patient: { id: patient.id, cxId: patient.cxId },
});
if (currentAmount + incomingAmount > MAX_RESOURCE_STORED_LIMIT) {
throw new BadRequestError(
`Reached maximum number of resources per patient in Sandbox mode.`,
null,
{ currentAmount, incomingAmount, MAX_RESOURCE_STORED_LIMIT }
);
}
// Limit the amount of resources that can be created at once
if (incomingAmount > MAX_RESOURCE_COUNT_PER_REQUEST) {
throw new BadRequestError(`Cannot create this many resources at a time.`, null, {
incomingAmount,
MAX_RESOURCE_COUNT_PER_REQUEST,
});
}
}
}

function hasCompositionResource(bundle: ValidBundle): boolean {
return bundle.entry.some(entry => entry.resource?.resourceType === "Composition");
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
makeDocumentReferenceWithMetriportId,
makeDocumentReference,
} from "./make-document-reference-with-metriport-id";
import { filterDocRefsWithMetriportId } from "../shared";
import { containsMetriportId, containsDuplicateMetriportId } from "../shared";
import { faker } from "@faker-js/faker";

describe("filterDocRefsWithMetriportId", () => {
Expand All @@ -15,10 +15,24 @@ describe("filterDocRefsWithMetriportId", () => {
makeDocumentReferenceWithMetriportId({ metriportId: metriportId2 }),
];

const filteredDocRefs = filterDocRefsWithMetriportId(docRefs);
const filteredDocRefs = docRefs.filter(containsMetriportId);

expect(filteredDocRefs.length).toBe(2);
expect(filteredDocRefs[0].metriportId).toBe(metriportId1);
expect(filteredDocRefs[1].metriportId).toBe(metriportId2);
});

it("should filter out identical docRefs", async () => {
const seenMetriportIds = new Set<string>();
const docRefsWithMetriportId = [
makeDocumentReferenceWithMetriportId({ metriportId: "123" }),
makeDocumentReferenceWithMetriportId({ metriportId: "123" }),
];

const deduplicatedDocRefsWithMetriportId = docRefsWithMetriportId.filter(
docRef => !containsDuplicateMetriportId(docRef, seenMetriportIds)
);

expect(deduplicatedDocRefsWithMetriportId.length).toBe(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { nanoid } from "nanoid";
import { Patient } from "@metriport/core/domain/patient";
import { capture } from "@metriport/core/util/notifications";
import {
OutboundDocumentQueryResp,
OutboundDocumentRetrievalReq,
} from "@metriport/ihe-gateway-sdk";
import { getGatewaySpecificDocRefsPerRequest } from "@metriport/core/external/carequality/ihe-gateway-v2/gateways";
import { v4 as uuidv4 } from "uuid";

import dayjs from "dayjs";
import { chunk } from "lodash";
import { HieInitiator } from "../../hie/get-hie-initiator";
Expand Down Expand Up @@ -67,7 +66,7 @@ export function createOutboundDocumentRetrievalReqs({
const request: OutboundDocumentRetrievalReq[] = docRefChunks.map(chunk => {
return {
...baseRequest,
subRequestId: uuidv4(),
requestChunkId: nanoid(),
documentReference: chunk,
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import { getCQData } from "../patient";
import {
cqToFHIR,
DocumentReferenceWithMetriportId,
filterDocRefsWithMetriportId,
containsMetriportId,
getContentTypeOrUnknown,
containsDuplicateMetriportId,
} from "./shared";
import { getDocumentReferenceContentTypeCounts } from "../../hie/get-docr-content-type-counts";
import { makeIHEGatewayV2 } from "../../ihe-gateway-v2/ihe-gateway-v2-factory";
Expand Down Expand Up @@ -262,6 +263,7 @@ async function getRespWithDocsToDownload({
response,
}: OutboundDocQueryRespParam): Promise<DqRespWithDocRefsWithMetriportId[]> {
const respWithDocsToDownload: DqRespWithDocRefsWithMetriportId[] = [];
const seenMetriportIds = new Set<string>();

await executeAsynchronously(
response,
Expand All @@ -273,8 +275,16 @@ async function getRespWithDocsToDownload({
response: gwResp,
});
const docRefs = resultsWithMetriportId.flatMap(result => result.documentReference ?? []);
const docRefsWithMetriportId = filterDocRefsWithMetriportId(docRefs);
const docsToDownload = await getNonExistentDocRefs(docRefsWithMetriportId, patientId, cxId);
const docRefsWithMetriportId = docRefs.filter(containsMetriportId);
const deduplicatedDocRefsWithMetriportId = docRefsWithMetriportId.filter(
docRef => !containsDuplicateMetriportId(docRef, seenMetriportIds)
);

const docsToDownload = await getNonExistentDocRefs(
deduplicatedDocRefsWithMetriportId,
patientId,
cxId
);

if (docsToDownload.length === 0) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { formatDate } from "../shared";
import {
DocumentReferenceWithMetriportId,
containsMetriportId,
containsDuplicateMetriportId,
cqToFHIR,
dedupeContainedResources,
} from "./shared";
Expand Down Expand Up @@ -89,14 +90,33 @@ export async function processOutboundDocumentRetrievalResps({
source: MedicalDataSource.CAREQUALITY,
});

const seenMetriportIds = new Set<string>();

const resultPromises = await Promise.allSettled(
results.map(async docRetrievalResp => {
const docRefs = docRetrievalResp.documentReference;

if (docRefs) {
const validDocRefs = docRefs.filter(containsMetriportId);
const deduplicatedDocRefs = validDocRefs.filter(docRef => {
const isDuplicate = containsDuplicateMetriportId(docRef, seenMetriportIds);
if (isDuplicate) {
capture.message(`Duplicate docRef found in DR Resp`, {
extra: {
context: `cq.processOutboundDocumentRetrievalResps`,
patientId,
requestId,
cxId,
docRef,
},
level: "warning",
});
}
return !isDuplicate;
});

await handleDocReferences(
validDocRefs,
deduplicatedDocRefs,
requestId,
patientId,
cxId,
Expand Down
16 changes: 10 additions & 6 deletions packages/api/src/external/carequality/document/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ export function containsMetriportId(
return docRef.metriportId != undefined;
}

export function filterDocRefsWithMetriportId(
documentReferences: DocumentReference[]
): DocumentReferenceWithMetriportId[] {
return documentReferences.filter((docRef): docRef is DocumentReferenceWithMetriportId => {
return docRef.metriportId != undefined;
});
export function containsDuplicateMetriportId(
docRef: DocumentReferenceWithMetriportId,
seenMetriportIds: Set<string>
): boolean {
if (seenMetriportIds.has(docRef.metriportId)) {
return true;
} else {
seenMetriportIds.add(docRef.metriportId);
return false;
}
}

export const cqToFHIR = (
Expand Down
Loading

0 comments on commit 47b931c

Please sign in to comment.