adding test framework/recordings for client.
Adding generic upsert fuction for models.
This commit is contained in:
parent
caa7315ad9
commit
96465f23df
|
@ -10,8 +10,9 @@ type DatabaseRepository interface {
|
|||
Close() error
|
||||
GetCurrentUser() models.User
|
||||
|
||||
UpsertProfile(context.Context, *models.Profile) error
|
||||
UpsertOrganziation(context.Context, *models.Organization) error
|
||||
UpsertResource(context.Context, interface{}) error
|
||||
//UpsertProfile(context.Context, *models.Profile) error
|
||||
//UpsertOrganziation(context.Context, *models.Organization) error
|
||||
|
||||
CreateSource(context.Context, *models.Source) error
|
||||
GetSources(context.Context) ([]models.Source, error)
|
||||
|
|
|
@ -48,6 +48,7 @@ func NewRepository(appConfig config.Interface, globalLogger logrus.FieldLogger)
|
|||
&models.Source{},
|
||||
&models.Profile{},
|
||||
&models.Organization{},
|
||||
&models.Encounter{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to automigrate! - %v", err)
|
||||
|
@ -86,16 +87,48 @@ func (sr *sqliteRepository) GetCurrentUser() models.User {
|
|||
return currentUser
|
||||
}
|
||||
|
||||
// UpsertSourceResource Create or Update record in database
|
||||
func (sr *sqliteRepository) UpsertProfile(ctx context.Context, profile *models.Profile) error {
|
||||
if sr.gormClient.Debug().WithContext(ctx).Model(profile).
|
||||
func (sr *sqliteRepository) UpsertResource(ctx context.Context, resourceModel interface{}) 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: profile.GetSourceID(),
|
||||
SourceResourceID: profile.GetSourceResourceID(),
|
||||
SourceResourceType: profile.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
|
||||
}).Updates(profile).RowsAffected == 0 {
|
||||
sr.logger.Infof("profile does not exist, creating: %s %s %s", profile.GetSourceID(), profile.GetSourceResourceID(), profile.GetSourceResourceType())
|
||||
return sr.gormClient.Debug().Create(profile).Error
|
||||
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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
"github.com/fastenhealth/gofhir-models/fhir401"
|
||||
fhirutils "github.com/fastenhealth/gofhir-models/fhir401/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
|
@ -23,28 +24,98 @@ func NewFHIR401Client(appConfig config.Interface, globalLogger logrus.FieldLogge
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// FHIR
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (c *FHIR401Client) GetPatientEverything(patientId string) (*fhir401.Bundle, error) {
|
||||
func (c *FHIR401Client) GetPatientBundle(patientId string) (fhir401.Bundle, error) {
|
||||
|
||||
// https://www.hl7.org/fhir/patient-operation-everything.html
|
||||
bundle := fhir401.Bundle{}
|
||||
err := c.GetRequest(fmt.Sprintf("Patient/%s/$everything", patientId), &bundle)
|
||||
return &bundle, err
|
||||
return bundle, err
|
||||
|
||||
}
|
||||
|
||||
func (c *FHIR401Client) GetPatient(patientId string) (*fhir401.Patient, error) {
|
||||
func (c *FHIR401Client) GetPatient(patientId string) (fhir401.Patient, error) {
|
||||
|
||||
patient := fhir401.Patient{}
|
||||
err := c.GetRequest(fmt.Sprintf("Patient/%s", patientId), &patient)
|
||||
return &patient, err
|
||||
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{}{}
|
||||
|
||||
// this map contains resource references, and a list of other resources that depend on it.
|
||||
resourceRefDependencyGraph := DependencyGraph{}
|
||||
|
||||
//list of all resources
|
||||
allResources := []interface{}{}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
return resourceRefDependencyGraph, resourceRefApiModelLookup, skippedResources, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Process & Generate API/Database Models
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
func (c *FHIR401Client) ProcessPatients(patients []fhir401.Patient) ([]models.Profile, error) {
|
||||
profiles := []models.Profile{}
|
||||
for _, item := range patients {
|
||||
func (c *FHIR401Client) ProcessPatient(item fhir401.Patient) (models.Profile, error) {
|
||||
c.Logger.Debugf("item %v", item)
|
||||
patientProfile := models.Profile{
|
||||
OriginBase: models.OriginBase{
|
||||
|
@ -101,44 +172,64 @@ func (c *FHIR401Client) ProcessPatients(patients []fhir401.Patient) ([]models.Pr
|
|||
patientProfile.Demographics.Name.Family = itemName.Family
|
||||
|
||||
}
|
||||
profiles = append(profiles, patientProfile)
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
return patientProfile, nil
|
||||
}
|
||||
|
||||
func (c *FHIR401Client) ProcessOrganizations(orgs []fhir401.Organization) ([]models.Organization, error) {
|
||||
apiOrganizations := []models.Organization{}
|
||||
for _, org := range orgs {
|
||||
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: *org.Id,
|
||||
SourceResourceID: *item.Id,
|
||||
SourceResourceType: fhir401.ResourceTypeOrganization.Code(),
|
||||
},
|
||||
Address: models.Address{},
|
||||
}
|
||||
|
||||
if org.Meta != nil && org.Meta.LastUpdated != nil {
|
||||
if parsed, err := time.Parse(time.RFC3339, *org.Meta.LastUpdated); err == nil {
|
||||
if item.Meta != nil && item.Meta.LastUpdated != nil {
|
||||
if parsed, err := time.Parse(time.RFC3339, *item.Meta.LastUpdated); err == nil {
|
||||
apiOrganization.UpdatedAt = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if org.Address != nil && len(org.Address) > 0 {
|
||||
itemAddress := org.Address[0]
|
||||
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 = org.Name
|
||||
apiOrganization.Active = org.Active
|
||||
apiOrganization.Name = item.Name
|
||||
apiOrganization.Active = item.Active
|
||||
|
||||
apiOrganizations = append(apiOrganizations, apiOrganization)
|
||||
}
|
||||
return apiOrganizations, nil
|
||||
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
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -47,7 +48,39 @@ func TestNewFHIR401Client(t *testing.T) {
|
|||
require.Equal(t, client.Source.RefreshToken, "test-refresh-token")
|
||||
}
|
||||
|
||||
func TestFHIR401Client_ProcessPatients_Cigna_Empty(t *testing.T) {
|
||||
func TestFHIR401Client_ProcessBundle(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/bundle/cigna_syntheticuser05-everything.json")
|
||||
require.NoError(t, err)
|
||||
var bundle fhir401.Bundle
|
||||
err = json.Unmarshal(jsonBytes, &bundle)
|
||||
require.NoError(t, err)
|
||||
|
||||
// test
|
||||
dependencyGraph, resoureceApiMap, skipped, err := client.ProcessBundle(bundle)
|
||||
log.Printf("%v", dependencyGraph)
|
||||
log.Printf("%v", resoureceApiMap)
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 8, len(skipped))
|
||||
require.Equal(t, 4, len(resoureceApiMap))
|
||||
//require.Equal(t, "A00000000000005", profile.SourceResourceID)
|
||||
}
|
||||
|
||||
func TestFHIR401Client_ProcessPatient_Cigna_Empty(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
|
@ -69,16 +102,15 @@ func TestFHIR401Client_ProcessPatients_Cigna_Empty(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// test
|
||||
profiles, err := client.ProcessPatients([]fhir401.Patient{patient})
|
||||
profile, err := client.ProcessPatient(patient)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(profiles))
|
||||
require.Equal(t, "Patient", profiles[0].SourceResourceType)
|
||||
require.Equal(t, "A00000000000005", profiles[0].SourceResourceID)
|
||||
require.Equal(t, "Patient", profile.SourceResourceType)
|
||||
require.Equal(t, "A00000000000005", profile.SourceResourceID)
|
||||
}
|
||||
|
||||
func TestFHIR401Client_ProcessPatients_Cigna_Populated(t *testing.T) {
|
||||
func TestFHIR401Client_ProcessPatient_Cigna_Populated(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
|
@ -100,21 +132,20 @@ func TestFHIR401Client_ProcessPatients_Cigna_Populated(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// test
|
||||
profiles, err := client.ProcessPatients([]fhir401.Patient{patient})
|
||||
profile, err := client.ProcessPatient(patient)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(profiles))
|
||||
require.Equal(t, "Patient", profiles[0].SourceResourceType)
|
||||
require.Equal(t, "ifp-A00000000000005", profiles[0].SourceResourceID)
|
||||
require.Equal(t, "2022-06-20T15:45:22.043Z", profiles[0].UpdatedAt.Format(time.RFC3339Nano))
|
||||
require.Equal(t, "2013-01-12", *profiles[0].Demographics.Dob)
|
||||
require.Equal(t, "female", *profiles[0].Demographics.Gender)
|
||||
require.Equal(t, "female", *profiles[0].Demographics.GenderCodes)
|
||||
require.Equal(t, "UNK", *profiles[0].Demographics.MaritalStatusCodes)
|
||||
require.Equal(t, "unknown", *profiles[0].Demographics.MaritalStatus)
|
||||
require.Equal(t, "Monahan", *profiles[0].Demographics.Name.Family)
|
||||
require.Equal(t, []string{"Felecita"}, profiles[0].Demographics.Name.Given)
|
||||
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) {
|
||||
|
@ -139,22 +170,21 @@ func TestFHIR401Client_ProcessPatients_Synthea_Populated(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// test
|
||||
profiles, err := client.ProcessPatients([]fhir401.Patient{patient})
|
||||
profile, err := client.ProcessPatient(patient)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(profiles))
|
||||
require.Equal(t, "Patient", profiles[0].SourceResourceType)
|
||||
require.Equal(t, "c088b7af-fc41-43cc-ab80-4a9ab8d47cd9", profiles[0].SourceResourceID)
|
||||
require.Equal(t, "0001-01-01T00:00:00Z", profiles[0].UpdatedAt.Format(time.RFC3339Nano))
|
||||
require.Equal(t, "1965-11-04", *profiles[0].Demographics.Dob)
|
||||
require.Equal(t, "female", *profiles[0].Demographics.Gender)
|
||||
require.Equal(t, "female", *profiles[0].Demographics.GenderCodes)
|
||||
require.Equal(t, "S", *profiles[0].Demographics.MaritalStatusCodes)
|
||||
require.Equal(t, "S", *profiles[0].Demographics.MaritalStatus)
|
||||
require.Equal(t, "Marks830", *profiles[0].Demographics.Name.Family)
|
||||
require.Equal(t, []string{"Alesha810"}, profiles[0].Demographics.Name.Given)
|
||||
require.Equal(t, "Ms.", *profiles[0].Demographics.Name.Prefix)
|
||||
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) {
|
||||
|
@ -179,19 +209,18 @@ func TestFHIR401Client_ProcessOrganizations_Cigna(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// test
|
||||
orgs, err := client.ProcessOrganizations([]fhir401.Organization{org})
|
||||
apiOrg, err := client.ProcessOrganization(org)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(orgs))
|
||||
require.Equal(t, "Organization", orgs[0].SourceResourceType)
|
||||
require.Equal(t, "ifp-51fb06f37e5ec973ce69132a9a2571f3", orgs[0].SourceResourceID)
|
||||
require.Equal(t, "2022-06-20T15:45:45.155Z", orgs[0].UpdatedAt.Format(time.RFC3339Nano))
|
||||
require.Equal(t, true, *orgs[0].Active)
|
||||
require.Equal(t, "SURPRISE", *orgs[0].Address.City)
|
||||
require.Equal(t, "AZ", *orgs[0].Address.State)
|
||||
require.Equal(t, []string{"13991 W GRAND AVE STE 105"}, orgs[0].Address.Street)
|
||||
require.Nil(t, orgs[0].Address.Country)
|
||||
require.Equal(t, "85374", *orgs[0].Address.Zip)
|
||||
require.Equal(t, "CIGNA MED GRP PHCY-SUN CITY WE", *orgs[0].Name)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -24,3 +24,7 @@ type Client interface {
|
|||
//Demographics()
|
||||
//SocialHistory()
|
||||
}
|
||||
|
||||
type ResourceInterface interface {
|
||||
ResourceRef() (string, *string)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"github.com/seborama/govcr"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func OAuthVcrSetup(t *testing.T, enableRecording bool) *http.Client {
|
||||
accessToken := "PLACEHOLDER"
|
||||
if enableRecording {
|
||||
//this has to be disabled because CI is empty inside docker containers.
|
||||
accessToken = ""
|
||||
}
|
||||
|
||||
ts := oauth2.StaticTokenSource(
|
||||
//setting a real access token here will allow API calls to connect successfully
|
||||
&oauth2.Token{AccessToken: accessToken},
|
||||
)
|
||||
|
||||
tr := http.DefaultTransport.(*http.Transport)
|
||||
tr.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true, //disable certificate validation because we're playing back http requests.
|
||||
}
|
||||
insecureClient := http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(oauth2.NoContext, oauth2.HTTPClient, insecureClient)
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
|
||||
vcrConfig := govcr.VCRConfig{
|
||||
Logging: true,
|
||||
CassettePath: path.Join("testdata", "govcr-fixtures"),
|
||||
Client: tc,
|
||||
|
||||
//this line ensures that we do not attempt to create new recordings.
|
||||
//Comment this out if you would like to make recordings.
|
||||
DisableRecording: !enableRecording,
|
||||
}
|
||||
|
||||
// HTTP headers are case-insensitive
|
||||
vcrConfig.RequestFilters.Add(govcr.RequestDeleteHeaderKeys("User-Agent", "user-agent"))
|
||||
|
||||
vcr := govcr.NewVCR(t.Name(), &vcrConfig)
|
||||
return vcr.Client
|
||||
}
|
80
backend/pkg/hub/internal/fhir/base/testdata/fixtures/401-R4/encounter/cigna_encounter.json
vendored
Normal file
80
backend/pkg/hub/internal/fhir/base/testdata/fixtures/401-R4/encounter/cigna_encounter.json
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"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"
|
||||
}
|
41
backend/pkg/hub/internal/fhir/base/testdata/fixtures/401-R4/encounter/synthea_encounter.json
vendored
Normal file
41
backend/pkg/hub/internal/fhir/base/testdata/fixtures/401-R4/encounter/synthea_encounter.json
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -2,14 +2,10 @@ package cigna
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/base"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
"github.com/fastenhealth/gofhir-models/fhir401"
|
||||
fhirutils "github.com/fastenhealth/gofhir-models/fhir401/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
@ -27,57 +23,20 @@ func NewClient(appConfig config.Interface, globalLogger logrus.FieldLogger, sour
|
|||
|
||||
func (c CignaClient) SyncAll(db database.DatabaseRepository) error {
|
||||
|
||||
bundle, err := c.GetPatientEverything(c.Source.PatientId)
|
||||
bundle, err := c.GetPatientBundle(c.Source.PatientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resources := []interface{}{}
|
||||
for _, bundleEntry := range bundle.Entry {
|
||||
resource, _ := fhirutils.MapToResource(bundleEntry.Resource, false)
|
||||
resources = append(resources, resource)
|
||||
}
|
||||
_, resourceRefApiModelLookup, _, err := c.ProcessBundle(bundle)
|
||||
|
||||
resourceRefLookup := map[string]uuid.UUID{}
|
||||
//todo, create the resources in dependency order
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Patient
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
patientResources := []fhir401.Patient{}
|
||||
for _, resource := range resources {
|
||||
if patient, isPatient := resource.(fhir401.Patient); isPatient {
|
||||
patientResources = append(patientResources, patient)
|
||||
}
|
||||
}
|
||||
apiProfiles, err := c.ProcessPatients(patientResources)
|
||||
for _, profile := range apiProfiles {
|
||||
err = db.UpsertProfile(context.Background(), &profile)
|
||||
for _, apiModel := range resourceRefApiModelLookup {
|
||||
err = db.UpsertResource(context.Background(), apiModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//add upserted resource uuids to lookup
|
||||
resourceRefLookup[fmt.Sprintf("%s/%s", profile.SourceResourceType, profile.SourceResourceID)] = profile.ID
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Organization
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
organizations := []fhir401.Organization{}
|
||||
for _, resource := range resources {
|
||||
if org, isOrganization := resource.(fhir401.Organization); isOrganization {
|
||||
organizations = append(organizations, org)
|
||||
}
|
||||
}
|
||||
apiOrgs, err := c.ProcessOrganizations(organizations)
|
||||
for _, apiOrg := range apiOrgs {
|
||||
err = db.UpsertOrganziation(context.Background(), &apiOrg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//add upserted resource uuids to lookup
|
||||
resourceRefLookup[fmt.Sprintf("%s/%s", apiOrg.SourceResourceType, apiOrg.SourceResourceID)] = apiOrg.ID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package cigna
|
||||
|
||||
import (
|
||||
mock_config "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config/mock"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/hub/internal/fhir/base"
|
||||
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCignaClient_SyncAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
//setup
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
fakeConfig := mock_config.NewMockInterface(mockCtrl)
|
||||
|
||||
testDatabase, err := ioutil.TempFile("testdata", "fasten.db")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(testDatabase.Name())
|
||||
fakeConfig.EXPECT().GetString("web.database.location").AnyTimes().Return(testDatabase.Name())
|
||||
testLogger := logrus.WithFields(logrus.Fields{
|
||||
"type": "test",
|
||||
})
|
||||
httpClient := base.OAuthVcrSetup(t, false)
|
||||
client, err := NewClient(fakeConfig, testLogger, models.Source{
|
||||
ProviderId: "cigna",
|
||||
PatientId: "A00000000000005",
|
||||
ApiEndpointBaseUrl: "https://p-hi2.digitaledge.cigna.com/PatientAccess/v1-devportal",
|
||||
ClientId: "e434426c-2aaf-413a-a39a-8f5f6130f287",
|
||||
}, httpClient)
|
||||
|
||||
db, err := database.NewRepository(fakeConfig, testLogger)
|
||||
require.NoError(t, err)
|
||||
|
||||
//test
|
||||
err = client.SyncAll(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
}
|
1101
backend/pkg/hub/internal/fhir/cigna/testdata/govcr-fixtures/TestCignaClient_SyncAll.cassette
vendored
Normal file
1101
backend/pkg/hub/internal/fhir/cigna/testdata/govcr-fixtures/TestCignaClient_SyncAll.cassette
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -13,7 +13,7 @@ type Encounter struct {
|
|||
//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"` // reasons Array[String] (optional) A list of reasons for the encounter (e.g. [‘Follow-up’, 'Consult’, 'DYSPHONIA', 'Back Pain’])
|
||||
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)
|
||||
|
|
5
go.mod
5
go.mod
|
@ -4,11 +4,13 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
|
||||
github.com/fastenhealth/gofhir-models v0.0.3
|
||||
github.com/fastenhealth/gofhir-models v0.0.4
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/glebarez/sqlite v1.4.6
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/samber/lo v1.27.1
|
||||
github.com/seborama/govcr v4.5.0+incompatible
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
|
@ -55,6 +57,7 @@ require (
|
|||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
|
|
Loading…
Reference in New Issue