Skip to content

Commit

Permalink
feat: patient segmentation (#369)
Browse files Browse the repository at this point in the history
Signed-off-by: Kathurima Kimathi <kathurimakimathi415@gmail.com>
  • Loading branch information
KathurimaKimathi committed Feb 21, 2024
1 parent bdd5444 commit 48d7cbf
Show file tree
Hide file tree
Showing 37 changed files with 823 additions and 132 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ env:
MYCAREHUB_INTROSPECT_URL: ${{ secrets.MYCAREHUB_INTROSPECT_URL }}
CLINICAL_BUCKET_NAME: ${{ secrets.CLINICAL_BUCKET_NAME }}
SENTRY_TRACE_SAMPLE_RATE: ${{ secrets.SENTRY_TRACE_SAMPLE_RATE }}
ADVANTAGE_BASE_URL: ${{ secrets.ADVANTAGE_BASE_URL }}

jobs:
golangci:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/staging_multitenant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ env:
MYCAREHUB_INTROSPECT_URL: ${{ secrets.MYCAREHUB_INTROSPECT_URL }}
CLINICAL_BUCKET_NAME: ${{ secrets.CLINICAL_BUCKET_NAME }}
SENTRY_TRACE_SAMPLE_RATE: ${{ secrets.SENTRY_TRACE_SAMPLE_RATE }}
ADVANTAGE_BASE_URL: ${{ secrets.ADVANTAGE_BASE_URL }}

jobs:
deploy_to_multitenant_staging:
Expand Down
3 changes: 3 additions & 0 deletions deploy/charts/clinical/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ spec:
- name: SENTRY_TRACE_SAMPLE_RATE
value: {{ .Values.app.container.env.defaultSentryTraceSampleRate | quote }}

- name: ADVANTAGE_BASE_URL
value: {{.Values.app.container.env.advantageBaseURL | quote }}

volumeMounts:
- name: {{ .Values.app.container.env.googleApplicationCredentialsSecret.name }}
mountPath: {{ .Values.app.container.env.googleApplicationCredentialsSecret.mountPath }}
Expand Down
1 change: 1 addition & 0 deletions deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ helm upgrade \
--set networking.issuer.privateKeySecretRef="letsencrypt-prod"\
--set networking.ingress.host="${APP_DOMAIN}"\
--set app.container.env.defaultSentryTraceSampleRate="${SENTRY_TRACE_SAMPLE_RATE}"\
--set app.container.env.advantageBaseURL="${ADVANTAGE_BASE_URL}"\
--wait \
--timeout 300s \
-f ./charts/clinical/values.yaml \
Expand Down
3 changes: 3 additions & 0 deletions pkg/clinical/application/common/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const (
// TenantTopicName is the topic where program is registered in clinical as a tenant
TenantTopicName = "mycarehub.tenant.create"

// SegmentationTopicName topic sends patient segmentation information to slade advantage
SegmentationTopicName = "patient.segmentation.create"

// MedicalDataCount is the count of medical records
MedicalDataCount = "3"

Expand Down
8 changes: 8 additions & 0 deletions pkg/clinical/application/dto/advantage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dto

// SegmentationPayload is used to stratify clients in advantage EMR.
type SegmentationPayload struct {
// ClinicalID represents the patient's ID in this service
ClinicalID string `json:"clinical_id,omitempty"`
SegmentLabel SegmentationCategory `json:"segment_label,omitempty"`
}
45 changes: 45 additions & 0 deletions pkg/clinical/application/dto/enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,48 @@ func (e *ObservationStatusEnum) UnmarshalGQL(v interface{}) error {
func (e ObservationStatusEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

// SegmentationCategory models advantage segmentation categories for clients
type SegmentationCategory string

const (
SegmentationCategoryNoRisk SegmentationCategory = "CERVICAL_CANCER_TIPS"
SegmentationCategoryLowRisk SegmentationCategory = "CERVICAL_CANCER_LOW_RISK"
SegmentationCategoryHighRiskPositive SegmentationCategory = "CERVICAL_CANCER_POSITIVE"
SegmentationCategoryHighRiskNegative SegmentationCategory = "CERVICAL_CANCER_HIGH_RISK"
)

// IsValid checks validity of a SegmentationCategory enum
func (c SegmentationCategory) IsValid() bool {
switch c {
case SegmentationCategoryNoRisk, SegmentationCategoryLowRisk, SegmentationCategoryHighRiskPositive, SegmentationCategoryHighRiskNegative:
return true
}

return false
}

// String converts segmentation to string
func (c SegmentationCategory) String() string {
return string(c)
}

// MarshalGQL writes the segmentation as a quoted string
func (c SegmentationCategory) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(c.String()))
}

// UnmarshalGQL reads a json and converts it to a segmentation enum
func (c *SegmentationCategory) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be a string")
}

*c = SegmentationCategory(str)
if !c.IsValid() {
return fmt.Errorf("%s is not a valid SegmentationCategory Enum", str)
}

return nil
}
149 changes: 149 additions & 0 deletions pkg/clinical/application/dto/enums_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package dto

import (
"bytes"
"strconv"
"testing"
)

func TestConsentState_MarshalGQL(t *testing.T) {
tests := []struct {
name string
c SegmentationCategory
wantW string
}{
{
name: "valid type s",
c: SegmentationCategoryLowRisk,
wantW: strconv.Quote("CERVICAL_CANCER_LOW_RISK"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &bytes.Buffer{}
tt.c.MarshalGQL(w)
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("SegmentationCategory.MarshalGQL() = %v, want %v", gotW, tt.wantW)
}
})
}
}

func TestSegmentationCategory_String(t *testing.T) {
tests := []struct {
name string
e SegmentationCategory
want string
}{
{
name: "CERVICAL_CANCER_TIPS",
e: SegmentationCategoryNoRisk,
want: "CERVICAL_CANCER_TIPS",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.e.String(); got != tt.want {
t.Errorf("SegmentationCategory.String() = %v, want %v", got, tt.want)
}
})
}
}

func TestSegmentationCategory_IsValid(t *testing.T) {
tests := []struct {
name string
e SegmentationCategory
want bool
}{
{
name: "valid type",
e: SegmentationCategoryHighRiskNegative,
want: true,
},
{
name: "invalid type",
e: SegmentationCategory("invalid"),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.e.IsValid(); got != tt.want {
t.Errorf("SegmentationCategory.IsValid() = %v, want %v", got, tt.want)
}
})
}
}

func TestSegmentationCategory_UnmarshalGQL(t *testing.T) {
value := SegmentationCategoryHighRiskNegative
invalid := SegmentationCategory("invalid")
type args struct {
v interface{}
}
tests := []struct {
name string
e *SegmentationCategory
args args
wantErr bool
}{
{
name: "valid type",
e: &value,
args: args{
v: "CERVICAL_CANCER_HIGH_RISK",
},
wantErr: false,
},
{
name: "invalid type",
e: &invalid,
args: args{
v: "this is not a valid type",
},
wantErr: true,
},
{
name: "non string type",
e: &invalid,
args: args{
v: 1,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.e.UnmarshalGQL(tt.args.v); (err != nil) != tt.wantErr {
t.Errorf("SegmentationCategory.UnmarshalGQL() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestSegmentationCategory_MarshalGQL(t *testing.T) {
w := &bytes.Buffer{}
tests := []struct {
name string
e SegmentationCategory
b *bytes.Buffer
wantW string
panic bool
}{
{
name: "valid type enums",
e: SegmentationCategoryHighRiskNegative,
b: w,
wantW: strconv.Quote("CERVICAL_CANCER_HIGH_RISK"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.e.MarshalGQL(tt.b)
if gotW := w.String(); gotW != tt.wantW {
t.Errorf("SegmentationCategory.MarshalGQL() = %v, want %v", gotW, tt.wantW)
}
})
}
}
14 changes: 9 additions & 5 deletions pkg/clinical/infrastructure/infrastructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/savannahghi/clinical/pkg/clinical/application/dto"
"github.com/savannahghi/clinical/pkg/clinical/domain"
"github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/advantage"
pubsubmessaging "github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/pubsub"
"github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/upload"
"github.com/savannahghi/clinical/pkg/clinical/repository"
Expand Down Expand Up @@ -53,11 +54,12 @@ type BaseExtension interface {

// Infrastructure ...
type Infrastructure struct {
FHIR repository.FHIR
OpenConceptLab ServiceOCL
BaseExtension BaseExtension
Upload upload.ServiceUpload
Pubsub pubsubmessaging.ServicePubsub
FHIR repository.FHIR
OpenConceptLab ServiceOCL
BaseExtension BaseExtension
Upload upload.ServiceUpload
Pubsub pubsubmessaging.ServicePubsub
AdvantageService advantage.AdvantageService
}

// NewInfrastructureInteractor initializes a new Infrastructure
Expand All @@ -67,12 +69,14 @@ func NewInfrastructureInteractor(
openconceptlab ServiceOCL,
upload upload.ServiceUpload,
pubsub pubsubmessaging.ServicePubsub,
advantage advantage.AdvantageService,
) Infrastructure {
return Infrastructure{
fhir,
openconceptlab,
ext,
upload,
pubsub,
advantage,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package mock

import "github.com/savannahghi/authutils"

// AuthClientMock mocks the authentication client methods
type AuthClientMock struct {
MockAuthenticateFn func() (*authutils.OAUTHResponse, error)
}

// NewAuthUtilsClientMock constructor initializes the auth utils client mock
func NewAuthUtilsClientMock() *AuthClientMock {
return &AuthClientMock{
MockAuthenticateFn: func() (*authutils.OAUTHResponse, error) {
return &authutils.OAUTHResponse{
Scope: "",
ExpiresIn: 0,
AccessToken: "token",
RefreshToken: "refresh_token",
TokenType: "Bearer",
}, nil
},
}
}

// Authenticate mocks the implementation of the authentication mechanism provided by auth utils
func (a *AuthClientMock) Authenticate() (*authutils.OAUTHResponse, error) {
return a.MockAuthenticateFn()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package mock

import (
"context"

"github.com/savannahghi/clinical/pkg/clinical/application/dto"
)

// FakeAdvantage mocks the implementation of advantage API methods
type FakeAdvantage struct {
MockSegmentPatient func(ctx context.Context, payload dto.SegmentationPayload) error
}

// NewFakeAdvantageMock is the advantage's mock methods constructor
func NewFakeAdvantageMock() *FakeAdvantage {
return &FakeAdvantage{
MockSegmentPatient: func(ctx context.Context, payload dto.SegmentationPayload) error {
return nil
},
}
}

// SegmentPatient mocks the implementation of patient segmentation usecase
func (f *FakeAdvantage) SegmentPatient(ctx context.Context, payload dto.SegmentationPayload) error {
return f.MockSegmentPatient(ctx, payload)
}
Loading

0 comments on commit 48d7cbf

Please sign in to comment.