Skip to content

Commit

Permalink
feat: [OHE-472] load questionnaire to fhir registry
Browse files Browse the repository at this point in the history
Signed-off-by: Kathurima Kimathi <kathurimakimathi415@gmail.com>

Link to documenation:
https://savannahghi.atlassian.net/wiki/spaces/OHE/pages/468025506/API+Documentation#Load-Questionnaires
  • Loading branch information
KathurimaKimathi committed Jan 24, 2024
1 parent 2df4beb commit c4a2bbe
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 0 deletions.
126 changes: 126 additions & 0 deletions pkg/clinical/domain/questionnaire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package domain

import (
"time"

"github.com/savannahghi/scalarutils"
)

// FHIRQuestionnaire models the FHIR questionnaire model as described in https://www.hl7.org/fhir/questionnaire.html
type FHIRQuestionnaire struct {
ID *string `json:"id,omitempty"`
Meta *FHIRMetaInput `json:"meta,omitempty"`
ImplicitRules *string `json:"implicitRules,omitempty"`
Language *string `json:"language,omitempty"`
Text *FHIRNarrative `json:"text,omitempty"`
FHIRExtension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
URL *scalarutils.URI `json:"url,omitempty"`
Identifier []*FHIRIdentifier `json:"identifier,omitempty"`
Version *string `json:"version,omitempty"`
Name *string `json:"name,omitempty"`
Title *string `json:"title,omitempty"`
DerivedFrom []*string `json:"derivedFrom,omitempty"`
Status *scalarutils.Code `json:"status,omitempty"`
Experimental *bool `json:"experimental,omitempty"`
Date *scalarutils.DateTime `json:"date,omitempty"`
Publisher *string `json:"publisher,omitempty"`
Description *string `json:"description,omitempty"`
UseContext *FHIRUsageContext `json:"useContext,omitempty"`
Jurisdiction []*FHIRCodeableConcept `json:"jurisdiction,omitempty"`
Purpose *string `json:"purpose,omitempty"`
EffectivePeriod *FHIRPeriod `json:"effectivePeriod,omitempty"`
Code []*FHIRCoding `json:"code,omitempty"`
Item []*FHIRQuestionnaireItem `json:"item,omitempty"`
}

// FHIRQuestionnaireItem represents the questions and sections within a FHIR questionnaire
type FHIRQuestionnaireItem struct {
ID *string `json:"id,omitempty"`
Meta *FHIRMeta `json:"meta,omitempty"`
FHIRExtension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
LinkID *string `json:"linkId,omitempty"`
Definition *scalarutils.URI `json:"definition,omitempty"`
Code []*FHIRCoding `json:"code,omitempty"`
Prefix *string `json:"prefix,omitempty"`
Text *string `json:"text,omitempty"`
Type *scalarutils.Code `json:"type,omitempty"`
EnableWhen []*FHIRQuestionnaireItemEnableWhen `json:"enableWhen,omitempty"`
EnableBehavior *scalarutils.Code `json:"enableBehavior,omitempty"`
DisabledDisplay *scalarutils.Code `json:"disabledDisplay,omitempty"`
Required *bool `json:"required,omitempty"`
Repeats *bool `json:"repeats,omitempty"`
ReadOnly *bool `json:"readOnly,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
AnswerValueSet *string `json:"answerValueSet,omitempty"`
AnswerOption []*FHIRQuestionnaireItemAnswerOption `json:"answerOption,omitempty"`
Initial []*FHIRQuestionnaireItemInitial `json:"initial,omitempty"`
Item []*FHIRQuestionnaireItem `json:"item,omitempty"`
}

// FHIRQuestionnaireItemEnableWhen defines when to enable the FHIR Questionnaire item.
type FHIRQuestionnaireItemEnableWhen struct {
ID *string `json:"id,omitempty"`
FHIRExtension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
Question *string `json:"question,omitempty"`
Operator *scalarutils.Code `json:"operator,omitempty"`
AnswerBoolean *bool `json:"answerBoolean,omitempty"`
AnswerDecimal *float64 `json:"answerDecimal,omitempty"`
AnswerInteger *int `json:"answerInteger,omitempty"`
AnswerDate *scalarutils.Date `json:"answerDate,omitempty"`
AnswerDateTime *scalarutils.DateTime `json:"answerDateTime,omitempty"`
AnswerTime *scalarutils.DateTime `json:"answerTime,omitempty"`
AnswerString *string `json:"answerString,omitempty"`
AnswerCoding *FHIRCoding `json:"answerCoding,omitempty"`
AnswerQuantity *FHIRQuantity `json:"answerQuantity,omitempty"`
AnswerReference *FHIRReference `json:"answerReference,omitempty"`
}

// FHIRQuestionnaireItemAnswerOption represents the permitted answers to a questionnaire.
// ! Rule: A question cannot have both answerOption and answerValueSet
// ! Rule: Only coding, decimal, integer, date, dateTime, time, string or quantity items can have answerOption or answerValueSet
// ! Rule: If one or more answerOption is present, initial cannot be present. Use answerOption.initialSelected instead
type FHIRQuestionnaireItemAnswerOption struct {
ID *string `json:"id,omitempty"`
FHIRExtension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
ValueInteger *int `json:"valueInteger,omitempty"`
ValueDate *scalarutils.Date `json:"valueDate,omitempty"`
ValueTime *time.Time `json:"valueTime,omitempty"`
ValueString string `json:"valueString,omitempty"`
ValueCoding *FHIRCoding `json:"valueCoding,omitempty"`
ValueReference *FHIRReference `json:"valueReference,omitempty"`
InitialSelected *bool `json:"initialSelected,omitempty"`
}

// FHIRQuestionnaireItemInitial defines the initial value(s) when a questionnaire item is first rendered
type FHIRQuestionnaireItemInitial struct {
ID *string `json:"id,omitempty"`
FHIRExtension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
ValueBoolean *bool `json:"valueBoolean,omitempty"`
ValueDecimal *float64 `json:"valueDecimal,omitempty"`
ValueInteger *int `json:"valueInteger,omitempty"`
ValueDate *scalarutils.Date `json:"valueDate,omitempty"`
ValueDateTime *scalarutils.DateTime `json:"valueDateTime,omitempty"`
ValueTime *time.Time `json:"valueTime,omitempty"`
ValueString string `json:"valueString,omitempty"`
ValueURI *scalarutils.URI `json:"valueUri,omitempty"`
ValueAttachment *FHIRAttachment `json:"valueAttachment,omitempty"`
ValueCoding *FHIRCoding `json:"valueCoding,omitempty"`
ValueQuantity *FHIRQuantity `json:"valueQuantity,omitempty"`
ValueReference *FHIRReference `json:"valueReference,omitempty"`
}

// FHIRUsageContext describes the context that the questionnaire content is intended to support
type FHIRUsageContext struct {
ID *string `json:"id,omitempty"`
FHIRExtension []*Extension `json:"extension,omitempty"`
Code *FHIRCoding `json:"code,omitempty"`
ValueCodeableConcept *FHIRCodeableConcept `json:"valueCodeableConcept,omitempty"`
ValueQuantity *FHIRQuantity `json:"valueQuantity,omitempty"`
ValueRange *FHIRRange `json:"valueRange,omitempty"`
ValueReference *FHIRReference `json:"valueReference,omitempty"`
}
18 changes: 18 additions & 0 deletions pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
medicationStatementResourceType = "MedicationStatement"
medicationResourceType = "Medication"
mediaResourceType = "Media"
questionnaireResourceType = "Questionnaire"
)

// Dataset ...
Expand Down Expand Up @@ -1682,3 +1683,20 @@ func (fh StoreImpl) PatchFHIRObservation(_ context.Context, id string, input dom

return resource, nil
}

// CreateFHIRQuestionnaire is used to create a FHIR Questionnaire resource
func (fh StoreImpl) CreateFHIRQuestionnaire(_ context.Context, input *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error) {
payload, err := converterandformatter.StructToMap(input)
if err != nil {
return nil, fmt.Errorf("unable to turn %s input into a map: %w", questionnaireResourceType, err)
}

resource := &domain.FHIRQuestionnaire{}

err = fh.Dataset.CreateFHIRResource(questionnaireResourceType, payload, resource)
if err != nil {
return nil, fmt.Errorf("unable to create %s resource: %w", questionnaireResourceType, err)
}

return resource, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -4390,3 +4390,80 @@ func TestStoreImpl_SearchPatientMedia(t *testing.T) {
})
}
}

func TestStoreImpl_CreateFHIRQuestionnaire(t *testing.T) {
ID := gofakeit.UUID()
type args struct {
ctx context.Context
input *domain.FHIRQuestionnaire
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Happy case: successfully create a questionnaire resource",
args: args{
ctx: context.Background(),
input: &domain.FHIRQuestionnaire{
ID: &ID,
Meta: &domain.FHIRMetaInput{},
ImplicitRules: new(string),
Language: new(string),
Text: &domain.FHIRNarrative{},
FHIRExtension: []*domain.Extension{},
ModifierExtension: []*domain.Extension{},
Identifier: []*domain.FHIRIdentifier{},
Version: new(string),
Name: new(string),
Title: new(string),
DerivedFrom: []*string{},
Experimental: new(bool),
Publisher: new(string),
Description: new(string),
UseContext: &domain.FHIRUsageContext{},
Jurisdiction: []*domain.FHIRCodeableConcept{},
Purpose: new(string),
EffectivePeriod: &domain.FHIRPeriod{},
Code: []*domain.FHIRCoding{},
Item: []*domain.FHIRQuestionnaireItem{},
},
},
wantErr: false,
},
{
name: "Sad case: unable to create a questionnaire resource",
args: args{
ctx: context.Background(),
input: &domain.FHIRQuestionnaire{
ID: &ID,
Meta: &domain.FHIRMetaInput{},
ImplicitRules: new(string),
Language: new(string),
Text: &domain.FHIRNarrative{},
FHIRExtension: []*domain.Extension{},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeDataset := fakeDataset.NewFakeFHIRRepositoryMock()
fh := FHIR.NewFHIRStoreImpl(fakeDataset)

if tt.name == "Sad case: unable to create a questionnaire resource" {
fakeDataset.MockCreateFHIRResourceFn = func(resourceType string, payload map[string]interface{}, resource interface{}) error {
return fmt.Errorf("an error ocurred")
}
}

_, err := fh.CreateFHIRQuestionnaire(tt.args.ctx, tt.args.input)
if (err != nil) != tt.wantErr {
t.Errorf("StoreImpl.CreateFHIRQuestionnaire() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type FHIRMock struct {
MockSearchPatientAllergyIntoleranceFn func(ctx context.Context, patientReference string, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRAllergy, error)
MockCreateFHIRMediaFn func(ctx context.Context, input domain.FHIRMedia) (*domain.FHIRMedia, error)
MockSearchPatientMediaFn func(ctx context.Context, patientReference string, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRMedia, error)
MockCreateFHIRQuestionnaireFn func(ctx context.Context, input *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error)
}

// NewFHIRMock initializes a new instance of FHIR mock
Expand Down Expand Up @@ -1841,6 +1842,31 @@ func NewFHIRMock() *FHIRMock {
TotalCount: 0,
}, nil
},
MockCreateFHIRQuestionnaireFn: func(ctx context.Context, input *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error) {
return &domain.FHIRQuestionnaire{
ID: new(string),
Meta: &domain.FHIRMetaInput{},
ImplicitRules: new(string),
Language: new(string),
Text: &domain.FHIRNarrative{},
FHIRExtension: []*domain.Extension{},
ModifierExtension: []*domain.Extension{},
Identifier: []*domain.FHIRIdentifier{},
Version: new(string),
Name: new(string),
Title: new(string),
DerivedFrom: []*string{},
Experimental: new(bool),
Publisher: new(string),
Description: new(string),
UseContext: &domain.FHIRUsageContext{},
Jurisdiction: []*domain.FHIRCodeableConcept{},
Purpose: new(string),
EffectivePeriod: &domain.FHIRPeriod{},
Code: []*domain.FHIRCoding{},
Item: []*domain.FHIRQuestionnaireItem{},
}, nil
},
}
}

Expand Down Expand Up @@ -2143,3 +2169,8 @@ func (fh *FHIRMock) CreateFHIRMedia(ctx context.Context, input domain.FHIRMedia)
func (fh *FHIRMock) SearchPatientMedia(ctx context.Context, patientReference string, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRMedia, error) {
return fh.MockSearchPatientMediaFn(ctx, patientReference, tenant, pagination)
}

// CreateFHIRQuestionnaire mocks the creation of a new Questionnaire resource.
func (fh *FHIRMock) CreateFHIRQuestionnaire(ctx context.Context, input *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error) {
return fh.MockCreateFHIRQuestionnaireFn(ctx, input)
}
5 changes: 5 additions & 0 deletions pkg/clinical/presentation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ func SetupRoutes(r *gin.Engine, cacheStore persist.CacheStore, authclient *authu
upload.Use(rest.AuthenticationGinMiddleware(cacheStore, *authclient))
upload.Use(rest.TenantIdentifierExtractionMiddleware(infra.FHIR))
upload.POST("", handlers.UploadMedia)

questionnaire := v1.Group("/questionnaire")
questionnaire.Use(rest.AuthenticationGinMiddleware(cacheStore, *authclient))
questionnaire.Use(rest.TenantIdentifierExtractionMiddleware(infra.FHIR))
questionnaire.POST("", handlers.LoadQuestionnaire)
}

// GQLHandler sets up a GraphQL resolver
Expand Down
20 changes: 20 additions & 0 deletions pkg/clinical/presentation/rest/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/savannahghi/clinical/pkg/clinical/application/common"
"github.com/savannahghi/clinical/pkg/clinical/application/dto"
"github.com/savannahghi/clinical/pkg/clinical/application/utils"
"github.com/savannahghi/clinical/pkg/clinical/domain"
"github.com/savannahghi/clinical/pkg/clinical/usecases"
"github.com/savannahghi/errorcodeutil"
"github.com/savannahghi/pubsubtools"
Expand Down Expand Up @@ -312,3 +313,22 @@ func (p PresentationHandlersImpl) UploadMedia(c *gin.Context) {

c.JSON(http.StatusOK, response)
}

// LoadQuestionnaire is used to upload a user defined questionnaire for the purpose of soliciting client data.
func (p PresentationHandlersImpl) LoadQuestionnaire(c *gin.Context) {
input := domain.FHIRQuestionnaire{}

err := c.BindJSON(&input)
if err != nil {
jsonErrorResponse(c, http.StatusBadRequest, err)
return
}

questionnaire, err := p.usecases.CreateQuestionnaire(c.Request.Context(), &input)
if err != nil {
jsonErrorResponse(c, http.StatusBadRequest, err)
return
}

c.JSON(http.StatusOK, questionnaire)
}
5 changes: 5 additions & 0 deletions pkg/clinical/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type FHIR interface {
FHIRMedicationStatement
FHIRMedication
FHIRMedia
FHIRQuestionnaire
}

type FHIROrganization interface {
Expand Down Expand Up @@ -111,3 +112,7 @@ type FHIRMedia interface {
CreateFHIRMedia(ctx context.Context, input domain.FHIRMedia) (*domain.FHIRMedia, error)
SearchPatientMedia(ctx context.Context, patientReference string, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRMedia, error)
}

type FHIRQuestionnaire interface {
CreateFHIRQuestionnaire(ctx context.Context, input *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error)
}
27 changes: 27 additions & 0 deletions pkg/clinical/usecases/clinical/questionnaire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package clinical

import (
"context"

"github.com/savannahghi/clinical/pkg/clinical/domain"
)

// CreateQuestionnaire is used to create a new Questionnaire.
// These questionnaire are used to solicit various types of information from patients to server organisation usecases.
func (q *UseCasesClinicalImpl) CreateQuestionnaire(ctx context.Context, questionnaireInput *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error) {
tags, err := q.GetTenantMetaTags(ctx)
if err != nil {
return nil, err
}

questionnaireInput.Meta = &domain.FHIRMetaInput{
Tag: tags,
}

questionnaire, err := q.infrastructure.FHIR.CreateFHIRQuestionnaire(ctx, questionnaireInput)
if err != nil {
return nil, err
}

return questionnaire, nil
}
Loading

0 comments on commit c4a2bbe

Please sign in to comment.