Skip to content

Commit

Permalink
Merge pull request #769 from tidepool-org/tk-ehr-disable-scheduled-re…
Browse files Browse the repository at this point in the history
…ports

[BACK-3177] Allow specifying custom cadence for EHR reports
  • Loading branch information
toddkazakov authored Dec 4, 2024
2 parents ee8f782 + 6b8d3ca commit c499583
Show file tree
Hide file tree
Showing 15 changed files with 482 additions and 126 deletions.
15 changes: 15 additions & 0 deletions clinics/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 41 additions & 6 deletions clinics/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package clinics

import (
"context"
"fmt"
"net/http"

"github.com/tidepool-org/platform/errors"
Expand All @@ -27,6 +26,7 @@ var ClientModule = fx.Provide(NewClient)

type Client interface {
GetClinician(ctx context.Context, clinicID, clinicianID string) (*clinic.Clinician, error)
GetEHRSettings(ctx context.Context, clinicId string) (*clinic.EHRSettings, error)
SharePatientAccount(ctx context.Context, clinicID, patientID string) (*clinic.Patient, error)
ListEHREnabledClinics(ctx context.Context) ([]clinic.Clinic, error)
SyncEHRData(ctx context.Context, clinicID string) error
Expand Down Expand Up @@ -84,7 +84,11 @@ func (d *defaultClient) GetClinician(ctx context.Context, clinicID, clinicianID
return nil, nil
}
if response.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.Preparedf(ErrorCodeClinicClientFailure,
"Unexpected status code from clinic service",
"unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.WithMeta(err, response.HTTPResponse)
return nil, err
}
return response.JSON200, nil
}
Expand All @@ -104,7 +108,11 @@ func (d *defaultClient) ListEHREnabledClinics(ctx context.Context) ([]clinic.Cli
return nil, err
}
if response.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.Preparedf(ErrorCodeClinicClientFailure,
"Unexpected status code from clinic service",
"unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.WithMeta(err, response.HTTPResponse)
return nil, err
}
if response.JSON200 == nil {
break
Expand All @@ -121,6 +129,21 @@ func (d *defaultClient) ListEHREnabledClinics(ctx context.Context) ([]clinic.Cli
return clinics, nil
}

func (d *defaultClient) GetEHRSettings(ctx context.Context, clinicId string) (*clinic.EHRSettings, error) {
response, err := d.httpClient.GetEHRSettingsWithResponse(ctx, clinicId)
if err != nil {
return nil, err
}
if response.StatusCode() != http.StatusOK || response.StatusCode() != http.StatusOK {
err = errors.Preparedf(ErrorCodeClinicClientFailure,
"Unexpected status code from clinic service",
"unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.WithMeta(err, response.HTTPResponse)
return nil, err
}
return response.JSON200, nil
}

func (d *defaultClient) SharePatientAccount(ctx context.Context, clinicID, patientID string) (*clinic.Patient, error) {
permission := make(map[string]interface{}, 0)
body := clinic.CreatePatientFromUserJSONRequestBody{
Expand All @@ -138,7 +161,11 @@ func (d *defaultClient) SharePatientAccount(ctx context.Context, clinicID, patie
return d.getPatient(ctx, clinicID, patientID)
}
if response.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.Preparedf(ErrorCodeClinicClientFailure,
"Unexpected status code from clinic service",
"unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.WithMeta(err, response.HTTPResponse)
return nil, err
}
return response.JSON200, nil
}
Expand All @@ -149,7 +176,11 @@ func (d *defaultClient) SyncEHRData(ctx context.Context, clinicID string) error
return err
}
if response.StatusCode() != http.StatusAccepted {
return fmt.Errorf("unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.Preparedf(ErrorCodeClinicClientFailure,
"Unexpected status code from clinic service",
"unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.WithMeta(err, response.HTTPResponse)
return err
}
return nil
}
Expand All @@ -160,7 +191,11 @@ func (d *defaultClient) getPatient(ctx context.Context, clinicID, patientID stri
return nil, err
}
if response.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.Preparedf(ErrorCodeClinicClientFailure,
"Unexpected status code from clinic service",
"unexpected response status code %v from %v", response.StatusCode(), response.HTTPResponse.Request.URL)
err = errors.WithMeta(err, response.HTTPResponse)
return nil, err
}
return response.JSON200, nil
}
Expand Down
25 changes: 25 additions & 0 deletions clinics/test/clinics.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,28 @@ func NewRandomClinic() api.Clinic {
Website: pointer.FromAny(faker.Internet().Url()),
}
}

func NewRandomEHRSettings() *api.EHRSettings {
return &api.EHRSettings{
DestinationIds: &api.EHRDestinationIds{
Flowsheet: faker.RandomString(16),
Notes: faker.RandomString(16),
Results: faker.RandomString(16),
},
Enabled: true,
MrnIdType: "MRN",
ProcedureCodes: api.EHRProcedureCodes{
CreateAccount: pointer.FromAny(faker.RandomString(5)),
CreateAccountAndEnableReports: pointer.FromAny(faker.RandomString(5)),
DisableSummaryReports: pointer.FromAny(faker.RandomString(5)),
EnableSummaryReports: pointer.FromAny(faker.RandomString(5)),
},
Provider: "redox",
ScheduledReports: api.ScheduledReports{
Cadence: api.N14d,
OnUploadEnabled: true,
OnUploadNoteEventType: pointer.FromAny(api.ScheduledReportsOnUploadNoteEventTypeNew),
},
SourceId: faker.RandomString(16),
}
}
115 changes: 115 additions & 0 deletions ehr/reconcile/planner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package reconcile

import (
"context"
"regexp"
"strconv"
"strings"
"time"

api "github.com/tidepool-org/clinic/client"

"github.com/tidepool-org/platform/errors"

"github.com/tidepool-org/platform/clinics"
"github.com/tidepool-org/platform/ehr/sync"
"github.com/tidepool-org/platform/log"
"github.com/tidepool-org/platform/task"
)

var (
cadenceRegexp = regexp.MustCompile("(\\d{1,3})d")
)

type Planner struct {
clinicsClient clinics.Client
logger log.Logger
}

func NewPlanner(clinicsClient clinics.Client, logger log.Logger) *Planner {
return &Planner{
clinicsClient: clinicsClient,
logger: logger,
}
}

func (p *Planner) GetReconciliationPlan(ctx context.Context, syncTasks map[string]task.Task) (*ReconciliationPlan, error) {
toCreate := make([]task.TaskCreate, 0)
toDelete := make([]task.Task, 0)
toUpdate := make(map[string]*task.TaskUpdate)

// Get the list of all EHR enabled clinics
clinicsList, err := p.clinicsClient.ListEHREnabledClinics(ctx)
if err != nil {
return nil, err
}

// At the end of the loop syncTasks will contain only the tasks that need to be deleted,
// and toCreate will contain tasks for new clinics that need to be synced.
for _, clinic := range clinicsList {
clinicId := *clinic.Id
settings, err := p.clinicsClient.GetEHRSettings(ctx, clinicId)
if err != nil {
return nil, err
} else if settings == nil || !settings.Enabled {
continue
}

// Use the default value for all clinics which don't have a cadence
cadenceFromSettings := sync.DefaultCadence
scheduledReportsDisabled := false

if settings.ScheduledReports.Cadence == api.DISABLED {
scheduledReportsDisabled = true
}

if !scheduledReportsDisabled {
cadence := string(settings.ScheduledReports.Cadence)
if !cadenceRegexp.MatchString(string(settings.ScheduledReports.Cadence)) {
err = errors.New("invalid scheduled report cadence format")
} else {
var days int
cadence = strings.TrimSuffix(cadence, "d")
days, err = strconv.Atoi(cadence)
cadenceFromSettings = time.Duration(days) * time.Hour * 24
if cadenceFromSettings == 0 {
scheduledReportsDisabled = true
}
}
if err != nil {
p.logger.WithField("clinicId", clinicId).WithError(err).Error("unable to parse scheduled report cadence")
continue
}
}

tsk, exists := syncTasks[clinicId]
if !exists && !scheduledReportsDisabled {
// Create the tasks for the clinic if reports are not disabled and we don't have a task already
create := sync.NewTaskCreate(clinicId, cadenceFromSettings)
toCreate = append(toCreate, *create)
} else if exists && scheduledReportsDisabled {
// Delete the task if it tasks exists but reports are now disabled
delete(syncTasks, clinicId)
toDelete = append(toDelete, tsk)
} else if exists && !scheduledReportsDisabled {
delete(syncTasks, clinicId)
// Update the task if the configured cadence has been changed
cadenceFromTask := sync.GetCadence(tsk.Data)
if cadenceFromTask == nil || *cadenceFromTask != cadenceFromSettings {
sync.SetCadence(tsk.Data, cadenceFromSettings)
update := task.NewTaskUpdate()
update.Data = &tsk.Data
toUpdate[tsk.ID] = update
}
}
}

for _, tsk := range syncTasks {
toDelete = append(toDelete, tsk)
}
return &ReconciliationPlan{
ToCreate: toCreate,
ToDelete: toDelete,
ToUpdate: toUpdate,
}, nil
}
Loading

0 comments on commit c499583

Please sign in to comment.