Skip to content

Commit

Permalink
fix: update questionnaire response score calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Salaton committed Apr 2, 2024
1 parent 7cb651f commit a12d992
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 47 deletions.
124 changes: 88 additions & 36 deletions pkg/clinical/usecases/clinical/questionnaire_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,9 @@ func (u *UseCasesClinicalImpl) generateQuestionnaireReviewSummary(
switch *questionnaire.Resource.Title {
// 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
}
}
}
scores := calculateScoresFromResponses(questionnaire.Resource, questionnaireResponse)

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

totalScore = symptomsScore + riskFactorsScore
totalScore := scores["symptoms"] + scores["risk-factors"]

switch {
case totalScore >= 2:
Expand Down Expand Up @@ -183,23 +165,9 @@ func (u *UseCasesClinicalImpl) generateQuestionnaireReviewSummary(
}

case "Breast Cancer Screening":
var riskScore int

for _, item := range questionnaireResponse.Item {
if item.LinkID == "risk-assessment" {
for _, answer := range item.Item {
if answer.LinkID == "high-risk" {
for _, ans := range answer.Item {
if ans.LinkID == "high-risk-score" {
riskScore = *ans.Answer[0].ValueInteger
}
}
}
}
}
}
scores := calculateScoresFromResponses(questionnaire.Resource, questionnaireResponse)

if riskScore >= 1 {
if scores["risk-assessment"] >= 1 {
riskLevel, err = u.recordRiskAssessment(
ctx,
encounter,
Expand Down Expand Up @@ -232,6 +200,90 @@ func (u *UseCasesClinicalImpl) generateQuestionnaireReviewSummary(
return riskLevel, nil
}

// calculateScoresFromResponses Calculates scores for each group in the questionnaire based on responses.
// This function iterates through each item in the QuestionnaireResponse, matching it with its corresponding item in the Questionnaire
// to apply scoring rules based on 'ordinalValue'. This matching is essential to accurately compute scores since it directly ties
// the respondent's answers back to the structured definitions within the Questionnaire, including the consideration of 'ordinalValue'
// for scoring purposes.
func calculateScoresFromResponses(questionnaire *domain.FHIRQuestionnaire, response *dto.QuestionnaireResponse) map[string]int {
groupScores := make(map[string]int) // Holds the calculated scores for each group.

for _, responseGroup := range response.Item {
score := 0
groupLinkId := responseGroup.LinkID

for _, questionnaireGroup := range questionnaire.Item {
if *questionnaireGroup.LinkID == groupLinkId {
// Calculate score for this group based on responses
score = calculateGroupScore(questionnaireGroup.Item, responseGroup.Item)
break
}
}

groupScores[groupLinkId] = score
}

return groupScores
}

func calculateGroupScore(questionnaireItems []*domain.FHIRQuestionnaireItem, responseItems []dto.QuestionnaireResponseItem) int {
score := 0

// Extract the ordinal values and their corresponding display text (e.g., "Yes").
ordinalValues := mapLinkIdsToOrdinalValuesAndDisplay(questionnaireItems)

for _, responseItem := range responseItems {
if val, ok := ordinalValues[responseItem.LinkID]; ok {
for _, answer := range responseItem.Answer {
if answer.ValueCoding.Display == val.display {
score += val.ordinalValue
}
}
}
}

return score
}

// mapLinkIdsToOrdinalValuesAndDisplay Maps each linkId in the Questionnaire to its corresponding 'ordinalValue', if available.
// This mapping facilitates a more efficient scoring process by eliminating the need to repeatedly search through the
// questionnaire structure for 'ordinalValue' extensions during scoring.
func mapLinkIdsToOrdinalValuesAndDisplay(items []*domain.FHIRQuestionnaireItem) map[string]struct {
ordinalValue int
display string
} {
ordinalValues := make(map[string]struct {
ordinalValue int
display string
})

for _, item := range items {
for _, answerOption := range item.AnswerOption {
// Check if this answerOption has an ordinalValue and is a "Yes".
hasOrdinalValue := false
ordinalValue := 0
displayText := ""

for _, ext := range answerOption.Extension {
if ext.URL == "http://hl7.org/fhir/StructureDefinition/ordinalValue" {
hasOrdinalValue = true
ordinalValue = int(*ext.ValueDecimal)
}
}

if hasOrdinalValue && answerOption.ValueCoding.Display == "Yes" {
displayText = answerOption.ValueCoding.Display
ordinalValues[*item.LinkID] = struct {
ordinalValue int
display string
}{ordinalValue, displayText}
}
}
}

return ordinalValues
}

func (u *UseCasesClinicalImpl) recordRiskAssessment(
ctx context.Context,
encounter *domain.FHIREncounterRelayPayload,
Expand Down
77 changes: 66 additions & 11 deletions pkg/clinical/usecases/clinical/questionnaire_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,39 @@ func setupMockFHIRFunctions(fakeFHIR *fakeFHIRMock.FHIRMock, score int) {
ID := gofakeit.UUID()
fakeFHIR.MockGetFHIRQuestionnaireFn = func(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error) {
questionnaireName := "Cervical Cancer Screening"
linkID := "symptoms"
questionLinkID := "symptoms-question-one"
valueDecimalOne := 1.0
return &domain.FHIRQuestionnaireRelayPayload{
Resource: &domain.FHIRQuestionnaire{
ID: &ID,
Name: &questionnaireName,
Title: &questionnaireName,
Item: []*domain.FHIRQuestionnaireItem{
{
LinkID: &linkID,
Item: []*domain.FHIRQuestionnaireItem{
{
ID: &questionLinkID,
LinkID: &questionLinkID,
Meta: &domain.FHIRMeta{},
AnswerOption: []*domain.FHIRQuestionnaireItemAnswerOption{
{
Extension: []*domain.Extension{
{
URL: "http://hl7.org/fhir/StructureDefinition/ordinalValue",
ValueDecimal: &valueDecimalOne,
},
},
ValueCoding: &domain.FHIRCoding{
Display: "Yes",
},
},
},
},
},
},
},
},
}, nil
}
Expand All @@ -39,10 +67,12 @@ func setupMockFHIRFunctions(fakeFHIR *fakeFHIRMock.FHIRMock, score int) {
LinkID: "symptoms",
Item: []domain.FHIRQuestionnaireResponseItem{
{
LinkID: "symptoms-score",
LinkID: "symptoms-question-one",
Answer: []domain.FHIRQuestionnaireResponseItemAnswer{
{
ValueInteger: &score,
ValueCoding: &domain.FHIRCoding{
Display: "Yes",
},
},
},
},
Expand Down Expand Up @@ -367,16 +397,43 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) {
if tt.name == "Happy Case - Create questionnaire response and generate review summary - Breast Cancer - High Risk" {
fakeFHIR.MockGetFHIRQuestionnaireFn = func(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error) {
questionnaireName := "Breast Cancer Screening"
linkID := "risk-assessment"
questionLinkID := "risk-assessment-question-one"
valueDecimalOne := 1.0
return &domain.FHIRQuestionnaireRelayPayload{
Resource: &domain.FHIRQuestionnaire{
ID: &ID,
Name: &questionnaireName,
Title: &questionnaireName,
Item: []*domain.FHIRQuestionnaireItem{
{
LinkID: &linkID,
Item: []*domain.FHIRQuestionnaireItem{
{
ID: &questionLinkID,
LinkID: &questionLinkID,
Meta: &domain.FHIRMeta{},
AnswerOption: []*domain.FHIRQuestionnaireItemAnswerOption{
{
Extension: []*domain.Extension{
{
URL: "http://hl7.org/fhir/StructureDefinition/ordinalValue",
ValueDecimal: &valueDecimalOne,
},
},
ValueCoding: &domain.FHIRCoding{
Display: "Yes",
},
},
},
},
},
},
},
},
}, nil
}

score := 3
fakeFHIR.MockCreateFHIRQuestionnaireResponseFn = func(ctx context.Context, input *domain.FHIRQuestionnaireResponse) (*domain.FHIRQuestionnaireResponse, error) {
return &domain.FHIRQuestionnaireResponse{
ID: &ID,
Expand All @@ -385,15 +442,11 @@ func TestUseCasesClinicalImpl_CreateQuestionnaireResponse(t *testing.T) {
LinkID: "risk-assessment",
Item: []domain.FHIRQuestionnaireResponseItem{
{
LinkID: "high-risk",

Item: []domain.FHIRQuestionnaireResponseItem{
LinkID: "risk-assessment-question-one",
Answer: []domain.FHIRQuestionnaireResponseItemAnswer{
{
LinkID: "high-risk-score",
Answer: []domain.FHIRQuestionnaireResponseItemAnswer{
{
ValueInteger: &score,
},
ValueCoding: &domain.FHIRCoding{
Display: "Yes",
},
},
},
Expand Down Expand Up @@ -472,11 +525,13 @@ 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 - fail 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 - fail to publish to pusbsub" {
fakePubSub.MockNotifySegmentationFn = func(ctx context.Context, data dto.SegmentationPayload) error {
return fmt.Errorf("failed to publish to pubsub")
Expand Down

0 comments on commit a12d992

Please sign in to comment.