Skip to content

Commit

Permalink
feat: record mammography result
Browse files Browse the repository at this point in the history
- This API is used to record mammography results

Link to documentation: [https://savannahghi.atlassian.net/wiki/spaces/OHE/pages/468025506/API+Documentation#Record-Mammography-Result]

Signed-off-by: Kathurima Kimathi <kathurimakimathi415@gmail.com>
  • Loading branch information
KathurimaKimathi committed Feb 5, 2024
1 parent f08bec5 commit 4f926da
Show file tree
Hide file tree
Showing 16 changed files with 758 additions and 46 deletions.
6 changes: 6 additions & 0 deletions pkg/clinical/application/common/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ const (

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

// MammogramTerminologyCode is the terminology code used to represent mammogram results.
MammogramTerminologyCode = "163591"

// BenignNeoplasmOfBreastOfSkinTerminologyCode is the terminology code used to represent benign of skin results.
BenignNeoplasmOfBreastOfSkinTerminologyCode = "147661"
)

// DefaultIdentifier assigns a patient a code to function as their
Expand Down
11 changes: 9 additions & 2 deletions pkg/clinical/application/dto/diagnostic_report_output.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package dto

type DiagnosticReportOutput struct {
ID string `json:"id,omitempty"`
type DiagnosticReport struct {
ID string `json:"id,omitempty"`
Status ObservationStatus `json:"status,omitempty"`
PatientID string `json:"patientID,omitempty"`
EncounterID string `json:"encounterID,omitempty"`
Issued string `json:"issued,omitempty"`
Result []*Observation `json:"result,omitempty"`
Media []*Media `json:"media,omitempty"`
Conclusion string `json:"conclusion,omitempty"`
}
19 changes: 18 additions & 1 deletion pkg/clinical/application/dto/input.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dto

import (
"io"
"mime/multipart"

"github.com/go-playground/validator"
Expand Down Expand Up @@ -31,7 +32,7 @@ type EncounterInput struct {
type ObservationInput struct {
Status ObservationStatus `json:"status,omitempty" validate:"required"`
EncounterID string `json:"encounterID,omitempty" validate:"required"`
Value string `json:"value,omitempty" validate:"required"`
Value string `json:"value,omitempty"`
}

func (o ObservationInput) Validate() error {
Expand Down Expand Up @@ -222,3 +223,19 @@ type Expression struct {
Expression *string `json:"expression,omitempty"`
Reference *string `json:"reference,omitempty"`
}

// DiagnosticReportInput represents the data class used to provide diagnostic report information
type DiagnosticReportInput struct {
EncounterID string `json:"encounterID,omitempty" validate:"required"`
Note string `json:"note,omitempty"`
Attachment map[string][]*multipart.FileHeader `form:"attachment" json:"attachment" validate:"required"`
Findings string `json:"findings,omitempty" validate:"required"`
File io.Reader `json:"file,omitempty"`
}

func (d DiagnosticReportInput) Validate() error {
v := validator.New()
err := v.Struct(d)

return err
}
1 change: 1 addition & 0 deletions pkg/clinical/application/extensions/mock/extension_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func NewFakeBaseExtensionMock() *FakeBaseExtension {
MockGetTenantIdentifiersFn: func(ctx context.Context) (*dto.TenantIdentifiers, error) {
return &dto.TenantIdentifiers{
OrganizationID: uuid.New().String(),
FacilityID: uuid.New().String(),
}, nil
},
MockVerifyPubSubJWTAndDecodePayloadFn: func(w http.ResponseWriter, r *http.Request) (*pubsubtools.PubSubPayload, error) {
Expand Down
101 changes: 71 additions & 30 deletions pkg/clinical/domain/diagnostic_report.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,81 @@
package domain

// DiagnosticReport is documented here http://hl7.org/fhir/StructureDefinition/DiagnosticReport
type DiagnosticReport struct {
ID *string `json:"id,omitempty"`
Meta *FHIRMeta `json:"meta,omitempty"`
ImplicitRules *string `json:"implicitRules,omitempty"`
Language *string `json:"language,omitempty"`
Text *FHIRNarrative `json:"text,omitempty"`
Extension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
Identifier []*FHIRIdentifier `json:"identifier,omitempty"`
BasedOn []*FHIRReference `json:"basedOn,omitempty"`
Status DiagnosticReportStatusEnum `json:"status"`
Category []*FHIRCodeableConcept `json:"category,omitempty"`
Code FHIRCodeableConcept `json:"code"`
Subject *FHIRReference `json:"subject,omitempty"`
Encounter *FHIRReference `json:"encounter,omitempty"`
EffectiveDateTime *string `json:"effectiveDateTime,omitempty"`
EffectivePeriod *FHIRPeriod `json:"effectivePeriod,omitempty"`
Issued *string `json:"issued,omitempty"`
Performer []*FHIRReference `json:"performer,omitempty"`
ResultsInterpreter []*FHIRReference `json:"resultsInterpreter,omitempty"`
Specimen []*FHIRReference `json:"specimen,omitempty"`
Result []*FHIRReference `json:"result,omitempty"`
ImagingStudy []*FHIRReference `json:"imagingStudy,omitempty"`
Media []*DiagnosticReportMedia `json:"media,omitempty"`
Conclusion *string `json:"conclusion,omitempty"`
ConclusionCode []*FHIRCodeableConcept `json:"conclusionCode,omitempty"`
PresentedForm []*FHIRAttachment `json:"presentedForm,omitempty"`
import "github.com/savannahghi/scalarutils"

// FHIRDiagnosticReport is documented here http://hl7.org/fhir/StructureDefinition/DiagnosticReport
type FHIRDiagnosticReport struct {
ID *string `json:"id,omitempty"`
Meta *FHIRMeta `json:"meta,omitempty"`
ImplicitRules *string `json:"implicitRules,omitempty"`
Language *string `json:"language,omitempty"`
Text *FHIRNarrative `json:"text,omitempty"`
Extension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
Identifier []*FHIRIdentifier `json:"identifier,omitempty"`
BasedOn []*FHIRReference `json:"basedOn,omitempty"`
Status DiagnosticReportStatusEnum `json:"status"`
Category []*FHIRCodeableConcept `json:"category,omitempty"`
Code FHIRCodeableConcept `json:"code"`
Subject *FHIRReference `json:"subject,omitempty"`
Encounter *FHIRReference `json:"encounter,omitempty"`
EffectiveDateTime *scalarutils.DateTime `json:"effectiveDateTime,omitempty"`
EffectivePeriod *FHIRPeriod `json:"effectivePeriod,omitempty"`
Issued *string `json:"issued,omitempty"`
Performer []*FHIRReference `json:"performer,omitempty"`
ResultsInterpreter []*FHIRReference `json:"resultsInterpreter,omitempty"`
Specimen []*FHIRReference `json:"specimen,omitempty"`
Result []*FHIRReference `json:"result,omitempty"`
ImagingStudy []*FHIRReference `json:"imagingStudy,omitempty"`
Media []*FHIRDiagnosticReportMedia `json:"media,omitempty"`
Conclusion *string `json:"conclusion,omitempty"`
ConclusionCode []*FHIRCodeableConcept `json:"conclusionCode,omitempty"`
PresentedForm []*FHIRAttachment `json:"presentedForm,omitempty"`
}

// DiagnosticReportMedia represents the key images associated with this report
type DiagnosticReportMedia struct {
// FHIRDiagnosticReportMedia represents the key images associated with this report
type FHIRDiagnosticReportMedia struct {
ID *string `json:"id,omitempty"`
Extension []*Extension `json:"extension,omitempty"`
ModifierExtension []*Extension `json:"modifierExtension,omitempty"`
Comment *string `json:"comment,omitempty"`
Link *FHIRReference `json:"link"`
}

// FHIRDiagnosticReportInput is documented here http://hl7.org/fhir/StructureDefinition/DiagnosticReport
type FHIRDiagnosticReportInput 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 []*FHIRExtension `json:"extension,omitempty"`
ModifierExtension []*FHIRExtension `json:"modifierExtension,omitempty"`
Identifier []*FHIRIdentifierInput `json:"identifier,omitempty"`
BasedOn []*FHIRReferenceInput `json:"basedOn,omitempty"`
Status DiagnosticReportStatusEnum `json:"status"`
Category []*FHIRCodeableConceptInput `json:"category,omitempty"`
Code FHIRCodeableConceptInput `json:"code"`
Subject *FHIRReferenceInput `json:"subject,omitempty"`
Encounter *FHIRReferenceInput `json:"encounter,omitempty"`
EffectiveDateTime *scalarutils.DateTime `json:"effectiveDateTime,omitempty"`
EffectivePeriod *FHIRPeriodInput `json:"effectivePeriod,omitempty"`
Issued *string `json:"issued,omitempty"`
Performer []*FHIRReferenceInput `json:"performer,omitempty"`
ResultsInterpreter []*FHIRReferenceInput `json:"resultsInterpreter,omitempty"`
Specimen []*FHIRReferenceInput `json:"specimen,omitempty"`
Result []*FHIRReferenceInput `json:"result,omitempty"`
ImagingStudy []*FHIRReferenceInput `json:"imagingStudy,omitempty"`
Media []*FHIRDiagnosticReportMediaInput `json:"media,omitempty"`
Conclusion *string `json:"conclusion,omitempty"`
ConclusionCode []*FHIRCodeableConceptInput `json:"conclusionCode,omitempty"`
PresentedForm []*FHIRAttachmentInput `json:"presentedForm,omitempty"`
}

// FHIRDiagnosticReportMediaInput represents the key images associated with this report
type FHIRDiagnosticReportMediaInput struct {
ID *string `json:"id,omitempty"`
Extension []*FHIRExtension `json:"extension,omitempty"`
ModifierExtension []*FHIRExtension `json:"modifierExtension,omitempty"`
Comment *string `json:"comment,omitempty"`
Link *FHIRReferenceInput `json:"link"`
}
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 @@ -39,6 +39,7 @@ const (
consentResourceType = "Consent"
questionnaireResponseResourceType = "QuestionnaireResponse"
riskAssessmentResourceType = "RiskAssessment"
diagnosticReportResourceType = "DiagnosticReport"
)

// Dataset ...
Expand Down Expand Up @@ -1809,3 +1810,20 @@ func (fh StoreImpl) GetFHIRQuestionnaire(_ context.Context, id string) (*domain.

return payload, nil
}

// CreateFHIRDiagnosticReport is used to create a diagnostic report resource for a patient
func (fh StoreImpl) CreateFHIRDiagnosticReport(_ context.Context, input *domain.FHIRDiagnosticReportInput) (*domain.FHIRDiagnosticReport, error) {
payload, err := converterandformatter.StructToMap(input)
if err != nil {
return nil, fmt.Errorf("unable to turn %s input into a map: %w", diagnosticReportResourceType, err)
}

resource := &domain.FHIRDiagnosticReport{}

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

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

func TestStoreImpl_CreateFHIRDiagnosticReport(t *testing.T) {
ID := gofakeit.UUID()
type args struct {
in0 context.Context
input *domain.FHIRDiagnosticReportInput
}
tests := []struct {
name string
args args
want *domain.FHIRDiagnosticReport
wantErr bool
}{
{
name: "Happy case: create diagnostic report",
args: args{
input: &domain.FHIRDiagnosticReportInput{
ID: &ID,
},
},
wantErr: false,
},
{
name: "Sad case: unable to create diagnostic report",
args: args{
input: &domain.FHIRDiagnosticReportInput{
ID: &ID,
},
},
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 diagnostic report" {
fakeDataset.MockCreateFHIRResourceFn = func(resourceType string, payload map[string]interface{}, resource interface{}) error {
return fmt.Errorf("an error ocurred")
}
}

_, err := fh.CreateFHIRDiagnosticReport(tt.args.in0, tt.args.input)
if (err != nil) != tt.wantErr {
t.Errorf("StoreImpl.CreateFHIRDiagnosticReport() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type FHIRMock struct {
MockCreateFHIRQuestionnaireResponseFn func(ctx context.Context, input *domain.FHIRQuestionnaireResponse) (*domain.FHIRQuestionnaireResponse, error)
MockCreateFHIRRiskAssessmentFn func(ctx context.Context, input *domain.FHIRRiskAssessmentInput) (*domain.FHIRRiskAssessmentRelayPayload, error)
MockGetFHIRQuestionnaireFn func(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error)
MockCreateFHIRDiagnosticReportFn func(_ context.Context, input *domain.FHIRDiagnosticReportInput) (*domain.FHIRDiagnosticReport, error)
}

// NewFHIRMock initializes a new instance of FHIR mock
Expand Down Expand Up @@ -1572,10 +1573,11 @@ func NewFHIRMock() *FHIRMock {
return &domain.FHIRMedicationRelayPayload{}, nil
},
MockCreateFHIRMediaFn: func(ctx context.Context, input domain.FHIRMedia) (*domain.FHIRMedia, error) {
id := uuid.New().String()
id := "1"
url := gofakeit.URL()
title := gofakeit.BeerName()
return &domain.FHIRMedia{
ID: &id,
Status: "",
Subject: &domain.FHIRReferenceInput{
ID: &id,
Expand Down Expand Up @@ -1982,6 +1984,40 @@ func NewFHIRMock() *FHIRMock {
}
return &domain.FHIRQuestionnaireRelayPayload{Resource: resource}, nil
},
MockCreateFHIRDiagnosticReportFn: func(_ context.Context, input *domain.FHIRDiagnosticReportInput) (*domain.FHIRDiagnosticReport, error) {
ID := "1234"
return &domain.FHIRDiagnosticReport{
ID: new(string),
Meta: &domain.FHIRMeta{},
ImplicitRules: new(string),
Language: new(string),
Text: &domain.FHIRNarrative{},
Extension: []*domain.Extension{},
ModifierExtension: []*domain.Extension{},
Identifier: []*domain.FHIRIdentifier{},
BasedOn: []*domain.FHIRReference{},
Status: "",
Category: []*domain.FHIRCodeableConcept{},
Code: domain.FHIRCodeableConcept{},
Subject: &domain.FHIRReference{
ID: &ID,
},
Encounter: &domain.FHIRReference{
ID: &ID,
},
EffectivePeriod: &domain.FHIRPeriod{},
Issued: new(string),
Performer: []*domain.FHIRReference{},
ResultsInterpreter: []*domain.FHIRReference{},
Specimen: []*domain.FHIRReference{},
Result: []*domain.FHIRReference{},
ImagingStudy: []*domain.FHIRReference{},
Media: []*domain.FHIRDiagnosticReportMedia{},
Conclusion: new(string),
ConclusionCode: []*domain.FHIRCodeableConcept{},
PresentedForm: []*domain.FHIRAttachment{},
}, nil
},
}
}

Expand Down Expand Up @@ -2314,3 +2350,8 @@ func (fh *FHIRMock) CreateFHIRRiskAssessment(ctx context.Context, input *domain.
func (fh *FHIRMock) GetFHIRQuestionnaire(ctx context.Context, id string) (*domain.FHIRQuestionnaireRelayPayload, error) {
return fh.MockGetFHIRQuestionnaireFn(ctx, id)
}

// CreateFHIRDiagnosticReport mocks the implementation of creating a diagnostic report.
func (fh *FHIRMock) CreateFHIRDiagnosticReport(ctx context.Context, input *domain.FHIRDiagnosticReportInput) (*domain.FHIRDiagnosticReport, error) {
return fh.MockCreateFHIRDiagnosticReportFn(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 @@ -193,6 +193,11 @@ func SetupRoutes(r *gin.Engine, cacheStore persist.CacheStore, authclient *authu
questionnaire.Use(rest.AuthenticationGinMiddleware(cacheStore, *authclient))
questionnaire.Use(rest.TenantIdentifierExtractionMiddleware(infra.FHIR))
questionnaire.POST("", handlers.LoadQuestionnaire)

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

// GQLHandler sets up a GraphQL resolver
Expand Down
38 changes: 38 additions & 0 deletions pkg/clinical/presentation/rest/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,41 @@ func (p PresentationHandlersImpl) LoadQuestionnaire(c *gin.Context) {

c.JSON(http.StatusOK, questionnaire)
}

// RecordMammographyResult is used to record mammography results
func (p PresentationHandlersImpl) RecordMammographyResult(c *gin.Context) {
input := &dto.DiagnosticReportInput{
EncounterID: c.Request.FormValue("encounterID"),
Note: c.Request.FormValue("note"),
Attachment: c.Request.MultipartForm.File,
Findings: c.Request.FormValue("findings"),
}

if err := c.Request.ParseMultipartForm(50 << 20); err != nil {
jsonErrorResponse(c, http.StatusBadRequest, err)
return
}

var response *dto.DiagnosticReport

for _, fileHeaders := range input.Attachment {
for _, fileHeader := range fileHeaders {
file, err := fileHeader.Open()
if err != nil {
jsonErrorResponse(c, http.StatusBadRequest, err)
return
}
defer file.Close()

input.File = file

response, err = p.usecases.RecordMammographyResult(c.Request.Context(), *input)
if err != nil {
jsonErrorResponse(c, http.StatusInternalServerError, err)
return
}
}
}

c.JSON(http.StatusOK, response)
}

0 comments on commit 4f926da

Please sign in to comment.