diff --git a/pkg/clinical/application/common/defaults.go b/pkg/clinical/application/common/defaults.go index 88d2fea3..334ee1ba 100644 --- a/pkg/clinical/application/common/defaults.go +++ b/pkg/clinical/application/common/defaults.go @@ -128,8 +128,11 @@ const ( // LOINCExamination defines LOINC Examination note terminology code LOINCExamination = "29545-1" - // LOINCPLANOFCARE defines LOINC Plan of care note terminology code - LOINCPLANOFCARE = "18776-5" + // LOINCPlanOfCare defines LOINC Plan of care note terminology code + LOINCPlanOfCare = "18776-5" + + // LOINCProviderUnspecifiedProgressNote defines LOINC Provider unspecified progress note terminology code + LOINCProviderUnspecifiedProgressNote = "11506-3" // ColposcopyCIELTerminologyCode is the terminology code for colposcopy findings ColposcopyCIELTerminologyCode = "162816" diff --git a/pkg/clinical/application/dto/enums.go b/pkg/clinical/application/dto/enums.go index 739fb8b3..7448fdc4 100644 --- a/pkg/clinical/application/dto/enums.go +++ b/pkg/clinical/application/dto/enums.go @@ -162,12 +162,13 @@ const ( type CompositionCategory string const ( - AssessmentAndPlan CompositionCategory = "ASSESSMENT_PLAN" - HistoryOfPresentingIllness CompositionCategory = "HISTORY_OF_PRESENTING_ILLNESS" - SocialHistory CompositionCategory = "SOCIAL_HISTORY" - FamilyHistory CompositionCategory = "FAMILY_HISTORY" - Examination CompositionCategory = "EXAMINATION" - PlanOfCare CompositionCategory = "PLAN_OF_CARE" + AssessmentAndPlan CompositionCategory = "ASSESSMENT_PLAN" + HistoryOfPresentingIllness CompositionCategory = "HISTORY_OF_PRESENTING_ILLNESS" + SocialHistory CompositionCategory = "SOCIAL_HISTORY" + FamilyHistory CompositionCategory = "FAMILY_HISTORY" + Examination CompositionCategory = "EXAMINATION" + PlanOfCare CompositionCategory = "PLAN_OF_CARE" + ProviderUnspecifiedProgressNote CompositionCategory = "PROGRESS_NOTE" ) // Type enum represents type composition attribute diff --git a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go index 9caa7e45..2a545e22 100644 --- a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go +++ b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go @@ -343,6 +343,14 @@ func NewFHIRMock() *FHIRMock { return &domain.FHIREncounterRelayPayload{ Resource: &domain.FHIREncounter{ ID: &resourceID, + Meta: &domain.FHIRMeta{ + Source: resourceID, + Tag: []domain.FHIRCoding{ + { + Code: (*scalarutils.Code)(&resourceID), + }, + }, + }, }, }, nil }, diff --git a/pkg/clinical/usecases/clinical/composition.go b/pkg/clinical/usecases/clinical/composition.go index 068859bd..cb4fda02 100644 --- a/pkg/clinical/usecases/clinical/composition.go +++ b/pkg/clinical/usecases/clinical/composition.go @@ -54,31 +54,12 @@ func (c *UseCasesClinicalImpl) CreateComposition(ctx context.Context, input dto. id := uuid.New().String() - var compositionCategoryCode string - - switch input.Category { - case "ASSESSMENT_PLAN": - compositionCategoryCode = common.LOINCAssessmentPlanCode - case "HISTORY_OF_PRESENTING_ILLNESS": - compositionCategoryCode = common.LOINCHistoryOfPresentingIllness - case "SOCIAL_HISTORY": - compositionCategoryCode = common.LOINCSocialHistory - case "FAMILY_HISTORY": - compositionCategoryCode = common.LOINCFamilyHistory - case "EXAMINATION": - compositionCategoryCode = common.LOINCExamination - case "PLAN_OF_CARE": - compositionCategoryCode = common.LOINCPLANOFCARE - default: - return nil, fmt.Errorf("category is needed") - } - - compositionCategoryConcept, err := c.GetConcept(ctx, dto.TerminologySourceLOINC, compositionCategoryCode) + compositionCategoryCode, err := c.mapCategoryEnumToCode(input.Category) if err != nil { return nil, err } - compositionTypeConcept, err := c.GetConcept(ctx, dto.TerminologySourceLOINC, common.LOINCProgressNoteCode) + compositionConcept, err := c.mapCompositionConcepts(ctx, compositionCategoryCode, common.LOINCProgressNoteCode) if err != nil { return nil, err } @@ -91,24 +72,24 @@ func (c *UseCasesClinicalImpl) CreateComposition(ctx context.Context, input dto. Type: &domain.FHIRCodeableConceptInput{ Coding: []*domain.FHIRCodingInput{ { - System: (*scalarutils.URI)(&compositionTypeConcept.URL), - Code: scalarutils.Code(compositionTypeConcept.ID), - Display: compositionTypeConcept.DisplayName, + System: (*scalarutils.URI)(&compositionConcept.CompositionTypeConcept.URL), + Code: scalarutils.Code(compositionConcept.CompositionTypeConcept.ID), + Display: compositionConcept.CompositionTypeConcept.DisplayName, }, }, - Text: compositionTypeConcept.DisplayName, + Text: compositionConcept.CompositionTypeConcept.DisplayName, }, Category: []*domain.FHIRCodeableConceptInput{ { ID: &id, Coding: []*domain.FHIRCodingInput{ { - System: (*scalarutils.URI)(&compositionCategoryConcept.URL), - Code: scalarutils.Code(compositionCategoryConcept.ID), - Display: compositionCategoryConcept.DisplayName, + System: (*scalarutils.URI)(&compositionConcept.CompositionCategoryConcept.URL), + Code: scalarutils.Code(compositionConcept.CompositionCategoryConcept.ID), + Display: compositionConcept.CompositionCategoryConcept.DisplayName, }, }, - Text: compositionCategoryConcept.DisplayName, + Text: compositionConcept.CompositionCategoryConcept.DisplayName, }, }, Subject: &domain.FHIRReferenceInput{ @@ -132,18 +113,18 @@ func (c *UseCasesClinicalImpl) CreateComposition(ctx context.Context, input dto. Section: []*domain.FHIRCompositionSectionInput{ { ID: &id, - Title: &compositionCategoryConcept.DisplayName, + Title: &compositionConcept.CompositionCategoryConcept.DisplayName, Code: &domain.FHIRCodeableConceptInput{ ID: &id, Coding: []*domain.FHIRCodingInput{ { ID: &id, - System: (*scalarutils.URI)(&compositionCategoryConcept.URL), - Code: scalarutils.Code(compositionCategoryConcept.ID), - Display: compositionCategoryConcept.DisplayName, + System: (*scalarutils.URI)(&compositionConcept.CompositionCategoryConcept.URL), + Code: scalarutils.Code(compositionConcept.CompositionCategoryConcept.ID), + Display: compositionConcept.CompositionCategoryConcept.DisplayName, }, }, - Text: compositionTypeConcept.DisplayName, + Text: compositionConcept.CompositionTypeConcept.DisplayName, }, Author: []*domain.FHIRReferenceInput{ { @@ -178,12 +159,12 @@ func (c *UseCasesClinicalImpl) CreateComposition(ctx context.Context, input dto. ID: &id, Coding: []*domain.FHIRCodingInput{ { - System: (*scalarutils.URI)(&compositionCategoryConcept.URL), - Code: scalarutils.Code(compositionCategoryConcept.ID), - Display: compositionCategoryConcept.DisplayName, + System: (*scalarutils.URI)(&compositionConcept.CompositionCategoryConcept.URL), + Code: scalarutils.Code(compositionConcept.CompositionCategoryConcept.ID), + Display: compositionConcept.CompositionCategoryConcept.DisplayName, }, }, - Text: compositionCategoryConcept.DisplayName, + Text: compositionConcept.CompositionCategoryConcept.DisplayName, }, } @@ -332,21 +313,9 @@ func (c *UseCasesClinicalImpl) AppendNoteToComposition(ctx context.Context, id s organizationRef := fmt.Sprintf("Organization/%s", identifiers.OrganizationID) - var compositionCategoryCode string - - switch input.Category { - case "ASSESSMENT_PLAN": - compositionCategoryCode = common.LOINCAssessmentPlanCode - case "HISTORY_OF_PRESENTING_ILLNESS": - compositionCategoryCode = common.LOINCHistoryOfPresentingIllness - case "SOCIAL_HISTORY": - compositionCategoryCode = common.LOINCSocialHistory - case "FAMILY_HISTORY": - compositionCategoryCode = common.LOINCFamilyHistory - case "EXAMINATION": - compositionCategoryCode = common.LOINCExamination - case "PLAN_OF_CARE": - compositionCategoryCode = common.LOINCPLANOFCARE + compositionCategoryCode, err := c.mapCategoryEnumToCode(input.Category) + if err != nil { + return nil, err } compositionCategoryConcept, err := c.GetConcept(ctx, dto.TerminologySourceLOINC, compositionCategoryCode) diff --git a/pkg/clinical/usecases/clinical/composition_helpers.go b/pkg/clinical/usecases/clinical/composition_helpers.go new file mode 100644 index 00000000..105554f3 --- /dev/null +++ b/pkg/clinical/usecases/clinical/composition_helpers.go @@ -0,0 +1,60 @@ +package clinical + +import ( + "context" + "fmt" + + "github.com/savannahghi/clinical/pkg/clinical/application/common" + "github.com/savannahghi/clinical/pkg/clinical/application/dto" + "github.com/savannahghi/clinical/pkg/clinical/domain" +) + +// CompositionConcept is used to map composition concepts +type CompositionConcept struct { + CompositionCategoryConcept *domain.Concept + CompositionTypeConcept *domain.Concept +} + +// mapCompositionConcepts composes a unified representation of composition concepts and types into a single model +func (c *UseCasesClinicalImpl) mapCompositionConcepts(ctx context.Context, compositionCategoryCode, conceptID string) (*CompositionConcept, error) { + compositionCategoryConcept, err := c.GetConcept(ctx, dto.TerminologySourceLOINC, compositionCategoryCode) + if err != nil { + return nil, err + } + + compositionTypeConcept, err := c.GetConcept(ctx, dto.TerminologySourceLOINC, conceptID) + if err != nil { + return nil, err + } + + return &CompositionConcept{ + CompositionCategoryConcept: compositionCategoryConcept, + CompositionTypeConcept: compositionTypeConcept, + }, nil +} + +// mapCategoryEnumToCode is used to map various composition categories to respective LOINC codes +func (*UseCasesClinicalImpl) mapCategoryEnumToCode(category dto.CompositionCategory) (string, error) { + var compositionCategoryCode string + + switch category { + case "ASSESSMENT_PLAN": + compositionCategoryCode = common.LOINCAssessmentPlanCode + case "HISTORY_OF_PRESENTING_ILLNESS": + compositionCategoryCode = common.LOINCHistoryOfPresentingIllness + case "SOCIAL_HISTORY": + compositionCategoryCode = common.LOINCSocialHistory + case "FAMILY_HISTORY": + compositionCategoryCode = common.LOINCFamilyHistory + case "EXAMINATION": + compositionCategoryCode = common.LOINCExamination + case "PLAN_OF_CARE": + compositionCategoryCode = common.LOINCPlanOfCare + case "PROGRESS_NOTE": + compositionCategoryCode = common.LOINCProviderUnspecifiedProgressNote + default: + return "", fmt.Errorf("category is needed") + } + + return compositionCategoryCode, nil +} diff --git a/pkg/clinical/usecases/clinical/encounter.go b/pkg/clinical/usecases/clinical/encounter.go index 26ce721f..8fa1983f 100644 --- a/pkg/clinical/usecases/clinical/encounter.go +++ b/pkg/clinical/usecases/clinical/encounter.go @@ -8,6 +8,7 @@ import ( "time" "github.com/mitchellh/mapstructure" + "github.com/savannahghi/clinical/pkg/clinical/application/common" "github.com/savannahghi/clinical/pkg/clinical/application/dto" "github.com/savannahghi/clinical/pkg/clinical/domain" "github.com/savannahghi/scalarutils" @@ -78,9 +79,88 @@ func (c *UseCasesClinicalImpl) StartEncounter(ctx context.Context, episodeID str return "", err } + // Create a blank composition + _, err = c.RecordFHIRComposition(ctx, encounter, tags, episodeOfCare) + if err != nil { + return "", err + } + return *encounter.Resource.ID, nil } +func (c *UseCasesClinicalImpl) RecordFHIRComposition(ctx context.Context, + encounter *domain.FHIREncounterRelayPayload, + tags []domain.FHIRCodingInput, episodeOfCare *domain.FHIREpisodeOfCareRelayPayload) (*domain.FHIRCompositionRelayPayload, error) { + encounterRef := fmt.Sprintf("Encounter/%s", *encounter.Resource.ID) + encounterType := scalarutils.URI("Encounter") + + today := time.Now() + + date, err := scalarutils.NewDate(today.Day(), int(today.Month()), today.Year()) + if err != nil { + return nil, err + } + + preliminaryStatus := domain.CompositionStatusEnumPreliminary + + organizationRef := fmt.Sprintf("Organization/%s", *encounter.Resource.Meta.Tag[0].Code) + + compositionCategoryCode, err := c.mapCategoryEnumToCode(dto.ProviderUnspecifiedProgressNote) + if err != nil { + return nil, err + } + + compositionConcept, err := c.mapCompositionConcepts(ctx, compositionCategoryCode, common.LOINCProviderUnspecifiedProgressNote) + if err != nil { + return nil, err + } + + compositionTitle := fmt.Sprintf("%s's %s", episodeOfCare.Resource.Patient.Display, compositionConcept.CompositionCategoryConcept.DisplayName) + + compositionInput := domain.FHIRCompositionInput{ + Status: &preliminaryStatus, + Meta: &domain.FHIRMetaInput{ + Tag: tags, + }, + Type: &domain.FHIRCodeableConceptInput{ + Coding: []*domain.FHIRCodingInput{ + { + System: (*scalarutils.URI)(&compositionConcept.CompositionTypeConcept.URL), + Code: scalarutils.Code(compositionConcept.CompositionTypeConcept.ID), + Display: compositionConcept.CompositionTypeConcept.DisplayName, + }, + }, + Text: compositionConcept.CompositionTypeConcept.DisplayName, + }, + Subject: &domain.FHIRReferenceInput{ + ID: episodeOfCare.Resource.Patient.ID, + Reference: episodeOfCare.Resource.Patient.Reference, + Type: episodeOfCare.Resource.Patient.Type, + Display: episodeOfCare.Resource.Patient.Display, + }, + Encounter: &domain.FHIRReferenceInput{ + ID: encounter.Resource.ID, + Reference: &encounterRef, + Display: *encounter.Resource.ID, + Type: &encounterType, + }, + Date: date, + Author: []*domain.FHIRReferenceInput{ + { + Reference: &organizationRef, + }, + }, + Title: &compositionTitle, + } + + output, err := c.infrastructure.FHIR.CreateFHIRComposition(ctx, compositionInput) + if err != nil { + return nil, err + } + + return output, nil +} + func (c *UseCasesClinicalImpl) PatchEncounter(ctx context.Context, encounterID string, input dto.EncounterInput) (*dto.Encounter, error) { if encounterID == "" { return nil, fmt.Errorf("an encounterID is required") diff --git a/pkg/clinical/usecases/clinical/encounter_test.go b/pkg/clinical/usecases/clinical/encounter_test.go index acdbcca9..f15d3f19 100644 --- a/pkg/clinical/usecases/clinical/encounter_test.go +++ b/pkg/clinical/usecases/clinical/encounter_test.go @@ -69,6 +69,22 @@ func TestUseCasesClinicalImpl_StartEncounter(t *testing.T) { }, wantErr: true, }, + { + name: "Sad Case - failed to get concept", + args: args{ + ctx: ctx, + episodeID: uuid.New().String(), + }, + wantErr: true, + }, + { + name: "Sad Case - failed to create FHIR composition", + args: args{ + ctx: ctx, + episodeID: uuid.New().String(), + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -100,6 +116,16 @@ func TestUseCasesClinicalImpl_StartEncounter(t *testing.T) { return nil, fmt.Errorf("failed to get tenant identifiers") } } + if tt.name == "Sad Case - failed to get concept" { + fakeOCL.MockGetConceptFn = func(ctx context.Context, org, source, concept string, includeMappings, includeInverseMappings bool) (*domain.Concept, error) { + return nil, fmt.Errorf("failed to get concept") + } + } + if tt.name == "Sad Case - failed to create FHIR composition" { + fakeFHIR.MockCreateFHIRCompositionFn = func(ctx context.Context, input domain.FHIRCompositionInput) (*domain.FHIRCompositionRelayPayload, error) { + return nil, fmt.Errorf("failed to create FHIR composition") + } + } got, err := u.StartEncounter(tt.args.ctx, tt.args.episodeID) if (err != nil) != tt.wantErr {