diff --git a/pkg/clinical/application/dto/output.go b/pkg/clinical/application/dto/output.go index 49f65a3..588be4d 100644 --- a/pkg/clinical/application/dto/output.go +++ b/pkg/clinical/application/dto/output.go @@ -160,3 +160,11 @@ type Section struct { type ConsentOutput struct { Status *ConsentStatusEnum `json:"status"` } + +// PatientMedicationHistoryOutput object models all information about a patient +type PatientMedicationHistoryOutput struct { + Conditions []*Condition `json:"condition"` + Observations []*Observation `json:"observations"` + Patient Patient `json:"patient"` + Medications []*MedicationStatement `json:"medications"` +} diff --git a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go index 2c746d2..17f87fe 100644 --- a/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go +++ b/pkg/clinical/infrastructure/datastore/cloudhealthcare/mock/fhir_mock.go @@ -1298,6 +1298,7 @@ func NewFHIRMock() *FHIRMock { MockSearchFHIRObservationFn: func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRObservations, error) { uuid := uuid.New().String() finalStatus := domain.ObservationStatusEnumFinal + instant := gofakeit.TimeZone() return &domain.PagedFHIRObservations{ Observations: []domain.FHIRObservation{ { @@ -1309,6 +1310,18 @@ func NewFHIRMock() *FHIRMock { Encounter: &domain.FHIRReference{ ID: &uuid, }, + Code: &domain.FHIRCodeableConcept{ + ID: new(string), + Coding: []*domain.FHIRCoding{ + { + ID: new(string), + Display: "Test", + UserSelected: new(bool), + }, + }, + Text: "", + }, + EffectiveInstant: (*scalarutils.Instant)(&instant), }, }, HasNextPage: false, @@ -1506,6 +1519,7 @@ func NewFHIRMock() *FHIRMock { MockGetFHIRPatientFn: func(ctx context.Context, id string) (*domain.FHIRPatientRelayPayload, error) { patientID := uuid.New().String() patientName := gofakeit.Name() + trueBool := true gender := domain.PatientGenderEnumFemale return &domain.FHIRPatientRelayPayload{ Resource: &domain.FHIRPatient{ @@ -1516,6 +1530,7 @@ func NewFHIRMock() *FHIRMock { }, }, Gender: &gender, + Active: &trueBool, BirthDate: &scalarutils.Date{ Year: 1990, Month: 12, @@ -1535,18 +1550,27 @@ func NewFHIRMock() *FHIRMock { }, MockSearchFHIRMedicationStatementFn: func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.FHIRMedicationStatementRelayConnection, error) { codingCode := "123" + patientID := "1" + status := domain.MedicationStatementStatusEnumActive return &domain.FHIRMedicationStatementRelayConnection{ Edges: []*domain.FHIRMedicationStatementRelayEdge{ { Cursor: new(string), Node: &domain.FHIRMedicationStatement{ - ID: new(string), - Text: &domain.FHIRNarrative{}, - Identifier: []*domain.FHIRIdentifier{}, - BasedOn: []*domain.FHIRReference{}, - PartOf: []*domain.FHIRReference{}, - // Status: &"", + ID: new(string), + Text: &domain.FHIRNarrative{}, + Identifier: []*domain.FHIRIdentifier{}, + BasedOn: []*domain.FHIRReference{}, + PartOf: []*domain.FHIRReference{}, + Status: &status, StatusReason: []*domain.FHIRCodeableConcept{}, + MedicationCodeableConcept: &domain.FHIRCodeableConcept{ + Coding: []*domain.FHIRCoding{ + { + Display: "Panadol", + }, + }, + }, Category: &domain.FHIRCodeableConcept{ ID: new(string), Coding: []*domain.FHIRCoding{ @@ -1560,21 +1584,22 @@ func NewFHIRMock() *FHIRMock { }, Text: "", }, - MedicationCodeableConcept: &domain.FHIRCodeableConcept{}, - MedicationReference: &domain.FHIRMedication{}, - Subject: &domain.FHIRReference{}, - Context: &domain.FHIRReference{}, - EffectiveDateTime: &scalarutils.Date{}, - EffectivePeriod: &domain.FHIRPeriod{}, - DateAsserted: &scalarutils.Date{}, - InformationSource: &domain.FHIRReference{}, - DerivedFrom: []*domain.FHIRReference{}, - ReasonCode: []*domain.FHIRCodeableConcept{}, - ReasonReference: []*domain.FHIRReference{}, - Note: []*domain.FHIRAnnotation{}, - Dosage: []*domain.FHIRDosage{}, - Meta: &domain.FHIRMeta{}, - Extension: []*domain.FHIRExtension{}, + MedicationReference: &domain.FHIRMedication{}, + Subject: &domain.FHIRReference{ + ID: &patientID, + }, + Context: &domain.FHIRReference{}, + EffectiveDateTime: &scalarutils.Date{}, + EffectivePeriod: &domain.FHIRPeriod{}, + DateAsserted: &scalarutils.Date{}, + InformationSource: &domain.FHIRReference{}, + DerivedFrom: []*domain.FHIRReference{}, + ReasonCode: []*domain.FHIRCodeableConcept{}, + ReasonReference: []*domain.FHIRReference{}, + Note: []*domain.FHIRAnnotation{}, + Dosage: []*domain.FHIRDosage{}, + Meta: &domain.FHIRMeta{}, + Extension: []*domain.FHIRExtension{}, }, }, }, diff --git a/pkg/clinical/presentation/graph/clinical.graphql b/pkg/clinical/presentation/graph/clinical.graphql index 34a6bf5..6e2c522 100644 --- a/pkg/clinical/presentation/graph/clinical.graphql +++ b/pkg/clinical/presentation/graph/clinical.graphql @@ -133,6 +133,8 @@ extend type Query { encounterID: String! screeningType: ScreeningTypeEnum! ): String! + + getPatientMedicationHistory(patientID: ID!, pagination: Pagination!): PatientMedicationHistoryOutput! } extend type Mutation { diff --git a/pkg/clinical/presentation/graph/clinical.resolvers.go b/pkg/clinical/presentation/graph/clinical.resolvers.go index fac4eb1..71c4108 100644 --- a/pkg/clinical/presentation/graph/clinical.resolvers.go +++ b/pkg/clinical/presentation/graph/clinical.resolvers.go @@ -461,6 +461,13 @@ func (r *queryResolver) GetQuestionnaireResponseRiskLevel(ctx context.Context, e return r.usecases.GetQuestionnaireResponseRiskLevel(ctx, encounterID, screeningType) } +// GetPatientMedicationHistory is the resolver for the getPatientMedicationHistory field. +func (r *queryResolver) GetPatientMedicationHistory(ctx context.Context, patientID string, pagination dto.Pagination) (*dto.PatientMedicationHistoryOutput, error) { + r.CheckDependencies() + + return r.usecases.PatientMedicationHistory(ctx, patientID, &pagination) +} + // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } diff --git a/pkg/clinical/presentation/graph/generated/generated.go b/pkg/clinical/presentation/graph/generated/generated.go index 6fba85f..ee46924 100644 --- a/pkg/clinical/presentation/graph/generated/generated.go +++ b/pkg/clinical/presentation/graph/generated/generated.go @@ -406,6 +406,13 @@ type ComplexityRoot struct { PhoneNumber func(childComplexity int) int } + PatientMedicationHistoryOutput struct { + Conditions func(childComplexity int) int + Medications func(childComplexity int) int + Observations func(childComplexity int) int + Patient func(childComplexity int) int + } + Period struct { End func(childComplexity int) int ID func(childComplexity int) int @@ -430,6 +437,7 @@ type ComplexityRoot struct { GetPatientDiastolicBloodPressureEntries func(childComplexity int, patientID string, encounterID *string, date *scalarutils.Date, pagination dto.Pagination) int GetPatientHeightEntries func(childComplexity int, patientID string, encounterID *string, date *scalarutils.Date, pagination dto.Pagination) int GetPatientLastMenstrualPeriodEntries func(childComplexity int, patientID string, encounterID *string, date *scalarutils.Date, pagination dto.Pagination) int + GetPatientMedicationHistory func(childComplexity int, patientID string, pagination dto.Pagination) int GetPatientMuacEntries func(childComplexity int, patientID string, encounterID *string, date *scalarutils.Date, pagination dto.Pagination) int GetPatientOxygenSaturationEntries func(childComplexity int, patientID string, encounterID *string, date *scalarutils.Date, pagination dto.Pagination) int GetPatientPulseRateEntries func(childComplexity int, patientID string, encounterID *string, date *scalarutils.Date, pagination dto.Pagination) int @@ -757,6 +765,7 @@ type QueryResolver interface { ListPatientAllergies(ctx context.Context, patientID string, pagination dto.Pagination) (*dto.AllergyConnection, error) ListPatientMedia(ctx context.Context, patientID string, pagination dto.Pagination) (*dto.MediaConnection, error) GetQuestionnaireResponseRiskLevel(ctx context.Context, encounterID string, screeningType domain.ScreeningTypeEnum) (string, error) + GetPatientMedicationHistory(ctx context.Context, patientID string, pagination dto.Pagination) (*dto.PatientMedicationHistoryOutput, error) } type executableSchema struct { @@ -2695,6 +2704,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Patient.PhoneNumber(childComplexity), true + case "PatientMedicationHistoryOutput.conditions": + if e.complexity.PatientMedicationHistoryOutput.Conditions == nil { + break + } + + return e.complexity.PatientMedicationHistoryOutput.Conditions(childComplexity), true + + case "PatientMedicationHistoryOutput.medications": + if e.complexity.PatientMedicationHistoryOutput.Medications == nil { + break + } + + return e.complexity.PatientMedicationHistoryOutput.Medications(childComplexity), true + + case "PatientMedicationHistoryOutput.observations": + if e.complexity.PatientMedicationHistoryOutput.Observations == nil { + break + } + + return e.complexity.PatientMedicationHistoryOutput.Observations(childComplexity), true + + case "PatientMedicationHistoryOutput.patient": + if e.complexity.PatientMedicationHistoryOutput.Patient == nil { + break + } + + return e.complexity.PatientMedicationHistoryOutput.Patient(childComplexity), true + case "Period.end": if e.complexity.Period.End == nil { break @@ -2859,6 +2896,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.GetPatientLastMenstrualPeriodEntries(childComplexity, args["patientID"].(string), args["encounterID"].(*string), args["date"].(*scalarutils.Date), args["pagination"].(dto.Pagination)), true + case "Query.getPatientMedicationHistory": + if e.complexity.Query.GetPatientMedicationHistory == nil { + break + } + + args, err := ec.field_Query_getPatientMedicationHistory_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.GetPatientMedicationHistory(childComplexity, args["patientID"].(string), args["pagination"].(dto.Pagination)), true + case "Query.getPatientMuacEntries": if e.complexity.Query.GetPatientMuacEntries == nil { break @@ -4439,6 +4488,8 @@ var sources = []*ast.Source{ encounterID: String! screeningType: ScreeningTypeEnum! ): String! + + getPatientMedicationHistory(patientID: ID!, pagination: Pagination!): PatientMedicationHistoryOutput! } extend type Mutation { @@ -5422,6 +5473,13 @@ type EncounterAssociatedResources { riskAssessment: RiskAssessment consent: Consent observation: Observation +} + +type PatientMedicationHistoryOutput { + observations: [Observation] + conditions: [Condition] + patient: Patient + medications: [MedicationStatement] }`, BuiltIn: false}, {Name: "../../../../../federation/directives.graphql", Input: ` directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE @@ -6696,6 +6754,30 @@ func (ec *executionContext) field_Query_getPatientLastMenstrualPeriodEntries_arg return args, nil } +func (ec *executionContext) field_Query_getPatientMedicationHistory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["patientID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("patientID")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["patientID"] = arg0 + var arg1 dto.Pagination + if tmp, ok := rawArgs["pagination"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) + arg1, err = ec.unmarshalNPagination2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐPagination(ctx, tmp) + if err != nil { + return nil, err + } + } + args["pagination"] = arg1 + return args, nil +} + func (ec *executionContext) field_Query_getPatientMuacEntries_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -19328,6 +19410,238 @@ func (ec *executionContext) fieldContext_Patient_birthDate(ctx context.Context, return fc, nil } +func (ec *executionContext) _PatientMedicationHistoryOutput_observations(ctx context.Context, field graphql.CollectedField, obj *dto.PatientMedicationHistoryOutput) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PatientMedicationHistoryOutput_observations(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 obj.Observations, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*dto.Observation) + fc.Result = res + return ec.marshalOObservation2ᚕᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐObservation(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PatientMedicationHistoryOutput_observations(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PatientMedicationHistoryOutput", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Observation_id(ctx, field) + case "status": + return ec.fieldContext_Observation_status(ctx, field) + case "patientID": + return ec.fieldContext_Observation_patientID(ctx, field) + case "encounterID": + return ec.fieldContext_Observation_encounterID(ctx, field) + case "name": + return ec.fieldContext_Observation_name(ctx, field) + case "value": + return ec.fieldContext_Observation_value(ctx, field) + case "timeRecorded": + return ec.fieldContext_Observation_timeRecorded(ctx, field) + case "interpretation": + return ec.fieldContext_Observation_interpretation(ctx, field) + case "note": + return ec.fieldContext_Observation_note(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Observation", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PatientMedicationHistoryOutput_conditions(ctx context.Context, field graphql.CollectedField, obj *dto.PatientMedicationHistoryOutput) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PatientMedicationHistoryOutput_conditions(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 obj.Conditions, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*dto.Condition) + fc.Result = res + return ec.marshalOCondition2ᚕᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCondition(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PatientMedicationHistoryOutput_conditions(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PatientMedicationHistoryOutput", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Condition_id(ctx, field) + case "status": + return ec.fieldContext_Condition_status(ctx, field) + case "name": + return ec.fieldContext_Condition_name(ctx, field) + case "code": + return ec.fieldContext_Condition_code(ctx, field) + case "system": + return ec.fieldContext_Condition_system(ctx, field) + case "category": + return ec.fieldContext_Condition_category(ctx, field) + case "onsetDate": + return ec.fieldContext_Condition_onsetDate(ctx, field) + case "recordedDate": + return ec.fieldContext_Condition_recordedDate(ctx, field) + case "note": + return ec.fieldContext_Condition_note(ctx, field) + case "patientID": + return ec.fieldContext_Condition_patientID(ctx, field) + case "encounterID": + return ec.fieldContext_Condition_encounterID(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Condition", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PatientMedicationHistoryOutput_patient(ctx context.Context, field graphql.CollectedField, obj *dto.PatientMedicationHistoryOutput) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PatientMedicationHistoryOutput_patient(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 obj.Patient, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(dto.Patient) + fc.Result = res + return ec.marshalOPatient2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐPatient(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PatientMedicationHistoryOutput_patient(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PatientMedicationHistoryOutput", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Patient_id(ctx, field) + case "active": + return ec.fieldContext_Patient_active(ctx, field) + case "name": + return ec.fieldContext_Patient_name(ctx, field) + case "phoneNumber": + return ec.fieldContext_Patient_phoneNumber(ctx, field) + case "gender": + return ec.fieldContext_Patient_gender(ctx, field) + case "birthDate": + return ec.fieldContext_Patient_birthDate(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Patient", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _PatientMedicationHistoryOutput_medications(ctx context.Context, field graphql.CollectedField, obj *dto.PatientMedicationHistoryOutput) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PatientMedicationHistoryOutput_medications(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 obj.Medications, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*dto.MedicationStatement) + fc.Result = res + return ec.marshalOMedicationStatement2ᚕᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐMedicationStatement(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PatientMedicationHistoryOutput_medications(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PatientMedicationHistoryOutput", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_MedicationStatement_id(ctx, field) + case "status": + return ec.fieldContext_MedicationStatement_status(ctx, field) + case "medication": + return ec.fieldContext_MedicationStatement_medication(ctx, field) + case "patientID": + return ec.fieldContext_MedicationStatement_patientID(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type MedicationStatement", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Period_id(ctx context.Context, field graphql.CollectedField, obj *dto.Period) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Period_id(ctx, field) if err != nil { @@ -21109,6 +21423,71 @@ func (ec *executionContext) fieldContext_Query_getQuestionnaireResponseRiskLevel return fc, nil } +func (ec *executionContext) _Query_getPatientMedicationHistory(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_getPatientMedicationHistory(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.Query().GetPatientMedicationHistory(rctx, fc.Args["patientID"].(string), fc.Args["pagination"].(dto.Pagination)) + }) + 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.PatientMedicationHistoryOutput) + fc.Result = res + return ec.marshalNPatientMedicationHistoryOutput2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐPatientMedicationHistoryOutput(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_getPatientMedicationHistory(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "observations": + return ec.fieldContext_PatientMedicationHistoryOutput_observations(ctx, field) + case "conditions": + return ec.fieldContext_PatientMedicationHistoryOutput_conditions(ctx, field) + case "patient": + return ec.fieldContext_PatientMedicationHistoryOutput_patient(ctx, field) + case "medications": + return ec.fieldContext_PatientMedicationHistoryOutput_medications(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PatientMedicationHistoryOutput", 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_Query_getPatientMedicationHistory_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query__service(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query__service(ctx, field) if err != nil { @@ -34913,6 +35292,48 @@ func (ec *executionContext) _Patient(ctx context.Context, sel ast.SelectionSet, return out } +var patientMedicationHistoryOutputImplementors = []string{"PatientMedicationHistoryOutput"} + +func (ec *executionContext) _PatientMedicationHistoryOutput(ctx context.Context, sel ast.SelectionSet, obj *dto.PatientMedicationHistoryOutput) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, patientMedicationHistoryOutputImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("PatientMedicationHistoryOutput") + case "observations": + out.Values[i] = ec._PatientMedicationHistoryOutput_observations(ctx, field, obj) + case "conditions": + out.Values[i] = ec._PatientMedicationHistoryOutput_conditions(ctx, field, obj) + case "patient": + out.Values[i] = ec._PatientMedicationHistoryOutput_patient(ctx, field, obj) + case "medications": + out.Values[i] = ec._PatientMedicationHistoryOutput_medications(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var periodImplementors = []string{"Period"} func (ec *executionContext) _Period(ctx context.Context, sel ast.SelectionSet, obj *dto.Period) graphql.Marshaler { @@ -35480,6 +35901,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "getPatientMedicationHistory": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_getPatientMedicationHistory(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "_service": field := field @@ -37480,6 +37923,20 @@ func (ec *executionContext) unmarshalNPatientInput2githubᚗcomᚋsavannahghiᚋ return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalNPatientMedicationHistoryOutput2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐPatientMedicationHistoryOutput(ctx context.Context, sel ast.SelectionSet, v dto.PatientMedicationHistoryOutput) graphql.Marshaler { + return ec._PatientMedicationHistoryOutput(ctx, sel, &v) +} + +func (ec *executionContext) marshalNPatientMedicationHistoryOutput2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐPatientMedicationHistoryOutput(ctx context.Context, sel ast.SelectionSet, v *dto.PatientMedicationHistoryOutput) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._PatientMedicationHistoryOutput(ctx, sel, v) +} + func (ec *executionContext) unmarshalNQuestionnaireResponseInput2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐQuestionnaireResponse(ctx context.Context, v interface{}) (dto.QuestionnaireResponse, error) { res, err := ec.unmarshalInputQuestionnaireResponseInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -38377,6 +38834,54 @@ func (ec *executionContext) marshalOCondition2githubᚗcomᚋsavannahghiᚋclini return ec._Condition(ctx, sel, &v) } +func (ec *executionContext) marshalOCondition2ᚕᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCondition(ctx context.Context, sel ast.SelectionSet, v []*dto.Condition) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOCondition2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCondition(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + +func (ec *executionContext) marshalOCondition2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐCondition(ctx context.Context, sel ast.SelectionSet, v *dto.Condition) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Condition(ctx, sel, v) +} + func (ec *executionContext) marshalOConditionConnection2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐConditionConnection(ctx context.Context, sel ast.SelectionSet, v *dto.ConditionConnection) graphql.Marshaler { if v == nil { return graphql.Null @@ -39242,6 +39747,10 @@ func (ec *executionContext) marshalOPageInfo2githubᚗcomᚋsavannahghiᚋclinic return ec._PageInfo(ctx, sel, &v) } +func (ec *executionContext) marshalOPatient2githubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐPatient(ctx context.Context, sel ast.SelectionSet, v dto.Patient) graphql.Marshaler { + return ec._Patient(ctx, sel, &v) +} + func (ec *executionContext) marshalOPeriod2ᚖgithubᚗcomᚋsavannahghiᚋclinicalᚋpkgᚋclinicalᚋapplicationᚋdtoᚐPeriod(ctx context.Context, sel ast.SelectionSet, v *dto.Period) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/clinical/presentation/graph/types.graphql b/pkg/clinical/presentation/graph/types.graphql index d83e3b2..8c3f737 100644 --- a/pkg/clinical/presentation/graph/types.graphql +++ b/pkg/clinical/presentation/graph/types.graphql @@ -547,4 +547,11 @@ type EncounterAssociatedResources { riskAssessment: RiskAssessment consent: Consent observation: Observation +} + +type PatientMedicationHistoryOutput { + observations: [Observation] + conditions: [Condition] + patient: Patient + medications: [MedicationStatement] } \ No newline at end of file diff --git a/pkg/clinical/usecases/clinical/patient.go b/pkg/clinical/usecases/clinical/patient.go index f2b2f6d..4d94bbf 100644 --- a/pkg/clinical/usecases/clinical/patient.go +++ b/pkg/clinical/usecases/clinical/patient.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/google/uuid" "github.com/savannahghi/clinical/pkg/clinical/application/extensions" "github.com/savannahghi/scalarutils" @@ -511,3 +512,104 @@ func mapFHIRPatientToPatientDTO(patient *domain.FHIRPatient) *dto.Patient { BirthDate: *patient.BirthDate, } } + +// PatientMedicationHistory is used to retrieve all the patient clinical information. From Observations, Conditions, +// MedicationStatement etc. relevant to referral from one facility to another. +func (c *UseCasesClinicalImpl) PatientMedicationHistory(ctx context.Context, patientID string, pagination *dto.Pagination) (*dto.PatientMedicationHistoryOutput, error) { + _, err := uuid.Parse(patientID) + if err != nil { + return nil, fmt.Errorf("invalid patient id: %s", patientID) + } + + patient, err := c.infrastructure.FHIR.GetFHIRPatient(ctx, patientID) + if err != nil { + return nil, err + } + + patientReference := fmt.Sprintf("Patient/%s", *patient.Resource.ID) + + searchParams := map[string]interface{}{ + "patient": patientReference, + } + + identifiers, err := c.infrastructure.BaseExtension.GetTenantIdentifiers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tenant identifiers from context: %w", err) + } + + observationsChan := make(chan *domain.PagedFHIRObservations) + conditionsChan := make(chan *domain.PagedFHIRCondition) + medicationStatementsChan := make(chan *domain.FHIRMedicationStatementRelayConnection) + errChan := make(chan error, 3) + + go func() { + result, err := c.infrastructure.FHIR.SearchFHIRObservation(ctx, searchParams, *identifiers, *pagination) + if err != nil { + errChan <- err + return + } + observationsChan <- result + }() + + go func() { + result, err := c.infrastructure.FHIR.SearchFHIRCondition(ctx, searchParams, *identifiers, *pagination) + if err != nil { + errChan <- err + return + } + conditionsChan <- result + }() + + go func() { + result, err := c.infrastructure.FHIR.SearchFHIRMedicationStatement(ctx, searchParams, *identifiers, *pagination) + if err != nil { + errChan <- err + return + } + medicationStatementsChan <- result + }() + + var observations *domain.PagedFHIRObservations + + var conditions *domain.PagedFHIRCondition + + var medicationStatements *domain.FHIRMedicationStatementRelayConnection + + // ensure whichever goroutine finishes first is processed, whether it returns a result or an error. + for i := 0; i < 3; i++ { + select { + case obs := <-observationsChan: + observations = obs + case conds := <-conditionsChan: + conditions = conds + case meds := <-medicationStatementsChan: + medicationStatements = meds + case err := <-errChan: + return nil, err + } + } + + var conditionsResult []*dto.Condition + for _, item := range conditions.Conditions { + conditionsResult = append(conditionsResult, mapFHIRConditionToConditionDTO(item)) + } + + var observationList []*dto.Observation + for _, item := range observations.Observations { + observationList = append(observationList, mapFHIRObservationToObservationDTO(item)) + } + + var medicationStatement []*dto.MedicationStatement + for _, item := range medicationStatements.Edges { + medicationStatement = append(medicationStatement, mapFHIRMedicationStatementToMedicationStatementDTO(item.Node)) + } + + output := &dto.PatientMedicationHistoryOutput{ + Conditions: conditionsResult, + Observations: observationList, + Patient: *mapFHIRPatientToPatientDTO(patient.Resource), + Medications: medicationStatement, + } + + return output, nil +} diff --git a/pkg/clinical/usecases/clinical/patient_unit_test.go b/pkg/clinical/usecases/clinical/patient_unit_test.go index 24fe44a..c69270e 100644 --- a/pkg/clinical/usecases/clinical/patient_unit_test.go +++ b/pkg/clinical/usecases/clinical/patient_unit_test.go @@ -2133,3 +2133,140 @@ func TestUseCasesClinicalImpl_DeletePatient(t *testing.T) { }) } } + +func TestUseCasesClinicalImpl_PatientMedicationHistory(t *testing.T) { + firstTen := 10 + type args struct { + ctx context.Context + patientID string + pagination *dto.Pagination + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Happy case: get patient medical information", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + patientID: gofakeit.UUID(), + pagination: &dto.Pagination{ + First: &firstTen, + }, + }, + wantErr: false, + }, + { + name: "Sad case: invalid patient ID", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + patientID: "1", + pagination: &dto.Pagination{ + First: &firstTen, + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get patient medical information", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + patientID: gofakeit.UUID(), + pagination: &dto.Pagination{ + First: &firstTen, + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get patient observations", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + patientID: gofakeit.UUID(), + pagination: &dto.Pagination{ + First: &firstTen, + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get patient conditions", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + patientID: gofakeit.UUID(), + pagination: &dto.Pagination{ + First: &firstTen, + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get patient medication statement", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + patientID: gofakeit.UUID(), + pagination: &dto.Pagination{ + First: &firstTen, + }, + }, + wantErr: true, + }, + { + name: "Sad case: unable to get tenant identifiers from context", + args: args{ + ctx: addTenantIdentifierContext(context.Background()), + patientID: gofakeit.UUID(), + pagination: &dto.Pagination{ + First: &firstTen, + }, + }, + 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() + fakeAdvantage := fakeAdvantageMock.NewFakeAdvantageMock() + + infra := infrastructure.NewInfrastructureInteractor(fakeExt, fakeFHIR, fakeOCL, fakeUpload, fakePubSub, fakeAdvantage) + u := clinicalUsecase.NewUseCasesClinicalImpl(infra) + + if tt.name == "Sad case: unable to get patient medical information" { + fakeFHIR.MockGetFHIRPatientFn = func(ctx context.Context, id string) (*domain.FHIRPatientRelayPayload, error) { + return nil, fmt.Errorf("failed to get patient") + } + } + if tt.name == "Sad case: unable to get patient observations" { + fakeFHIR.MockSearchFHIRObservationFn = func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRObservations, error) { + return nil, fmt.Errorf("failed to get patient observations") + } + } + if tt.name == "Sad case: unable to get patient conditions" { + fakeFHIR.MockSearchFHIRConditionFn = func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.PagedFHIRCondition, error) { + return nil, fmt.Errorf("failed to get patient conditions") + } + } + if tt.name == "Sad case: unable to get patient medication statement" { + fakeFHIR.MockSearchFHIRMedicationStatementFn = func(ctx context.Context, params map[string]interface{}, tenant dto.TenantIdentifiers, pagination dto.Pagination) (*domain.FHIRMedicationStatementRelayConnection, error) { + return nil, fmt.Errorf("failed to get patient medications") + } + } + if tt.name == "Sad case: unable to get tenant identifiers from context" { + fakeExt.MockGetTenantIdentifiersFn = func(ctx context.Context) (*dto.TenantIdentifiers, error) { + return nil, fmt.Errorf("failed to get tenant identifiers") + } + } + + _, err := u.PatientMedicationHistory(tt.args.ctx, tt.args.patientID, tt.args.pagination) + if (err != nil) != tt.wantErr { + t.Errorf("UseCasesClinicalImpl.PatientMedicationHistory() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +}