Skip to content

Commit

Permalink
Merge pull request #13 from fastenhealth/contained_resource_extraction
Browse files Browse the repository at this point in the history
  • Loading branch information
AnalogJ authored Oct 6, 2023
2 parents b321fe1 + 7db880e commit d25f936
Show file tree
Hide file tree
Showing 10 changed files with 7,116 additions and 38 deletions.
67 changes: 51 additions & 16 deletions clients/internal/base/fhir401_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ func GetSourceClientFHIR401(env pkg.FastenLighthouseEnvType, ctx context.Context
}, err
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Sync
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (c *SourceClientFHIR401) SyncAll(db models.DatabaseRepository) (models.UpsertSummary, error) {
bundle, err := c.GetPatientBundle(c.SourceCredential.GetPatientId())
if err != nil {
Expand Down Expand Up @@ -243,9 +243,9 @@ func (c *SourceClientFHIR401) ProcessPendingResources(db models.DatabaseReposito
return lookupResourceReferences, syncErrors
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FHIR
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (c *SourceClientFHIR401) GetResourceBundle(relativeResourcePath string) (interface{}, error) {

// https://www.hl7.org/fhir/patient-operation-everything.html
Expand Down Expand Up @@ -322,9 +322,9 @@ func (c *SourceClientFHIR401) GetPatient(patientId string) (fhir401.Patient, err
return patient, err
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Process Bundles
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (c *SourceClientFHIR401) ProcessBundle(bundle fhir401.Bundle) ([]models.RawResourceFhir, map[string]string, error) {

//bundles may contain references to resources in one of 3 formats: - https://www.hl7.org/fhir/references.html#literal
Expand All @@ -345,12 +345,6 @@ func (c *SourceClientFHIR401) ProcessBundle(bundle fhir401.Bundle) ([]models.Raw
//no resourceId present for this resource, we'll ignore it.
return models.RawResourceFhir{}, false
}
// TODO find a way to safely/consistently get the resource updated date (and other metadata) which shoudl be added to the model.
//if originalResource.Meta != nil && originalResource.Meta.LastUpdated != nil {
// if parsed, err := time.Parse(time.RFC3339Nano, *originalResource.Meta.LastUpdated); err == nil {
// patientProfile.UpdatedAt = parsed
// }
//}

if bundleEntry.FullUrl != nil && strings.HasPrefix(*bundleEntry.FullUrl, "urn:uuid:") {
internalFragmentReferenceLookup[*bundleEntry.FullUrl] = fmt.Sprintf("%s/%s", resourceType, *resourceId)
Expand All @@ -367,10 +361,10 @@ func (c *SourceClientFHIR401) ProcessBundle(bundle fhir401.Bundle) ([]models.Raw
return wrappedResourceModels, internalFragmentReferenceLookup, nil
}

//process a resource by:
//- inserting into the database
//- increment the updatedResources list if the resource has been updated
//- extract all external references from the resource payload (adding the the lookup table)
// process a resource by:
// - inserting into the database
// - increment the updatedResources list if the resource has been updated
// - extract all external references from the resource payload (adding the the lookup table)
func (c *SourceClientFHIR401) ProcessResource(db models.DatabaseRepository, resource models.RawResourceFhir, referencedResourcesLookup map[string]bool, internalFragmentReferenceLookup map[string]string, summary *models.UpsertSummary) error {
referencedResourcesLookup[fmt.Sprintf("%s/%s", resource.SourceResourceType, resource.SourceResourceID)] = true
if len(resource.SourceUri) > 0 {
Expand All @@ -382,6 +376,47 @@ func (c *SourceClientFHIR401) ProcessResource(db models.DatabaseRepository, reso
if err != nil {
return err
}

resourceObjTyped := resourceObj.(models.ResourceInterface)
currentResourceType, currentResourceId := resourceObjTyped.ResourceRef()

//before processing this resource, we should check if it has any contained resources that we need to process first (recursively)
containedResources := resourceObj.(models.ResourceInterface).ContainedResources()
if containedResources != nil && len(containedResources) > 0 {
for cndx, containedResource := range containedResources {
containedResourceObj, err := fhirutils.MapToResource(containedResource, false)
if err != nil {
c.Logger.Warnf("Skipping contained resource (index %d) in %s/%s: %v", cndx, currentResourceType, *currentResourceId, err.Error())
continue
}

containedResourceTyped := containedResourceObj.(models.ResourceInterface)
containedResourceType, containedResourceId := containedResourceTyped.ResourceRef()
if containedResourceId == nil {
//no id present for this contained resource, we'll ignore it. (since theres no way to reference it anyways)
c.Logger.Warnf("Skipping contained resource missing id: (%s/%s#%s index: %d)", currentResourceType, *currentResourceId, containedResourceType, cndx)
continue
}
normalizedContainedResourceId := normalizeContainedResourceId(currentResourceType, *currentResourceId, *containedResourceId)

//generate a unique id for this contained resource by base64 url encoding this string
base64ContainedResourceId := base64.URLEncoding.EncodeToString([]byte(normalizedContainedResourceId))

//add this mapping to the internalFragmentReferenceLookup
internalFragmentReferenceLookup[normalizedContainedResourceId] = fmt.Sprintf("%s/%s", containedResourceType, base64ContainedResourceId)

containedResourceWrapped := models.RawResourceFhir{
SourceResourceID: base64ContainedResourceId,
SourceResourceType: containedResourceType,
ResourceRaw: containedResource,
}
err = c.ProcessResource(db, containedResourceWrapped, referencedResourcesLookup, internalFragmentReferenceLookup, summary)
if err != nil {
return err
}
}
}

SourceClientFHIR401ExtractResourceMetadata(resourceObj, &resource, internalFragmentReferenceLookup)

isUpdated, err := db.UpsertRawResource(c.Context, c.SourceCredential, resource)
Expand Down
61 changes: 61 additions & 0 deletions clients/internal/base/fhir401_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,64 @@ func TestFhir401Client_ProcessResource(t *testing.T) {
}, referencedResourcesLookup)
//require.Equal(t, "A00000000000005", profile.SourceResourceID)
}

func TestFhir401Client_ProcessResourceWithContainedResources(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
sc := mock_models.NewMockSourceCredential(mockCtrl)
db := mock_models.NewMockDatabaseRepository(mockCtrl)
testLogger := logrus.WithFields(logrus.Fields{
"type": "test",
})
client, err := GetSourceClientFHIR401(pkg.FastenLighthouseEnvSandbox, context.Background(), testLogger, sc, &http.Client{})
require.NoError(t, err)
db.EXPECT().UpsertRawResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil).AnyTimes()

jsonBytes, err := ReadTestFixture("testdata/fixtures/401-R4/document_reference/medicare-eob.json")
require.NoError(t, err)
referencedResourcesLookup := map[string]bool{}
internalFragmentReferenceLookup := map[string]string{}
summary := models.UpsertSummary{}

rawResource := models.RawResourceFhir{
SourceResourceID: "carrier--10000930037921",
SourceResourceType: "ExplanationOfBenefit",
ResourceRaw: jsonBytes,
}

// test
err = client.ProcessResource(db, rawResource, referencedResourcesLookup, internalFragmentReferenceLookup, &summary)

//assert
require.NoError(t, err)
require.Equal(t, 23, len(referencedResourcesLookup))
//notice how the contained resources are tagged as completed in the referencedResourcesLookup
require.Equal(t, map[string]bool{
"Coverage/part-b--10000010254618": false,
"ExplanationOfBenefit/carrier--10000930037921": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi00": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi01": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi02": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi03": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi04": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi05": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0x": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xMA==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xMQ==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xMg==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xMw==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xNA==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xNQ==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xNg==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xNw==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xOA==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0xOQ==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0y": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0yMA==": true,
"Observation/RXhwbGFuYXRpb25PZkJlbmVmaXQvY2Fycmllci0tMTAwMDA5MzAwMzc5MjEjbGluZS1vYnNlcnZhdGlvbi0z": true,
"Patient/-10000010254618": false,
}, referencedResourcesLookup)
//require.Equal(t, "A00000000000005", profile.SourceResourceID)
}
Loading

0 comments on commit d25f936

Please sign in to comment.