From 8e85a89ab305f00e6304943c6d0feb6a2f97d4ee Mon Sep 17 00:00:00 2001 From: Elsa Date: Tue, 16 Jul 2024 10:07:49 -0400 Subject: [PATCH 1/7] appliesTo stratifier functionality --- src/calculation/DetailedResultsBuilder.ts | 27 ++- src/calculation/MeasureReportBuilder.ts | 43 +++- test/unit/MeasureReportBuilder.test.ts | 101 +++++++++ .../proportion-measure-with-stratifiers.json | 193 ++++++++++++++++++ 4 files changed, 354 insertions(+), 10 deletions(-) create mode 100644 test/unit/fixtures/measure/proportion-measure-with-stratifiers.json diff --git a/src/calculation/DetailedResultsBuilder.ts b/src/calculation/DetailedResultsBuilder.ts index cfeb076f..8ff1f263 100644 --- a/src/calculation/DetailedResultsBuilder.ts +++ b/src/calculation/DetailedResultsBuilder.ts @@ -304,9 +304,32 @@ export function createPatientPopulationValues( populationGroup.stratifier.forEach(strata => { if (strata.criteria?.expression) { const value = patientResults[strata.criteria?.expression]; - const result = isStatementValueTruthy(value); + + // option 1: uncomment the following lines until option 2 to consider the stratifier result + // AND populationResult when creating the detailedResults.stratifierResults + + // if the cqfm-appliesTo extension is present, then we want to consider the result of that + // population in our stratifier result + const appliesToExtension = strata.extension?.find( + e => e.url === 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo' + ); + let popValue = true; + if (appliesToExtension) { + const popCode = appliesToExtension.valueCodeableConcept?.coding?.[0].code; + if (popCode) { + popValue = patientResults[popCode]; + } + } + const result = isStatementValueTruthy(value && popValue); + + // // option 2: keep things the way they were before; do not consider both the stratifierResult + // // and the populationResult when creating the detailedResults.stratifierResults + // const result = isStatementValueTruthy(value); + + // TO DO: in the case where strata.code.text doesn't exist but strata.id does, + // is this structure too redundant? Should we rethink the StratifierResult type? 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..3de44137 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; @@ -131,12 +132,27 @@ export default class MeasureReportBuilder exten measureGroup.stratifier.forEach(s => { const reportStratifier = {}; reportStratifier.code = s.code ? [s.code] : []; + reportStratifier.id = s.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 +233,13 @@ 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); + // THIS IS WHERE THE ERROR IS HAPPENING + // measure may not have s.code[0].text, where else can we get this strataCode? + // also in the case that s.code.text doesn't exist, the strata code is set to strata-index + // in DetailedResultsBuilder.ts so then strata would never be defined and this would error out + const strata: MeasureReportGroupStratifier | undefined = + group.stratifier?.find(s => s.code && s.code[0]?.text === stratResults.strataCode) || + group.stratifier?.find(s => s.id && s.id === stratResults.strataCode); const stratum = strata?.stratum?.[0]; if (stratum) { er.populationResults?.forEach(pr => { @@ -255,7 +277,10 @@ 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 + const strata: MeasureReportGroupStratifier | undefined = + group.stratifier?.find(s => s.code && s.code[0]?.text === stratResults.strataCode) || + group.stratifier?.find(s => s.id && s.id === stratResults.strataCode); const stratum = strata?.stratum?.[0]; if (stratum) { groupResults.populationResults?.forEach(pr => { @@ -318,11 +343,13 @@ 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.` - ); } + // I don't think we need to throw an error here anymore + // } 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..b6d3db95 100644 --- a/test/unit/MeasureReportBuilder.test.ts +++ b/test/unit/MeasureReportBuilder.test.ts @@ -20,6 +20,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 +37,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 +227,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 +375,48 @@ 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].measureScore?.value).toEqual(0); + }); + }); + }); + 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" + } + } + ] + } + ] +} From 91fb2c198129d2472248914bc8bbfe84ad65a2df Mon Sep 17 00:00:00 2001 From: Elsa Date: Wed, 17 Jul 2024 10:25:15 -0400 Subject: [PATCH 2/7] Remove DetailedResults changes, add comments --- src/calculation/DetailedResultsBuilder.ts | 25 +---------------------- src/calculation/MeasureReportBuilder.ts | 3 ++- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/calculation/DetailedResultsBuilder.ts b/src/calculation/DetailedResultsBuilder.ts index 8ff1f263..5b70067a 100644 --- a/src/calculation/DetailedResultsBuilder.ts +++ b/src/calculation/DetailedResultsBuilder.ts @@ -304,30 +304,7 @@ export function createPatientPopulationValues( populationGroup.stratifier.forEach(strata => { if (strata.criteria?.expression) { const value = patientResults[strata.criteria?.expression]; - - // option 1: uncomment the following lines until option 2 to consider the stratifier result - // AND populationResult when creating the detailedResults.stratifierResults - - // if the cqfm-appliesTo extension is present, then we want to consider the result of that - // population in our stratifier result - const appliesToExtension = strata.extension?.find( - e => e.url === 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-appliesTo' - ); - let popValue = true; - if (appliesToExtension) { - const popCode = appliesToExtension.valueCodeableConcept?.coding?.[0].code; - if (popCode) { - popValue = patientResults[popCode]; - } - } - const result = isStatementValueTruthy(value && popValue); - - // // option 2: keep things the way they were before; do not consider both the stratifierResult - // // and the populationResult when creating the detailedResults.stratifierResults - // const result = isStatementValueTruthy(value); - - // TO DO: in the case where strata.code.text doesn't exist but strata.id does, - // is this structure too redundant? Should we rethink the StratifierResult type? + const result = isStatementValueTruthy(value); stratifierResults?.push({ strataCode: strata.code?.text ?? strata.id ?? `strata-${strataIndex++}`, result, diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 3de44137..25cd4758 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -277,7 +277,8 @@ export default class MeasureReportBuilder exten groupResults.stratifierResults?.forEach(stratResults => { // only add to results if this patient is in the strata if (stratResults.result) { - // the strataCode has the potential to be a couple of things, either s,code + // 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 && s.id === stratResults.strataCode); From e8349c203424a9a35d7ca1e3f93f9b1126831fad Mon Sep 17 00:00:00 2001 From: Elsa Date: Wed, 17 Jul 2024 10:41:53 -0400 Subject: [PATCH 3/7] Clean up comments --- src/calculation/MeasureReportBuilder.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 25cd4758..567ba26e 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -233,10 +233,8 @@ export default class MeasureReportBuilder exten er.stratifierResults?.forEach(stratResults => { // only add to results if this episode is in the strata if (stratResults.result) { - // THIS IS WHERE THE ERROR IS HAPPENING - // measure may not have s.code[0].text, where else can we get this strataCode? - // also in the case that s.code.text doesn't exist, the strata code is set to strata-index - // in DetailedResultsBuilder.ts so then strata would never be defined and this would error out + // 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 && s.id === stratResults.strataCode); @@ -345,12 +343,6 @@ export default class MeasureReportBuilder exten if (!pop.count) pop.count = 0; pop.count += pr.result ? 1 : 0; } - // I don't think we need to throw an error here anymore - // } else { - // throw new UnexpectedProperty( - // `Population ${pr.populationType} in stratum ${stratum.id} not found in measure report.` - // ); - // } } } From c774f7dbc242f5540abae98786f4e0c83dd65bd3 Mon Sep 17 00:00:00 2001 From: Elsa Date: Thu, 18 Jul 2024 13:09:25 -0400 Subject: [PATCH 4/7] Only add id and/or code on MeasureReport if they exist --- src/calculation/MeasureReportBuilder.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index 567ba26e..aafb3d1a 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -131,8 +131,12 @@ export default class MeasureReportBuilder exten group.stratifier = []; measureGroup.stratifier.forEach(s => { const reportStratifier = {}; - reportStratifier.code = s.code ? [s.code] : []; - reportStratifier.id = s.id ? s.id : ''; + 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 From 5f1776230dfb1b1a36cccb08a9a6ebf7afbf0b12 Mon Sep 17 00:00:00 2001 From: Elsa Date: Thu, 18 Jul 2024 13:36:04 -0400 Subject: [PATCH 5/7] Additional unit tests --- test/unit/MeasureReportBuilder.test.ts | 157 +++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/test/unit/MeasureReportBuilder.test.ts b/test/unit/MeasureReportBuilder.test.ts index b6d3db95..fbb13751 100644 --- a/test/unit/MeasureReportBuilder.test.ts +++ b/test/unit/MeasureReportBuilder.test.ts @@ -284,6 +284,117 @@ 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' + } + ] + }, + { + patientId: patient2Id, + 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', @@ -291,6 +402,14 @@ const calculationOptions: CalculationOptions = { calculateSDEs: true }; +const calculationOptionsWithSummary: CalculationOptions = { + reportType: 'summary', + measurementPeriodStart: '2021-01-01', + measurementPeriodEnd: '2021-12-31', + calculateHTML: true, + calculateSDEs: true +}; + describe('MeasureReportBuilder Static', () => { describe('Simple Measure Report', () => { let measureReports: fhir4.MeasureReport[]; @@ -412,6 +531,44 @@ describe('MeasureReportBuilder Static', () => { 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 measureReports: fhir4.MeasureReport[]; + beforeAll(() => { + measureReports = MeasureReportBuilder.buildMeasureReports( + propWithStratMeasureBundle, + propWithStratExecutionResultsTwoPatients, + calculationOptionsWithSummary + ); + }); + + 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); }); }); From 42f724ff088fd9882c74d8f0d6a94b0244d374b2 Mon Sep 17 00:00:00 2001 From: Elsa Date: Thu, 18 Jul 2024 14:25:43 -0400 Subject: [PATCH 6/7] Unit test for summary MeasureReport --- test/unit/MeasureReportBuilder.test.ts | 204 +++++++------------------ 1 file changed, 56 insertions(+), 148 deletions(-) diff --git a/test/unit/MeasureReportBuilder.test.ts b/test/unit/MeasureReportBuilder.test.ts index fbb13751..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( @@ -284,117 +289,6 @@ 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' - } - ] - }, - { - patientId: patient2Id, - 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', @@ -402,14 +296,6 @@ const calculationOptions: CalculationOptions = { calculateSDEs: true }; -const calculationOptionsWithSummary: CalculationOptions = { - reportType: 'summary', - measurementPeriodStart: '2021-01-01', - measurementPeriodEnd: '2021-12-31', - calculateHTML: true, - calculateSDEs: true -}; - describe('MeasureReportBuilder Static', () => { describe('Simple Measure Report', () => { let measureReports: fhir4.MeasureReport[]; @@ -538,40 +424,62 @@ describe('MeasureReportBuilder Static', () => { }); describe('Measure Report from Proportion Measure with stratifiers and two patient results', () => { - let measureReports: fhir4.MeasureReport[]; + let builder: MeasureReportBuilder; beforeAll(() => { - measureReports = MeasureReportBuilder.buildMeasureReports( - propWithStratMeasureBundle, - propWithStratExecutionResultsTwoPatients, - calculationOptionsWithSummary - ); - }); - - 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(); + builder = new MeasureReportBuilder(propWithStratMeasure, { + reportType: 'summary', + measurementPeriodStart: '2021-01-01', + measurementPeriodEnd: '2021-12-31' + }); - 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); + 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' + } + ] + } + ] }); - 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); + 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', () => { From ccd7cd3405d903670fc92f86ea8876a0466d71b5 Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 19 Jul 2024 12:15:19 -0400 Subject: [PATCH 7/7] Remove unnecessary check --- src/calculation/MeasureReportBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calculation/MeasureReportBuilder.ts b/src/calculation/MeasureReportBuilder.ts index aafb3d1a..514ff695 100644 --- a/src/calculation/MeasureReportBuilder.ts +++ b/src/calculation/MeasureReportBuilder.ts @@ -241,7 +241,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 && s.id === stratResults.strataCode); + group.stratifier?.find(s => s.id === stratResults.strataCode); const stratum = strata?.stratum?.[0]; if (stratum) { er.populationResults?.forEach(pr => { @@ -283,7 +283,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 && s.id === stratResults.strataCode); + group.stratifier?.find(s => s.id === stratResults.strataCode); const stratum = strata?.stratum?.[0]; if (stratum) { groupResults.populationResults?.forEach(pr => {