sync all resources on completion.

This commit is contained in:
Jason Kulatunga 2022-09-08 21:14:03 -07:00
parent 6a8df64691
commit c9fc23e3a9
18 changed files with 133 additions and 944 deletions

View File

@ -10,7 +10,7 @@ type DatabaseRepository interface {
Close() error
GetCurrentUser() models.User
UpsertResource(context.Context, interface{}) error
UpsertResource(context.Context, models.ResourceFhir) error
//UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error

View File

@ -46,9 +46,7 @@ func NewRepository(appConfig config.Interface, globalLogger logrus.FieldLogger)
err = database.AutoMigrate(
&models.User{},
&models.Source{},
&models.Profile{},
&models.Organization{},
&models.Encounter{},
&models.ResourceFhir{},
)
if err != nil {
return nil, fmt.Errorf("Failed to automigrate! - %v", err)
@ -87,61 +85,17 @@ func (sr *sqliteRepository) GetCurrentUser() models.User {
return currentUser
}
func (sr *sqliteRepository) UpsertResource(ctx context.Context, resourceModel interface{}) error {
func (sr *sqliteRepository) UpsertResource(ctx context.Context, resourceModel models.ResourceFhir) error {
sr.logger.Infof("insert/update (%T) %v", resourceModel, resourceModel)
switch (resourceModel).(type) {
case models.Encounter:
var apiEncounter models.Encounter
apiEncounter = resourceModel.(models.Encounter)
if sr.gormClient.Debug().WithContext(ctx).Model(&apiEncounter).
Where(models.OriginBase{
SourceID: apiEncounter.GetSourceID(),
SourceResourceID: apiEncounter.GetSourceResourceID(),
SourceResourceType: apiEncounter.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
}).Updates(&apiEncounter).RowsAffected == 0 {
sr.logger.Infof("organization does not exist, creating: %s %s %s", apiEncounter.GetSourceID(), apiEncounter.GetSourceResourceID(), apiEncounter.GetSourceResourceType())
return sr.gormClient.Debug().Model(&apiEncounter).Create(&apiEncounter).Error
}
case models.Organization:
var apiOrganization models.Organization
apiOrganization = (resourceModel).(models.Organization)
if sr.gormClient.Debug().WithContext(ctx).Model(&apiOrganization).
Where(models.OriginBase{
SourceID: apiOrganization.GetSourceID(),
SourceResourceID: apiOrganization.GetSourceResourceID(),
SourceResourceType: apiOrganization.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
}).Updates(&apiOrganization).RowsAffected == 0 {
sr.logger.Infof("organization does not exist, creating: %s %s %s", apiOrganization.GetSourceID(), apiOrganization.GetSourceResourceID(), apiOrganization.GetSourceResourceType())
return sr.gormClient.Debug().Model(&apiOrganization).Create(&apiOrganization).Error
}
case models.Profile:
var apiProfile models.Profile
apiProfile = (resourceModel).(models.Profile)
if sr.gormClient.Debug().WithContext(ctx).Model(&apiProfile).
Where(models.OriginBase{
SourceID: apiProfile.GetSourceID(),
SourceResourceID: apiProfile.GetSourceResourceID(),
SourceResourceType: apiProfile.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
}).Updates(&apiProfile).RowsAffected == 0 {
sr.logger.Infof("profile does not exist, creating: %s %s %s", apiProfile.GetSourceID(), apiProfile.GetSourceResourceID(), apiProfile.GetSourceResourceType())
return sr.gormClient.Debug().Model(&apiProfile).Create(&apiProfile).Error
}
default:
return fmt.Errorf("unknown model (%T) %v", resourceModel, resourceModel)
}
return nil
}
func (sr *sqliteRepository) UpsertOrganziation(ctx context.Context, org *models.Organization) error {
if sr.gormClient.Debug().WithContext(ctx).Model(org).
if sr.gormClient.Debug().WithContext(ctx).Model(&resourceModel).
Where(models.OriginBase{
SourceID: org.GetSourceID(),
SourceResourceID: org.GetSourceResourceID(),
SourceResourceType: org.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
}).Updates(org).RowsAffected == 0 {
sr.logger.Infof("org does not exist, creating: %s %s %s", org.GetSourceID(), org.GetSourceResourceID(), org.GetSourceResourceType())
return sr.gormClient.Debug().Create(org).Error
SourceID: resourceModel.GetSourceID(),
SourceResourceID: resourceModel.GetSourceResourceID(),
SourceResourceType: resourceModel.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
}).Updates(&resourceModel).RowsAffected == 0 {
sr.logger.Infof("resource does not exist, creating: %s %s %s", resourceModel.GetSourceID(), resourceModel.GetSourceResourceID(), resourceModel.GetSourceResourceType())
return sr.gormClient.Debug().Model(&resourceModel).Create(&resourceModel).Error
}
return nil
}

View File

@ -1,28 +0,0 @@
package base
import "github.com/samber/lo"
//based on algorithm - https://stackoverflow.com/a/5288547
type DependencyGraph map[string][]string
func (d DependencyGraph) AddDependencies(resourceRef string, dependencyResourceRefs []string) {
//check to see if the resourceRef already has an entry in the graph, if not, add it
if _, dependencyListExists := d[resourceRef]; !dependencyListExists {
//the dependency list doesnt exist yet for this ref
d[resourceRef] = []string{}
}
for _, dependencyResourceRef := range dependencyResourceRefs {
dependencyList, dependencyListExists := d[dependencyResourceRef]
if !dependencyListExists {
//the dependency list doesnt exist yet for this ref
dependencyList = []string{}
}
//add the current item to the list, then make sure the list is unique.
dependencyList = append(dependencyList, resourceRef)
uniqDependencyList := lo.Uniq[string](dependencyList)
d[dependencyResourceRef] = uniqDependencyList
}
}

View File

@ -6,9 +6,10 @@ import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/fastenhealth/gofhir-models/fhir401"
fhirutils "github.com/fastenhealth/gofhir-models/fhir401/utils"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"gorm.io/datatypes"
"net/http"
"time"
)
type FHIR401Client struct {
@ -40,196 +41,100 @@ func (c *FHIR401Client) GetPatient(patientId string) (fhir401.Patient, error) {
return patient, err
}
// GenerateResourceDependencyGraph
// FHIR resources can reference/depend on other resources.
// When storing processed models in the database, we need to make sure that we insert them in dependency order,
// so that we can correctly update all references
func (c *FHIR401Client) GenerateResourceDependencyGraph() {
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Process Bundles
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (c *FHIR401Client) ProcessBundle(bundle fhir401.Bundle) (DependencyGraph, map[string]interface{}, []string, error) {
// this lookup dict maps resource references to API models
resourceRefApiModelLookup := map[string]interface{}{}
func (c *FHIR401Client) ProcessBundle(bundle fhir401.Bundle) ([]models.ResourceFhir, error) {
// this map contains resource references, and a list of other resources that depend on it.
resourceRefDependencyGraph := DependencyGraph{}
//process each entry in bundle
wrappedResourceModels := lo.FilterMap[fhir401.BundleEntry, models.ResourceFhir](bundle.Entry, func(bundleEntry fhir401.BundleEntry, _ int) (models.ResourceFhir, bool) {
originalResource, _ := fhirutils.MapToResource(bundleEntry.Resource, false)
//list of all resources
allResources := []interface{}{}
resourceType, resourceId := originalResource.(ResourceInterface).ResourceRef()
skippedResources := []string{}
for _, bundleEntry := range bundle.Entry {
resource, _ := fhirutils.MapToResource(bundleEntry.Resource, false)
allResources = append(allResources, resource)
switch resource.(type) {
case fhir401.Patient:
typedResource := resource.(fhir401.Patient)
apiProfile, err := c.ProcessPatient(typedResource)
if err != nil {
return nil, nil, nil, err
}
resourceType, resourceId := typedResource.ResourceRef()
resourceRef := fmt.Sprintf("%s/%s", resourceType, *resourceId)
resourceRefApiModelLookup[resourceRef] = apiProfile
resourceRefDependencyGraph.AddDependencies(resourceRef, []string{})
case fhir401.Organization:
typedResource := resource.(fhir401.Organization)
apiOrganization, err := c.ProcessOrganization(typedResource)
if err != nil {
return nil, nil, nil, err
}
resourceType, resourceId := typedResource.ResourceRef()
resourceRef := fmt.Sprintf("%s/%s", resourceType, *resourceId)
resourceRefApiModelLookup[resourceRef] = apiOrganization
resourceRefDependencyGraph.AddDependencies(resourceRef, []string{})
case fhir401.Encounter:
typedResource := resource.(fhir401.Encounter)
apiEncounter, err := c.ProcessEncounter(typedResource)
if err != nil {
return nil, nil, nil, err
}
resourceType, resourceId := typedResource.ResourceRef()
resourceRef := fmt.Sprintf("%s/%s", resourceType, *resourceId)
resourceRefApiModelLookup[resourceRef] = apiEncounter
resourceRefDependencyGraph.AddDependencies(resourceRef, []string{})
default:
typedResource := resource.(ResourceInterface)
resourceType, resourceId := typedResource.ResourceRef()
var resourceRef string
if resourceId != nil {
resourceRef = fmt.Sprintf("%s/%s", resourceType, *resourceId)
} else {
resourceRef = resourceType
}
skippedResources = append(skippedResources, resourceRef)
// 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 resourceId == nil {
//no resourceId present for this resource, we'll ignore it.
return models.ResourceFhir{}, false
}
}
return resourceRefDependencyGraph, resourceRefApiModelLookup, skippedResources, nil
wrappedResourceModel := models.ResourceFhir{
OriginBase: models.OriginBase{
ModelBase: models.ModelBase{},
UserID: c.Source.UserID,
SourceID: c.Source.ID,
SourceResourceID: *resourceId,
SourceResourceType: resourceType,
},
Payload: datatypes.JSON(bundleEntry.Resource),
}
return wrappedResourceModel, true
})
return wrappedResourceModels, nil
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Process & Generate API/Database Models
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (c *FHIR401Client) ProcessPatient(item fhir401.Patient) (models.Profile, error) {
c.Logger.Debugf("item %v", item)
patientProfile := models.Profile{
OriginBase: models.OriginBase{
ModelBase: models.ModelBase{},
UserID: c.Source.UserID,
SourceID: c.Source.ID,
SourceResourceID: *item.Id,
SourceResourceType: fhir401.ResourceTypePatient.Code(),
},
Demographics: models.Demographics{
Address: models.Address{},
Name: models.Name{},
},
}
if item.Meta != nil && item.Meta.LastUpdated != nil {
if parsed, err := time.Parse(time.RFC3339Nano, *item.Meta.LastUpdated); err == nil {
patientProfile.UpdatedAt = parsed
}
}
if item.Address != nil && len(item.Address) > 0 {
itemAddress := item.Address[0]
patientProfile.Demographics.Address.City = itemAddress.City
patientProfile.Demographics.Address.Country = itemAddress.Country
patientProfile.Demographics.Address.State = itemAddress.State
patientProfile.Demographics.Address.Street = itemAddress.Line
patientProfile.Demographics.Address.Zip = itemAddress.PostalCode
}
patientProfile.Demographics.Dob = item.BirthDate
if item.Gender != nil {
itemGenderStr := item.Gender.String()
itemGenderCode := item.Gender.Code()
patientProfile.Demographics.Gender = &itemGenderStr
patientProfile.Demographics.GenderCodes = &itemGenderCode
}
patientProfile.Demographics.Language = item.Language
if item.MaritalStatus != nil {
patientProfile.Demographics.MaritalStatus = item.MaritalStatus.Text
if len(item.MaritalStatus.Coding) > 0 {
patientProfile.Demographics.MaritalStatusCodes = item.MaritalStatus.Coding[0].Code
}
}
if item.Name != nil && len(item.Name) > 0 {
itemName := item.Name[0]
if itemName.Prefix != nil && len(itemName.Prefix) > 0 {
itemNamePrefix := itemName.Prefix[0]
patientProfile.Demographics.Name.Prefix = &itemNamePrefix
}
patientProfile.Demographics.Name.Given = itemName.Given
patientProfile.Demographics.Name.Family = itemName.Family
}
return patientProfile, nil
}
func (c *FHIR401Client) ProcessOrganization(item fhir401.Organization) (models.Organization, error) {
apiOrganization := models.Organization{
OriginBase: models.OriginBase{
ModelBase: models.ModelBase{},
UserID: c.Source.UserID,
SourceID: c.Source.ID,
SourceResourceID: *item.Id,
SourceResourceType: fhir401.ResourceTypeOrganization.Code(),
},
Address: models.Address{},
}
if item.Meta != nil && item.Meta.LastUpdated != nil {
if parsed, err := time.Parse(time.RFC3339, *item.Meta.LastUpdated); err == nil {
apiOrganization.UpdatedAt = parsed
}
}
if item.Address != nil && len(item.Address) > 0 {
itemAddress := item.Address[0]
apiOrganization.Address.City = itemAddress.City
apiOrganization.Address.Country = itemAddress.Country
apiOrganization.Address.State = itemAddress.State
apiOrganization.Address.Street = itemAddress.Line
apiOrganization.Address.Zip = itemAddress.PostalCode
}
apiOrganization.Name = item.Name
apiOrganization.Active = item.Active
return apiOrganization, nil
}
//TODO
func (c *FHIR401Client) ProcessEncounter(item fhir401.Encounter) (models.Encounter, error) {
apiEncounter := models.Encounter{
OriginBase: models.OriginBase{
ModelBase: models.ModelBase{},
UserID: c.Source.UserID,
SourceID: c.Source.ID,
SourceResourceID: *item.Id,
SourceResourceType: fhir401.ResourceTypeEncounter.Code(),
},
Provider: models.Provider{},
Orders: []models.Order{},
}
if item.Meta != nil && item.Meta.LastUpdated != nil {
if parsed, err := time.Parse(time.RFC3339, *item.Meta.LastUpdated); err == nil {
apiEncounter.UpdatedAt = parsed
}
}
if item.Type != nil && len(item.Type) > 0 && item.Type[0].Coding != nil && len(item.Type[0].Coding) > 0 {
apiEncounter.VisitType = item.Type[0].Coding[0].Code
}
return apiEncounter, nil
}
//func (c *FHIR401Client) ProcessPatient(item fhir401.Patient) (models.Profile, error) {
// c.Logger.Debugf("item %v", item)
// patientProfile := models.Profile{
// OriginBase: models.OriginBase{
// ModelBase: models.ModelBase{},
// UserID: c.Source.UserID,
// SourceID: c.Source.ID,
// SourceResourceID: *item.Id,
// SourceResourceType: fhir401.ResourceTypePatient.Code(),
// },
// Demographics: models.Demographics{
// Address: models.Address{},
// Name: models.Name{},
// },
// }
//
//
//
// if item.Address != nil && len(item.Address) > 0 {
// itemAddress := item.Address[0]
// patientProfile.Demographics.Address.City = itemAddress.City
// patientProfile.Demographics.Address.Country = itemAddress.Country
// patientProfile.Demographics.Address.State = itemAddress.State
// patientProfile.Demographics.Address.Street = itemAddress.Line
// patientProfile.Demographics.Address.Zip = itemAddress.PostalCode
//
// }
// patientProfile.Demographics.Dob = item.BirthDate
//
// if item.Gender != nil {
// itemGenderStr := item.Gender.String()
// itemGenderCode := item.Gender.Code()
// patientProfile.Demographics.Gender = &itemGenderStr
// patientProfile.Demographics.GenderCodes = &itemGenderCode
// }
// patientProfile.Demographics.Language = item.Language
//
// if item.MaritalStatus != nil {
// patientProfile.Demographics.MaritalStatus = item.MaritalStatus.Text
// if len(item.MaritalStatus.Coding) > 0 {
// patientProfile.Demographics.MaritalStatusCodes = item.MaritalStatus.Coding[0].Code
// }
// }
// if item.Name != nil && len(item.Name) > 0 {
// itemName := item.Name[0]
// if itemName.Prefix != nil && len(itemName.Prefix) > 0 {
// itemNamePrefix := itemName.Prefix[0]
// patientProfile.Demographics.Name.Prefix = &itemNamePrefix
// }
// patientProfile.Demographics.Name.Given = itemName.Given
// patientProfile.Demographics.Name.Family = itemName.Family
//
// }
//
// return patientProfile, nil
//}

View File

@ -9,10 +9,8 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"io/ioutil"
"log"
"os"
"testing"
"time"
)
// helpers
@ -70,157 +68,11 @@ func TestFHIR401Client_ProcessBundle(t *testing.T) {
require.NoError(t, err)
// test
dependencyGraph, resoureceApiMap, skipped, err := client.ProcessBundle(bundle)
log.Printf("%v", dependencyGraph)
log.Printf("%v", resoureceApiMap)
wrappedResourceModels, err := client.ProcessBundle(bundle)
//log.Printf("%v", wrappedResourceModels)
//assert
require.NoError(t, err)
require.Equal(t, 8, len(skipped))
require.Equal(t, 4, len(resoureceApiMap))
require.Equal(t, 11, len(wrappedResourceModels))
//require.Equal(t, "A00000000000005", profile.SourceResourceID)
}
func TestFHIR401Client_ProcessPatient_Cigna_Empty(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
testLogger := logrus.WithFields(logrus.Fields{
"type": "test",
})
client, err := NewFHIR401Client(fakeConfig, testLogger, models.Source{
RefreshToken: "test-refresh-token",
AccessToken: "test-access-token",
})
require.NoError(t, err)
jsonBytes, err := readTestFixture("testdata/fixtures/401-R4/patient/cigna_syntheticuser05-patient-A00000000000005.json")
require.NoError(t, err)
var patient fhir401.Patient
err = json.Unmarshal(jsonBytes, &patient)
require.NoError(t, err)
// test
profile, err := client.ProcessPatient(patient)
//assert
require.NoError(t, err)
require.Equal(t, "Patient", profile.SourceResourceType)
require.Equal(t, "A00000000000005", profile.SourceResourceID)
}
func TestFHIR401Client_ProcessPatient_Cigna_Populated(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
testLogger := logrus.WithFields(logrus.Fields{
"type": "test",
})
client, err := NewFHIR401Client(fakeConfig, testLogger, models.Source{
RefreshToken: "test-refresh-token",
AccessToken: "test-access-token",
})
require.NoError(t, err)
jsonBytes, err := readTestFixture("testdata/fixtures/401-R4/patient/cigna_syntheticuser05-patient-ifp-A00000000000005.json")
require.NoError(t, err)
var patient fhir401.Patient
err = json.Unmarshal(jsonBytes, &patient)
require.NoError(t, err)
// test
profile, err := client.ProcessPatient(patient)
//assert
require.NoError(t, err)
require.Equal(t, "Patient", profile.SourceResourceType)
require.Equal(t, "ifp-A00000000000005", profile.SourceResourceID)
require.Equal(t, "2022-06-20T15:45:22.043Z", profile.UpdatedAt.Format(time.RFC3339Nano))
require.Equal(t, "2013-01-12", *profile.Demographics.Dob)
require.Equal(t, "female", *profile.Demographics.Gender)
require.Equal(t, "female", *profile.Demographics.GenderCodes)
require.Equal(t, "UNK", *profile.Demographics.MaritalStatusCodes)
require.Equal(t, "unknown", *profile.Demographics.MaritalStatus)
require.Equal(t, "Monahan", *profile.Demographics.Name.Family)
require.Equal(t, []string{"Felecita"}, profile.Demographics.Name.Given)
}
func TestFHIR401Client_ProcessPatients_Synthea_Populated(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
testLogger := logrus.WithFields(logrus.Fields{
"type": "test",
})
client, err := NewFHIR401Client(fakeConfig, testLogger, models.Source{
RefreshToken: "test-refresh-token",
AccessToken: "test-access-token",
})
require.NoError(t, err)
jsonBytes, err := readTestFixture("testdata/fixtures/401-R4/patient/synthea_Alesha810_Marks830_1e0a8bd3-3b82-4f17-b1d6-19043aa0db6b.json")
require.NoError(t, err)
var patient fhir401.Patient
err = json.Unmarshal(jsonBytes, &patient)
require.NoError(t, err)
// test
profile, err := client.ProcessPatient(patient)
//assert
require.NoError(t, err)
require.Equal(t, "Patient", profile.SourceResourceType)
require.Equal(t, "c088b7af-fc41-43cc-ab80-4a9ab8d47cd9", profile.SourceResourceID)
require.Equal(t, "0001-01-01T00:00:00Z", profile.UpdatedAt.Format(time.RFC3339Nano))
require.Equal(t, "1965-11-04", *profile.Demographics.Dob)
require.Equal(t, "female", *profile.Demographics.Gender)
require.Equal(t, "female", *profile.Demographics.GenderCodes)
require.Equal(t, "S", *profile.Demographics.MaritalStatusCodes)
require.Equal(t, "S", *profile.Demographics.MaritalStatus)
require.Equal(t, "Marks830", *profile.Demographics.Name.Family)
require.Equal(t, []string{"Alesha810"}, profile.Demographics.Name.Given)
require.Equal(t, "Ms.", *profile.Demographics.Name.Prefix)
}
func TestFHIR401Client_ProcessOrganizations_Cigna(t *testing.T) {
t.Parallel()
//setup
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
fakeConfig := mock_config.NewMockInterface(mockCtrl)
testLogger := logrus.WithFields(logrus.Fields{
"type": "test",
})
client, err := NewFHIR401Client(fakeConfig, testLogger, models.Source{
RefreshToken: "test-refresh-token",
AccessToken: "test-access-token",
})
require.NoError(t, err)
jsonBytes, err := readTestFixture("testdata/fixtures/401-R4/organization/cigna_syntheticuser05-organziation-ifp-51fb06f37e5ec973ce69132a9a2571f3.json")
require.NoError(t, err)
var org fhir401.Organization
err = json.Unmarshal(jsonBytes, &org)
require.NoError(t, err)
// test
apiOrg, err := client.ProcessOrganization(org)
//assert
require.NoError(t, err)
require.Equal(t, "Organization", apiOrg.SourceResourceType)
require.Equal(t, "ifp-51fb06f37e5ec973ce69132a9a2571f3", apiOrg.SourceResourceID)
require.Equal(t, "2022-06-20T15:45:45.155Z", apiOrg.UpdatedAt.Format(time.RFC3339Nano))
require.Equal(t, true, *apiOrg.Active)
require.Equal(t, "SURPRISE", *apiOrg.Address.City)
require.Equal(t, "AZ", *apiOrg.Address.State)
require.Equal(t, []string{"13991 W GRAND AVE STE 105"}, apiOrg.Address.Street)
require.Nil(t, apiOrg.Address.Country)
require.Equal(t, "85374", *apiOrg.Address.Zip)
require.Equal(t, "CIGNA MED GRP PHCY-SUN CITY WE", *apiOrg.Name)
}

View File

@ -1,80 +0,0 @@
{
"id": "ifp-1C9A46F07B3CADE33EFC75ABA3DC37CF",
"meta": {
"lastUpdated": "2022-06-20T15:45:29.685000+00:00",
"profile": ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter"],
"source": "IFP#e7zkeLCSmQFgUX0Z",
"versionId": "1"
},
"class": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
},
"diagnosis": [{
"condition": {
"reference": "Condition/ifp-A00000000000005-OP0447575426-K57.80-Y-"
},
"use": {
"coding": [{
"display": "Admission diagnosis"
}]
}
}],
"identifier": [{
"system": "http://dev.cigna.com/system/TRCR",
"value": "TRCR-OP0447575426-1"
}],
"location": [{
"location": {
"reference": "Location/ifp-7312532"
}
}, {
"location": {
"reference": "Location/ifp-0566265"
}
}],
"participant": [{
"individual": {
"reference": "Practitioner/ifp-50ef4542ecb5ae5eddc0dac76a10aaed"
}
}, {
"individual": {
"reference": "Practitioner/ifp-6a4d3d6fe165b6a0a79b6a976d043c2c"
}
}],
"period": {
"end": "2019-12-31",
"start": "2019-11-07"
},
"reasonCode": [{
"coding": [{
"code": "K57.80",
"display": "Dvtrcli of intest, part unsp, w perf and abscess w/o bleed"
}]
}],
"reasonReference": [{
"reference": "Condition/ifp-A00000000000005-OP0447575426-K57.80-Y-"
}],
"serviceProvider": {
"reference": "Organization/ifp-0566265"
},
"serviceType": {
"coding": [{
"code": "302",
"display": "Medical Outpatient"
}]
},
"status": "unknown",
"subject": {
"reference": "Patient/ifp-A00000000000005"
},
"type": [{
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "unknown"
}]
}],
"resourceType": "Encounter"
}

View File

@ -1,41 +0,0 @@
{
"resourceType": "Encounter",
"id": "3476a881-bb6b-4670-b0d9-aa3eb12be267",
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB"
},
"type": [
{
"coding": [
{
"system": "http://snomed.info/sct",
"code": "162673000",
"display": "General examination of patient (procedure)"
}
],
"text": "General examination of patient (procedure)"
}
],
"subject": {
"reference": "urn:uuid:d7714835-c7e2-408f-9652-c8c2bdd9d2bf",
"display": "Mr. Blair400 Grady603"
},
"participant": [
{
"individual": {
"reference": "urn:uuid:0000016d-3a85-4cca-0000-0000000016d0",
"display": "Dr. María Soledad68 Marrero674"
}
}
],
"period": {
"start": "2011-08-10T08:19:16-04:00",
"end": "2011-08-10T08:34:16-04:00"
},
"serviceProvider": {
"reference": "urn:uuid:a0123c36-2436-3609-a5eb-3c3857ed711d",
"display": "PCP8367"
}
}

View File

@ -1,45 +0,0 @@
{
"id": "ifp-51fb06f37e5ec973ce69132a9a2571f3",
"meta": {
"lastUpdated": "2022-06-20T15:45:45.155000+00:00",
"profile": ["http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Organization"],
"source": "IFP#1Hc7lo3uMBmGNP1z",
"versionId": "1"
},
"active": true,
"address": [{
"city": "SURPRISE",
"line": ["13991 W GRAND AVE STE 105"],
"postalCode": "85374",
"state": "AZ",
"text": "13991 W GRAND AVE STE 105 SURPRISE AZ 85374"
}],
"identifier": [{
"system": "http://hl7.org/fhir/sid/us-npi",
"type": {
"coding": [{
"code": "npi",
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/IdentifierTypeCS"
}]
},
"value": "1609868678"
}, {
"system": "https://developer.cigna.com",
"type": {
"coding": [{
"code": "provid",
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/IdentifierTypeCS"
}]
}
}, {
"system": "urn:oid:2.16.840.1.113883.4.4",
"type": {
"coding": [{
"code": "TAX",
"system": "http://terminology.hl7.org/CodeSystem/v2-0203"
}]
}
}],
"name": "CIGNA MED GRP PHCY-SUN CITY WE",
"resourceType": "Organization"
}

View File

@ -1,15 +0,0 @@
{
"id": "A00000000000005",
"link": [{
"other": {
"reference": "Patient/ifp-A00000000000005"
},
"type": "seealso"
}, {
"other": {
"reference": "Patient/com-44a04ebb-7aba-4e66-99c7-12b99405f30d"
},
"type": "seealso"
}],
"resourceType": "Patient"
}

View File

@ -1,53 +0,0 @@
{
"id": "ifp-A00000000000005",
"meta": {
"lastUpdated": "2022-06-20T15:45:22.043000+00:00",
"profile": ["http://hl7.org/fhir/us/carin-bb/StructureDefinition/C4BB-Patient"],
"source": "IFP#CqP06ARvJo9XS9Cl",
"versionId": "1"
},
"text": {
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Felecita <b>MONAHAN </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>A00000000000005</td></tr><tr><td>Date of birth</td><td><span>12 January 2013</span></td></tr></tbody></table></div>",
"status": "generated"
},
"birthDate": "2013-01-12",
"gender": "female",
"identifier": [{
"system": "https://developer.cigna.com",
"type": {
"coding": [{
"code": "um",
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/IdentifierTypeCS"
}]
},
"value": "A00000000000005"
}, {
"system": "https://developer.cigna.com",
"type": {
"coding": [{
"code": "mb",
"system": "http://hl7.org/fhir/us/carin-bb/CodeSystem/IdentifierTypeCS"
}]
},
"value": "unknown"
}],
"maritalStatus": {
"coding": [{
"code": "UNK",
"display": "unknown",
"system": "http://terminology.hl7.org/CodeSystem/v3-NullFlavor"
}],
"text": "unknown"
},
"name": [{
"family": "Monahan",
"given": ["Felecita"],
"use": "official"
}],
"telecom": [{
"system": "phone",
"use": "mobile",
"value": "9404535496"
}],
"resourceType": "Patient"
}

View File

@ -1,201 +0,0 @@
{
"resourceType": "Patient",
"id": "c088b7af-fc41-43cc-ab80-4a9ab8d47cd9",
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Generated by <a href=\"https://github.com/synthetichealth/synthea\">Synthea</a>.Version identifier: v2.4.0-404-ge7ce2295\n . Person seed: -2282623849526784568 Population seed: 0</div>"
},
"extension": [
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2106-3",
"display": "White"
}
},
{
"url": "text",
"valueString": "White"
}
]
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2186-5",
"display": "Not Hispanic or Latino"
}
},
{
"url": "text",
"valueString": "Not Hispanic or Latino"
}
]
},
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName",
"valueString": "Sandra485 Cormier289"
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
"valueCode": "F"
},
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
"valueAddress": {
"city": "Tewksbury",
"state": "Massachusetts",
"country": "US"
}
},
{
"url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years",
"valueDecimal": 0.7742822202801629
},
{
"url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years",
"valueDecimal": 52.22571777971984
}
],
"identifier": [
{
"system": "https://github.com/synthetichealth/synthea",
"value": "1e0a8bd3-3b82-4f17-b1d6-19043aa0db6b"
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR",
"display": "Medical Record Number"
}
],
"text": "Medical Record Number"
},
"system": "http://hospital.smarthealthit.org",
"value": "1e0a8bd3-3b82-4f17-b1d6-19043aa0db6b"
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "SS",
"display": "Social Security Number"
}
],
"text": "Social Security Number"
},
"system": "http://hl7.org/fhir/sid/us-ssn",
"value": "999-76-3236"
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "DL",
"display": "Driver's License"
}
],
"text": "Driver's License"
},
"system": "urn:oid:2.16.840.1.113883.4.3.25",
"value": "S99995466"
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "PPN",
"display": "Passport Number"
}
],
"text": "Passport Number"
},
"system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber",
"value": "X41129973X"
}
],
"name": [
{
"use": "official",
"family": "Marks830",
"given": [
"Alesha810"
],
"prefix": [
"Ms."
]
}
],
"telecom": [
{
"system": "phone",
"value": "555-664-6121",
"use": "home"
}
],
"gender": "female",
"birthDate": "1965-11-04",
"address": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/geolocation",
"extension": [
{
"url": "latitude",
"valueDecimal": 42.60588776678466
},
{
"url": "longitude",
"valueDecimal": -71.0695322588603
}
]
}
],
"line": [
"165 Shanahan View"
],
"city": "North Reading",
"state": "Massachusetts",
"country": "US"
}
],
"maritalStatus": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus",
"code": "S",
"display": "S"
}
],
"text": "S"
},
"multipleBirthBoolean": false,
"communication": [
{
"language": {
"coding": [
{
"system": "urn:ietf:bcp:47",
"code": "en-US",
"display": "English"
}
],
"text": "English"
}
}
]
}

View File

@ -28,11 +28,11 @@ func (c CignaClient) SyncAll(db database.DatabaseRepository) error {
return err
}
_, resourceRefApiModelLookup, _, err := c.ProcessBundle(bundle)
wrappedResourceModels, err := c.ProcessBundle(bundle)
//todo, create the resources in dependency order
for _, apiModel := range resourceRefApiModelLookup {
for _, apiModel := range wrappedResourceModels {
err = db.UpsertResource(context.Background(), apiModel)
if err != nil {
return err

View File

@ -1,40 +0,0 @@
package models
import "time"
// https://reference.humanapi.co/reference/encounters
type Encounter struct {
OriginBase
DateTime time.Time `json:"time,omitempty"` //dateTime Date (optional) The date of the encounter
VisitType *string `json:"visitType,omitempty"` // visitType String (optional) The type of visit
Provider Provider `json:"provider,omitempty" gorm:"serializer:json;default:'{}'"` // provider Object (optional) The provider for the encounter (see provider object)
//Prescriptions []Prescription `json:"prescriptions,omitempty"` // prescriptions Array[Object] (optional) A list of prescriptions provided during the encounter (see link object)
//Diagnoses []Diagnosis `json:"diagnoses,omitempty"` // diagnoses Array[Object] (optional) A list of diagnoses for the encounter where object contains a "name" field e.g. *[{"name": "Sacroiliac dysfunction"}, {"name": "Bilateral hand pain"}]
//vitals Object (optional) Vitals captured during the encounter (e.g. {"temperature" : 95.2 [degF]","weight" : 180 [lb_av]","height" : "69 [in_us]"})
//vitalSigns Array[Object] (optional) A list of vital signs from the encounter (see link object)
Reasons []string `json:"reasons,omitempty" gorm:"serializer:json;default:'[]'"` // reasons Array[String] (optional) A list of reasons for the encounter (e.g. [Follow-up, 'Consult, 'DYSPHONIA', 'Back Pain])
Orders []Order `json:"orders,omitempty" gorm:"serializer:json;default:'{}'"` // orders Array[Object] (optional) A list of medication orders for the patient (see orders object)
//testResults Array[Object] (optional) A list of test results for the patient (see link object)
//plansOfCare Array[Object] (optional) A list of plans of care from the encounter (see link object)
//medications Array[Object] (optional) A list of medications used by the patient. Objects in array can have some or many of the properties of medications. Common properties are "name", "productName", "startDate", "endDate", "instructions".
FollowUpInstructions *string `json:"followUpInstructions,omitempty"` // followUpInstructions String (optional) Follow-up instructions
//Organization Organization `json:"organization,omitempty"` // organization Object (optional) (See organizations object)
//codes Array[Object] (optional) (See codes)
}
type Order struct {
Name *string `json:"name,omitempty"` // name String (optional) The name of the order
CodeType *string `json:"codeType,omitempty"` // codeType String (optional) The code type of the order (e.g. “CPT( R )”, “Custom”)
ExpectedDate time.Time `json:"expectedDate,omitempty"` // expectedDate Date (optional) The date the order is expected
ExpireDate time.Time `json:"expireDate,omitempty"` // expireDate Date (optional) The date the order expires
ProcedureCode *string `json:"procedureCode,omitempty"` // procedureCode String (optional) The procedure code of the order
OrderType *string `json:"type,omitempty"` // type String (optional) The type of the order (e.g. “Lab”, “Procedures”)
//Name string `codes:"codes,omitempty"` // codes Array[Object] (optional) (See codes)
}
type Provider struct {
Name *string `json:"name,omitempty"` //name String (optional) Name of the provider
DepartmentName *string `json:"departmentName,omitempty"` //departmentName String (optional) Name of the provider department
Address *string `json:"address,omitempty"` //address String (optional) Address of the provider
}

View File

@ -1,9 +0,0 @@
package models
type Organization struct {
OriginBase
Name *string `json:"name,omitempty"` //name String (optional) The name of the organization
Address Address `json:"address,omitempty" gorm:"serializer:json;default:'{}'"` //address String (optional) Address of the provider
Active *bool `json:"active,omitempty"`
}

View File

@ -1,39 +0,0 @@
package models
//demographics Object (optional) (See demographics)
//alcohol Object (optional) The users alcohol usage. See alcohol object.
//smoking Object (optional) The users smoking habits. See smoking object.
type Profile struct {
OriginBase
//embedded structs
Demographics Demographics `json:"demographics" gorm:"serializer:json;default:'{}'"`
}
type Demographics struct {
Address Address `json:"address,omitempty"` // Object (See address object)
Dob *string `json:"dob,omitempty"` //String (optional) The users date of birth e.g. "04/21/1965"
Ethnicity *string `json:"ethnicity,omitempty"` // String (optional) The ethnicity of the user e.g. "Not Hispanic of Latino"
Gender *string `json:"gender,omitempty"` // String (optional) The users gender e.g. "male"
Language *string `json:"language,omitempty"` //String (optional) The users primary language e.g. "eng"
MaritalStatus *string `json:"maritalStatus,omitempty"` // String (optional) The users marital status (eg: “married”, “single”)
Name Name `json:"name,omitempty"` // Object (optional) (See name object)
Race *string `json:"race,omitempty"` // String (optional) The users race e.g. "White"
EthnicityCodes *string `json:"ethnicityCodes,omitempty"` // ethnicityCodes Array[Object] (optional) CDC Race & Ethnicity and SNOMED CT Ethnicity codes: See codes
MaritalStatusCodes *string `json:"maritalStatusCodes,omitempty"` // String (optional) SNOMED CT Marital status codes: see codes object
GenderCodes *string `json:"genderCodes,omitempty"` //String (optional) SNOMED CT Gender codes: See codes
}
type Address struct {
City *string `json:"city,omitempty"` // (optional) City of address e.g. "SAN FRANCISCO"
Country *string `json:"country,omitempty"` // (optional) Country of address e.g. "US"
State *string `json:"state,omitempty"` // (optional) State of address e.g. "CA"
Street []string `json:"street,omitempty"` // Array[String] (optional) Street of address e.g. ["156 22ND AVE NW"]
Zip *string `json:"zip,omitempty"` // (optional) Zip of address e.g. "94123"
}
type Name struct {
Prefix *string `json:"prefix,omitempty"` // String (optional) The title of the provider e.g. "MD"
Given []string `json:"given,omitempty"` // Array[String] Name values associated with the provider e.g. ["Travis", "R"]
Family *string `json:"family,omitempty"` // String (optional) Family name of the provider e.g. "Liddell"
}

View File

@ -0,0 +1,10 @@
package models
import "gorm.io/datatypes"
type ResourceFhir struct {
OriginBase
//embedded data
Payload datatypes.JSON `json:"payload" gorm:"payload"`
}

View File

@ -2,6 +2,7 @@ package handler
import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
@ -27,6 +28,21 @@ func CreateSource(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
// after creating the source, we should do a bulk import
sourceClient, err := hub.NewClient(providerCred.ProviderId, nil, logger, providerCred)
if err != nil {
logger.Errorln("An error occurred while initializing hub client using source credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = sourceClient.SyncAll(databaseRepo)
if err != nil {
logger.Errorln("An error occurred while bulk import of resources from source", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": providerCred})
}

5
go.mod
View File

@ -16,6 +16,7 @@ require (
github.com/stretchr/testify v1.7.1
github.com/urfave/cli/v2 v2.11.2
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
gorm.io/datatypes v1.0.7
gorm.io/gorm v1.23.8
)
@ -29,11 +30,12 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
@ -67,6 +69,7 @@ require (
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.3.2 // indirect
modernc.org/libc v1.16.8 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect