Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IHE gateway v2: adding support for telecoms, ssns, multiple names and addresses + adding types #2143

Merged
merged 15 commits into from
May 30, 2024
Merged
Copy link
Member Author

Choose a reason for hiding this comment

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

reverting redox specific logic

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const gatewaysThatAcceptOneDocRefPerRequest = [pointClickCareOid, redoxOid, redo
* in the gateway. But these gateways also return different home community IDs in the DQ response
* than in the gateway. So we need to handle this and use the request home community ID instead of the response.
*/
const enforceSameHomeCommunityIdList = [pointClickCareOid, redoxOid, redoxGatewayOid];
jonahkaye marked this conversation as resolved.
Show resolved Hide resolved

function requiresMetriportOidInsteadOfInitiatorOid(gateway: XCPDGateway | XCAGateway): boolean {
return requiresMetriportOidUrl.includes(gateway.url);
Expand All @@ -51,7 +50,3 @@ export function requiresUrnInSoapBody(gateway: XCPDGateway): boolean {
export function requiresOnlyOneDocRefPerRequest(gateway: XCAGateway): boolean {
return gatewaysThatAcceptOneDocRefPerRequest.includes(gateway.homeCommunityId);
}

export function requiresRequestHomeCommunityId(gateway: XCAGateway): boolean {
return enforceSameHomeCommunityIdList.includes(gateway.homeCommunityId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ export const expectedMultiNameAddressResponse: OutboundPatientDiscoveryRespSucce
country: "USA",
},
],
telecom: [
{
system: "MC",
value: "tel:+1310-000-0000",
},
{
system: "H",
value: "mailto:test@test.com",
},
],
identifier: [
{
value: "987564321",
system: "2.16.840.1.113883.3.9621",
},
],
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@
</addr>
<telecom use="MC" value="tel:+1310-000-0000" />
<telecom use="H" value="mailto:test@test.com" />
<asOtherIDs classCode="PAT">
<id root="2.16.840.1.113883.3.9621"
extension="987564321" />
<scopingOrganization classCode="ORG"
determinerCode="INSTANCE">
<id root="2.16.840.1.113883.3.9621" />
</scopingOrganization>
</asOtherIDs>
</patientPerson>
<subjectOf1>
<queryMatchObservation classCode="OBS" moodCode="EVN">
Expand Down
Copy link
Member Author

Choose a reason for hiding this comment

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

Redox endpoints actually do want the home community id of the response and not the request. I misunderstood this before, possibly because PCC works with both, while the other 2 redox endpoints only work with the response home community id.

This was clarified over slack with redox technical support

Copy link
Member Author

Choose a reason for hiding this comment

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

reverting redox specific logic

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
} from "../../../../shared";
import { successStatus, partialSuccessStatus } from "./constants";
import { capture } from "../../../../../../util/notifications";
import { requiresRequestHomeCommunityId } from "../../../gateways";

type Identifier = {
_identificationScheme: string;
Expand All @@ -40,10 +39,6 @@ type Slot = {
};
};

function getRequestHomeCommunityId(request: OutboundDocumentQueryReq): string {
return request.gateway.homeCommunityId;
}

function getResponseHomeCommunityId(
//eslint-disable-next-line @typescript-eslint/no-explicit-any
extrinsicObject: any
Expand All @@ -56,9 +51,6 @@ function getHomeCommunityIdForDr(
//eslint-disable-next-line @typescript-eslint/no-explicit-any
extrinsicObject: any
): string {
if (requiresRequestHomeCommunityId(request.gateway)) {
return getRequestHomeCommunityId(request);
}
return getResponseHomeCommunityId(extrinsicObject);
}

Expand Down
jonahkaye marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { XMLBuilder } from "fast-xml-parser";
import dayjs from "dayjs";
import { Address } from "@medplum/fhirtypes";
import { Address, Telecom, Name, PersonalIdentifier } from "@metriport/ihe-gateway-sdk";
import { createSecurityHeader } from "../../../saml/security/security-header";
import { signFullSaml } from "../../../saml/security/sign";
import { SamlCertsAndKeys } from "../../../saml/security/types";
Expand Down Expand Up @@ -31,10 +31,10 @@ function createSoapBodyContent({
toUrl,
patientGender,
patientBirthtime,
patientFamilyName,
patientGivenName,
patientAddress,
patientTelecom,
patientNames,
patientAddresses,
patientTelecoms,
identifiers,
providerId,
useUrn = true,
}: {
Expand All @@ -44,11 +44,11 @@ function createSoapBodyContent({
receiverDeviceId: string;
toUrl: string;
patientGender: string;
patientBirthtime: string;
patientFamilyName: string;
patientGivenName: string;
patientAddress: Address | undefined;
patientTelecom: string | undefined;
patientBirthtime: string | undefined;
patientNames: Name[] | undefined;
patientAddresses: Address[] | undefined;
patientTelecoms: Telecom[] | undefined;
identifiers: PersonalIdentifier[] | undefined;
providerId: string | undefined;
useUrn?: boolean;
}): object {
Expand Down Expand Up @@ -150,37 +150,53 @@ function createSoapBodyContent({
},
[`${prefix}semanticsText`]: "LivingSubject.administrativeGender",
},
[`${prefix}livingSubjectBirthTime`]: {
[`${prefix}value`]: {
"@_value": patientBirthtime,
},
[`${prefix}semanticsText`]: "LivingSubject.birthTime",
},
[`${prefix}livingSubjectName`]: {
[`${prefix}value`]: {
[`${prefix}family`]: patientFamilyName,
[`${prefix}given`]: patientGivenName,
},
[`${prefix}semanticsText`]: "LivingSubject.name",
},
[`${prefix}patientAddress`]: patientAddress
[`${prefix}livingSubjectBirthTime`]: patientBirthtime
? {
[`${prefix}value`]: {
[`${prefix}streetAddressLine`]: patientAddress.line?.join(", "),
[`${prefix}city`]: patientAddress?.city,
[`${prefix}state`]: patientAddress?.state,
[`${prefix}postalCode`]: patientAddress?.postalCode,
[`${prefix}country`]: patientAddress?.country,
"@_value": patientBirthtime,
},
[`${prefix}semanticsText`]: "LivingSubject.birthTime",
}
: {},
[`${prefix}livingSubjectId`]: identifiers
? {
[`${prefix}value`]: identifiers.map(identifier => ({
"@_extension": identifier.value,
"@_root": identifier.system,
})),
[`${prefix}semanticsText`]: "LivingSubject.id",
}
: {},
[`${prefix}livingSubjectName`]: patientNames
? {
[`${prefix}value`]: patientNames.map(name => ({
leite08 marked this conversation as resolved.
Show resolved Hide resolved
[`${prefix}family`]: name.family,
...name.given?.reduce((acc: { [key: string]: string }, givenName) => {
jonahkaye marked this conversation as resolved.
Show resolved Hide resolved
acc[`${prefix}given`] = givenName;
return acc;
}, {}),
})),
[`${prefix}semanticsText`]: "LivingSubject.name",
}
: {},
[`${prefix}patientAddress`]: patientAddresses
? {
leite08 marked this conversation as resolved.
Show resolved Hide resolved
[`${prefix}value`]: patientAddresses.map(address => ({
[`${prefix}streetAddressLine`]: address.line?.join(", "),
[`${prefix}city`]: address.city,
[`${prefix}state`]: address.state,
[`${prefix}postalCode`]: address.postalCode,
[`${prefix}country`]: address.country,
})),
[`${prefix}semanticsText`]: "Patient.addr",
}
: {},
[`${prefix}patientTelecom`]: patientTelecom
[`${prefix}patientTelecom`]: patientTelecoms
? {
leite08 marked this conversation as resolved.
Show resolved Hide resolved
[`${prefix}value`]: {
"@_use": "HP",
"@_value": patientTelecom,
},
[`${prefix}value`]: patientTelecoms.map(telecom => ({
"@_use": telecom.system,
"@_value": telecom.value,
})),
[`${prefix}semanticsText`]: "Patient.telecom",
}
: {},
Expand Down Expand Up @@ -221,11 +237,11 @@ function createSoapBody({
const providerId = bodyData.principalCareProviderIds[0];
const homeCommunityId = getHomeCommunityId(gateway, bodyData.samlAttributes);
const patientGender = bodyData.patientResource.gender === "female" ? "F" : "M";
const patientBirthtime = bodyData.patientResource.birthDate.replace(DATE_DASHES_REGEX, "");
const patientFamilyName = bodyData.patientResource.name?.[0]?.family;
const patientGivenName = bodyData.patientResource.name?.[0]?.given?.[0];
const patientAddress = bodyData.patientResource.address?.[0];
const patientTelecom = bodyData.patientResource.telecom?.[0]?.value ?? undefined;
const patientBirthtime = bodyData.patientResource.birthDate?.replace(DATE_DASHES_REGEX, "");
const patientNames = bodyData.patientResource.name;
const patientAddresses = bodyData.patientResource.address;
const patientTelecoms = bodyData.patientResource.telecom;
const identifiers = bodyData.patientResource.identifier;

const useUrn = requiresUrnInSoapBody(gateway);
const soapBody = {
Expand All @@ -237,10 +253,10 @@ function createSoapBody({
toUrl,
patientGender,
patientBirthtime,
patientFamilyName,
patientGivenName,
patientAddress,
patientTelecom,
patientNames,
patientAddresses,
patientTelecoms,
identifiers,
providerId,
useUrn,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {
OutboundPatientDiscoveryReq,
XCPDGateway,
OperationOutcome,
Name,
Address,
Telecom,
PersonalIdentifier,
} from "@metriport/ihe-gateway-sdk";
import { normalizeGender } from "../../../utils";
import { XCPDSamlClientResponse } from "../send/xcpd-requests";
Expand All @@ -22,26 +26,34 @@ type IheAddress = {
county: string | undefined;
};

type CarequalityAddress = {
line: string[] | undefined;
city: string | undefined;
state: string | undefined;
postalCode: string | undefined;
country: string | undefined;
};

type IheName = {
given: string | string[] | undefined;
family: string | undefined;
delimiter: string | undefined;
};

type CarequalityName = {
given: string[];
family: string | undefined;
type IheTelecom = {
_use: string;
_value: string;
};

type IheIdentifier = {
_extension: string;
_root: string;
};

function convertIheAddressToCarequalityAddress(address: IheAddress): CarequalityAddress {
function convertIheIdentifierToPersonalIdentifier(identifier: IheIdentifier): PersonalIdentifier {
return {
value: identifier?._extension,
system: identifier?._root,
};
jonahkaye marked this conversation as resolved.
Show resolved Hide resolved
}

function iheIdentifiersToPersonalIdentifiers(otherIds: IheIdentifier[]): PersonalIdentifier[] {
return otherIds.map(convertIheIdentifierToPersonalIdentifier);
}

function convertIheAddressToAddress(address: IheAddress): Address {
return {
line: toArray(address?.streetAddressLine).filter((l): l is string => Boolean(l)),
city: address?.city,
Expand All @@ -51,21 +63,32 @@ function convertIheAddressToCarequalityAddress(address: IheAddress): Carequality
};
}

function iheAddressesToCarequalityAddresses(iheAddresses: IheAddress[]): CarequalityAddress[] {
return iheAddresses.map(convertIheAddressToCarequalityAddress);
function iheAddressesToAddresses(iheAddresses: IheAddress[]): Address[] {
return iheAddresses.map(convertIheAddressToAddress);
}

function convertIheNameToCarequalityName(name: IheName): CarequalityName {
function convertIheNameToCarequalityName(name: IheName): Name {
return {
given: toArray(name?.given).filter((g): g is string => Boolean(g)),
jonahkaye marked this conversation as resolved.
Show resolved Hide resolved
family: name?.family,
};
}

function iheNamesToCarequalityNames(iheNames: IheName[]): CarequalityName[] {
function iheNamesToNames(iheNames: IheName[]): Name[] {
return iheNames.map(convertIheNameToCarequalityName);
}

function convertIheTelecomToTelecom(iheTelecom: IheTelecom): Telecom {
return {
system: iheTelecom?._use,
value: iheTelecom?._value,
};
jonahkaye marked this conversation as resolved.
Show resolved Hide resolved
}

function iheTelecomsToTelecoms(iheTelecom: IheTelecom[]): Telecom[] {
return iheTelecom.map(convertIheTelecomToTelecom);
}

function handleHTTPErrorResponse({
httpError,
outboundRequest,
Expand Down Expand Up @@ -113,15 +136,21 @@ function handlePatientMatchResponse({
getPatientRegistryProfile(jsonObj)?.controlActProcess?.subject?.registrationEvent?.subject1;
const addr = toArray(subject1?.patient?.patientPerson?.addr);
const names = toArray(subject1?.patient?.patientPerson?.name);
const telecoms = toArray(subject1?.patient?.patientPerson?.telecom);
const otherIds = toArray(subject1?.patient?.patientPerson?.asOtherIDs?.id);

const addresses = iheAddressesToCarequalityAddresses(addr);
const patientNames = iheNamesToCarequalityNames(names);
const addresses = iheAddressesToAddresses(addr);
const patientNames = iheNamesToNames(names);
const patientTelecoms = iheTelecomsToTelecoms(telecoms);
const patientIdentifiers = iheIdentifiersToPersonalIdentifiers(otherIds);

const patientResource = {
name: patientNames,
gender: normalizeGender(subject1?.patient?.patientPerson?.administrativeGenderCode?._code),
birthDate: subject1?.patient?.patientPerson?.birthTime?._value,
address: addresses,
...(patientTelecoms.length > 0 && { telecom: patientTelecoms }),
...(patientIdentifiers.length > 0 && { identifier: patientIdentifiers }),
};

const response: OutboundPatientDiscoveryResp = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as uuid from "uuid";
import { Gender } from "@metriport/ihe-gateway-sdk";

export const iti55BodyData = {
id: uuid.v4(),
Expand Down Expand Up @@ -29,11 +30,11 @@ export const iti55BodyData = {
id: uuid.v4(),
name: [
{
family: "Obama",
given: ["Barack"],
family: "John",
given: ["Doe"],
jonahkaye marked this conversation as resolved.
Show resolved Hide resolved
},
],
gender: "male",
gender: "male" as Gender,
birthDate: "1981-01-01",
address: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as dotenv from "dotenv";
dotenv.config();
// keep that ^ on top
import { inboundPatientResourceSchema } from "@metriport/ihe-gateway-sdk";
import { patientResourceSchema } from "@metriport/ihe-gateway-sdk";
import { QueryTypes } from "sequelize";
import z from "zod";
import { MPIMetriportAPI } from "../../../mpi/patient-mpi-metriport-api";
Expand All @@ -15,7 +15,7 @@ export const rowWithDataSchema = z.object({
status: z.string(),
data: z
.object({
patientResource: inboundPatientResourceSchema.optional(),
patientResource: patientResourceSchema.optional(),
timestamp: z.string(),
})
.optional(),
Expand Down
Loading
Loading