From 115e2620dddd9797b3cb4813fce07a5ddb3cc800 Mon Sep 17 00:00:00 2001 From: Allan Sifuna Date: Tue, 13 Feb 2024 10:27:05 +0300 Subject: [PATCH] feat: add record MRI api --- pkg/clinical/application/common/defaults.go | 3 + .../presentation/graph/clinical.graphql | 1 + .../presentation/graph/clinical.resolvers.go | 5 + .../presentation/graph/generated/generated.go | 110 +++++++++++++ .../usecases/clinical/diagnostic_report.go | 25 ++- .../clinical/diagnostic_report_helpers.go | 24 ++- .../clinical/diagnostic_report_test.go | 146 ++++++++++++++++++ pkg/clinical/usecases/usecases.go | 1 + 8 files changed, 311 insertions(+), 4 deletions(-) diff --git a/pkg/clinical/application/common/defaults.go b/pkg/clinical/application/common/defaults.go index 7ce8ef67..03cab215 100644 --- a/pkg/clinical/application/common/defaults.go +++ b/pkg/clinical/application/common/defaults.go @@ -166,6 +166,9 @@ const ( // BiopsyTerminologySystem is the terminology code used to represent Biopsy of cervix. BiopsyTerminologySystem = "161826" + + // MRITerminologySystem is the terminology code used to represent MRI scan of the breast + MRITerminologySystem = "168651" ) // DefaultIdentifier assigns a patient a code to function as their diff --git a/pkg/clinical/presentation/graph/clinical.graphql b/pkg/clinical/presentation/graph/clinical.graphql index d8137691..475d9c5d 100644 --- a/pkg/clinical/presentation/graph/clinical.graphql +++ b/pkg/clinical/presentation/graph/clinical.graphql @@ -219,4 +219,5 @@ extend type Mutation { # Diagnostic Report recordMammographyResult(input: DiagnosticReportInput!): DiagnosticReport! recordBiopsy(input: DiagnosticReportInput!): DiagnosticReport! + recordMRI(input: DiagnosticReportInput!): DiagnosticReport! } diff --git a/pkg/clinical/presentation/graph/clinical.resolvers.go b/pkg/clinical/presentation/graph/clinical.resolvers.go index 288903b6..c2777879 100644 --- a/pkg/clinical/presentation/graph/clinical.resolvers.go +++ b/pkg/clinical/presentation/graph/clinical.resolvers.go @@ -290,6 +290,11 @@ func (r *mutationResolver) RecordBiopsy(ctx context.Context, input dto.Diagnosti return r.usecases.RecordBiopsy(ctx, input) } +// RecordMri is the resolver for the recordMRI field. +func (r *mutationResolver) RecordMri(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) { + return r.usecases.RecordMRI(ctx, input) +} + // PatientHealthTimeline is the resolver for the patientHealthTimeline field. func (r *queryResolver) PatientHealthTimeline(ctx context.Context, input dto.HealthTimelineInput) (*dto.HealthTimeline, error) { r.CheckDependencies() diff --git a/pkg/clinical/presentation/graph/generated/generated.go b/pkg/clinical/presentation/graph/generated/generated.go index 5c2dfd39..3582986d 100644 --- a/pkg/clinical/presentation/graph/generated/generated.go +++ b/pkg/clinical/presentation/graph/generated/generated.go @@ -321,6 +321,7 @@ type ComplexityRoot struct { RecordHpv func(childComplexity int, input dto.ObservationInput) int RecordLastMenstrualPeriod func(childComplexity int, input dto.ObservationInput) int RecordMammographyResult func(childComplexity int, input dto.DiagnosticReportInput) int + RecordMri func(childComplexity int, input dto.DiagnosticReportInput) int RecordMuac func(childComplexity int, input dto.ObservationInput) int RecordOxygenSaturation func(childComplexity int, input dto.ObservationInput) int RecordPapSmear func(childComplexity int, input dto.ObservationInput) int @@ -685,6 +686,7 @@ type MutationResolver interface { CreateQuestionnaireResponse(ctx context.Context, questionnaireID string, encounterID string, input dto.QuestionnaireResponse) (string, error) RecordMammographyResult(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) RecordBiopsy(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) + RecordMri(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) } type QueryResolver interface { PatientHealthTimeline(ctx context.Context, input dto.HealthTimelineInput) (*dto.HealthTimeline, error) @@ -2216,6 +2218,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.RecordMammographyResult(childComplexity, args["input"].(dto.DiagnosticReportInput)), true + case "Mutation.recordMRI": + if e.complexity.Mutation.RecordMri == nil { + break + } + + args, err := ec.field_Mutation_recordMRI_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.RecordMri(childComplexity, args["input"].(dto.DiagnosticReportInput)), true + case "Mutation.recordMUAC": if e.complexity.Mutation.RecordMuac == nil { break @@ -4303,6 +4317,7 @@ extend type Mutation { # Diagnostic Report recordMammographyResult(input: DiagnosticReportInput!): DiagnosticReport! recordBiopsy(input: DiagnosticReportInput!): DiagnosticReport! + recordMRI(input: DiagnosticReportInput!): DiagnosticReport! } `, BuiltIn: false}, {Name: "../enums.graphql", Input: `enum EpisodeOfCareStatusEnum { @@ -5880,6 +5895,21 @@ func (ec *executionContext) field_Mutation_recordLastMenstrualPeriod_args(ctx co return args, nil } +func (ec *executionContext) field_Mutation_recordMRI_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 dto.DiagnosticReportInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNDiagnosticReportInput2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐDiagnosticReportInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_recordMUAC_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -17023,6 +17053,79 @@ func (ec *executionContext) fieldContext_Mutation_recordBiopsy(ctx context.Conte return fc, nil } +func (ec *executionContext) _Mutation_recordMRI(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_recordMRI(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RecordMri(rctx, fc.Args["input"].(dto.DiagnosticReportInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*dto.DiagnosticReport) + fc.Result = res + return ec.marshalNDiagnosticReport2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐDiagnosticReport(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_recordMRI(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_DiagnosticReport_id(ctx, field) + case "status": + return ec.fieldContext_DiagnosticReport_status(ctx, field) + case "patientID": + return ec.fieldContext_DiagnosticReport_patientID(ctx, field) + case "encounterID": + return ec.fieldContext_DiagnosticReport_encounterID(ctx, field) + case "issued": + return ec.fieldContext_DiagnosticReport_issued(ctx, field) + case "result": + return ec.fieldContext_DiagnosticReport_result(ctx, field) + case "media": + return ec.fieldContext_DiagnosticReport_media(ctx, field) + case "conclusion": + return ec.fieldContext_DiagnosticReport_conclusion(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DiagnosticReport", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_recordMRI_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Narrative_id(ctx context.Context, field graphql.CollectedField, obj *dto.Narrative) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Narrative_id(ctx, field) if err != nil { @@ -32826,6 +32929,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "recordMRI": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_recordMRI(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/clinical/usecases/clinical/diagnostic_report.go b/pkg/clinical/usecases/clinical/diagnostic_report.go index 35fe608a..0598dc71 100644 --- a/pkg/clinical/usecases/clinical/diagnostic_report.go +++ b/pkg/clinical/usecases/clinical/diagnostic_report.go @@ -35,7 +35,7 @@ func (c *UseCasesClinicalImpl) RecordMammographyResult(ctx context.Context, inpu return c.RecordDiagnosticReport(ctx, common.MammogramTerminologyCode, input, observationOutput, nil) } -// RecordBiopsy is used to record biopsy test results as observations +// RecordBiopsy is used to record biopsy test results as a diagnostic report // FHIR recommends use of diagnostic resource to record the findings and interpretation of biopsy test results // performed on patients, groups of patients, devices, and locations, and/or specimens. func (c *UseCasesClinicalImpl) RecordBiopsy(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) { @@ -55,7 +55,28 @@ func (c *UseCasesClinicalImpl) RecordBiopsy(ctx context.Context, input dto.Diagn return nil, err } - return c.RecordDiagnosticReport(ctx, common.BiopsyTerminologySystem, input, observationOutput, []DiagnosticReportMutatorFunc{addCytologyCategory}) + return c.RecordDiagnosticReport(ctx, common.BiopsyTerminologySystem, input, observationOutput, []DiagnosticReportMutatorFunc{addCytopathologyCategory}) +} + +// RecordMRI is used to record MRI scan results as a diagnostic report +func (c *UseCasesClinicalImpl) RecordMRI(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, + Value: input.Findings, + } + + observationOutput, err := c.RecordObservation(ctx, *observationInput, common.MRITerminologySystem, []ObservationInputMutatorFunc{addProcedureCategory}) + if err != nil { + return nil, err + } + + return c.RecordDiagnosticReport(ctx, common.MRITerminologySystem, input, observationOutput, []DiagnosticReportMutatorFunc{addNuclearMagneticResonanceCategory}) } // RecordDiagnosticReport is a re-usable method to help with diagnostic report recording diff --git a/pkg/clinical/usecases/clinical/diagnostic_report_helpers.go b/pkg/clinical/usecases/clinical/diagnostic_report_helpers.go index 41809608..e7a15773 100644 --- a/pkg/clinical/usecases/clinical/diagnostic_report_helpers.go +++ b/pkg/clinical/usecases/clinical/diagnostic_report_helpers.go @@ -11,8 +11,8 @@ import ( // with the aapropriate data to suit its use case. type DiagnosticReportMutatorFunc func(context.Context, *domain.FHIRDiagnosticReportInput) error -// addProcedureCategory is used to add procedure category for various observations records such as biopsy etc. -var addCytologyCategory = func(ctx context.Context, diagnosticReport *domain.FHIRDiagnosticReportInput) error { +// addCytopathologyCategory is used to add a cytopathology category for various diagnostic reports such as biopsy etc. +var addCytopathologyCategory = func(ctx context.Context, diagnosticReport *domain.FHIRDiagnosticReportInput) error { category := []*domain.FHIRCodeableConceptInput{ { Coding: []*domain.FHIRCodingInput{ @@ -30,3 +30,23 @@ var addCytologyCategory = func(ctx context.Context, diagnosticReport *domain.FHI return nil } + +// addNuclearMagneticResonanceCategory is used to add a nuclear magnetic resonance category for diagnostic report such as MRI reports etc. +var addNuclearMagneticResonanceCategory = func(ctx context.Context, diagnosticReport *domain.FHIRDiagnosticReportInput) error { + category := []*domain.FHIRCodeableConceptInput{ + { + Coding: []*domain.FHIRCodingInput{ + { + System: (*scalarutils.URI)(&diagnosticReportCategorySystem), + Code: scalarutils.Code("NMR"), + Display: "Nuclear Magnetic Resonance", + }, + }, + Text: "Nuclear Magnetic Resonance", + }, + } + + diagnosticReport.Category = append(diagnosticReport.Category, category...) + + return nil +} diff --git a/pkg/clinical/usecases/clinical/diagnostic_report_test.go b/pkg/clinical/usecases/clinical/diagnostic_report_test.go index d40d71b7..b9909154 100644 --- a/pkg/clinical/usecases/clinical/diagnostic_report_test.go +++ b/pkg/clinical/usecases/clinical/diagnostic_report_test.go @@ -405,3 +405,149 @@ func TestUseCasesClinicalImpl_RecordBiopsy(t *testing.T) { }) } } + +func TestUseCasesClinicalImpl_RecordMRI(t *testing.T) { + type args struct { + ctx context.Context + input dto.DiagnosticReportInput + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Happy case: successfully record mri results", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "No Tumours observed", + Media: &dto.Media{ + URL: gofakeit.URL(), + Name: gofakeit.Name(), + }, + Findings: gofakeit.HipsterSentence(20), + }, + }, + wantErr: false, + }, + { + name: "Sad case: unable to successfully record mri results", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "No Tumours observed", + Media: &dto.Media{ + URL: gofakeit.URL(), + Name: gofakeit.Name(), + }, + Findings: gofakeit.HipsterSentence(20), + }, + }, + wantErr: true, + }, + { + name: "Sad case: fail validation", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Media: &dto.Media{ + URL: gofakeit.URL(), + Name: gofakeit.Name(), + }, + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to record observation", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "No Tumours observed", + Media: &dto.Media{ + URL: gofakeit.URL(), + Name: gofakeit.Name(), + }, + Findings: gofakeit.HipsterSentence(20), + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get tenant identifiers", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "No Tumours observed", + Media: &dto.Media{ + URL: gofakeit.URL(), + Name: gofakeit.Name(), + }, + Findings: gofakeit.HipsterSentence(20), + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get organization", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + input: dto.DiagnosticReportInput{ + EncounterID: "12345678905432345", + Note: "No Tumours observed", + Media: &dto.Media{ + URL: gofakeit.URL(), + Name: gofakeit.Name(), + }, + Findings: gofakeit.HipsterSentence(20), + }, + }, + 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 successfully record mri results" { + fakeFHIR.MockCreateFHIRDiagnosticReportFn = func(_ context.Context, input *domain.FHIRDiagnosticReportInput) (*domain.FHIRDiagnosticReport, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to record observation" { + fakeFHIR.MockGetFHIREncounterFn = func(ctx context.Context, id string) (*domain.FHIREncounterRelayPayload, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to get tenant identifiers" { + fakeExt.MockGetTenantIdentifiersFn = func(ctx context.Context) (*dto.TenantIdentifiers, error) { + return nil, fmt.Errorf("an error occurred") + } + } + if tt.name == "Sad case: unable to get organization" { + fakeFHIR.MockGetFHIROrganizationFn = func(ctx context.Context, organisationID string) (*domain.FHIROrganizationRelayPayload, error) { + return nil, fmt.Errorf("an error occurred") + } + } + + _, err := u.RecordMRI(tt.args.ctx, tt.args.input) + if (err != nil) != tt.wantErr { + t.Errorf("UseCasesClinicalImpl.RecordMRI() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/pkg/clinical/usecases/usecases.go b/pkg/clinical/usecases/usecases.go index 7dad942f..d6878a37 100644 --- a/pkg/clinical/usecases/usecases.go +++ b/pkg/clinical/usecases/usecases.go @@ -104,6 +104,7 @@ type Clinical interface { RecordHPV(ctx context.Context, input dto.ObservationInput) (*dto.Observation, error) RecordPapSmear(ctx context.Context, input dto.ObservationInput) (*dto.Observation, error) RecordBiopsy(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) + RecordMRI(ctx context.Context, input dto.DiagnosticReportInput) (*dto.DiagnosticReport, error) // Questionnaire CreateQuestionnaire(ctx context.Context, questionnaireInput *domain.FHIRQuestionnaire) (*domain.FHIRQuestionnaire, error)