From f5306d30d81312b2a866513016a4f719d2d59680 Mon Sep 17 00:00:00 2001 From: Kathurima Kimathi Date: Thu, 11 Apr 2024 16:56:12 +0300 Subject: [PATCH] feat: hookup sending patient referral form via SMS Signed-off-by: Kathurima Kimathi --- pkg/clinical/application/dto/advantage.go | 7 +++ .../cloudhealthcare/mock/fhir_mock.go | 12 ++++ .../services/advantage/mock/service_mock.go | 9 +++ .../services/advantage/service.go | 41 ++++++++++++- .../services/advantage/service_test.go | 56 ++++++++++++++++++ .../presentation/graph/clinical.graphql | 2 +- .../presentation/graph/clinical.resolvers.go | 4 +- .../presentation/graph/generated/generated.go | 19 ++++-- .../presentation/rest/handlers_test.go | 2 +- .../usecases/clinical/referral_report.go | 30 ++++++++-- .../usecases/clinical/referral_report_test.go | 58 ++++++++++++++++++- 11 files changed, 225 insertions(+), 15 deletions(-) diff --git a/pkg/clinical/application/dto/advantage.go b/pkg/clinical/application/dto/advantage.go index 7a625312..d58c39a4 100644 --- a/pkg/clinical/application/dto/advantage.go +++ b/pkg/clinical/application/dto/advantage.go @@ -6,3 +6,10 @@ type SegmentationPayload struct { ClinicalID string `json:"clinical_id,omitempty"` SegmentLabel SegmentationCategory `json:"segment_label,omitempty"` } + +// SMSPayload is used to model data used to send sms to patients through the advantage service +type SMSPayload struct { + Intention string `json:"intention"` + Message string `json:"message"` + Recipients []string `json:"recipients"` +} diff --git a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go index ac6e6afb..2f5f2ab2 100644 --- a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go +++ b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go @@ -2334,6 +2334,7 @@ func NewFHIRMock() *FHIRMock { }, MockSearchFHIRDocumentReferenceFn: func(ctx context.Context, searchParams map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRDocumentReference, error) { resourceID := gofakeit.UUID() + URL := gofakeit.URL() return &domain.PagedFHIRDocumentReference{ DocumentReferences: []domain.FHIRDocumentReference{ { @@ -2346,6 +2347,17 @@ func NewFHIRMock() *FHIRMock { }, }, }, + Subject: &domain.FHIRReference{ + ID: &resourceID, + }, + Content: []domain.FHIRDocumentReferenceContent{ + { + ID: resourceID, + Attachment: domain.FHIRAttachment{ + URL: (*scalarutils.URL)(&URL), + }, + }, + }, }, }, HasNextPage: false, diff --git a/pkg/clinical/infrastructure/services/advantage/mock/service_mock.go b/pkg/clinical/infrastructure/services/advantage/mock/service_mock.go index baa083c9..3a74cb31 100644 --- a/pkg/clinical/infrastructure/services/advantage/mock/service_mock.go +++ b/pkg/clinical/infrastructure/services/advantage/mock/service_mock.go @@ -9,6 +9,7 @@ import ( // FakeAdvantage mocks the implementation of advantage API methods type FakeAdvantage struct { MockSegmentPatient func(ctx context.Context, payload dto.SegmentationPayload) error + MockSendSMSFn func(ctx context.Context, workstationID string, payload dto.SMSPayload) error } // NewFakeAdvantageMock is the advantage's mock methods constructor @@ -17,6 +18,9 @@ func NewFakeAdvantageMock() *FakeAdvantage { MockSegmentPatient: func(ctx context.Context, payload dto.SegmentationPayload) error { return nil }, + MockSendSMSFn: func(ctx context.Context, workstationID string, payload dto.SMSPayload) error { + return nil + }, } } @@ -24,3 +28,8 @@ func NewFakeAdvantageMock() *FakeAdvantage { func (f *FakeAdvantage) SegmentPatient(ctx context.Context, payload dto.SegmentationPayload) error { return f.MockSegmentPatient(ctx, payload) } + +// SendSMS mocks the implementation of SMS notification to patient +func (f *FakeAdvantage) SendSMS(ctx context.Context, workstationID string, payload dto.SMSPayload) error { + return f.MockSendSMSFn(ctx, workstationID, payload) +} diff --git a/pkg/clinical/infrastructure/services/advantage/service.go b/pkg/clinical/infrastructure/services/advantage/service.go index 03f20ff6..fd8a4a24 100644 --- a/pkg/clinical/infrastructure/services/advantage/service.go +++ b/pkg/clinical/infrastructure/services/advantage/service.go @@ -16,6 +16,7 @@ import ( var ( AdvantageBaseURL = serverutils.MustGetEnvVar("ADVANTAGE_BASE_URL") segmentationPath = "/api/segments/segment/clinical/" + smsPath = "/api/notifications/sms/" ) // AuthUtilsLib holds the method defined in authutils library @@ -26,6 +27,7 @@ type AuthUtilsLib interface { // AdvantageService represents methods that can be used to communicate with the advantage server type AdvantageService interface { SegmentPatient(ctx context.Context, payload dto.SegmentationPayload) error + SendSMS(ctx context.Context, workstationID string, payload dto.SMSPayload) error } // ServiceAdvantageImpl represents advantage server's implementations @@ -46,7 +48,6 @@ func (s *ServiceAdvantageImpl) SegmentPatient(ctx context.Context, payload dto.S payloadBytes, err := json.Marshal(payload) if err != nil { - fmt.Println("Error marshaling JSON:", err) return err } @@ -77,3 +78,41 @@ func (s *ServiceAdvantageImpl) SegmentPatient(ctx context.Context, payload dto.S return nil } + +// SegmentPatient is used to create segmentation information in advantage +func (s *ServiceAdvantageImpl) SendSMS(ctx context.Context, workstationID string, payload dto.SMSPayload) error { + url := fmt.Sprintf("%s/%s", AdvantageBaseURL, smsPath) + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return err + } + + body := bytes.NewReader(payloadBytes) + + req, err := http.NewRequest(http.MethodPost, url, body) + if err != nil { + return err + } + + token, err := s.client.Authenticate() + if err != nil { + return err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Set("X-Workstation", workstationID) + + httpClient := &http.Client{Timeout: time.Second * 30} + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + return nil +} diff --git a/pkg/clinical/infrastructure/services/advantage/service_test.go b/pkg/clinical/infrastructure/services/advantage/service_test.go index a8aef69b..8f7de79c 100644 --- a/pkg/clinical/infrastructure/services/advantage/service_test.go +++ b/pkg/clinical/infrastructure/services/advantage/service_test.go @@ -62,3 +62,59 @@ func TestServiceAdvantageImpl_PatientSegmentation(t *testing.T) { }) } } + +func TestServiceAdvantageImpl_SendSMS(t *testing.T) { + type args struct { + ctx context.Context + payload dto.SMSPayload + workstationID string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Happy case: send SMS", + args: args{ + ctx: context.Background(), + payload: dto.SMSPayload{ + Intention: "DIRECT_MESSAGE", + Message: "message", + Recipients: []string{}, + }, + workstationID: gofakeit.UUID(), + }, + wantErr: false, + }, + { + name: "Sad case: unable to send SMS", + args: args{ + ctx: context.Background(), + payload: dto.SMSPayload{ + Intention: "DIRECT_MESSAGE", + Message: "message", + Recipients: []string{}, + }, + workstationID: gofakeit.UUID(), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeAuthUtils := advantageMock.NewAuthUtilsClientMock() + s := advantage.NewServiceAdvantage(fakeAuthUtils) + + if tt.name == "Sad case: unable to send SMS" { + fakeAuthUtils.MockAuthenticateFn = func() (*authutils.OAUTHResponse, error) { + return nil, errors.New("unable to authenticate") + } + } + + if err := s.SendSMS(tt.args.ctx, tt.args.workstationID, tt.args.payload); (err != nil) != tt.wantErr { + t.Errorf("ServiceAdvantageImpl.SendSMS() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/clinical/presentation/graph/clinical.graphql b/pkg/clinical/presentation/graph/clinical.graphql index bc5872d3..d6ec99e2 100644 --- a/pkg/clinical/presentation/graph/clinical.graphql +++ b/pkg/clinical/presentation/graph/clinical.graphql @@ -226,5 +226,5 @@ extend type Mutation { # Referral referPatient(input: ReferralInput!): ServiceRequest! - shareReferralForm(serviceRequestID: ID!): Boolean! + shareReferralForm(serviceRequestID: ID!, workstationID: String!): Boolean! } diff --git a/pkg/clinical/presentation/graph/clinical.resolvers.go b/pkg/clinical/presentation/graph/clinical.resolvers.go index 6805115b..d7a9fcf0 100644 --- a/pkg/clinical/presentation/graph/clinical.resolvers.go +++ b/pkg/clinical/presentation/graph/clinical.resolvers.go @@ -317,10 +317,10 @@ func (r *mutationResolver) ReferPatient(ctx context.Context, input dto.ReferralI } // ShareReferralForm is the resolver for the shareReferralForm field. -func (r *mutationResolver) ShareReferralForm(ctx context.Context, serviceRequestID string) (bool, error) { +func (r *mutationResolver) ShareReferralForm(ctx context.Context, serviceRequestID string, workstationID string) (bool, error) { r.CheckDependencies() - return r.usecases.ShareReferralForm(ctx, serviceRequestID) + return r.usecases.ShareReferralForm(ctx, serviceRequestID, workstationID) } // PatientHealthTimeline is the resolver for the patientHealthTimeline field. diff --git a/pkg/clinical/presentation/graph/generated/generated.go b/pkg/clinical/presentation/graph/generated/generated.go index e66e2ccb..fa232e09 100644 --- a/pkg/clinical/presentation/graph/generated/generated.go +++ b/pkg/clinical/presentation/graph/generated/generated.go @@ -360,7 +360,7 @@ type ComplexityRoot struct { RecordViralLoad func(childComplexity int, input dto.ObservationInput) int RecordWeight func(childComplexity int, input dto.ObservationInput) int ReferPatient func(childComplexity int, input dto.ReferralInput) int - ShareReferralForm func(childComplexity int, serviceRequestID string) int + ShareReferralForm func(childComplexity int, serviceRequestID string, workstationID string) int StartEncounter func(childComplexity int, episodeID string) int } @@ -745,7 +745,7 @@ type MutationResolver interface { RecordCbe(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) GetEncounterAssociatedResources(ctx context.Context, encounterID string) (*dto.EncounterAssociatedResourceOutput, error) ReferPatient(ctx context.Context, input dto.ReferralInput) (*dto.ServiceRequest, error) - ShareReferralForm(ctx context.Context, serviceRequestID string) (bool, error) + ShareReferralForm(ctx context.Context, serviceRequestID string, workstationID string) (bool, error) } type QueryResolver interface { PatientHealthTimeline(ctx context.Context, input dto.HealthTimelineInput) (*dto.HealthTimeline, error) @@ -2538,7 +2538,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.ShareReferralForm(childComplexity, args["serviceRequestID"].(string)), true + return e.complexity.Mutation.ShareReferralForm(childComplexity, args["serviceRequestID"].(string), args["workstationID"].(string)), true case "Mutation.startEncounter": if e.complexity.Mutation.StartEncounter == nil { @@ -4629,7 +4629,7 @@ extend type Mutation { # Referral referPatient(input: ReferralInput!): ServiceRequest! - shareReferralForm(serviceRequestID: ID!): Boolean! + shareReferralForm(serviceRequestID: ID!, workstationID: String!): Boolean! } `, BuiltIn: false}, {Name: "../enums.graphql", Input: `enum EpisodeOfCareStatusEnum { @@ -6531,6 +6531,15 @@ func (ec *executionContext) field_Mutation_shareReferralForm_args(ctx context.Co } } args["serviceRequestID"] = arg0 + var arg1 string + if tmp, ok := rawArgs["workstationID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("workstationID")) + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["workstationID"] = arg1 return args, nil } @@ -18444,7 +18453,7 @@ func (ec *executionContext) _Mutation_shareReferralForm(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().ShareReferralForm(rctx, fc.Args["serviceRequestID"].(string)) + return ec.resolvers.Mutation().ShareReferralForm(rctx, fc.Args["serviceRequestID"].(string), fc.Args["workstationID"].(string)) }) if err != nil { ec.Error(ctx, err) diff --git a/pkg/clinical/presentation/rest/handlers_test.go b/pkg/clinical/presentation/rest/handlers_test.go index cc14c02e..1ddc1346 100644 --- a/pkg/clinical/presentation/rest/handlers_test.go +++ b/pkg/clinical/presentation/rest/handlers_test.go @@ -77,7 +77,7 @@ func TestPresentationHandlersImpl_ReceivePubSubPushMessage(t *testing.T) { body: nil, }, wantStatus: http.StatusOK, - wantErr: false, + // wantErr: false, }, { name: "sad case: publish create organization message", diff --git a/pkg/clinical/usecases/clinical/referral_report.go b/pkg/clinical/usecases/clinical/referral_report.go index eec10736..615727d6 100644 --- a/pkg/clinical/usecases/clinical/referral_report.go +++ b/pkg/clinical/usecases/clinical/referral_report.go @@ -324,9 +324,9 @@ func (c *UseCasesClinicalImpl) CreateDocumentReference(ctx context.Context, payl return nil } -// ShareReferralForm is searched for a document reference associated with a service request, retrieves the document URL and sends it to the -// patient via SMS -func (c *UseCasesClinicalImpl) ShareReferralForm(ctx context.Context, serviceRequestID string) (bool, error) { +// ShareReferralForm is searches for a document reference using the service request ID associated and retrieves the document URL +// (which is the url of the referral form that we want to share) and sends it to the patient via SMS +func (c *UseCasesClinicalImpl) ShareReferralForm(ctx context.Context, serviceRequestID string, workstationID string) (bool, error) { identifiers, err := c.infrastructure.BaseExtension.GetTenantIdentifiers(ctx) if err != nil { return false, err @@ -348,7 +348,29 @@ func (c *UseCasesClinicalImpl) ShareReferralForm(ctx context.Context, serviceReq return false, errors.New("no document reference found") } - // TODO: Send SMS here + var patientID string + if output.DocumentReferences[0].Subject != nil { + patientID = *output.DocumentReferences[0].Subject.ID + } else { + return false, errors.New("no subject found") + } + + patient, err := c.infrastructure.FHIR.GetFHIRPatient(ctx, patientID) + if err != nil { + utils.ReportErrorToSentry(err) + return false, err + } + + smsPayload := &dto.SMSPayload{ + Intention: "DIRECT_MESSAGE", + Message: string(*output.DocumentReferences[0].Content[0].Attachment.URL), + Recipients: []string{*patient.Resource.Telecom[0].Value}, + } + + err = c.infrastructure.AdvantageService.SendSMS(ctx, workstationID, *smsPayload) + if err != nil { + return false, err + } return true, nil } diff --git a/pkg/clinical/usecases/clinical/referral_report_test.go b/pkg/clinical/usecases/clinical/referral_report_test.go index 62b27351..f2140af9 100644 --- a/pkg/clinical/usecases/clinical/referral_report_test.go +++ b/pkg/clinical/usecases/clinical/referral_report_test.go @@ -301,6 +301,7 @@ func TestUseCasesClinicalImpl_ShareReferralForm(t *testing.T) { type args struct { ctx context.Context serviceRequestID string + workstationID string } tests := []struct { name string @@ -312,6 +313,7 @@ func TestUseCasesClinicalImpl_ShareReferralForm(t *testing.T) { args: args{ ctx: addTenantIdentifierContext(context.Background()), serviceRequestID: gofakeit.UUID(), + workstationID: gofakeit.UUID(), }, wantErr: false, }, @@ -339,6 +341,30 @@ func TestUseCasesClinicalImpl_ShareReferralForm(t *testing.T) { }, wantErr: true, }, + { + name: "Sad case: unable to get patient", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + serviceRequestID: gofakeit.UUID(), + }, + wantErr: true, + }, + { + name: "Sad case: unable to get send SMS", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + serviceRequestID: gofakeit.UUID(), + }, + wantErr: true, + }, + { + name: "Sad case: unable to subject associated with the document reference", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + serviceRequestID: gofakeit.UUID(), + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -367,8 +393,38 @@ func TestUseCasesClinicalImpl_ShareReferralForm(t *testing.T) { return &domain.PagedFHIRDocumentReference{}, nil } } + if tt.name == "Sad case: unable to get patient" { + fakeFHIR.MockGetFHIRPatientFn = func(ctx context.Context, id string) (*domain.FHIRPatientRelayPayload, error) { + return nil, fmt.Errorf("failed to get patient") + } + } + if tt.name == "Sad case: unable to get send SMS" { + fakeAdvantage.MockSendSMSFn = func(ctx context.Context, workstationID string, payload dto.SMSPayload) error { + return fmt.Errorf("failed to send SMS") + } + } + if tt.name == "Sad case: unable to subject associated with the document reference" { + fakeFHIR.MockSearchFHIRDocumentReferenceFn = func(ctx context.Context, searchParams map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRDocumentReference, error) { + resourceID := uuid.NewString() + return &domain.PagedFHIRDocumentReference{ + DocumentReferences: []domain.FHIRDocumentReference{ + { + ID: resourceID, + Meta: &domain.FHIRMeta{}, + Type: &domain.FHIRCodeableConcept{}, + Category: []domain.FHIRCodeableConcept{}, + }, + }, + HasNextPage: false, + NextCursor: "", + HasPreviousPage: false, + PreviousCursor: "", + TotalCount: 0, + }, nil + } + } - _, err := c.ShareReferralForm(tt.args.ctx, tt.args.serviceRequestID) + _, err := c.ShareReferralForm(tt.args.ctx, tt.args.serviceRequestID, tt.args.workstationID) if (err != nil) != tt.wantErr { t.Errorf("UseCasesClinicalImpl.ShareReferralForm() error = %v, wantErr %v", err, tt.wantErr) return