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/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 29338f77..4726ce68 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.strataCode); // strataCode may have an id value if code did not originally exist const stratum = strata?.stratum?.[0]; if (stratum) { er.populationResults?.forEach(pr => { @@ -647,8 +647,8 @@ export default class MeasureReportBuilder exten options: CalculationOptions ): fhir4.MeasureReport[] { const reports: fhir4.MeasureReport[] = []; + const measure = extractMeasureFromBundle(measureBundle); 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..1a3eedaa 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', @@ -311,7 +427,7 @@ describe('MeasureReportBuilder Static', () => { ); }); - test('should generate 1 result', () => { + test('should generate one measure report', () => { expect(measureReports).toBeDefined(); expect(measureReports).toHaveLength(1); }); @@ -394,7 +510,7 @@ describe('MeasureReportBuilder Static', () => { ); }); - test('should generate 1 result', () => { + test('should generate one measure report', () => { expect(measureReports).toBeDefined(); expect(measureReports).toHaveLength(1); }); @@ -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 one measure report', () => { + 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" + } + } + ] + } + ] +}