Custom Matching Script
EAP

 

To make the member-match operations more adaptive to different health providers, the System-to-System Data Exchange Module supports custom matching scripts that can be defined by clients in the form of JavaScript. In addition to customizing patient matching, clients can customize the consent validation that runs after a patient is matched — see Consent Validation Functions below.

Matching Functions

The Module selects the script function to invoke based on the operation being handled. Define the function that corresponds to the operation(s) you support:

OperationFunction to define
$member-match, $bulk-member-match (payer-initiated)payerMatchPatient
$provider-member-match (provider-initiated)providerMatchAndValidateRelationship

Each function takes the same two parameters: the first exposes the request resources, and the second contains details about the request that is about to be processed.

function payerMatchPatient(theMemberMatchRequest, theRequestDetails) {
    // your implementation goes here
}

function providerMatchAndValidateRelationship(theMemberMatchRequest, theRequestDetails) {
    // your implementation goes here
}

The first parameter exposes the resources supplied to the operation:

  • theMemberMatchRequest.getPatient() — the MemberPatient to match
  • theMemberMatchRequest.getCoverage() — the CoverageToMatch
  • theMemberMatchRequest.getConsent() — the Consent resource, when one was supplied on the request

The function may return a Patient if a matching patient is found, or throw an UnprocessableEntityException to describe the failure. If an UnprocessableEntityException is thrown, it will be automatically converted to a 422 Unprocessable Entity response containing an OperationOutcome. If null or any other type is returned, a generic 422 Unprocessable Entity is thrown.

The returned Patient does not have to be a local resource: a custom match function may return a Patient with no resource id, for example a member matched at an external plan. Such a Patient is preserved verbatim (identifiers, tags, and demographics untouched) as a contained resource on the matched-members Group, and the group member entity references it with a fragment reference. The contained resource id is deterministic but opaque — consumers must not rely on any particular format or prefix. Because there is no local Patient, no Consent resource is written for such a member during the match; the matching plan remains the system of record for the consent attestation.

As per the HRex Member Match Operation specification, where there is a match failure, the OperationOutcome should specify the nature of the failure.

The OperationOutcome should set an issue value with a code, severity and diagnostics matching the following:

Failure ScenarioCodeSeverityDiagnostics
No MatchesprocessingerrorNo matches were found.
Multiple MatchesprocessingerrorMultiple matches were found.

To conform to this, simply pass the diagnostic value and status code 422 into the Exceptions.newTrustedException(422, message) call, the server will handle the creation of the OperationOutcome and creating the issue component.

Legacy matchPatient Function

Earlier releases used a single matchPatient function for all member-match operations. It is still supported as a fallback when no operation-specific function (payerMatchPatient or providerMatchAndValidateRelationship) is defined, but it is deprecated and should not be used in new scripts. Prefer the operation-specific functions above.

function matchPatient(theMemberMatchRequest, theRequestDetails) {
    // deprecated: prefer payerMatchPatient / providerMatchAndValidateRelationship
}

Example

Below is an example of the custom matching script for the payer-initiated $member-match operation, which fetches Coverage resources that have the same health plan Identifier as the coverageToMatch parameter to $member-match, and performs some validation on the referenced patient:

function payerMatchPatient(theMemberMatchRequest, theRequestDetails){
    // get the memberPatient and the coverageToMatch from the first parameter
    let memberPatient = theMemberMatchRequest.getPatient();
    let coverageToMatch = theMemberMatchRequest.getCoverage();
    
    // search for coverages that have the same identifier as coverageToMatch on
    // an accepted list of health plans
    var coverageList = ["http://oldhealthplan.example.com",
                        "http://oldhealthplan.example.com2",
                        "http://oldhealthplan.example.com3"];
    var coverageSystem = null;
    var coverageValue = null;
    for (let i = 0; i < coverageToMatch.identifier.length; i++) {
        if (coverageList.includes(coverageToMatch.identifier[i].system)) {
            coverageSystem = coverageToMatch.identifier[i].system;
            coverageValue = coverageToMatch.identifier[i].value;
            break;
        }
    }

    // throw UnprocessableEntityException if coverageToMatch does not fit to any of the health plans
    if (coverageSystem == null) {
        throw Exceptions.newTrustedException(422, 'No matches were found.');
    }

    // perform actual search
    var coverageSearchResult = Fhir
    .search()
    .forResource('Coverage')
    .whereToken('identifier', coverageSystem, coverageValue)
    .asList();

   // If we find zero or more than one matching Coverage, we throw an UnprocessableEntityException
   if (coverageSearchResult.length === 0) {
      throw Exceptions.newTrustedException(422, 'No matches were found.');
   } else if (coverageSearchResult.length > 1) {
      throw Exceptions.newTrustedException(422, 'Multiple matches were found.');
   }

    // get the patient ids from the resulting coverages
    patientIds = '';
    for (let i = 0; i < coverageSearchResult.length; i++) {
        Log.info(coverageSearchResult[i].beneficiary.reference);
        patientIds = patientIds
        .concat(coverageSearchResult[i].beneficiary.reference)
        .concat(',');
    }
    
    // throw UnprocessableEntityException if no patient reference was found
    if (patientIds == '') {
        throw Exceptions.newTrustedException(422, 'No matches were found.');
    }

    // search for the patient ids
    var patientSearchResult = Fhir
    .search()
    .forResource('Patient')
    .where('_id', patientIds)
    .asList();

    // perform validation on the patients
    for (let i = 0; i < patientSearchResult.length; i++) {
        if (validatePatient(patientSearchResult[i], memberPatient)){
            return patientSearchResult[i];
        }
    }

    throw Exceptions.newTrustedException(422, 'No matches were found.');
}

// validate the patient by comparing gender with the input member patient,
// and return the first one that matches
function validatePatient(thePatient, theMemberPatient) {
    if (thePatient && theMemberPatient) {
        if (thePatient.gender && theMemberPatient.gender) {
            return thePatient.gender.toString().toLowerCase == theMemberPatient.gender.toString().toLowerCase;
        } else if (!thePatient.gender && !theMemberPatient.gender) {
            return true; // neither have a gender - we'll say they're the same
        }
    }
    return false;
}

Consent Validation Functions

After a patient is matched, the Module validates the submitted Consent before releasing data. You can customize this check with JavaScript by defining a consent-validation function for the operation(s) you support:

OperationFunction to define
$member-match, $bulk-member-match (payer-initiated)payerValidateConsent
$provider-member-match (provider-initiated)providerValidateConsent

Each function takes the same two parameters as the matching functions: the first exposes the request resources (including the matched Consent), and the second contains details about the request that is about to be processed.

function payerValidateConsent(theMemberMatchRequest, theRequestDetails) {
    // your implementation goes here
}

function providerValidateConsent(theMemberMatchRequest, theRequestDetails) {
    // your implementation goes here
}

The first parameter exposes the resources supplied to the operation:

  • theMemberMatchRequest.getPatient() — the matched MemberPatient
  • theMemberMatchRequest.getCoverage() — the CoverageToMatch
  • theMemberMatchRequest.getConsent() — the Consent resource submitted on the request

The function should return a Boolean:

  • true — the consent determination is valid and the operation proceeds.
  • false — the consent is constrained; the operation fails with a 422 Unprocessable Entity response.

When the operation-specific function is not defined, the default consent validation is applied. This preserves existing behaviour for clients that do not define a consent-validation function. If a defined function returns any non-Boolean value, the default consent validation is also applied and a warning is logged, since a non-Boolean result usually indicates a defect in the script.

Consent Validation Example

The following provider consent-validation function releases data only when an active Consent was submitted on the request:

function providerValidateConsent(theMemberMatchRequest, theRequestDetails) {
    let consent = theMemberMatchRequest.getConsent();
    // only release data when an active consent was provided
    return consent != null && consent.status.toString().toLowerCase() === 'active';
}