adding test framework/recordings for client.

Adding generic upsert fuction for models.
This commit is contained in:
Jason Kulatunga 2022-09-01 18:54:01 -07:00
parent caa7315ad9
commit 96465f23df
14 changed files with 1673 additions and 205 deletions

View File

@ -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)

View File

@ -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).
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
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: 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
}

View File

@ -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
}
}

View File

@ -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,122 +24,212 @@ 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 {
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
}
profiles = append(profiles, patientProfile)
}
return profiles, nil
}
func (c *FHIR401Client) ProcessOrganizations(orgs []fhir401.Organization) ([]models.Organization, error) {
apiOrganizations := []models.Organization{}
for _, org := range orgs {
apiOrganization := models.Organization{
OriginBase: models.OriginBase{
ModelBase: models.ModelBase{},
UserID: c.Source.UserID,
SourceID: c.Source.ID,
SourceResourceID: *org.Id,
SourceResourceType: fhir401.ResourceTypeOrganization.Code(),
},
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{},
}
if org.Meta != nil && org.Meta.LastUpdated != nil {
if parsed, err := time.Parse(time.RFC3339, *org.Meta.LastUpdated); err == nil {
apiOrganization.UpdatedAt = parsed
}
}
if org.Address != nil && len(org.Address) > 0 {
itemAddress := org.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
apiOrganizations = append(apiOrganizations, apiOrganization)
Name: models.Name{},
},
}
return apiOrganizations, nil
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
}

View File

@ -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)
}

View File

@ -24,3 +24,7 @@ type Client interface {
//Demographics()
//SocialHistory()
}
type ResourceInterface interface {
ResourceRef() (string, *string)
}

View File

@ -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
}

View 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"
}

View 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"
}
}

View File

@ -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
}

View File

@ -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)
}

File diff suppressed because one or more lines are too long

View File

@ -13,8 +13,8 @@ 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])
Orders []Order `json:"orders,omitempty" gorm:"serializer:json;default:'{}'"` // orders Array[Object] (optional) A list of medication orders for the patient (see orders 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".

5
go.mod
View File

@ -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