diff --git a/src/calculation/DetailedResultsBuilder.ts b/src/calculation/DetailedResultsBuilder.ts index cfeb076f..5b70067a 100644 --- a/src/calculation/DetailedResultsBuilder.ts +++ b/src/calculation/DetailedResultsBuilder.ts @@ -306,7 +306,7 @@ export function createPatientPopulationValues( const value = patientResults[strata.criteria?.expression]; const result = isStatementValueTruthy(value); stratifierResults?.push({ - strataCode: strata.code?.text ?? `strata-${strataIndex++}`, + strataCode: strata.code?.text ?? strata.id ?? `strata-${strataIndex++}`, result, ...(strata.id ? { strataId: strata.id } : {}) }); diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 49530998..514ff695 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -9,6 +9,7 @@ import { import { UnexpectedProperty, UnsupportedProperty } from '../types/errors/CustomErrors'; import { isDetailedResult } from '../helpers/DetailedResultsHelpers'; import { AbstractMeasureReportBuilder } from './AbstractMeasureReportBuilder'; +import { MeasureReportGroupStratifier } from 'fhir/r4'; export default class MeasureReportBuilder extends AbstractMeasureReportBuilder { report: fhir4.MeasureReport; @@ -130,13 +131,32 @@ export default class MeasureReportBuilder exten group.stratifier = []; measureGroup.stratifier.forEach(s => { const reportStratifier = {}; - reportStratifier.code = s.code ? [s.code] : []; + if (s.code) { + reportStratifier.code = [s.code]; + } + if (s.id) { + reportStratifier.id = s.id; + } const strat = {}; // use existing populations, but reduce count as appropriate // Deep copy population with matching attributes but different interface - strat.population = ( - JSON.parse(JSON.stringify(group.population)) + // if a stratifier has a cqfm-appliesTo extension, then we only want to + // include that population. If none is specified, the stratification applies + // to all populations in a group + const appliesToExtension = s.extension?.find( + e => e.url === 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo' ); + if (appliesToExtension) { + const popCode = appliesToExtension.valueCodeableConcept?.coding?.[0].code; + const matchingPop = group.population?.find(p => p.code?.coding?.[0].code === popCode); + strat.population = ( + JSON.parse(JSON.stringify([matchingPop])) + ); + } else { + strat.population = ( + JSON.parse(JSON.stringify(group.population)) + ); + } reportStratifier.stratum = [strat]; group.stratifier?.push(reportStratifier); @@ -217,7 +237,11 @@ export default class MeasureReportBuilder exten er.stratifierResults?.forEach(stratResults => { // only add to results if this episode is in the strata if (stratResults.result) { - const strata = group.stratifier?.find(s => s.code && s.code[0].text === stratResults.strataCode); + // the strataCode has the potential to be a couple of things, either s.code[0].text (previous measures) + // 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); const stratum = strata?.stratum?.[0]; if (stratum) { er.populationResults?.forEach(pr => { @@ -255,7 +279,11 @@ export default class MeasureReportBuilder exten groupResults.stratifierResults?.forEach(stratResults => { // only add to results if this patient is in the strata if (stratResults.result) { - const strata = group.stratifier?.find(s => s.code && s.code[0].text === stratResults.strataCode); + // the strataCode has the potential to be a couple of things, either s.code[0].text (previous measures) + // 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); const stratum = strata?.stratum?.[0]; if (stratum) { groupResults.populationResults?.forEach(pr => { @@ -318,10 +346,6 @@ export default class MeasureReportBuilder exten // add to pop count creating it if not already created. if (!pop.count) pop.count = 0; pop.count += pr.result ? 1 : 0; - } else { - throw new UnexpectedProperty( - `Population ${pr.populationType} in stratum ${stratum.id} not found in measure report.` - ); } } } diff --git a/test/unit/MeasureReportBuilder.test.ts b/test/unit/MeasureReportBuilder.test.ts index 232d989f..043ed595 100644 --- a/test/unit/MeasureReportBuilder.test.ts +++ b/test/unit/MeasureReportBuilder.test.ts @@ -3,7 +3,12 @@ import { PatientSource } from 'cql-exec-fhir'; import MeasureReportBuilder from '../../src/calculation/MeasureReportBuilder'; import { getJSONFixture } from './helpers/testHelpers'; -import { ExecutionResult, CalculationOptions, DetailedPopulationGroupResult } from '../../src/types/Calculator'; +import { + ExecutionResult, + CalculationOptions, + DetailedPopulationGroupResult, + PopulationGroupResult +} from '../../src/types/Calculator'; import { PopulationType } from '../../src/types/Enums'; const patient1 = getJSONFixture( @@ -20,6 +25,7 @@ const patient1Id = '3413754c-73f0-4559-9f67-df8e593ce7e1'; 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 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'); @@ -36,6 +42,7 @@ function buildTestMeasureBundle(measure: fhir4.Measure): fhir4.Bundle { }; } const simpleMeasureBundle = buildTestMeasureBundle(simpleMeasure); +const propWithStratMeasureBundle = buildTestMeasureBundle(propWithStratMeasure); const ratioMeasureBundle = buildTestMeasureBundle(ratioMeasure); const cvMeasureBundle = buildTestMeasureBundle(cvMeasure); @@ -225,6 +232,63 @@ const cvExecutionResults: ExecutionResult[] = [ } ]; +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, + strataId: '93f5f1c7-8638-40a4-a596-8b5831599209' + }, + { + strataCode: '5baf37c7-8887-4576-837e-ea20a8938282', + result: false, + strataId: '5baf37c7-8887-4576-837e-ea20a8938282' + }, + { + strataCode: '125b3d95-2d00-455f-8a6e-d53614a2a50e', + result: false, + strataId: '125b3d95-2d00-455f-8a6e-d53614a2a50e' + }, + { + strataCode: 'c06647b9-e134-4189-858d-80cee23c0f8d', + result: false, + strataId: 'c06647b9-e134-4189-858d-80cee23c0f8d' + } + ], + html: 'example-html' + } + ] + } +]; + const calculationOptions: CalculationOptions = { measurementPeriodStart: '2021-01-01', measurementPeriodEnd: '2021-12-31', @@ -316,6 +380,108 @@ describe('MeasureReportBuilder Static', () => { }); }); + describe('Measure Report from Proportion Measure with stratifiers', () => { + let measureReports: fhir4.MeasureReport[]; + beforeAll(() => { + measureReports = MeasureReportBuilder.buildMeasureReports( + propWithStratMeasureBundle, + propWithStratExecutionResults, + 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 = propWithStratExecutionResults[0].detailedResults?.[0]; + + expect(group.id).toEqual(result!.groupId); + expect(group.measureScore).toBeDefined(); + expect(group.population).toBeDefined(); + + result!.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!.stratifierResults!.forEach(sr => { + const stratifierResult = group.stratifier?.find(s => s.id === sr.strataId); + expect(stratifierResult).toBeDefined(); + expect(stratifierResult!.stratum?.[0].population?.length).toEqual(1); + expect(stratifierResult!.stratum?.[0].measureScore?.value).toEqual(0); + }); + }); + }); + + describe('Measure Report from Proportion Measure with stratifiers and two patient results', () => { + let builder: MeasureReportBuilder; + beforeAll(() => { + builder = new MeasureReportBuilder(propWithStratMeasure, { + reportType: 'summary', + measurementPeriodStart: '2021-01-01', + measurementPeriodEnd: '2021-12-31' + }); + + builder.addPatientResults({ + patientId: patient1Id, + detailedResults: [ + { + groupId: 'group-1', + stratifierResults: [ + { + strataCode: '93f5f1c7-8638-40a4-a596-8b5831599209', + result: false, + strataId: '93f5f1c7-8638-40a4-a596-8b5831599209' + }, + { + strataCode: '5baf37c7-8887-4576-837e-ea20a8938282', + result: false, + strataId: '5baf37c7-8887-4576-837e-ea20a8938282' + } + ] + } + ] + }); + + builder.addPatientResults({ + patientId: patient2Id, + detailedResults: [ + { + groupId: 'group-1', + stratifierResults: [ + { + strataCode: '125b3d95-2d00-455f-8a6e-d53614a2a50e', + result: false, + strataId: '125b3d95-2d00-455f-8a6e-d53614a2a50e' + }, + { + strataCode: 'c06647b9-e134-4189-858d-80cee23c0f8d', + result: false, + strataId: 'c06647b9-e134-4189-858d-80cee23c0f8d' + } + ] + } + ] + }); + }); + + test('should generate a summary MeasureReport whose stratifierResults only contain one population in the stratum', () => { + const { report } = builder; + expect(report).toBeDefined(); + expect(report.group?.[0].stratifier?.[0].stratum?.[0].population?.length).toEqual(1); + }); + }); + describe('Ratio Measure Report', () => { let measureReports: fhir4.MeasureReport[]; beforeAll(() => { diff --git a/test/unit/fixtures/measure/proportion-measure-with-stratifiers.json b/test/unit/fixtures/measure/proportion-measure-with-stratifiers.json new file mode 100644 index 00000000..77b76724 --- /dev/null +++ b/test/unit/fixtures/measure/proportion-measure-with-stratifiers.json @@ -0,0 +1,193 @@ +{ + "resourceType": "Measure", + "id": "example", + "extension": [ + { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } + ], + "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"], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "proportion" + } + ] + }, + "improvementNotation": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-improvement-notation", + "code": "increase" + } + ] + }, + "group": [ + { + "id": "group-1", + "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" + } + } + ] + } + ] +}