Skip to content

Commit

Permalink
feat: generate questionnaire review summary (#350)
Browse files Browse the repository at this point in the history
This PR introduces a feature that generates a questionnaire review summary
Workflow:
1. User answers a questionnaire and a questionnaire response is generated
2. Save the response in cloud healthcare
3. Generate a review summary i.e Determine the patient's risk factors based on the
give algorithms
4. Record the risk assessment under the RiskAssessment FHIR resource type - Prediction is either
`High Risk` or `Low Risk`
  • Loading branch information
Salaton committed Feb 5, 2024
1 parent 3f5b1dc commit f08bec5
Show file tree
Hide file tree
Showing 12 changed files with 681 additions and 30 deletions.
9 changes: 9 additions & 0 deletions pkg/clinical/application/common/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ const (

// PapSmearTerminologyCode is the terminology code used to represent pap smear test.
PapSmearTerminologyCode = "154451"

// HighRiskCIELCode represents the CIEL code for a high-risk condition
HighRiskCIELCode = "166674"

// LowRiskCIELCode represents the CIEL code for a low-risk condition
LowRiskCIELCode = "166675"

// CIELTerminologySystem is the identity of ciel terminology system
CIELTerminologySystem = "https://CIELterminology.org"
)

// DefaultIdentifier assigns a patient a code to function as their
Expand Down
12 changes: 6 additions & 6 deletions pkg/clinical/application/dto/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,12 @@ type QuestionnaireResponseItemAnswer struct {

// Coding : an input for a code defined by a terminology system.
type Coding struct {
ID string `json:"id,omitempty"`
System scalarutils.URI `json:"system,omitempty"`
Version string `json:"version,omitempty"`
Code scalarutils.Code `json:"code,omitempty"`
Display string `json:"display,omitempty"`
UserSelected bool `json:"userSelected,omitempty"`
ID string `json:"id,omitempty"`
System scalarutils.URI `json:"system,omitempty"`
Version string `json:"version,omitempty"`
Code *scalarutils.Code `json:"code,omitempty"`
Display string `json:"display,omitempty"`
UserSelected bool `json:"userSelected,omitempty"`
}

// Attachment definition: input for referring to data content defined in other formats.
Expand Down
28 changes: 28 additions & 0 deletions pkg/clinical/domain/risk_assessment.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,31 @@ type FHIRRiskAssessmentPrediction struct {
type FHIRRiskAssessmentRelayPayload struct {
Resource *FHIRRiskAssessment `json:"resource,omitempty"`
}

type FHIRRiskAssessmentInput struct {
ID *string `json:"id,omitempty"`
Meta *FHIRMetaInput `json:"meta,omitempty"`
ImplicitRules *string `json:"implicitRules,omitempty"`
Language *string `json:"language,omitempty"`
Text *FHIRNarrativeInput `json:"text,omitempty"`
Extension []Extension `json:"extension,omitempty"`
ModifierExtension []Extension `json:"modifierExtension,omitempty"`
Identifier []FHIRIdentifierInput `json:"identifier,omitempty"`
BasedOn *FHIRReferenceInput `json:"basedOn,omitempty"`
Parent *FHIRReferenceInput `json:"parent,omitempty"`
Status ObservationStatusEnum `json:"status,omitempty"`
Method *FHIRCodeableConceptInput `json:"method,omitempty"`
Code *FHIRCodeableConceptInput `json:"code,omitempty"`
Subject FHIRReferenceInput `json:"subject,omitempty"`
Encounter *FHIRReferenceInput `json:"encounter,omitempty"`
OccurrenceDateTime *string `json:"occurrenceDateTime,omitempty"`
OccurrencePeriod *FHIRPeriodInput `json:"occurrencePeriod,omitempty"`
Condition *FHIRReferenceInput `json:"condition,omitempty"`
Performer *FHIRReferenceInput `json:"performer,omitempty"`
ReasonCode []FHIRCodeableConceptInput `json:"reasonCode,omitempty"`
ReasonReference []FHIRReferenceInput `json:"reasonReference,omitempty"`
Basis []FHIRReferenceInput `json:"basis,omitempty"`
Prediction []FHIRRiskAssessmentPrediction `json:"prediction,omitempty"`
Mitigation *string `json:"mitigation,omitempty"`
Note []FHIRAnnotationInput `json:"note,omitempty"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -1776,7 +1776,7 @@ func (fh StoreImpl) CreateFHIRQuestionnaireResponse(_ context.Context, input *do
// CreateFHIRRiskAssessment creates a RiskAssessment on FHIR
// The RiskAssessment resource represents an assessment of the likely outcome(s) for a patient's health over
// a period of time, considering various factors.
func (fh StoreImpl) CreateFHIRRiskAssessment(_ context.Context, input *domain.FHIRRiskAssessment) (*domain.FHIRRiskAssessmentRelayPayload, error) {
func (fh StoreImpl) CreateFHIRRiskAssessment(_ context.Context, input *domain.FHIRRiskAssessmentInput) (*domain.FHIRRiskAssessmentRelayPayload, error) {
payload, err := converterandformatter.StructToMap(input)
if err != nil {
return nil, fmt.Errorf("unable to turn %s input into a map: %w", riskAssessmentResourceType, err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4776,7 +4776,7 @@ func TestStoreImpl_CreateFHIRQuestionnaireResponse(t *testing.T) {
func TestStoreImpl_CreateFHIRRiskAssessment(t *testing.T) {
type args struct {
ctx context.Context
input *domain.FHIRRiskAssessment
input *domain.FHIRRiskAssessmentInput
}
tests := []struct {
name string
Expand All @@ -4788,15 +4788,15 @@ func TestStoreImpl_CreateFHIRRiskAssessment(t *testing.T) {
name: "Happy Case - Successfully create a risk assessment",
args: args{
ctx: context.Background(),
input: &domain.FHIRRiskAssessment{},
input: &domain.FHIRRiskAssessmentInput{},
},
wantErr: false,
},
{
name: "Sad Case - Fail to create a risk assessment",
args: args{
ctx: context.Background(),
input: &domain.FHIRRiskAssessment{},
input: &domain.FHIRRiskAssessmentInput{},
},
wantErr: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type FHIRMock struct {
MockCreateFHIRQuestionnaireFn func(ctx context.Context, input *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error)
MockCreateFHIRConsentFn func(ctx context.Context, input domain.FHIRConsent) (*domain.FHIRConsent, error)
MockCreateFHIRQuestionnaireResponseFn func(ctx context.Context, input *domain.FHIRQuestionnaireResponse) (*domain.FHIRQuestionnaireResponse, error)
MockCreateFHIRRiskAssessmentFn func(ctx context.Context, input *domain.FHIRRiskAssessment) (*domain.FHIRRiskAssessmentRelayPayload, error)
MockCreateFHIRRiskAssessmentFn func(ctx context.Context, input *domain.FHIRRiskAssessmentInput) (*domain.FHIRRiskAssessmentRelayPayload, error)
MockGetFHIRQuestionnaireFn func(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error)
}

Expand Down Expand Up @@ -1898,11 +1898,62 @@ func NewFHIRMock() *FHIRMock {
return &input, nil
},
MockCreateFHIRQuestionnaireResponseFn: func(ctx context.Context, input *domain.FHIRQuestionnaireResponse) (*domain.FHIRQuestionnaireResponse, error) {
return input, nil
ID := gofakeit.UUID()
highScore := 8
lowScore := 1
return &domain.FHIRQuestionnaireResponse{
ID: &ID,
Item: []domain.FHIRQuestionnaireResponseItem{
{
LinkID: "symptoms",
Answer: []domain.FHIRQuestionnaireResponseItemAnswer{},
Item: []domain.FHIRQuestionnaireResponseItem{
{
ID: new(string),
Extension: []domain.FHIRExtension{},
ModifierExtension: []domain.FHIRExtension{},
LinkID: "symptoms-score",
Definition: new(string),
Text: new(string),
Answer: []domain.FHIRQuestionnaireResponseItemAnswer{
{
ValueInteger: &highScore,
},
},
Item: []domain.FHIRQuestionnaireResponseItem{},
},
},
},
{
LinkID: "risk-factors",
Answer: []domain.FHIRQuestionnaireResponseItemAnswer{},
Item: []domain.FHIRQuestionnaireResponseItem{
{
ID: new(string),
Extension: []domain.FHIRExtension{},
ModifierExtension: []domain.FHIRExtension{},
LinkID: "risk-factors-score",
Definition: new(string),
Text: new(string),
Answer: []domain.FHIRQuestionnaireResponseItemAnswer{
{
ValueInteger: &lowScore,
},
},
Item: []domain.FHIRQuestionnaireResponseItem{},
},
},
},
},
}, nil
},
MockCreateFHIRRiskAssessmentFn: func(ctx context.Context, input *domain.FHIRRiskAssessment) (*domain.FHIRRiskAssessmentRelayPayload, error) {
MockCreateFHIRRiskAssessmentFn: func(ctx context.Context, input *domain.FHIRRiskAssessmentInput) (*domain.FHIRRiskAssessmentRelayPayload, error) {
riskAssessment := &domain.FHIRRiskAssessment{
ID: new(string),
Meta: &domain.FHIRMeta{},
}
return &domain.FHIRRiskAssessmentRelayPayload{
Resource: input,
Resource: riskAssessment,
}, nil
},
MockGetFHIRQuestionnaireFn: func(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error) {
Expand Down Expand Up @@ -2255,7 +2306,7 @@ func (fh *FHIRMock) CreateFHIRQuestionnaireResponse(ctx context.Context, input *
}

// CreateFHIRRiskAssessment mocks the method for creating a fhir risk assessment record
func (fh *FHIRMock) CreateFHIRRiskAssessment(ctx context.Context, input *domain.FHIRRiskAssessment) (*domain.FHIRRiskAssessmentRelayPayload, error) {
func (fh *FHIRMock) CreateFHIRRiskAssessment(ctx context.Context, input *domain.FHIRRiskAssessmentInput) (*domain.FHIRRiskAssessmentRelayPayload, error) {
return fh.MockCreateFHIRRiskAssessmentFn(ctx, input)
}

Expand Down
22 changes: 19 additions & 3 deletions pkg/clinical/presentation/graph/generated/generated.go

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

2 changes: 1 addition & 1 deletion pkg/clinical/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,5 @@ type FHIRQuestionnaireResponse interface {
}

type FHIRRiskAssessment interface {
CreateFHIRRiskAssessment(_ context.Context, input *domain.FHIRRiskAssessment) (*domain.FHIRRiskAssessmentRelayPayload, error)
CreateFHIRRiskAssessment(_ context.Context, input *domain.FHIRRiskAssessmentInput) (*domain.FHIRRiskAssessmentRelayPayload, error)
}
119 changes: 118 additions & 1 deletion pkg/clinical/usecases/clinical/questionnaire_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"fmt"

"github.com/mitchellh/mapstructure"
"github.com/savannahghi/clinical/pkg/clinical/application/common"
"github.com/savannahghi/clinical/pkg/clinical/application/dto"
"github.com/savannahghi/clinical/pkg/clinical/domain"
"github.com/savannahghi/scalarutils"
)

// CreateQuestionnaireResponse creates a questionnaire response
Expand Down Expand Up @@ -58,17 +60,132 @@ func (u *UseCasesClinicalImpl) CreateQuestionnaireResponse(ctx context.Context,
questionnaireResponse.Status = input.Status

resp, err := u.infrastructure.FHIR.CreateFHIRQuestionnaireResponse(ctx, questionnaireResponse)

if err != nil {
return nil, err
}

output := &dto.QuestionnaireResponse{}

err = mapstructure.Decode(resp, output)
if err != nil {
return nil, err
}

// TODO: This will affect the API performance. Optimize it
err = u.generateQuestionnaireReviewSummary(
ctx,
questionnaireID,
*resp.ID,
encounterID,
output,
)
if err != nil {
return nil, err
}

return output, 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
// questionnaire has groups with linkIds: symptoms, family_history, and risk-factors.
// The function looks into the responses saved under the tags <group_name>-score,
// calculates the total scores for each group, and returns a summary indicating
// whether the individual is high risk, low risk, or average risk.
func (u *UseCasesClinicalImpl) generateQuestionnaireReviewSummary(
ctx context.Context,
questionnaireID,
questionnaireResponseID,
encounterID string,
questionnaireResponse *dto.QuestionnaireResponse,
) error {
questionnaire, err := u.infrastructure.FHIR.GetFHIRQuestionnaire(ctx, questionnaireID)
if err != nil {
return err
}

switch *questionnaire.Resource.Name {
// TODO: Make this a controlled enum?
case "Cervical Cancer Screening":
var symptomsScore, riskFactorsScore, totalScore int

for _, item := range questionnaireResponse.Item {
if item.LinkID == "symptoms" {
for _, answer := range item.Item {
if answer.LinkID == "symptoms-score" {
symptomsScore = *answer.Answer[0].ValueInteger
}
}
}

if item.LinkID == "risk-factors" {
for _, answer := range item.Item {
if answer.LinkID == "risk-factors-score" {
riskFactorsScore = *answer.Answer[0].ValueInteger
}
}
}
}

totalScore = symptomsScore + riskFactorsScore

switch {
case totalScore >= 2:
err := u.recordRiskAssessment(
ctx,
encounterID,
questionnaireResponseID,
common.HighRiskCIELCode,
"High Risk",
)
if err != nil {
return err
}

case totalScore < 2:
err := u.recordRiskAssessment(
ctx,
encounterID,
questionnaireResponseID,
common.LowRiskCIELCode,
"Low Risk",
)
if err != nil {
return err
}
}
default:
return fmt.Errorf("questionnaire does not exist")
}

return nil
}

func (u *UseCasesClinicalImpl) recordRiskAssessment(
ctx context.Context,
encounterID,
questionnaireResponseID string,
outcomeCode, outcomeDisplay string,
) error {
CIELTerminologySystem := scalarutils.URI(common.CIELTerminologySystem)
codingCode := scalarutils.Code(outcomeCode)

outcome := domain.FHIRCodeableConcept{
Coding: []*domain.FHIRCoding{
{
System: &CIELTerminologySystem,
Code: &codingCode,
Display: outcomeDisplay,
},
},
Text: outcomeDisplay,
}

_, err := u.RecordRiskAssessment(ctx, encounterID, questionnaireResponseID, outcome)
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit f08bec5

Please sign in to comment.