From e995fb025b300ac78c3f3d756883db20921d1e3c Mon Sep 17 00:00:00 2001 From: Chris Hossenlopp Date: Tue, 8 Oct 2024 15:42:38 -0400 Subject: [PATCH 1/5] potential one line fix --- src/calculation/MeasureReportBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 29338f77..325f4252 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -227,7 +227,7 @@ export default class MeasureReportBuilder exten // or s.id (newer measures) const strata: MeasureReportGroupStratifier | undefined = group.stratifier?.find(s => s.code && s.code[0]?.text === stratResults.strataCode) || - group.stratifier?.find(s => s.id === stratResults.strataCode); + group.stratifier?.find(s => s.id === stratResults.strataId); const stratum = strata?.stratum?.[0]; if (stratum) { er.populationResults?.forEach(pr => { From b8936ae2325cb87e86425a4e588826adbb9cb7d4 Mon Sep 17 00:00:00 2001 From: LaurenD Date: Wed, 9 Oct 2024 13:38:34 -0400 Subject: [PATCH 2/5] Update episode strataCode to have id as backup value --- src/calculation/DetailedResultsBuilder.ts | 4 ++-- src/calculation/MeasureReportBuilder.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calculation/DetailedResultsBuilder.ts b/src/calculation/DetailedResultsBuilder.ts index fa2c94c6..730434a5 100644 --- a/src/calculation/DetailedResultsBuilder.ts +++ b/src/calculation/DetailedResultsBuilder.ts @@ -462,7 +462,7 @@ export function createEpisodePopulationValues( // loop over stratifications and collect episode results for the strata let strataIndex = 1; populationGroup.stratifier?.forEach(strata => { - const strataCode = strata.code?.text ?? `strata-${strataIndex++}`; + const strataCode = strata.code?.text ?? strata.id ?? `strata-${strataIndex++}`; if (strata.criteria?.expression) { const rawEpisodeResults = patientResults[strata.criteria?.expression]; createOrSetValueOfEpisodes( @@ -563,7 +563,7 @@ function createOrSetValueOfEpisodes( newEpisodeResults.stratifierResults = []; let strataIndex = 1; populationGroup.stratifier?.forEach(strata => { - const newStrataCode = strata.code?.text ?? `strata-${strataIndex++}`; + const newStrataCode = strata.code?.text ?? strata.id ?? `strata-${strataIndex++}`; newEpisodeResults.stratifierResults?.push({ ...(strataId ? { strataId } : {}), strataCode: newStrataCode, diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 325f4252..1121346b 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -227,7 +227,7 @@ export default class MeasureReportBuilder exten // or s.id (newer measures) const strata: MeasureReportGroupStratifier | undefined = group.stratifier?.find(s => s.code && s.code[0]?.text === stratResults.strataCode) || - group.stratifier?.find(s => s.id === stratResults.strataId); + group.stratifier?.find(s => s.id === stratResults.strataCode); // strataCode may have an id value if code did not originally exist const stratum = strata?.stratum?.[0]; if (stratum) { er.populationResults?.forEach(pr => { From 55b3ac4028ad731c69948dd2c3e17916602fa41f Mon Sep 17 00:00:00 2001 From: LaurenD Date: Thu, 10 Oct 2024 16:05:05 -0400 Subject: [PATCH 3/5] Add tests --- src/calculation/MeasureReportBuilder.ts | 4 +- test/unit/DetailedResultsBuilder.test.ts | 74 +++++++ test/unit/MeasureReportBuilder.test.ts | 159 ++++++++++++++ .../episode-measure-with-stratifiers.json | 200 ++++++++++++++++++ 4 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 test/unit/fixtures/measure/episode-measure-with-stratifiers.json diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 1121346b..6eccd34b 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -647,9 +647,9 @@ export default class MeasureReportBuilder exten options: CalculationOptions ): fhir4.MeasureReport[] { const reports: fhir4.MeasureReport[] = []; + const measure = extractMeasureFromBundle(measureBundle); + const builder = new MeasureReportBuilder(measure, options); executionResults.forEach(result => { - const measure = extractMeasureFromBundle(measureBundle); - const builder = new MeasureReportBuilder(measure, options); builder.addPatientResults(result); reports.push(builder.getReport()); }); diff --git a/test/unit/DetailedResultsBuilder.test.ts b/test/unit/DetailedResultsBuilder.test.ts index 819a6dd5..403abd51 100644 --- a/test/unit/DetailedResultsBuilder.test.ts +++ b/test/unit/DetailedResultsBuilder.test.ts @@ -1225,6 +1225,80 @@ describe('DetailedResultsBuilder', () => { ]) ); }); + + test('it should add stratificationIds to stratification results of an individual episode for episode-based measure', () => { + const episodeMeasureStrat: fhir4.Measure = { + resourceType: 'Measure', + status: 'unknown', + extension: [ + { + url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis', + valueCode: 'Encounter' + } + ], + group: [ + { + population: [ + { + id: 'example-population-id', + code: { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/measure-population', + code: 'initial-population' + } + ] + }, + criteria: { + expression: 'ipp', + language: 'text/cql' + } + } + ], + stratifier: [ + { + id: 'example-stratifier-id', + criteria: { + language: 'text/cql-identifier', + expression: 'Strat1' + } + } + ] + } + ] + }; + + const group = (episodeMeasureStrat.group as [fhir4.MeasureGroup])[0]; + + const statementResults: StatementResults = { + ipp: [ + { + id: { + value: 'example-encounter' + } + } + ] + }; + + const { episodeResults } = DetailedResultsBuilder.createPopulationValues( + episodeMeasureStrat, + group, + statementResults + ); + + expect(episodeResults).toBeDefined(); + expect(episodeResults).toHaveLength(1); + + const episodeStratifierResults = (episodeResults as EpisodeResults[])[0].stratifierResults; + + expect(episodeStratifierResults).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + strataCode: 'example-stratifier-id' + }) + ]) + ); + }); }); describe('ELM JSON Function', () => { diff --git a/test/unit/MeasureReportBuilder.test.ts b/test/unit/MeasureReportBuilder.test.ts index 80543c14..a0e986b4 100644 --- a/test/unit/MeasureReportBuilder.test.ts +++ b/test/unit/MeasureReportBuilder.test.ts @@ -26,6 +26,7 @@ const patient2Id = '08fc9439-b7ff-4309-b409-4d143388594c'; const simpleMeasure = getJSONFixture('measure/simple-measure.json') as fhir4.Measure; const propWithStratMeasure = getJSONFixture('measure/proportion-measure-with-stratifiers.json') as fhir4.Measure; +const episodeWithStratMeasure = getJSONFixture('measure/episode-measure-with-stratifiers.json') as fhir4.Measure; const ratioMeasure = getJSONFixture('measure/ratio-measure.json') as fhir4.Measure; const cvMeasure = getJSONFixture('measure/cv-measure.json') as fhir4.Measure; const cvMeasureScoringOnGroup = getJSONFixture('measure/group-score-cv-measure.json'); @@ -43,6 +44,7 @@ function buildTestMeasureBundle(measure: fhir4.Measure): fhir4.Bundle { } const simpleMeasureBundle = buildTestMeasureBundle(simpleMeasure); const propWithStratMeasureBundle = buildTestMeasureBundle(propWithStratMeasure); +const episodeWithStratMeasureBundle = buildTestMeasureBundle(episodeWithStratMeasure); const ratioMeasureBundle = buildTestMeasureBundle(ratioMeasure); const cvMeasureBundle = buildTestMeasureBundle(cvMeasure); @@ -293,6 +295,120 @@ const propWithStratExecutionResults: ExecutionResult[] = [ + { + patientId: patient1Id, + detailedResults: [ + { + groupId: 'group-1', + statementResults: [], + populationResults: [ + { + populationType: PopulationType.NUMER, + criteriaExpression: 'Numerator', + result: false + }, + { + populationType: PopulationType.DENOM, + criteriaExpression: 'Denominator', + result: true + }, + { + populationType: PopulationType.IPP, + criteriaExpression: 'Initial Population', + result: true + }, + { + populationType: PopulationType.DENEX, + criteriaExpression: 'Denominator Exclusion', + result: false + } + ], + stratifierResults: [ + { + strataCode: '93f5f1c7-8638-40a4-a596-8b5831599209', + result: false, + appliesResult: false, + strataId: '93f5f1c7-8638-40a4-a596-8b5831599209' + }, + { + strataCode: '5baf37c7-8887-4576-837e-ea20a8938282', + result: false, + appliesResult: false, + strataId: '5baf37c7-8887-4576-837e-ea20a8938282' + }, + { + strataCode: '125b3d95-2d00-455f-8a6e-d53614a2a50e', + result: false, + appliesResult: false, + strataId: '125b3d95-2d00-455f-8a6e-d53614a2a50e' + }, + { + strataCode: 'c06647b9-e134-4189-858d-80cee23c0f8d', + result: false, + appliesResult: false, + strataId: 'c06647b9-e134-4189-858d-80cee23c0f8d' + } + ], + episodeResults: [ + { + episodeId: '5ca62964b8484628b8de1f2b', + populationResults: [ + { + populationType: PopulationType.NUMER, + criteriaExpression: 'Numerator', + result: false + }, + { + populationType: PopulationType.DENOM, + criteriaExpression: 'Denominator', + result: true + }, + { + populationType: PopulationType.IPP, + criteriaExpression: 'Initial Population', + result: true + }, + { + populationType: PopulationType.DENEX, + criteriaExpression: 'Denominator Exclusion', + result: false + } + ], + stratifierResults: [ + { + strataCode: '93f5f1c7-8638-40a4-a596-8b5831599209', + result: false, + appliesResult: false, + strataId: '93f5f1c7-8638-40a4-a596-8b5831599209' + }, + { + strataCode: '5baf37c7-8887-4576-837e-ea20a8938282', + result: false, + appliesResult: false, + strataId: '5baf37c7-8887-4576-837e-ea20a8938282' + }, + { + strataCode: '125b3d95-2d00-455f-8a6e-d53614a2a50e', + result: false, + appliesResult: false, + strataId: '125b3d95-2d00-455f-8a6e-d53614a2a50e' + }, + { + strataCode: 'c06647b9-e134-4189-858d-80cee23c0f8d', + result: false, + appliesResult: false, + strataId: 'c06647b9-e134-4189-858d-80cee23c0f8d' + } + ] + } + ], + html: 'example-html' + } + ] + } +]; + const calculationOptions: CalculationOptions = { measurementPeriodStart: '2021-01-01', measurementPeriodEnd: '2021-12-31', @@ -490,6 +606,49 @@ describe('MeasureReportBuilder Static', () => { }); }); + describe('Measure Report from Episode Measure with stratifiers', () => { + let measureReports: fhir4.MeasureReport[]; + beforeAll(() => { + measureReports = MeasureReportBuilder.buildMeasureReports( + episodeWithStratMeasureBundle, + episodeWithStratExecutionResults, + calculationOptions + ); + }); + + test('should generate 1 result', () => { + expect(measureReports).toBeDefined(); + expect(measureReports).toHaveLength(1); + }); + + test('should contain proper stratifierResults', () => { + const [mr] = measureReports; + + expect(mr.group).toBeDefined(); + expect(mr.group).toHaveLength(1); + + const [group] = mr.group!; + const result = episodeWithStratExecutionResults[0].detailedResults?.[0]; + + expect(group.id).toEqual(result!.groupId); + expect(group.measureScore).toBeDefined(); + expect(group.population).toBeDefined(); + + result!.episodeResults![0].populationResults!.forEach(pr => { + const populationResult = group.population?.find(p => p.code?.coding?.[0].code === pr.populationType); + expect(populationResult).toBeDefined(); + expect(populationResult!.count).toEqual(pr.result === true ? 1 : 0); + }); + + result!.episodeResults![0].stratifierResults!.forEach(sr => { + const stratifierResult = group.stratifier?.find(s => s.id === sr.strataId); + expect(stratifierResult).toBeDefined(); + expect(stratifierResult!.stratum?.[0].population?.length).toEqual(4); + expect(stratifierResult!.stratum?.[0].measureScore?.value).toEqual(0); + }); + }); + }); + describe('Ratio Measure Report', () => { let measureReports: fhir4.MeasureReport[]; beforeAll(() => { diff --git a/test/unit/fixtures/measure/episode-measure-with-stratifiers.json b/test/unit/fixtures/measure/episode-measure-with-stratifiers.json new file mode 100644 index 00000000..0681e722 --- /dev/null +++ b/test/unit/fixtures/measure/episode-measure-with-stratifiers.json @@ -0,0 +1,200 @@ +{ + "resourceType": "Measure", + "id": "example", + + "status": "active", + "url": "http://example.com/example", + "identifier": [ + { + "system": "http://example.com", + "value": "example" + } + ], + "name": "Example Measure", + "effectivePeriod": { + "start": "2021-01-01", + "end": "2021-12-31" + }, + "library": ["Library/example"], + "group": [ + { + "id": "group-1", + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + }, + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-improvementNotation", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-improvement-notation", + "code": "increase" + } + ] + } + }, + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-scoring", + "code": "proportion" + } + ] + } + } + ], + "population": [ + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Initial Population" + } + }, + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator", + "display": "Numerator" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Numerator" + } + }, + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Denominator" + } + }, + { + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator-exclusion", + "display": "Denominator Exclusion" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Denominator Exclusion" + } + } + ], + "stratifier": [ + { + "id": "93f5f1c7-8638-40a4-a596-8b5831599209", + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + } + } + ], + "criteria": { + "language": "text/cql-identifier", + "expression": "Strat1" + } + }, + { + "id": "5baf37c7-8887-4576-837e-ea20a8938282", + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + } + } + ], + "criteria": { + "language": "text/cql-identifier", + "expression": "Strat2" + } + }, + { + "id": "125b3d95-2d00-455f-8a6e-d53614a2a50e", + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + } + } + ], + "criteria": { + "language": "text/cql-identifier", + "expression": "Strat1" + } + }, + { + "id": "c06647b9-e134-4189-858d-80cee23c0f8d", + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + } + } + ], + "criteria": { + "language": "text/cql-identifier", + "expression": "Strat2" + } + } + ] + } + ] +} From 5d31c679c1e9e3b741cacd7258a8c0bdad7df55b Mon Sep 17 00:00:00 2001 From: LaurenD Date: Tue, 15 Oct 2024 09:39:31 -0400 Subject: [PATCH 4/5] create MeasureReportBuilder for each result --- src/calculation/MeasureReportBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 6eccd34b..4726ce68 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -648,8 +648,8 @@ export default class MeasureReportBuilder exten ): fhir4.MeasureReport[] { const reports: fhir4.MeasureReport[] = []; const measure = extractMeasureFromBundle(measureBundle); - const builder = new MeasureReportBuilder(measure, options); executionResults.forEach(result => { + const builder = new MeasureReportBuilder(measure, options); builder.addPatientResults(result); reports.push(builder.getReport()); }); From a3c69d3bf0aabcd4b7e33d1c37f4ca9f09660d58 Mon Sep 17 00:00:00 2001 From: LaurenD Date: Tue, 15 Oct 2024 11:14:08 -0400 Subject: [PATCH 5/5] Update test descriptions and README --- README.md | 2 +- test/unit/MeasureReportBuilder.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 001470e1..6924a90e 100644 --- a/README.md +++ b/README.md @@ -522,7 +522,7 @@ fqm-execution reports -m /path/to/measure/bundle.json -p /path/to/patient1/bundl ## Stratification -The results for each stratifier on a Measure (if they exist) are reported on the DetailedResults array for each group. The StratifierResult object contains two result attributes: `result` and `appliesResult`. `result` is simply the raw result of the stratifier and `appliesResult` is the same unless that stratifier contains a [cqfm-appliesTo extension](https://hl7.org/fhir/us/cqfmeasures/STU4/StructureDefinition-cqfm-appliesTo.html). In the case that a stratifier applies to a specified population, the `appliesResult` is the result of the stratifier result AND the result of the specified population. The following is an example of what the DetailedResults would look like for a Measure whose single stratifier has a result of `true` but appliesTo the numerator population that has a result of `false`. +The results for each stratifier on a Measure (if they exist) are reported on the DetailedResults array for each group. For episode-based measures, stratifier results may also be found within each of the `episodeResults`. The StratifierResult object contains two result attributes: `result` and `appliesResult`. `result` is simply the raw result of the stratifier and `appliesResult` is the same unless that stratifier contains a [cqfm-appliesTo extension](https://hl7.org/fhir/us/cqfmeasures/STU4/StructureDefinition-cqfm-appliesTo.html). In the case that a stratifier applies to a specified population, the `appliesResult` is the result of the stratifier result AND the result of the specified population. The following is an example of what the DetailedResults would look like for a Measure whose single stratifier has a result of `true` but appliesTo the numerator population that has a result of `false`. ```typescript [ diff --git a/test/unit/MeasureReportBuilder.test.ts b/test/unit/MeasureReportBuilder.test.ts index a0e986b4..1a3eedaa 100644 --- a/test/unit/MeasureReportBuilder.test.ts +++ b/test/unit/MeasureReportBuilder.test.ts @@ -427,7 +427,7 @@ describe('MeasureReportBuilder Static', () => { ); }); - test('should generate 1 result', () => { + test('should generate one measure report', () => { expect(measureReports).toBeDefined(); expect(measureReports).toHaveLength(1); }); @@ -510,7 +510,7 @@ describe('MeasureReportBuilder Static', () => { ); }); - test('should generate 1 result', () => { + test('should generate one measure report', () => { expect(measureReports).toBeDefined(); expect(measureReports).toHaveLength(1); }); @@ -616,7 +616,7 @@ describe('MeasureReportBuilder Static', () => { ); }); - test('should generate 1 result', () => { + test('should generate one measure report', () => { expect(measureReports).toBeDefined(); expect(measureReports).toHaveLength(1); });