From 2d21d2f59043cbd70d4d0c3c253f42c3dd8c03fe Mon Sep 17 00:00:00 2001 From: Kathurima Kimathi Date: Thu, 1 Feb 2024 17:01:55 +0300 Subject: [PATCH] feat: record mammography result - 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 --- pkg/clinical/application/common/defaults.go | 6 + .../dto/diagnostic_report_output.go | 11 +- pkg/clinical/application/dto/input.go | 19 +- .../extensions/mock/extension_mock.go | 1 + pkg/clinical/domain/diagnostic_report.go | 101 +++++-- .../datastore/cloudhealthcare/fhir.go | 18 ++ .../cloudhealthcare/fhir_unit_test.go | 51 ++++ .../cloudhealthcare/mock/fhir_mock.go | 43 ++- pkg/clinical/presentation/config.go | 5 + pkg/clinical/presentation/rest/handlers.go | 38 +++ pkg/clinical/repository/repository.go | 5 + .../usecases/clinical/diagnostic_report.go | 195 ++++++++++++ .../clinical/diagnostic_report_test.go | 277 ++++++++++++++++++ .../usecases/clinical/observation_helpers.go | 21 ++ .../usecases/clinical/observation_test.go | 12 - pkg/clinical/usecases/usecases.go | 1 + 16 files changed, 758 insertions(+), 46 deletions(-) create mode 100644 pkg/clinical/usecases/clinical/diagnostic_report_test.go diff --git a/pkg/clinical/application/common/defaults.go b/pkg/clinical/application/common/defaults.go index 5128ac3f..bf25d8a2 100644 --- a/pkg/clinical/application/common/defaults.go +++ b/pkg/clinical/application/common/defaults.go @@ -148,6 +148,12 @@ const ( // PapSmearTerminologyCode is the terminology code used to represent pap smear test. PapSmearTerminologyCode = "154451" + + // 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 diff --git a/pkg/clinical/application/dto/diagnostic_report_output.go b/pkg/clinical/application/dto/diagnostic_report_output.go index 09aae275..3810488a 100644 --- a/pkg/clinical/application/dto/diagnostic_report_output.go +++ b/pkg/clinical/application/dto/diagnostic_report_output.go @@ -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"` } diff --git a/pkg/clinical/application/dto/input.go b/pkg/clinical/application/dto/input.go index 70cc129b..7df32ced 100644 --- a/pkg/clinical/application/dto/input.go +++ b/pkg/clinical/application/dto/input.go @@ -1,6 +1,7 @@ package dto import ( + "io" "mime/multipart" "github.com/go-playground/validator" @@ -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 { @@ -222,3 +223,19 @@ type Expression struct { Expression *string `json:"expression,omitempty"` Reference *string `json:"reference,omitempty"` } + +// DiagnosticReportInput represents the data class used to fetch diagnostic 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 +} diff --git a/pkg/clinical/application/extensions/mock/extension_mock.go b/pkg/clinical/application/extensions/mock/extension_mock.go index 6d8960fe..67050077 100644 --- a/pkg/clinical/application/extensions/mock/extension_mock.go +++ b/pkg/clinical/application/extensions/mock/extension_mock.go @@ -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) { diff --git a/pkg/clinical/domain/diagnostic_report.go b/pkg/clinical/domain/diagnostic_report.go index e7960b0e..ca2769a9 100644 --- a/pkg/clinical/domain/diagnostic_report.go +++ b/pkg/clinical/domain/diagnostic_report.go @@ -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"` +} diff --git a/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir.go b/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir.go index 8a722914..24ef1088 100644 --- a/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir.go +++ b/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir.go @@ -39,6 +39,7 @@ const ( consentResourceType = "Consent" questionnaireResponseResourceType = "QuestionnaireResponse" riskAssessmentResourceType = "RiskAssessment" + diagnosticReportResourceType = "DiagnosticReport" ) // Dataset ... @@ -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 +} diff --git a/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir_unit_test.go b/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir_unit_test.go index 17980cec..6cc14fec 100644 --- a/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir_unit_test.go +++ b/pkg/clinical/infrastructure/datastore/cloudhealthcare/fhir_unit_test.go @@ -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 + } + }) + } +} diff --git a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go index f8afa7d4..743ac2df 100644 --- a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go +++ b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go @@ -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.FHIRRiskAssessment) (*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 @@ -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, @@ -1931,6 +1933,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 + }, } } @@ -2263,3 +2299,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) +} diff --git a/pkg/clinical/presentation/config.go b/pkg/clinical/presentation/config.go index 3593d13d..8d0c8f31 100644 --- a/pkg/clinical/presentation/config.go +++ b/pkg/clinical/presentation/config.go @@ -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 diff --git a/pkg/clinical/presentation/rest/handlers.go b/pkg/clinical/presentation/rest/handlers.go index 3dda5ba1..138d835d 100644 --- a/pkg/clinical/presentation/rest/handlers.go +++ b/pkg/clinical/presentation/rest/handlers.go @@ -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) +} diff --git a/pkg/clinical/repository/repository.go b/pkg/clinical/repository/repository.go index dd34bec9..5b9e7b80 100644 --- a/pkg/clinical/repository/repository.go +++ b/pkg/clinical/repository/repository.go @@ -26,6 +26,7 @@ type FHIR interface { FHIRConsent FHIRQuestionnaireResponse FHIRRiskAssessment + FHIRDiagnosticReport } type FHIROrganization interface { @@ -132,3 +133,7 @@ type FHIRQuestionnaireResponse interface { type FHIRRiskAssessment interface { CreateFHIRRiskAssessment(_ context.Context, input *domain.FHIRRiskAssessment) (*domain.FHIRRiskAssessmentRelayPayload, error) } + +type FHIRDiagnosticReport interface { + CreateFHIRDiagnosticReport(_ context.Context, input *domain.FHIRDiagnosticReportInput) (*domain.FHIRDiagnosticReport, error) +} diff --git a/pkg/clinical/usecases/clinical/diagnostic_report.go b/pkg/clinical/usecases/clinical/diagnostic_report.go index be973de9..cb20ea9c 100644 --- a/pkg/clinical/usecases/clinical/diagnostic_report.go +++ b/pkg/clinical/usecases/clinical/diagnostic_report.go @@ -1 +1,196 @@ package clinical + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/savannahghi/clinical/pkg/clinical/application/common" + "github.com/savannahghi/clinical/pkg/clinical/application/dto" + "github.com/savannahghi/clinical/pkg/clinical/application/extensions" + "github.com/savannahghi/clinical/pkg/clinical/domain" + "github.com/savannahghi/scalarutils" +) + +// RecordMammographyResult is used to record mammography diagnostic reports as specified in https://hl7.org/fhir/R4/diagnosticreport.html#scope +func (c *UseCasesClinicalImpl) RecordMammographyResult(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) { + err := input.Validate() + if err != nil { + return nil, err + } + + observationInput := &dto.ObservationInput{ + Status: dto.ObservationStatusFinal, + EncounterID: input.EncounterID, + } + + // !NOTE: The terminology code used here is used TEMPORARILY. Pending discussion about how to represent BI-RADs conclusions/observation + observationOutput, err := c.RecordObservation(ctx, *observationInput, common.BenignNeoplasmOfBreastOfSkinTerminologyCode, []ObservationInputMutatorFunc{addImagingCategory}) + if err != nil { + return nil, err + } + + observationsReference := fmt.Sprintf("Observation/%s", observationOutput.ID) + observationType := scalarutils.URI("Observation") + encounterReference := fmt.Sprintf("Encounter/%s", observationOutput.EncounterID) + encounterType := scalarutils.URI("Encounter") + patientReference := fmt.Sprintf("Patient/%s", observationOutput.PatientID) + mediaObjectName := fmt.Sprintf("%s@%s", patientReference, time.Now()) + patientType := scalarutils.URI("Patient") + + tags, err := c.GetTenantMetaTags(ctx) + if err != nil { + return nil, err + } + + facilityID, err := extensions.GetFacilityIDFromContext(ctx) + if err != nil { + return nil, err + } + + facility, err := c.infrastructure.FHIR.GetFHIROrganization(ctx, facilityID) + if err != nil { + return nil, err + } + + orgRef := fmt.Sprintf("Organization/%s", *facility.Resource.ID) + orgType := scalarutils.URI("Organization") + + mediaUploadOutput, err := c.infrastructure.Upload.UploadMedia(ctx, mediaObjectName, input.File, "") + if err != nil { + return nil, err + } + + id := uuid.New().String() + mediaSystem := scalarutils.URI("http://terminology.hl7.org/CodeSystem/media-type") + now := time.Now() + + mediaInput := &domain.FHIRMedia{ + ID: &id, + Identifier: []*domain.FHIRIdentifier{ + { + Use: "official", + System: &mediaSystem, + }, + }, + Status: domain.MediaStatusCompleted, + Subject: &domain.FHIRReferenceInput{ + ID: &observationOutput.PatientID, + Reference: &patientReference, + }, + Encounter: &domain.FHIRReferenceInput{ + ID: &observationOutput.EncounterID, + Reference: &encounterReference, + Type: &encounterType, + }, + Content: &domain.FHIRAttachmentInput{ + ContentType: (*scalarutils.Code)(&mediaUploadOutput.ContentType), + URL: (*scalarutils.URL)(&mediaUploadOutput.URL), + Title: &mediaUploadOutput.Name, + }, + Issued: &now, + } + + mediaInput.Operator = &domain.FHIRReferenceInput{ + ID: facility.Resource.ID, + Reference: &orgRef, + Display: *facility.Resource.Name, + Type: &orgType, + } + + mediaInput.Meta = &domain.FHIRMetaInput{ + Tag: tags, + } + + media, err := c.infrastructure.FHIR.CreateFHIRMedia(ctx, *mediaInput) + if err != nil { + return nil, err + } + + mediaReference := fmt.Sprintf("Media/%s", *media.ID) + mediaType := scalarutils.URI("Media") + instant := scalarutils.Instant(time.Now().Format(time.RFC3339)) + + concept, err := c.GetConcept(ctx, dto.TerminologySourceCIEL, common.MammogramTerminologyCode) + if err != nil { + return nil, err + } + + diagnosticReport := &domain.FHIRDiagnosticReportInput{ + ID: &id, + Status: domain.DiagnosticReportStatusPreliminary, + Code: domain.FHIRCodeableConceptInput{ + Coding: []*domain.FHIRCodingInput{ + { + System: (*scalarutils.URI)(&observationCategorySystem), + Code: scalarutils.Code(concept.ID), + Display: concept.DisplayName, + }, + }, + Text: concept.DisplayName, + }, + Subject: &domain.FHIRReferenceInput{ + ID: &observationOutput.PatientID, + Reference: &patientReference, + Type: &patientType, + }, + Encounter: &domain.FHIRReferenceInput{ + ID: &observationOutput.EncounterID, + Reference: &encounterReference, + Type: &encounterType, + }, + Issued: (*string)(&instant), + Performer: []*domain.FHIRReferenceInput{ + { + ID: facility.Resource.ID, + Reference: &orgRef, + Type: &orgType, + Display: *facility.Resource.Name, + }, + }, + ResultsInterpreter: []*domain.FHIRReferenceInput{ + { + ID: facility.Resource.ID, + Reference: &orgRef, + Type: &orgType, + Display: *facility.Resource.Name, + }, + }, + Media: []*domain.FHIRDiagnosticReportMediaInput{ + { + ID: media.ID, + Link: &domain.FHIRReferenceInput{ + Reference: &mediaReference, + Type: &mediaType, + }, + }, + }, + Conclusion: &input.Note, + Result: []*domain.FHIRReferenceInput{ + { + ID: &observationOutput.ID, + Reference: &observationsReference, + Type: &observationType, + }, + }, + } + + diagnosticReport.Meta = &domain.FHIRMetaInput{ + Tag: tags, + } + + result, err := c.infrastructure.FHIR.CreateFHIRDiagnosticReport(ctx, diagnosticReport) + if err != nil { + return nil, err + } + + return &dto.DiagnosticReport{ + ID: *result.ID, + Status: dto.ObservationStatus(result.Status), + PatientID: *result.Subject.ID, + EncounterID: *result.Encounter.ID, + Issued: *result.Issued, + Conclusion: *result.Conclusion, + }, nil +} diff --git a/pkg/clinical/usecases/clinical/diagnostic_report_test.go b/pkg/clinical/usecases/clinical/diagnostic_report_test.go new file mode 100644 index 00000000..e19f6ebb --- /dev/null +++ b/pkg/clinical/usecases/clinical/diagnostic_report_test.go @@ -0,0 +1,277 @@ +package clinical_test + +import ( + "context" + "fmt" + "io" + "mime/multipart" + "testing" + + "github.com/brianvoe/gofakeit" + "github.com/savannahghi/clinical/pkg/clinical/application/dto" + fakeExtMock "github.com/savannahghi/clinical/pkg/clinical/application/extensions/mock" + "github.com/savannahghi/clinical/pkg/clinical/domain" + "github.com/savannahghi/clinical/pkg/clinical/infrastructure" + fakeFHIRMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock" + fakeOCLMock "github.com/savannahghi/clinical/pkg/clinical/infrastructure/services/openconceptlab/mock" + 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" +) + +func TestUseCasesClinicalImpl_RecordMammographyResult(t *testing.T) { + dummyData := make(map[string][]*multipart.FileHeader) + + dummyData["images"] = []*multipart.FileHeader{ + { + Filename: "image1.jpg", + Size: 1024, + }, + } + + type args struct { + ctx context.Context + input dto.DiagnosticReportInput + } + tests := []struct { + name string + args args + want *dto.DiagnosticReport + wantErr bool + }{ + { + name: "Happy case: record mammography report", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: false, + }, + { + name: "Sad case: unable to get encounter", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: finished encounter", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get meta tags", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get facility ID from context", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get FHIR organisation", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to upload media", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to create FHIR media", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get concept", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to create FHIR observation", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to create FHIR diagnostic report", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + Attachment: dummyData, + Findings: "BI-RADs 0", + }, + }, + wantErr: true, + }, + { + name: "Sad case: required field omitted", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "Test", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeExt := fakeExtMock.NewFakeBaseExtensionMock() + fakeFHIR := fakeFHIRMock.NewFHIRMock() + fakeOCL := fakeOCLMock.NewFakeOCLMock() + fakePubSub := fakePubSubMock.NewPubSubServiceMock() + + fakeUpload := fakeUploadMock.NewFakeUploadMock() + + infra := infrastructure.NewInfrastructureInteractor(fakeExt, fakeFHIR, fakeOCL, fakeUpload, fakePubSub) + u := clinicalUsecase.NewUseCasesClinicalImpl(infra) + + if tt.name == "Sad case: unable to get encounter" { + fakeFHIR.MockGetFHIREncounterFn = func(ctx context.Context, id string) (*domain.FHIREncounterRelayPayload, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: finished encounter" { + fakeFHIR.MockGetFHIREncounterFn = func(ctx context.Context, id string) (*domain.FHIREncounterRelayPayload, error) { + return &domain.FHIREncounterRelayPayload{ + Resource: &domain.FHIREncounter{ + Status: domain.EncounterStatusEnumFinished, + }, + }, nil + } + } + if tt.name == "Sad case: unable to get meta tags" { + fakeExt.MockGetTenantIdentifiersFn = func(ctx context.Context) (*dto.TenantIdentifiers, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to get facility ID from context" { + fakeExt.MockGetTenantIdentifiersFn = func(ctx context.Context) (*dto.TenantIdentifiers, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to get FHIR organisation" { + fakeExt.MockGetTenantIdentifiersFn = func(ctx context.Context) (*dto.TenantIdentifiers, error) { + return &dto.TenantIdentifiers{ + OrganizationID: gofakeit.UUID(), + FacilityID: gofakeit.UUID(), + }, nil + } + fakeFHIR.MockGetFHIROrganizationFn = func(ctx context.Context, organisationID string) (*domain.FHIROrganizationRelayPayload, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to upload media" { + fakeUpload.MockUploadMediaFn = func(ctx context.Context, name string, file io.Reader, contentType string) (*dto.Media, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to create FHIR media" { + fakeFHIR.MockCreateFHIRMediaFn = func(ctx context.Context, input domain.FHIRMedia) (*domain.FHIRMedia, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to get concept" { + fakeOCL.MockGetConceptFn = func(ctx context.Context, org, source, concept string, includeMappings, includeInverseMappings bool) (*domain.Concept, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to create FHIR observation" { + fakeFHIR.MockCreateFHIRObservationFn = func(ctx context.Context, input domain.FHIRObservationInput) (*domain.FHIRObservation, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to create FHIR diagnostic report" { + fakeFHIR.MockCreateFHIRDiagnosticReportFn = func(_ context.Context, input *domain.FHIRDiagnosticReportInput) (*domain.FHIRDiagnosticReport, error) { + return nil, fmt.Errorf("an error occurred") + } + } + + _, err := u.RecordMammographyResult(tt.args.ctx, tt.args.input) + if (err != nil) != tt.wantErr { + t.Errorf("UseCasesClinicalImpl.RecordMammographyResult() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/pkg/clinical/usecases/clinical/observation_helpers.go b/pkg/clinical/usecases/clinical/observation_helpers.go index 3d64422b..fdf81ddf 100644 --- a/pkg/clinical/usecases/clinical/observation_helpers.go +++ b/pkg/clinical/usecases/clinical/observation_helpers.go @@ -54,3 +54,24 @@ var addLabCategory = func(ctx context.Context, observation *domain.FHIRObservati observation.Category = append(observation.Category, category...) return nil } + +// addImagingCategory is used to add imaging categories for various observations records. +var addImagingCategory = func(ctx context.Context, observation *domain.FHIRObservationInput) error { + userSelected := false + category := []*domain.FHIRCodeableConceptInput{ + { + Coding: []*domain.FHIRCodingInput{ + { + System: (*scalarutils.URI)(&observationCategorySystem), + Code: "imaging", + Display: "Imaging", + UserSelected: &userSelected, + }, + }, + Text: "Imaging", + }, + } + + observation.Category = append(observation.Category, category...) + return nil +} diff --git a/pkg/clinical/usecases/clinical/observation_test.go b/pkg/clinical/usecases/clinical/observation_test.go index d85182c8..5d733a89 100644 --- a/pkg/clinical/usecases/clinical/observation_test.go +++ b/pkg/clinical/usecases/clinical/observation_test.go @@ -94,18 +94,6 @@ func TestUseCasesClinicalImpl_RecordObservation(t *testing.T) { }, wantErr: true, }, - { - name: "Sad Case - Fail validation", - args: args{ - ctx: ctx, - input: dto.ObservationInput{ - Status: dto.ObservationStatusFinal, - EncounterID: uuid.New().String(), - }, - mutators: []clinicalUsecase.ObservationInputMutatorFunc{addLabCategory}, - }, - wantErr: true, - }, { name: "Sad Case - Fail to get encounter", args: args{ diff --git a/pkg/clinical/usecases/usecases.go b/pkg/clinical/usecases/usecases.go index 46f1609a..635ca5dd 100644 --- a/pkg/clinical/usecases/usecases.go +++ b/pkg/clinical/usecases/usecases.go @@ -109,6 +109,7 @@ type Clinical interface { CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, input dto.QuestionnaireResponse) (*dto.QuestionnaireResponse, error) ListQuestionnaires(ctx context.Context, searchParam *string, pagination *dto.Pagination) (*dto.QuestionnaireConnection, error) RecordConsent(ctx context.Context, input dto.ConsentInput) (*dto.ConsentOutput, error) + RecordMammographyResult(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) } // Interactor is an implementation of the usecases interface