diff --git a/pkg/clinical/presentation/graph/clinical.graphql b/pkg/clinical/presentation/graph/clinical.graphql index 2e6c7591..ae622f48 100644 --- a/pkg/clinical/presentation/graph/clinical.graphql +++ b/pkg/clinical/presentation/graph/clinical.graphql @@ -213,6 +213,7 @@ extend type Mutation { createQuestionnaireResponse( questionnaireID: String! encounterID: String! + screeningType: ScreeningTypeEnum! input: QuestionnaireResponseInput! ): String! diff --git a/pkg/clinical/presentation/graph/clinical.resolvers.go b/pkg/clinical/presentation/graph/clinical.resolvers.go index c9328be0..a083b073 100644 --- a/pkg/clinical/presentation/graph/clinical.resolvers.go +++ b/pkg/clinical/presentation/graph/clinical.resolvers.go @@ -277,8 +277,8 @@ func (r *mutationResolver) RecordConsent(ctx context.Context, input dto.ConsentI } // CreateQuestionnaireResponse is the resolver for the createQuestionnaireResponse field. -func (r *mutationResolver) CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, input dto.QuestionnaireResponse) (string, error) { - return r.usecases.CreateQuestionnaireResponse(ctx, questionnaireID, encounterID, input) +func (r *mutationResolver) CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, screeningType domain.ScreeningTypeEnum, input dto.QuestionnaireResponse) (string, error) { + return r.usecases.CreateQuestionnaireResponse(ctx, questionnaireID, encounterID, screeningType, input) } // RecordMammographyResult is the resolver for the recordMammographyResult field. diff --git a/pkg/clinical/presentation/graph/generated/generated.go b/pkg/clinical/presentation/graph/generated/generated.go index 021bb860..dcacc41c 100644 --- a/pkg/clinical/presentation/graph/generated/generated.go +++ b/pkg/clinical/presentation/graph/generated/generated.go @@ -291,7 +291,7 @@ type ComplexityRoot struct { CreateCondition func(childComplexity int, input dto.ConditionInput) int CreateEpisodeOfCare func(childComplexity int, episodeOfCare dto.EpisodeOfCareInput) int CreatePatient func(childComplexity int, input dto.PatientInput) int - CreateQuestionnaireResponse func(childComplexity int, questionnaireID string, encounterID string, input dto.QuestionnaireResponse) int + CreateQuestionnaireResponse func(childComplexity int, questionnaireID string, encounterID string, screeningType domain.ScreeningTypeEnum, input dto.QuestionnaireResponse) int DeletePatient func(childComplexity int, id string) int EndEncounter func(childComplexity int, encounterID string) int EndEpisodeOfCare func(childComplexity int, id string) int @@ -685,7 +685,7 @@ type MutationResolver interface { PatchPatientLastMenstrualPeriod(ctx context.Context, id string, value string) (*dto.Observation, error) PatchPatientBloodSugar(ctx context.Context, id string, value string) (*dto.Observation, error) RecordConsent(ctx context.Context, input dto.ConsentInput) (*dto.ConsentOutput, error) - CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, input dto.QuestionnaireResponse) (string, error) + CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, screeningType domain.ScreeningTypeEnum, input dto.QuestionnaireResponse) (string, error) RecordMammographyResult(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) RecordBiopsy(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) RecordMri(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) @@ -1858,7 +1858,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.CreateQuestionnaireResponse(childComplexity, args["questionnaireID"].(string), args["encounterID"].(string), args["input"].(dto.QuestionnaireResponse)), true + return e.complexity.Mutation.CreateQuestionnaireResponse(childComplexity, args["questionnaireID"].(string), args["encounterID"].(string), args["screeningType"].(domain.ScreeningTypeEnum), args["input"].(dto.QuestionnaireResponse)), true case "Mutation.deletePatient": if e.complexity.Mutation.DeletePatient == nil { @@ -4321,6 +4321,7 @@ extend type Mutation { createQuestionnaireResponse( questionnaireID: String! encounterID: String! + screeningType: ScreeningTypeEnum! input: QuestionnaireResponseInput! ): String! @@ -5330,15 +5331,24 @@ func (ec *executionContext) field_Mutation_createQuestionnaireResponse_args(ctx } } args["encounterID"] = arg1 - var arg2 dto.QuestionnaireResponse + var arg2 domain.ScreeningTypeEnum + if tmp, ok := rawArgs["screeningType"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("screeningType")) + arg2, err = ec.unmarshalNScreeningTypeEnum2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋdomainᚐScreeningTypeEnum(ctx, tmp) + if err != nil { + return nil, err + } + } + args["screeningType"] = arg2 + var arg3 dto.QuestionnaireResponse if tmp, ok := rawArgs["input"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - arg2, err = ec.unmarshalNQuestionnaireResponseInput2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐQuestionnaireResponse(ctx, tmp) + arg3, err = ec.unmarshalNQuestionnaireResponseInput2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐQuestionnaireResponse(ctx, tmp) if err != nil { return nil, err } } - args["input"] = arg2 + args["input"] = arg3 return args, nil } @@ -16901,7 +16911,7 @@ func (ec *executionContext) _Mutation_createQuestionnaireResponse(ctx context.Co }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateQuestionnaireResponse(rctx, fc.Args["questionnaireID"].(string), fc.Args["encounterID"].(string), fc.Args["input"].(dto.QuestionnaireResponse)) + return ec.resolvers.Mutation().CreateQuestionnaireResponse(rctx, fc.Args["questionnaireID"].(string), fc.Args["encounterID"].(string), fc.Args["screeningType"].(domain.ScreeningTypeEnum), fc.Args["input"].(dto.QuestionnaireResponse)) }) if err != nil { ec.Error(ctx, err) diff --git a/pkg/clinical/usecases/clinical/questionnaire_response.go b/pkg/clinical/usecases/clinical/questionnaire_response.go index ed04cd6b..998367e9 100644 --- a/pkg/clinical/usecases/clinical/questionnaire_response.go +++ b/pkg/clinical/usecases/clinical/questionnaire_response.go @@ -13,7 +13,9 @@ import ( ) // CreateQuestionnaireResponse creates a questionnaire response -func (u *UseCasesClinicalImpl) CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, input dto.QuestionnaireResponse) (string, error) { +func (u *UseCasesClinicalImpl) CreateQuestionnaireResponse( + ctx context.Context, questionnaireID string, encounterID string, + screeningType domain.ScreeningTypeEnum, input dto.QuestionnaireResponse) (string, error) { questionnaireResponse := &domain.FHIRQuestionnaireResponse{} err := mapstructure.Decode(input, questionnaireResponse) @@ -30,10 +32,15 @@ func (u *UseCasesClinicalImpl) CreateQuestionnaireResponse(ctx context.Context, return "", fmt.Errorf("cannot create a questionnaire response in a finished encounter") } - // TODO: Ensure user cannot submit the same risk assessment twice in the same encounter - patientID := encounter.Resource.Subject.ID patientReference := fmt.Sprintf("Patient/%s", *patientID) + encounterReference := fmt.Sprintf("Encounter/%s", *encounter.Resource.ID) + + // check if an assessment has already been done with the same encounter + err = u.checkIfAssessmentHasBeenDone(ctx, screeningType, encounterReference, patientReference) + if err != nil { + return "", err + } questionnaireResponse.Source = &domain.FHIRReference{ ID: patientID, @@ -41,8 +48,6 @@ func (u *UseCasesClinicalImpl) CreateQuestionnaireResponse(ctx context.Context, Display: encounter.Resource.Subject.Display, } - encounterReference := fmt.Sprintf("Encounter/%s", *encounter.Resource.ID) - questionnaireResponse.Encounter = &domain.FHIRReference{ ID: encounter.Resource.ID, Reference: &encounterReference, @@ -87,6 +92,31 @@ func (u *UseCasesClinicalImpl) CreateQuestionnaireResponse(ctx context.Context, return riskLevel, nil } +// checkIfAssessmentHasBeenDone is a helper function to check if the assessment has already been completed +func (u *UseCasesClinicalImpl) checkIfAssessmentHasBeenDone(ctx context.Context, screeningType domain.ScreeningTypeEnum, encounterReference, patientReference string) error { + searchParams := map[string]interface{}{ + "encounter": encounterReference, + "patient": patientReference, + "_text": screeningType.Text(), + } + + identifiers, err := u.infrastructure.BaseExtension.GetTenantIdentifiers(ctx) + if err != nil { + return fmt.Errorf("failed to get tenant identifiers from context: %w", err) + } + + results, err := u.infrastructure.FHIR.SearchFHIRRiskAssessment(ctx, searchParams, *identifiers, dto.Pagination{}) + if err != nil { + return err + } + + if len(results.Edges) > 0 { + return fmt.Errorf("a %s questionnaire response for patient %s already exists for encounter %s", screeningType.Text(), patientReference, encounterReference) + } + + return nil +} + // generateQuestionnaireReviewSummary takes a questionnaire response and // analyzes it to determine the risk stratification based on three distinct groups: // symptoms, family history, and risk factors. The assumption is that the @@ -109,8 +139,7 @@ func (u *UseCasesClinicalImpl) generateQuestionnaireReviewSummary( } switch *questionnaire.Resource.Name { - // TODO: Make this a controlled enum? - case "Cervical Cancer Screening": + case domain.CervicalCancerScreeningTypeEnum.Text(): var symptomsScore, riskFactorsScore, totalScore int for _, item := range questionnaireResponse.Item { @@ -141,7 +170,7 @@ func (u *UseCasesClinicalImpl) generateQuestionnaireReviewSummary( questionnaireResponseID, common.HighRiskCIELCode, "High Risk", - domain.CervicalCancerScreeningTypeEnum.Text(), // TODO: This is TEMPORARY. A follow up PR is to follow supplying the value from params + domain.CervicalCancerScreeningTypeEnum.Text(), ) if err != nil { return "", err diff --git a/pkg/clinical/usecases/clinical/questionnaire_response_test.go b/pkg/clinical/usecases/clinical/questionnaire_response_test.go index 89465c9f..9b93f247 100644 --- a/pkg/clinical/usecases/clinical/questionnaire_response_test.go +++ b/pkg/clinical/usecases/clinical/questionnaire_response_test.go @@ -15,6 +15,7 @@ import ( fakePubSubMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/pubsub/mock" fakeUploadMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/upload/mock" clinicalUsecase "github.com/savannahghi/clinical/pkg/clinical/usecases/clinical" + "github.com/savannahghi/firebasetools" ) func setupMockFHIRFunctions(fakeFHIR *fakeFHIRMock.FHIRMock, score int) { @@ -75,6 +76,7 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { input dto.QuestionnaireResponse questionnaireID string encounterID string + screeningType domain.ScreeningTypeEnum } tests := []struct { name string @@ -88,6 +90,7 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { input: dto.QuestionnaireResponse{}, questionnaireID: ID, encounterID: ID, + screeningType: domain.CervicalCancerScreeningTypeEnum, }, wantErr: true, }, @@ -135,6 +138,7 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { ctx: context.Background(), encounterID: gofakeit.UUID(), questionnaireID: gofakeit.UUID(), + screeningType: domain.CervicalCancerScreeningTypeEnum, }, wantErr: false, }, @@ -144,6 +148,7 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { ctx: context.Background(), encounterID: gofakeit.UUID(), questionnaireID: gofakeit.UUID(), + screeningType: domain.CervicalCancerScreeningTypeEnum, }, wantErr: false, }, @@ -183,6 +188,24 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { }, wantErr: true, }, + { + name: "Sad Case - unable to get tenant identifiers", + args: args{ + ctx: context.Background(), + encounterID: gofakeit.UUID(), + questionnaireID: gofakeit.UUID(), + }, + wantErr: true, + }, + { + name: "Sad Case - unable to search for risk assessment", + args: args{ + ctx: context.Background(), + encounterID: gofakeit.UUID(), + questionnaireID: gofakeit.UUID(), + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -229,6 +252,13 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { } if tt.name == "Happy Case - Create questionnaire response and generate review summary - Cervical Cancer - High Risk" { + fakeFHIR.MockSearchFHIRRiskAssessmentFn = func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.FHIRRiskAssessmentRelayConnection, error) { + return &domain.FHIRRiskAssessmentRelayConnection{ + Edges: []*domain.FHIRRiskAssessmentRelayEdge{}, + PageInfo: &firebasetools.PageInfo{}, + }, nil + } + fakeFHIR.MockGetFHIRQuestionnaireFn = func(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error) { questionnaireName := "Cervical Cancer Screening" return &domain.FHIRQuestionnaireRelayPayload{ @@ -277,6 +307,13 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { } if tt.name == "Happy Case - Create questionnaire response and generate review summary - Cervical Cancer - Low Risk" { + fakeFHIR.MockSearchFHIRRiskAssessmentFn = func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.FHIRRiskAssessmentRelayConnection, error) { + return &domain.FHIRRiskAssessmentRelayConnection{ + Edges: []*domain.FHIRRiskAssessmentRelayEdge{}, + PageInfo: &firebasetools.PageInfo{}, + }, nil + } + fakeFHIR.MockGetFHIRQuestionnaireFn = func(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error) { questionnaireName := "Cervical Cancer Screening" return &domain.FHIRQuestionnaireRelayPayload{ @@ -347,8 +384,18 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) { if tt.name == "Sad Case - Fail to record risk assessment - High Risk" { setupMockFHIRFunctions(fakeFHIR, 3) } + if tt.name == "Sad Case - unable to get tenant identifiers" { + fakeExt.MockGetTenantIdentifiersFn = func(ctx context.Context) (*dto.TenantIdentifiers, error) { + return nil, fmt.Errorf("failed to get tenant identifiers") + } + } + if tt.name == "Sad Case - unable to search for risk assessment" { + fakeFHIR.MockSearchFHIRRiskAssessmentFn = func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.FHIRRiskAssessmentRelayConnection, error) { + return nil, fmt.Errorf("failed to search for risk assessment") + } + } - _, err := q.CreateQuestionnaireResponse(tt.args.ctx, tt.args.questionnaireID, tt.args.encounterID, tt.args.input) + _, err := q.CreateQuestionnaireResponse(tt.args.ctx, tt.args.questionnaireID, tt.args.encounterID, tt.args.screeningType, tt.args.input) if (err != nil) != tt.wantErr { t.Errorf("UseCasesClinicalImpl.CreateQuestionnaireResponse() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/clinical/usecases/usecases.go b/pkg/clinical/usecases/usecases.go index 28ec9a29..4fe56122 100644 --- a/pkg/clinical/usecases/usecases.go +++ b/pkg/clinical/usecases/usecases.go @@ -108,7 +108,7 @@ type Clinical interface { // Questionnaire CreateQuestionnaire(ctx context.Context, questionnaireInput *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error) - CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, input dto.QuestionnaireResponse) (string, error) + CreateQuestionnaireResponse(ctx context.Context, questionnaireID, encounterID string, screeningType domain.ScreeningTypeEnum, input dto.QuestionnaireResponse) (string, error) ListQuestionnaires(ctx context.Context, searchParam *string, pagination *dto.Pagination) (*dto.QuestionnaireConnection, error) RecordConsent(ctx context.Context, input dto.ConsentInput) (*dto.ConsentOutput, error)