diff --git a/api/gen_server.go b/api/gen_server.go index 5e37acfc..ad0fee61 100644 --- a/api/gen_server.go +++ b/api/gen_server.go @@ -58,6 +58,9 @@ type ServerInterface interface { // Sync EHR Data // (POST /v1/clinics/{clinicId}/ehr/sync) SyncEHRData(ctx echo.Context, clinicId ClinicId) error + // Export Patient List CSV + // (GET /v1/clinics/{clinicId}/export/patients) + ExportPatientList(ctx echo.Context, clinicId ClinicId, params ExportPatientListParams) error // Delete Invited Clinician // (DELETE /v1/clinics/{clinicId}/invites/clinicians/{inviteId}/clinician) DeleteInvitedClinician(ctx echo.Context, clinicId ClinicId, inviteId InviteId) error @@ -651,6 +654,33 @@ func (w *ServerInterfaceWrapper) SyncEHRData(ctx echo.Context) error { return err } +// ExportPatientList converts echo context to params. +func (w *ServerInterfaceWrapper) ExportPatientList(ctx echo.Context) error { + var err error + // ------------- Path parameter "clinicId" ------------- + var clinicId ClinicId + + err = runtime.BindStyledParameterWithOptions("simple", "clinicId", ctx.Param("clinicId"), &clinicId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter clinicId: %s", err)) + } + + ctx.Set(SessionTokenScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params ExportPatientListParams + // ------------- Required query parameter "period" ------------- + + err = runtime.BindQueryParameter("form", true, true, "period", ctx.QueryParams(), ¶ms.Period) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter period: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ExportPatientList(ctx, clinicId, params) + return err +} + // DeleteInvitedClinician converts echo context to params. func (w *ServerInterfaceWrapper) DeleteInvitedClinician(ctx echo.Context) error { var err error @@ -2951,6 +2981,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/v1/clinics/:clinicId/clinicians/:clinicianId", wrapper.GetClinician) router.PUT(baseURL+"/v1/clinics/:clinicId/clinicians/:clinicianId", wrapper.UpdateClinician) router.POST(baseURL+"/v1/clinics/:clinicId/ehr/sync", wrapper.SyncEHRData) + router.GET(baseURL+"/v1/clinics/:clinicId/export/patients", wrapper.ExportPatientList) router.DELETE(baseURL+"/v1/clinics/:clinicId/invites/clinicians/:inviteId/clinician", wrapper.DeleteInvitedClinician) router.GET(baseURL+"/v1/clinics/:clinicId/invites/clinicians/:inviteId/clinician", wrapper.GetInvitedClinician) router.PATCH(baseURL+"/v1/clinics/:clinicId/invites/clinicians/:inviteId/clinician", wrapper.AssociateClinicianToUser) diff --git a/api/gen_spec.go b/api/gen_spec.go index 73654c0e..1c8d2a39 100644 --- a/api/gen_spec.go +++ b/api/gen_spec.go @@ -18,350 +18,351 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "+wEA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/gen_types.go b/api/gen_types.go index 4b8cac2c..537765f6 100644 --- a/api/gen_types.go +++ b/api/gen_types.go @@ -2093,6 +2093,12 @@ type ListCliniciansParams struct { Role *Role `form:"role,omitempty" json:"role,omitempty"` } +// ExportPatientListParams defines parameters for ExportPatientList. +type ExportPatientListParams struct { + // Period Time Period to display + Period string `form:"period" json:"period"` +} + // ListPatientsParams defines parameters for ListPatients. type ListPatientsParams struct { // Search Full text search query diff --git a/api/patients.go b/api/patients.go index 9c492fb2..3b8751c3 100644 --- a/api/patients.go +++ b/api/patients.go @@ -15,6 +15,7 @@ import ( "github.com/tidepool-org/clinic/auth" "github.com/tidepool-org/clinic/clinics" + "github.com/tidepool-org/clinic/export" "github.com/tidepool-org/clinic/patients" "github.com/tidepool-org/clinic/store" ) @@ -527,3 +528,34 @@ func (h *Handler) ConnectProvider(ec echo.Context, clinicId ClinicId, patientId return ec.NoContent(http.StatusNoContent) } + +func (h *Handler) ExportPatientList(ec echo.Context, clinicId ClinicId, params ExportPatientListParams) error { + ctx := ec.Request().Context() + authData := auth.GetAuthData(ctx) + if authData == nil || authData.SubjectId == "" { + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: "expected authenticated user id", + } + } + if authData.ServerAccess { + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: "expected user access token", + } + } + filter := patients.ExportParams{ + Period: params.Period, + ExporterClinicianID: authData.SubjectId, + WorkspaceID: clinicId, + ReportDate: time.Now(), + } + exporter, err := export.NewPatientExport(ctx, h.Clinics, h.Clinicians, h.Patients, filter) + if err != nil { + return err + } + disposition := fmt.Sprintf("attachment; filename=patients-%d.csv", time.Now().Unix()) + ec.Response().Header().Set(echo.HeaderContentDisposition, disposition) + ec.Response().Header().Set(echo.HeaderContentType, "text/csv") + return exporter.Write(ctx, ec.Response()) +} diff --git a/auth/authorizer.go b/auth/authorizer.go index 67fe75f2..b907665b 100644 --- a/auth/authorizer.go +++ b/auth/authorizer.go @@ -10,6 +10,7 @@ import ( "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/rego" "github.com/tidepool-org/clinic/clinicians" + "github.com/tidepool-org/clinic/clinics" internalErrs "github.com/tidepool-org/clinic/errors" "go.uber.org/zap" @@ -36,7 +37,7 @@ type RequestAuthorizer interface { EvaluatePolicy(context.Context, map[string]interface{}) error } -func NewRequestAuthorizer(clinicians clinicians.Service, logger *zap.SugaredLogger) (RequestAuthorizer, error) { +func NewRequestAuthorizer(clinicians clinicians.Service, clinics clinics.Service, logger *zap.SugaredLogger) (RequestAuthorizer, error) { compiler, err := ast.CompileModules(map[string]string{ "policy.rego": authzPolicy, }) @@ -46,6 +47,7 @@ func NewRequestAuthorizer(clinicians clinicians.Service, logger *zap.SugaredLogg return &embeddedOpaAuthorizer{ clinicians: clinicians, + clinics: clinics, logger: logger, policy: compiler, }, nil @@ -53,6 +55,7 @@ func NewRequestAuthorizer(clinicians clinicians.Service, logger *zap.SugaredLogg type embeddedOpaAuthorizer struct { clinicians clinicians.Service + clinics clinics.Service logger *zap.SugaredLogger policy *ast.Compiler } @@ -75,6 +78,18 @@ func (e *embeddedOpaAuthorizer) Authorize(ctx context.Context, input *openapi3fi in["clinician"] = clinicianStruct.Map() } + if input.RequestValidationInput.PathParams[clinicIdPathParameter] != "" && strings.HasSuffix(input.RequestValidationInput.Route.Path, "/export/patients") { + clinic, err := e.getClinicRecord(ctx, input) + if err != nil { + return err + } + if clinic != nil { + clinicStruct := structs.New(*clinic) + clinicStruct.TagName = "bson" + in["clinic"] = clinicStruct.Map() + } + } + return e.EvaluatePolicy(ctx, in) } @@ -134,3 +149,16 @@ func (e *embeddedOpaAuthorizer) getClinicianRecord(ctx context.Context, input *o return clinician, nil } + +func (e *embeddedOpaAuthorizer) getClinicRecord(ctx context.Context, input *openapi3filter.AuthenticationInput) (*clinics.Clinic, error) { + clinicId := input.RequestValidationInput.PathParams[clinicIdPathParameter] + if clinicId == "" { + return nil, nil + } + clinic, err := e.clinics.Get(ctx, clinicId) + if err != nil && !errors.Is(err, clinics.ErrNotFound) { + return nil, err + } + + return clinic, nil +} diff --git a/auth/authorizer_test.go b/auth/authorizer_test.go index fa09d2c2..9381c64b 100644 --- a/auth/authorizer_test.go +++ b/auth/authorizer_test.go @@ -17,12 +17,20 @@ var clinicMember = map[string]interface{}{ "roles": []string{"CLINIC_MEMBER"}, } +var freeTierClinic = map[string]interface{}{ + "tier": "tier0100", +} + +var standardTierClinic = map[string]interface{}{ + "tier": "tier0200", +} + var _ = Describe("Request Authorizer", func() { var authorizer auth.RequestAuthorizer BeforeEach(func() { var err error - authorizer, err = auth.NewRequestAuthorizer(nil, zap.NewNop().Sugar()) + authorizer, err = auth.NewRequestAuthorizer(nil, nil, zap.NewNop().Sugar()) Expect(err).ToNot(HaveOccurred()) }) @@ -184,6 +192,65 @@ var _ = Describe("Request Authorizer", func() { Expect(err).ToNot(HaveOccurred()) }) + It("allow clinic admins of supported tiers to export a patient list", func() { + input := map[string]interface{}{ + "path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"}, + "method": "GET", + "auth": map[string]interface{}{ + "subjectId": "999999999", + "serverAccess": false, + }, + "clinician": clinicAdmin, + "clinic": standardTierClinic, + } + err := authorizer.EvaluatePolicy(context.Background(), input) + Expect(err).ToNot(HaveOccurred()) + }) + + It("allow clinic members of supported tiers to export a patient list", func() { + input := map[string]interface{}{ + "path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"}, + "method": "GET", + "auth": map[string]interface{}{ + "subjectId": "999999999", + "serverAccess": false, + }, + "clinician": clinicMember, + "clinic": standardTierClinic, + } + err := authorizer.EvaluatePolicy(context.Background(), input) + Expect(err).ToNot(HaveOccurred()) + }) + + It("prevents clinicians in unsupported tiers from generating a patient list export", func() { + input := map[string]interface{}{ + "path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"}, + "method": "GET", + "auth": map[string]interface{}{ + "subjectId": "999999999", + "serverAccess": false, + }, + "clinician": clinicMember, + "clinic": freeTierClinic, + } + err := authorizer.EvaluatePolicy(context.Background(), input) + Expect(err).To(Equal(auth.ErrUnauthorized)) + }) + + It("prevents users from generating a patient list export", func() { + input := map[string]interface{}{ + "path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "export", "patients"}, + "method": "GET", + "auth": map[string]interface{}{ + "subjectId": "999999999", + "serverAccess": false, + }, + "clinic": standardTierClinic, + } + err := authorizer.EvaluatePolicy(context.Background(), input) + Expect(err).To(Equal(auth.ErrUnauthorized)) + }) + It("prevents users to migrate patients to a clinic", func() { input := map[string]interface{}{ "path": []string{"v1", "clinics", "6066fbabc6f484277200ac64", "patients", "12345"}, diff --git a/auth/policy.rego b/auth/policy.rego index 79a138ed..85c0bdc7 100644 --- a/auth/policy.rego +++ b/auth/policy.rego @@ -17,6 +17,13 @@ write_access_roles := { "CLINIC_ADMIN" } +patient_export_supported_tiers := { + "tier0200", + "tier0300", + "tier0400", +} +clinic_tier := { input.clinic.tier } + # convert clinician roles to set clinician_roles := { x | x = input.clinician.roles[_] } @@ -28,6 +35,10 @@ clinician_has_write_access { count(clinician_roles & write_access_roles) > 0 } +clinic_tier_supports_patient_export { + count(clinic_tier & patient_export_supported_tiers) > 0 +} + default allow = false # Allow backend services to list all clinics @@ -426,6 +437,15 @@ allow { input.path = ["v1", "clinics", _, "patients", _] } +# Allow currently authenticated clinician to get patient list export +# GET /v1/clinics/:clinicId/export/patients +allow { + input.method == "GET" + input.path = ["v1", "clinics", _, "export", "patients"] + clinician_has_read_access + clinic_tier_supports_patient_export +} + # Allow backend services to create a patient from existing user # POST /v1/clinics/:clinicId/patients/:patientId allow { diff --git a/client/client.go b/client/client.go index 4d82a407..9769e5c6 100644 --- a/client/client.go +++ b/client/client.go @@ -143,6 +143,9 @@ type ClientInterface interface { // SyncEHRData request SyncEHRData(ctx context.Context, clinicId ClinicId, reqEditors ...RequestEditorFn) (*http.Response, error) + // ExportPatientList request + ExportPatientList(ctx context.Context, clinicId ClinicId, params *ExportPatientListParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // DeleteInvitedClinician request DeleteInvitedClinician(ctx context.Context, clinicId ClinicId, inviteId InviteId, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -616,6 +619,18 @@ func (c *Client) SyncEHRData(ctx context.Context, clinicId ClinicId, reqEditors return c.Client.Do(req) } +func (c *Client) ExportPatientList(ctx context.Context, clinicId ClinicId, params *ExportPatientListParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExportPatientListRequest(c.Server, clinicId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) DeleteInvitedClinician(ctx context.Context, clinicId ClinicId, inviteId InviteId, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewDeleteInvitedClinicianRequest(c.Server, clinicId, inviteId) if err != nil { @@ -2566,6 +2581,58 @@ func NewSyncEHRDataRequest(server string, clinicId ClinicId) (*http.Request, err return req, nil } +// NewExportPatientListRequest generates requests for ExportPatientList +func NewExportPatientListRequest(server string, clinicId ClinicId, params *ExportPatientListParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "clinicId", runtime.ParamLocationPath, clinicId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/clinics/%s/export/patients", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "period", runtime.ParamLocationQuery, params.Period); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewDeleteInvitedClinicianRequest generates requests for DeleteInvitedClinician func NewDeleteInvitedClinicianRequest(server string, clinicId ClinicId, inviteId InviteId) (*http.Request, error) { var err error @@ -7529,6 +7596,9 @@ type ClientWithResponsesInterface interface { // SyncEHRDataWithResponse request SyncEHRDataWithResponse(ctx context.Context, clinicId ClinicId, reqEditors ...RequestEditorFn) (*SyncEHRDataResponse, error) + // ExportPatientListWithResponse request + ExportPatientListWithResponse(ctx context.Context, clinicId ClinicId, params *ExportPatientListParams, reqEditors ...RequestEditorFn) (*ExportPatientListResponse, error) + // DeleteInvitedClinicianWithResponse request DeleteInvitedClinicianWithResponse(ctx context.Context, clinicId ClinicId, inviteId InviteId, reqEditors ...RequestEditorFn) (*DeleteInvitedClinicianResponse, error) @@ -8103,6 +8173,27 @@ func (r SyncEHRDataResponse) StatusCode() int { return 0 } +type ExportPatientListResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r ExportPatientListResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ExportPatientListResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type DeleteInvitedClinicianResponse struct { Body []byte HTTPResponse *http.Response @@ -9603,6 +9694,15 @@ func (c *ClientWithResponses) SyncEHRDataWithResponse(ctx context.Context, clini return ParseSyncEHRDataResponse(rsp) } +// ExportPatientListWithResponse request returning *ExportPatientListResponse +func (c *ClientWithResponses) ExportPatientListWithResponse(ctx context.Context, clinicId ClinicId, params *ExportPatientListParams, reqEditors ...RequestEditorFn) (*ExportPatientListResponse, error) { + rsp, err := c.ExportPatientList(ctx, clinicId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseExportPatientListResponse(rsp) +} + // DeleteInvitedClinicianWithResponse request returning *DeleteInvitedClinicianResponse func (c *ClientWithResponses) DeleteInvitedClinicianWithResponse(ctx context.Context, clinicId ClinicId, inviteId InviteId, reqEditors ...RequestEditorFn) (*DeleteInvitedClinicianResponse, error) { rsp, err := c.DeleteInvitedClinician(ctx, clinicId, inviteId, reqEditors...) @@ -10761,6 +10861,22 @@ func ParseSyncEHRDataResponse(rsp *http.Response) (*SyncEHRDataResponse, error) return response, nil } +// ParseExportPatientListResponse parses an HTTP response from a ExportPatientListWithResponse call +func ParseExportPatientListResponse(rsp *http.Response) (*ExportPatientListResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ExportPatientListResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + // ParseDeleteInvitedClinicianResponse parses an HTTP response from a DeleteInvitedClinicianWithResponse call func ParseDeleteInvitedClinicianResponse(rsp *http.Response) (*DeleteInvitedClinicianResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/client/mock_client.go b/client/mock_client.go index 63408a52..51fd0c1c 100644 --- a/client/mock_client.go +++ b/client/mock_client.go @@ -741,6 +741,26 @@ func (mr *MockClientInterfaceMockRecorder) EnableNewClinicExperience(ctx, userId return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableNewClinicExperience", reflect.TypeOf((*MockClientInterface)(nil).EnableNewClinicExperience), varargs...) } +// ExportPatientList mocks base method. +func (m *MockClientInterface) ExportPatientList(ctx context.Context, clinicId ClinicId, params *ExportPatientListParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, clinicId, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExportPatientList", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportPatientList indicates an expected call of ExportPatientList. +func (mr *MockClientInterfaceMockRecorder) ExportPatientList(ctx, clinicId, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, clinicId, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPatientList", reflect.TypeOf((*MockClientInterface)(nil).ExportPatientList), varargs...) +} + // FindPatients mocks base method. func (m *MockClientInterface) FindPatients(ctx context.Context, params *FindPatientsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { m.ctrl.T.Helper() @@ -2965,6 +2985,26 @@ func (mr *MockClientWithResponsesInterfaceMockRecorder) EnableNewClinicExperienc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableNewClinicExperienceWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).EnableNewClinicExperienceWithResponse), varargs...) } +// ExportPatientListWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) ExportPatientListWithResponse(ctx context.Context, clinicId ClinicId, params *ExportPatientListParams, reqEditors ...RequestEditorFn) (*ExportPatientListResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, clinicId, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExportPatientListWithResponse", varargs...) + ret0, _ := ret[0].(*ExportPatientListResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportPatientListWithResponse indicates an expected call of ExportPatientListWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) ExportPatientListWithResponse(ctx, clinicId, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, clinicId, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPatientListWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).ExportPatientListWithResponse), varargs...) +} + // FindPatientsWithResponse mocks base method. func (m *MockClientWithResponsesInterface) FindPatientsWithResponse(ctx context.Context, params *FindPatientsParams, reqEditors ...RequestEditorFn) (*FindPatientsResponse, error) { m.ctrl.T.Helper() diff --git a/client/types.go b/client/types.go index 7c114f77..856b7963 100644 --- a/client/types.go +++ b/client/types.go @@ -2093,6 +2093,12 @@ type ListCliniciansParams struct { Role *Role `form:"role,omitempty" json:"role,omitempty"` } +// ExportPatientListParams defines parameters for ExportPatientList. +type ExportPatientListParams struct { + // Period Time Period to display + Period string `form:"period" json:"period"` +} + // ListPatientsParams defines parameters for ListPatients. type ListPatientsParams struct { // Search Full text search query diff --git a/export/export.go b/export/export.go new file mode 100644 index 00000000..96e745ab --- /dev/null +++ b/export/export.go @@ -0,0 +1,332 @@ +package export + +import ( + "context" + "encoding/csv" + "fmt" + "io" + "math" + "strings" + "time" + + "github.com/tidepool-org/clinic/clinicians" + "github.com/tidepool-org/clinic/clinics" + "github.com/tidepool-org/clinic/patients" + "github.com/tidepool-org/clinic/store" +) + +const ( + timeFormat = "2006-01-02 03:04 PM" +) + +type exporter struct { + patientSvc patients.Service + + clinic *clinics.Clinic + tagNamesById map[string]string + clinicianNamesByUserId map[string]string + exportingClinician *clinicians.Clinician + params patients.ExportParams + days int +} + +func (e *exporter) ToCSVRow(p *patients.ExportedPatient) []string { + return []string{ + pstr(p.FullName), + pstr(p.UserId), + pstr(p.MRN), + fmtBirthdate(p.BirthDate), + pstr(p.Email), + fmtBool(p.Permissions.IsClaimed(), "Claimed", "Unclaimed"), + ptime(p.CreatedTime, "2006-01-02"), + e.clinicianNamesByUserId[pstr(p.InvitedBy)], + strings.Join(p.ClinicSiteNames, ","), + fmtPatientTags(p.TagIds, e.tagNamesById), + fmtGlycemicRanges(p.GlycemicRanges), + pstrd(p.DiagnosisType, "Other"), + fmtDataSourceStatus(p.DexcomDataSource, e.params.ReportDate), + fmtDataSourceLastDataDate(p.DexcomDataSource), + fmtDataSourceStatus(p.AbbottDataSource, e.params.ReportDate), + fmtDataSourceLastDataDate(p.AbbottDataSource), + fmtDataSourceStatus(p.TwiistDataSource, e.params.ReportDate), + fmtDataSourceLastDataDate(p.TwiistDataSource), + ptime(p.CgmLastDataDate, "2006-01-02"), + ppct(p.CgmActiveWearTime, 0), + pint(p.CgmDaysWithData), + pint(p.CgmHoursWithData), + ptomgdl(p.CgmAverageGlucose), + pfloat(p.CgmGmi, 2), + fmtPreferredUnits(p.CgmStdDev, e.clinic.PreferredBgUnits, 1), + ppct(p.CgmCV, 0), + ppct(p.CgmTimeInLevel2Hypo, 0), + ppct(p.CgmTimeInLevel1Hypo, 0), + ppct(p.CgmTimeInTarget, 0), + ppct(p.CgmTimeInLevel1Hyper, 0), + ppct(p.CgmTimeInLevel2Hyper, 0), + ptime(p.BgmLastDataDate, "2006-01-02"), + ptomgdl(p.BgmAverageGlucose), + pfloat(p.BgmReadingsPerDay, 0), + pint(p.BgmTotalReadings), + pint(p.BgmLowEvents), + pint(p.BgmHighEvents), + } +} + +func NewPatientExport(ctx context.Context, clinicSvc clinics.Service, clinicianSvc clinicians.Service, patientSvc patients.Service, params patients.ExportParams) (*exporter, error) { + clinic, err := clinicSvc.Get(ctx, params.WorkspaceID) + if err != nil { + return nil, err + } + + pagination := store.DefaultPagination().WithLimit(1000) + filter := &clinicians.Filter{ + ClinicId: ¶ms.WorkspaceID, + } + cs, err := clinicianSvc.List(ctx, filter, pagination) + if err != nil { + return nil, err + } + return NewPatientExportClinic(clinic, cs, patientSvc, params) +} + +func NewPatientExportClinic(clinic *clinics.Clinic, cs []*clinicians.Clinician, patientSvc patients.Service, params patients.ExportParams) (*exporter, error) { + days, err := periodToDays(params.Period) + if err != nil { + return nil, err + } + + tagNamesById := map[string]string{} + for _, patientTag := range clinic.PatientTags { + tagNamesById[patientTag.Id.Hex()] = patientTag.Name + } + + clinicianNamesByUserId := map[string]string{} + var exportingClinician *clinicians.Clinician + for _, clinician := range cs { + if clinician.UserId != nil && clinician.Name != nil { + clinicianNamesByUserId[*clinician.UserId] = pstr(clinician.Name) + } + if clinician.UserId != nil && pstr(clinician.UserId) == params.ExporterClinicianID { + exportingClinician = clinician + } + } + if exportingClinician == nil { + return nil, fmt.Errorf(`no clinician "%v" found.`, params.ExporterClinicianID) + } + return &exporter{ + patientSvc: patientSvc, + clinic: clinic, + exportingClinician: exportingClinician, + tagNamesById: tagNamesById, + clinicianNamesByUserId: clinicianNamesByUserId, + params: params, + days: days, + }, nil +} + +func (e *exporter) Write(ctx context.Context, w io.Writer) error { + ps, err := e.patientSvc.ListExportedPatients(ctx, e.params) + if err != nil { + return err + } + writer := csv.NewWriter(w) + metadataTitles := []string{ + "Report Date Time", + "Exported By", + "Exported By", + "Clinic Name", + "Workspace ID", + "Days of CGM Data Summarized", + "Total Patients", + } + metadata := []string{ + fmtClinicTime(e.params.ReportDate, e.clinic), + pstr(e.exportingClinician.Name), + pstr(e.exportingClinician.Email), + pstr(e.clinic.Name), + e.clinic.Id.Hex(), + fmtDays(e.days), + fmt.Sprintf("%v", len(ps)), + } + + if err := writer.Write(metadataTitles); err != nil { + return err + } + if err := writer.Write(metadata); err != nil { + return err + } + + header := []string{ + "Patient Name", + "Patient User ID", + "MRN", + "Date of Birth", + "Patient Email", + "Custodial Status", + "Account Created Date", + "Clinician Who Created Account", + "Clinic Sites", + "Patient Tags", + "Assigned Glycemic Range", + "Diabetes Type", + "Dexcom Status", + "Dexcom Last Modified Date", + "Abbott Status", + "Abbott Last Modified Date", + "Twiist Status", + "Twiist Last Modified Date", + "CGM Last Data Date", + "CGM Active Wear Time (%)", + "CGM Days with Data", + "CGM Hours with Data", + "CGM Average Glucose (mg/dL)", + "CGM GMI (%)", + fmt.Sprintf("CGM Standard Deviation (%s)", e.clinic.PreferredBgUnits), + "CGM Coefficient of Variation (%)", + "CGM Time in Level 2 Hypoglycemia (%)", + "CGM Time in Level 1 Hypoglycemia (%)", + "CGM Time in Target (%)", + "CGM Time in Level 1 Hyperglycemia (%)", + "CGM Time in Level 2 Hyperglycemia (%)", + "BGM Last Data Date", + "BGM Average Glucose", + "BGM Readings/Day", + "BGM Total Readings", + "BGM Low Events", + "BGM High Events", + } + if err := writer.Write(header); err != nil { + return err + } + for _, patient := range ps { + row := e.ToCSVRow(&patient) + if err := writer.Write(row); err != nil { + return err + } + } + writer.Flush() + if err := writer.Error(); err != nil { + return err + } + return nil +} + +func fmtDays(days int) string { + if days == 1 { + return "24 hours" + } + return fmt.Sprintf("%d days", days) +} + +func fmtDataSourceLastDataDate(ds *patients.DataSource) string { + if ds == nil || ds.LatestDataTime == nil { + return "" + } + return ds.LatestDataTime.Format(time.DateOnly) +} + +func fmtDataSourceStatus(ds *patients.DataSource, now time.Time) string { + if ds == nil || ds.State == "" { + return "NA" + } + inactiveCutoff := now.Add(-time.Hour * 24 * 2) + expiredCutoff := now + if (ds.State == patients.DataSourceStatePending || ds.State == patients.DataSourceStatePendingReconnect) && ds.ExpirationTime != nil && ds.ExpirationTime.Before(expiredCutoff) { + return "expired" + } + if ds.State == "connected" && ds.LatestDataTime != nil && ds.LatestDataTime.Before(inactiveCutoff) { + return "inactive" + } + return ds.State +} + +func fmtClinicTime(t time.Time, clinic *clinics.Clinic) string { + if clinic.Timezone == nil || *clinic.Timezone == "" { + return t.Format(timeFormat) + } + loc, err := time.LoadLocation(*clinic.Timezone) + if err != nil { + return t.Format(timeFormat) + } + return t.In(loc).Format(timeFormat) +} + +func fmtClinicDate(t time.Time, clinic *clinics.Clinic) string { + if clinic.Timezone == nil || *clinic.Timezone == "" { + return t.Format(time.DateOnly) + } + loc, err := time.LoadLocation(*clinic.Timezone) + if err != nil { + return t.Format(time.DateOnly) + } + return t.In(loc).Format(time.DateOnly) +} + +func fmtBool(b bool, valIfTrue string, valIfFalse string) string { + if b { + return valIfTrue + } + return valIfFalse +} + +func fmtGlycemicRanges(gr *patients.GlycemicRanges) string { + if gr == nil { + return "" + } + if gr.Type == patients.GlycemicRangeTypePreset { + return fmt.Sprintf("%v", gr.Preset) + } + return gr.Custom.Name +} + +func fmtPatientTags(tagIds *[]string, tagNamesById map[string]string) string { + if tagIds == nil || len(*tagIds) == 0 { + return "" + } + tagNames := make([]string, len(*tagIds)) + for i, tagId := range *tagIds { + if name, ok := tagNamesById[tagId]; ok { + tagNames[i] = name + } + } + return strings.Join(tagNames, ",") +} + +func fmtBirthdate(birthDate *string) string { + if birthDate == nil || *birthDate == "" { + return "" + } + t, err := time.Parse(time.DateOnly, *birthDate) + if err != nil || t.IsZero() { + return "" + } + return t.Format(time.DateOnly) +} + +func periodToDays(period string) (days int, err error) { + days, ok := map[string]int{ + "1d": 1, + "7d": 7, + "14d": 14, + "30d": 30, + }[period] + if !ok { + return 0, fmt.Errorf(`no days for given period, "%v"`, period) + } + return days, nil +} + +func fmtFloat(f float64, precision int) string { + shift := math.Pow(10, float64(precision)) + return fmt.Sprintf("%v", math.RoundToEven(f*shift*100)/shift) +} + +func fmtPreferredUnits(valMmolL *float64, preferredBgUnits string, precision int) string { + if valMmolL == nil { + return "" + } + if strings.ToLower(preferredBgUnits) == "mg/dl" { + return fmtFloat(toMgDl(*valMmolL), precision) + } + return fmtFloat(*valMmolL, precision) +} diff --git a/export/export_suite_test.go b/export/export_suite_test.go new file mode 100644 index 00000000..20a082f5 --- /dev/null +++ b/export/export_suite_test.go @@ -0,0 +1,10 @@ +package export_test + +import ( + "github.com/tidepool-org/clinic/test" + "testing" +) + +func TestSuite(t *testing.T) { + test.Test(t) +} diff --git a/export/export_test.go b/export/export_test.go new file mode 100644 index 00000000..cc6a842d --- /dev/null +++ b/export/export_test.go @@ -0,0 +1,160 @@ +package export_test + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/tidepool-org/clinic/clinicians" + "github.com/tidepool-org/clinic/clinics" + "github.com/tidepool-org/clinic/export" + "github.com/tidepool-org/clinic/patients" +) + +var _ = Describe("Export", func() { + Describe("ToCSVRow", func() { + It("matches expected columns", func() { + clinicianID := "a578d15f-73b6-4e25-9295-95707fca3520" + patientTagID := "6a5794ac4762b6efdb48aaaa" + patientTagOID, _ := primitive.ObjectIDFromHex(patientTagID) + clinicID := "6a5794ac4762b6efdb48aaab" + clinicOID, _ := primitive.ObjectIDFromHex(clinicID) + patientID := "7ce10e4c-8930-4770-8323-c8305db09c61" + reportDate := time.Date(2026, time.July, 13, 19, 0, 0, 0, time.UTC) + + inactiveDataSource := patients.DataSource{ + ProviderName: "dexcom", + State: "connected", + LatestDataTime: timep(time.Date(2026, time.July, 9, 0, 0, 0, 0, time.UTC)), + } + expiredDataSource := patients.DataSource{ + ExpirationTime: timep(time.Date(2025, time.January, 2, 3, 0, 0, 0, time.UTC)), + ProviderName: "abbott", + State: "pending", + } + connectedDataSource := patients.DataSource{ + ProviderName: "twiist", + State: "connected", + LatestDataTime: timep(time.Date(2026, time.July, 12, 20, 0, 0, 0, time.UTC)), + } + patientTags := []clinics.PatientTag{ + { + Id: &patientTagOID, + Name: "Some Tag", + Patients: 1, + }, + } + cs := []*clinicians.Clinician{ + { + UserId: &clinicianID, + Name: strp("Some Clinician"), + }, + } + clinic := &clinics.Clinic{ + Id: &clinicOID, + PatientTags: patientTags, + } + params := patients.ExportParams{ + Period: "1d", + ExporterClinicianID: clinicianID, + WorkspaceID: clinicID, + ReportDate: reportDate, + } + patient := &patients.ExportedPatient{ + FullName: strp("Some Patient"), + UserId: &patientID, + MRN: strp("123456789"), + InvitedBy: &clinicianID, + BirthDate: strp("2000-01-02"), + Email: strp("patient@tidepool.org"), + CreatedTime: timep(time.Date(2003, time.April, 5, 12, 0, 0, 0, time.UTC)), + Permissions: &patients.Permissions{}, + DiagnosisType: strp("type1"), + DexcomDataSource: &inactiveDataSource, + AbbottDataSource: &expiredDataSource, + TwiistDataSource: &connectedDataSource, + ClinicSiteNames: []string{"Site1", "Site2"}, + TagIds: &[]string{ + patientTagID, + "nameless-tag-id", + }, + CgmLastDataDate: timep(time.Date(2026, time.July, 13, 12, 0, 0, 0, time.UTC)), + CgmActiveWearTime: floatp(0.71428), + CgmDaysWithData: intp(13), + CgmHoursWithData: intp(238), + CgmAverageGlucose: floatp(5.131), + CgmTimeInLevel2Hypo: floatp(0.02134), + CgmTimeInLevel1Hypo: floatp(0.06013), + CgmTimeInTarget: floatp(0.82333), + BgmLastDataDate: timep(time.Date(2026, time.July, 10, 12, 0, 0, 0, time.UTC)), + BgmAverageGlucose: floatp(6.231), + BgmTotalReadings: intp(5), + BgmReadingsPerDay: floatp(2.5), + BgmLowEvents: intp(3), + BgmHighEvents: intp(1), + } + + e, err := export.NewPatientExportClinic(clinic, cs, nil, params) + Expect(err).ToNot(HaveOccurred()) + row := e.ToCSVRow(patient) + expectedRow := []string{ + "Some Patient", + patientID, + "123456789", + "2000-01-02", + "patient@tidepool.org", + "Claimed", + "2003-04-05", + "Some Clinician", + "Site1,Site2", + `Some Tag,`, + "", + "type1", + "inactive", + "2026-07-09", + "expired", + "", + "connected", + "2026-07-12", + "2026-07-13", + "71", + "13", + "238", + "92", + "", + "", + "", + "2", + "6", + "82", + "", + "", + "2026-07-10", + "112", + "2", + "5", + "3", + "1", + } + Expect(row).To(Equal(expectedRow)) + }) + }) +}) + +func strp(s string) *string { + return &s +} + +func floatp(f float64) *float64 { + return &f +} + +func timep(t time.Time) *time.Time { + return &t +} + +func intp(i int) *int { + return &i +} diff --git a/export/pointer.go b/export/pointer.go new file mode 100644 index 00000000..2627f378 --- /dev/null +++ b/export/pointer.go @@ -0,0 +1,80 @@ +package export + +import ( + "fmt" + "math" + "time" +) + +const ( + MmolLToMgdLConversionFactor float64 = 18.01559 + MmolLToMgdLPrecisionFactor float64 = 100000.0 +) + +func pfloat(f *float64, precision int) string { + if f == nil { + return "" + } + shift := math.Pow(10, float64(precision)) + return fmt.Sprintf("%v", math.RoundToEven(*f*shift)/shift) +} + +func pint(i *int) string { + if i == nil { + return "" + } + return fmt.Sprintf("%v", *i) +} + +func ptomgdl(valMmolL *float64) string { + if valMmolL == nil || *valMmolL < math.SmallestNonzeroFloat64 { + return "" + } + val := toMgDl(*valMmolL) + return pfloat(&val, 0) +} + +func ppct(f *float64, precision int) string { + if f == nil { + return "" + } + shift := math.Pow(10, float64(precision)) + return fmt.Sprintf("%v", math.RoundToEven(*f*shift*100)/shift) +} + +func ptime(t *time.Time, layout string) string { + if t == nil || t.IsZero() { + return "" + } + return t.Format(layout) +} +func ptimed(t *time.Time, layout, defaultVal string) string { + if t == nil || t.IsZero() { + return defaultVal + } + return t.Format(layout) +} + +func pstr(p *string) string { + if p == nil { + return "" + } + + return *p +} +func pstrd(p *string, defaultVal string) string { + if p == nil || *p == "" { + return defaultVal + } + + return *p +} + +func strp(s string) *string { + return &s +} + +func toMgDl(valMmolL float64) float64 { + intValue := int(valMmolL*MmolLToMgdLConversionFactor*MmolLToMgdLPrecisionFactor + 0.5) + return float64(intValue) / MmolLToMgdLPrecisionFactor +} diff --git a/patients/export.go b/patients/export.go new file mode 100644 index 00000000..3f98f772 --- /dev/null +++ b/patients/export.go @@ -0,0 +1,57 @@ +package patients + +import ( + "time" +) + +// ExportedPatient is an intermediate representation of the fields needed by a +// patient export list. It has some looked up fields where simpler / more +// convenient to do it in mongo and other that can be calculated from its +// fields where simpler to do in application code. +type ExportedPatient struct { + FullName *string `bson:"fullName,omitempty"` + UserId *string `bson:"userId,omitempty"` + MRN *string `bson:"mrn,omitempty"` + BirthDate *string `bson:"birthDate,omitempty"` + Email *string `bson:"email,omitempty"` + Permissions *Permissions `bson:"permissions,omitempty"` + CreatedTime *time.Time `bson:"createdTime,omitempty"` + InvitedBy *string `bson:"invitedBy,omitempty"` + + ClinicSiteNames []string `bson:"clinicSiteNames,omitempty"` + TagIds *[]string `bson:"tagIds,omitempty"` + GlycemicRanges *GlycemicRanges `bson:"glycemicRanges,omitempty"` + DiagnosisType *string `bson:"diagnosisType,omitempty"` + + DexcomDataSource *DataSource `bson:"dexcomDataSource,omitempty"` + AbbottDataSource *DataSource `bson:"abbottDataSource,omitempty"` + TwiistDataSource *DataSource `bson:"twiistDataSource,omitempty"` + + CgmLastDataDate *time.Time `bson:"cgmLastData,omitempty"` + CgmActiveWearTime *float64 `bson:"cgmActiveWearTime,omitempty"` + CgmDaysWithData *int `bson:"cgmDaysWithData,omitempty"` + CgmHoursWithData *int `bson:"cgmHoursWithData,omitempty"` + CgmAverageGlucose *float64 `bson:"cgmAverageGlucose,omitempty"` + CgmGmi *float64 `bson:"cgmGmi,omitempty"` + CgmStdDev *float64 `bson:"cgmStdDev,omitempty"` + CgmCV *float64 `bson:"cgmCV,omitempty"` + CgmTimeInLevel2Hypo *float64 `bson:"cgmTimeInLevel2Hypo,omitempty"` + CgmTimeInLevel1Hypo *float64 `bson:"cgmTimeInLevel1Hypo,omitempty"` + CgmTimeInTarget *float64 `bson:"cgmTimeInTarget,omitempty"` + CgmTimeInLevel2Hyper *float64 `bson:"cgmTimeInLevel2Hyper,omitempty"` + CgmTimeInLevel1Hyper *float64 `bson:"cgmTimeInLevel1Hyper,omitempty"` + + BgmLastDataDate *time.Time `bson:"bgmLastData,omitempty"` + BgmAverageGlucose *float64 `bson:"bgmAverageGlucose,omitempty"` + BgmReadingsPerDay *float64 `bson:"bgmReadingsPerDay,omitempty"` + BgmTotalReadings *int `bson:"bgmTotalReadings,omitempty"` + BgmLowEvents *int `bson:"bgmLowEvents,omitempty"` + BgmHighEvents *int `bson:"bgmHighEvents,omitempty"` +} + +type ExportParams struct { + Period string + ExporterClinicianID string + WorkspaceID string + ReportDate time.Time +} diff --git a/patients/patients.go b/patients/patients.go index 8beeb588..e4f0a157 100644 --- a/patients/patients.go +++ b/patients/patients.go @@ -70,6 +70,7 @@ type Service interface { ConvertPatientTagToSite(ctx context.Context, clinicId, patientTagId string, site *sites.Site) error UpdatePatientDataSources(ctx context.Context, userId string, dataSources *DataSources) error TideReport(ctx context.Context, clinicId string, params TideReportParams) (*Tide, error) + ListExportedPatients(ctx context.Context, params ExportParams) ([]ExportedPatient, error) UpdateEHRSubscription(ctx context.Context, clinicId, userId string, update SubscriptionUpdate) error RescheduleLastSubscriptionOrderForAllPatients(ctx context.Context, clinicId, subscription, ordersCollection, targetCollection string) error RescheduleLastSubscriptionOrderForPatient(ctx context.Context, clinicIds []string, userId, subscription, ordersCollection, targetCollection string) error diff --git a/patients/repository/repository.go b/patients/repository/repository.go index 5c2363e0..9c408ddc 100644 --- a/patients/repository/repository.go +++ b/patients/repository/repository.go @@ -1092,6 +1092,199 @@ func (r *repository) DeletePatientTagFromClinicPatients(ctx context.Context, cli return nil } +func coalesce(field string, defaultValue any) bson.M { + return bson.M{ + "$ifNull": bson.A{ + field, + defaultValue, + }, + } +} + +func toMongoString(field string, defaultValue string) bson.M { + return bson.M{ + "$convert": bson.M{ + "input": field, + "to": "string", + "onError": defaultValue, + "onNull": defaultValue, + }, + } +} + +func (r *repository) ListExportedPatients(ctx context.Context, params patients.ExportParams) ([]patients.ExportedPatient, error) { + clinicId := params.WorkspaceID + period := params.Period + clinicObjId, err := primitive.ObjectIDFromHex(clinicId) + if err != nil { + return nil, fmt.Errorf(`error converting workspace id string to ObjectId: %w`, err) + } + matchStage := bson.D{ + {"$match", bson.M{ + "clinicId": clinicObjId, + }}, + } + + cgmPathPrefix := fmt.Sprintf("$summary.cgmStats.periods.%s", period) + addCGMFieldsStage := bson.D{ + {"$addFields", bson.M{ + "cgmLastData": "$summary.cgmStats.dates.lastData", + "cgmActiveWearTime": cgmPathPrefix + ".timeCGMUsePercent", + "cgmDaysWithData": cgmPathPrefix + ".daysWithData", + "cgmHoursWithData": cgmPathPrefix + ".hoursWithData", + "cgmAverageGlucose": cgmPathPrefix + ".averageGlucoseMmol", + "cgmGmi": cgmPathPrefix + ".glucoseManagementIndicator", + "cgmStdDev": cgmPathPrefix + ".standardDeviation", + "cgmCV": cgmPathPrefix + ".coefficientOfVariation", + "cgmTimeInLevel2Hypo": cgmPathPrefix + ".timeInVeryLowPercent", + "cgmTimeInLevel1Hypo": cgmPathPrefix + ".timeInLowPercent", + "cgmTimeInTarget": cgmPathPrefix + ".timeInTargetPercent", + "cgmTimeInLevel2Hyper": cgmPathPrefix + ".timeInVeryHighPercent", + "cgmTimeInLevel1Hyper": cgmPathPrefix + ".timeInHighPercent", + }}, + } + + bgmPathPrefix := fmt.Sprintf("$summary.bgmStats.periods.%s", period) + addBGMFieldsStage := bson.D{ + {"$addFields", bson.M{ + "bgmLastData": "$summary.bgmStats.dates.lastData", + "bgmAverageGlucose": bgmPathPrefix + ".averageGlucoseMmol", + "bgmReadingsPerDay": bson.M{ + "$divide": bson.A{ + bgmPathPrefix + ".totalRecords", // no null check for numerator so that if field is null/missing result is missing as intended + bson.M{"$max": bson.A{bgmPathPrefix + ".daysWithData", 1}}, // default value for denominator to prevent divide by 0's + }, + }, + "bgmTotalReadings": bgmPathPrefix + ".totalRecords", + "bgmLowEvents": bgmPathPrefix + ".timeInLowRecords", + "bgmHighEvents": bgmPathPrefix + ".timeInHighRecords", + }}, + } + + addSiteNamesStage := bson.D{ + {"$addFields", bson.M{ + "clinicSiteNames": bson.M{ + "$map": bson.M{ + "input": coalesce("$sites", bson.A{}), + "as": "site", + "in": "$$site.name", + }, + }, + }}, + } + + addTagIdsStage := bson.D{ + {"$addFields", bson.M{ + "tagIds": bson.M{ + "$map": bson.M{ + "input": coalesce("$tags", bson.A{}), + "as": "tag", + "in": bson.M{"$toString": "$$tag"}, + }, + }, + }}, + } + + // extract individual data provider sources to their own field + addNamedProviders := bson.D{ + {"$addFields", bson.M{ + "dexcomDataSource": bson.M{ + "$first": bson.M{ + "$filter": bson.M{ + "input": "$dataSources", + "as": "dataSource", + "cond": bson.M{ + "$eq": bson.A{"$$dataSource.providerName", patients.DexcomDataSourceProviderName}, + }, + }, + }, + }, + "abbottDataSource": bson.M{ + "$first": bson.M{ + "$filter": bson.M{ + "input": "$dataSources", + "as": "dataSource", + "cond": bson.M{ + "$eq": bson.A{"$$dataSource.providerName", patients.AbbottDataSourceProviderName}, + }, + }, + }, + }, + "twiistDataSource": bson.M{ + "$first": bson.M{ + "$filter": bson.M{ + "input": "$dataSources", + "as": "dataSource", + "cond": bson.M{ + "$eq": bson.A{"$$dataSource.providerName", patients.TwiistDataSourceProviderName}, + }, + }, + }, + }, + }}, + } + + // Finally, explicitly list fields that we're exporting as they may be renamed and we are interested in only a small subset of them + projectStage := bson.D{ + {"$project", bson.M{ + "_id": 0, + "fullName": 1, + "userId": 1, + "mrn": 1, + "birthDate": 1, + "email": 1, + "permissions": 1, + "createdTime": 1, + "invitedBy": 1, + "clinicSiteNames": 1, + "tagIds": 1, + "glycemicRanges": 1, + "diagnosisType": 1, + "dexcomDataSource": 1, + "abbottDataSource": 1, + "twiistDataSource": 1, + "cgmLastData": 1, + "cgmActiveWearTime": 1, + "cgmDaysWithData": 1, + "cgmHoursWithData": 1, + "cgmAverageGlucose": 1, + "cgmGmi": 1, + "cgmStdDev": 1, + "cgmCV": 1, + "cgmTimeInLevel2Hypo": 1, + "cgmTimeInLevel1Hypo": 1, + "cgmTimeInTarget": 1, + "cgmTimeInLevel2Hyper": 1, + "cgmTimeInLevel1Hyper": 1, + "bgmLastData": 1, + "bgmAverageGlucose": 1, + "bgmReadingsPerDay": 1, + "bgmTotalReadings": 1, + "bgmLowEvents": 1, + "bgmHighEvents": 1, + }}, + } + stages := mongo.Pipeline{ + matchStage, + addNamedProviders, + addSiteNamesStage, + addTagIdsStage, + addCGMFieldsStage, + addBGMFieldsStage, + projectStage, + } + cursor, err := r.collection.Aggregate(ctx, stages) + if err != nil { + return nil, fmt.Errorf(`unable to get aggregation cursor for patients export: %w`, err) + } + var rows []patients.ExportedPatient + if err := cursor.All(ctx, &rows); err != nil { + return nil, fmt.Errorf(`unable to read from aggregation cursor for patients export: %w`, err) + } + + return rows, nil +} + func (r *repository) UpdatePatientDataSources(ctx context.Context, userId string, dataSources *patients.DataSources) error { selector := bson.M{ "userId": userId, diff --git a/patients/service/service.go b/patients/service/service.go index d0c6f61b..44663dbc 100644 --- a/patients/service/service.go +++ b/patients/service/service.go @@ -506,6 +506,21 @@ func (s *service) TideReport(ctx context.Context, clinicId string, params patien return s.patientsRepo.TideReport(ctx, clinicId, params) } +func (s *service) ListExportedPatients(ctx context.Context, params patients.ExportParams) ([]patients.ExportedPatient, error) { + ps, err := s.patientsRepo.ListExportedPatients(ctx, params) + args := []any{ + "clinicId", params.WorkspaceID, + "clinicianId", params.ExporterClinicianID, + "patientCount", len(ps), + } + if err != nil { + args = append(args, "error", err) + } + s.logger.Infow("ListExportedPatients invoked", args...) + + return ps, err +} + func mrnChanged(existing patients.Patient, updated patients.Patient) bool { return (existing.Mrn == nil && updated.Mrn != nil) || (existing.Mrn != nil && updated.Mrn == nil) || diff --git a/patients/test/mock_patients.go b/patients/test/mock_patients.go index 525c535f..9ba850be 100644 --- a/patients/test/mock_patients.go +++ b/patients/test/mock_patients.go @@ -262,6 +262,21 @@ func (mr *MockServiceMockRecorder) List(ctx, filter, pagination, sort any) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockService)(nil).List), ctx, filter, pagination, sort) } +// ListExportedPatients mocks base method. +func (m *MockService) ListExportedPatients(ctx context.Context, params patients.ExportParams) ([]patients.ExportedPatient, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListExportedPatients", ctx, params) + ret0, _ := ret[0].([]patients.ExportedPatient) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListExportedPatients indicates an expected call of ListExportedPatients. +func (mr *MockServiceMockRecorder) ListExportedPatients(ctx, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExportedPatients", reflect.TypeOf((*MockService)(nil).ListExportedPatients), ctx, params) +} + // MergeSites mocks base method. func (m *MockService) MergeSites(ctx context.Context, clinicId, sourceSiteId string, targetSite *sites.Site) error { m.ctrl.T.Helper() @@ -720,6 +735,21 @@ func (mr *MockRepositoryMockRecorder) List(ctx, filter, pagination, sort any) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRepository)(nil).List), ctx, filter, pagination, sort) } +// ListExportedPatients mocks base method. +func (m *MockRepository) ListExportedPatients(ctx context.Context, params patients.ExportParams) ([]patients.ExportedPatient, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListExportedPatients", ctx, params) + ret0, _ := ret[0].([]patients.ExportedPatient) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListExportedPatients indicates an expected call of ListExportedPatients. +func (mr *MockRepositoryMockRecorder) ListExportedPatients(ctx, params any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExportedPatients", reflect.TypeOf((*MockRepository)(nil).ListExportedPatients), ctx, params) +} + // MergeSites mocks base method. func (m *MockRepository) MergeSites(ctx context.Context, clinicId, sourceSiteId string, targetSite *sites.Site) error { m.ctrl.T.Helper() diff --git a/spec/clinic.v1.yaml b/spec/clinic.v1.yaml index a4938b99..a7fe4692 100644 --- a/spec/clinic.v1.yaml +++ b/spec/clinic.v1.yaml @@ -2183,6 +2183,7 @@ paths: responses: '200': description: OK + description: Get programs tags: - Clinics - Internal @@ -2194,7 +2195,7 @@ paths: responses: '200': description: OK - description: '' + description: Get program URL tags: - Clinics - Internal @@ -2206,7 +2207,7 @@ paths: responses: '200': description: OK - description: '' + description: Notification webhook tags: - Clinics - Internal @@ -2328,6 +2329,27 @@ paths: - Clinics - Internal description: Send a new request to the patient to connect a data provider + /v1/clinics/{clinicId}/export/patients: + parameters: + - $ref: '#/components/parameters/clinicId' + get: + summary: Export Patient List CSV + operationId: ExportPatientList + description: Generates a CSV of all patients in a clinic with various computed statuses. + tags: + - Clinics + responses: + '200': + description: OK + parameters: + - schema: + type: string + pattern: ^(1d|7d|14d|30d)$ + example: 7d + in: query + name: period + description: Time Period to display + required: true components: schemas: clinics.v1: