diff --git a/.claude/skills/measure-scoring/skill.md b/.claude/skills/measure-scoring/skill.md new file mode 100644 index 0000000000..0fecc6c2e6 --- /dev/null +++ b/.claude/skills/measure-scoring/skill.md @@ -0,0 +1,407 @@ +# Measure Scoring Architecture + +## Overview + +This skill provides comprehensive knowledge about how measure scoring works in the CQF Clinical Reasoning FHIR library, including the architectural patterns, scoring flow, and the distinction between measure-level and group-level scoring. + +## Core Concepts + +### Measure Scoring Types + +Measures can have different scoring methodologies defined by the `MeasureScoring` enum: + +- **PROPORTION**: Ratio of patients meeting numerator criteria to those in denominator + - Formula: `(numerator - numeratorExclusion) / (denominator - denominatorExclusion - denominatorException)` + - Example: 2/6 = 0.3333 (33.33% of patients achieved the quality measure) + +- **RATIO**: Ratio of two measure observations + - Used when comparing two different measurements (e.g., medication events per patient) + - Can include continuous variable observations for numerator and denominator + +- **CONTINUOUS_VARIABLE**: Aggregated observation values + - Uses aggregate functions (SUM, AVG, MIN, MAX, MEDIAN, COUNT) + - Example: Average patient age, total hospital days + +- **COHORT**: Simple patient count, no scoring + - Just counts patients meeting criteria + - No score is calculated + +- **COMPOSITE**: Combines multiple component measures + - Not a directly evaluated scoring type (validation error if used) + +## Measure-Level vs Group-Level Scoring + +**CRITICAL**: A measure can have EITHER measure-level OR group-level scoring, but NEVER both. + +### Measure-Level Scoring (Standard Case) + +The most common pattern where scoring is defined at the measure level: + +``` +Measure +├─ measureScoring: PROPORTION ✓ +└─ Group + ├─ measureScoring: null ✓ + ├─ populations (initial-population, denominator, numerator, etc.) + └─ score: 0.3333 (calculated) +``` + +**Characteristics:** +- `MeasureDef.measureScoring` is set (e.g., PROPORTION) +- `GroupDef.measureScoring` is null for all groups +- All groups use the same scoring methodology +- No `cqfm-scoring` extension on MeasureReport groups + +**Code validation** (R4MeasureReportBuilder.java:192-193): +```java +if (groupMeasureScoring != null) { + if (bc.measureDef().hasMeasureScoring()) { + throw new InternalErrorException("Cannot have both measure and group scoring"); + } +} +``` + +### Group-Level Scoring (Override Case) + +Less common pattern where each group defines its own scoring: + +``` +Measure +├─ measureScoring: null ✓ +└─ Group 1 + ├─ measureScoring: PROPORTION ✓ + ├─ populations + └─ score: 0.25 (calculated) +└─ Group 2 + ├─ measureScoring: RATIO ✓ + ├─ populations + └─ score: 1.5 (calculated) +``` + +**Characteristics:** +- `MeasureDef.measureScoring` is null +- Each `GroupDef.measureScoring` is set +- Groups can have different scoring methodologies +- `cqfm-scoring` extension IS added to each MeasureReport group + +**Extension details**: +- URL: `http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring` +- Value: CodeableConcept with Coding (system: `http://terminology.hl7.org/ValueSet/measure-scoring`) +- Set in: `R4MeasureReportBuilder.buildGroup()` via `R4MeasureReportUtils.createGroupScoringExtension()` + +## Scoring Architecture + +### Key Classes + +**Version-Agnostic (cqf-fhir-cr/measure/common/):** + +- **MeasureDef**: Container for measure-level metadata + - Fields: `measureScoring` (nullable), `url`, `version`, `groups`, `sdes` + - No score field (measures don't get scored, only groups do) + - Methods: `hasMeasureScoring()`, `measureScoring()` + +- **GroupDef**: Container for group-level data and scoring + - Fields: `measureScoring` (nullable), `score` (nullable), `populations`, `stratifiers` + - Added by: Claude Sonnet 4.5 on 2025-12-03 + - Methods: + - `getScore()`: Returns calculated score + - `setScoreAndAdaptToImprovementNotation(Double, MeasureScoring)`: Sets score with improvement notation adjustment + - `getMeasureOrGroupScoring(MeasureDef)`: Resolves effective scoring type + +- **MeasureReportDefScorer**: Primary scorer (version-agnostic) + - Location: `cqf-fhir-cr/measure/common/MeasureReportDefScorer.java` + - Purpose: Calculates and sets scores on Def objects + - Key method: `score(String measureUrl, MeasureDef measureDef)` + - **Integration**: As of 2025-12-16, this is the ONLY scorer used internally + - Operates on Def objects before FHIR report generation + +**Version-Specific (cqf-fhir-cr/measure/r4/):** + +- **R4MeasureReportBuilder**: FHIR R4 report builder + - Method: `copyScoresFromDef()` copies pre-computed scores from Def to FHIR MeasureReport + - Replaced old pattern of scoring in builders + +- **R4MeasureReportScorer**: **DEPRECATED for internal use** as of 2025-12-16 + - Retained ONLY for external callers + - Provides R4-specific helpers for stratifier population counts + - Internal usage replaced by MeasureReportDefScorer + +## Scoring Resolution Logic + +The `getMeasureOrGroupScoring(MeasureDef)` method determines which scoring type to use: + +```java +public MeasureScoring getMeasureOrGroupScoring(MeasureDef measureDef) { + // Check measure first (most common case) + if (measureDef.hasMeasureScoring()) { + return measureDef.measureScoring(); + } + + // If no measure scoring, check group + if (hasMeasureScoring()) { + return measureScoring(); + } + + // Error if neither + throw new InternalErrorException("Must have scoring at measure or group level"); +} +``` + +**Resolution order:** +1. If `MeasureDef.measureScoring` is set → use measure's scoring +2. Else if `GroupDef.measureScoring` is set → use group's scoring +3. Else → throw error (scoring must be defined somewhere) + +## Scoring Workflow + +### End-to-End Flow + +``` +1. Measure Evaluation + ↓ +2. Create MeasureDef + GroupDef objects with population results + ↓ +3. MeasureReportDefScorer.score(measureUrl, measureDef) + - Iterates through measureDef.groups() + - For each GroupDef: + a. Resolve scoring type via getMeasureOrGroupScoring() + b. Calculate score based on scoring type + c. MUTATE GroupDef via setScoreAndAdaptToImprovementNotation() + ↓ +4. R4MeasureReportBuilder.buildGroups() + - Creates FHIR MeasureReport structure + - If group has own scoring → add cqfm-scoring extension + ↓ +5. R4MeasureReportBuilder.copyScoresFromDef() + - Copies pre-computed scores from GroupDef to MeasureReport + - Sets reportGroup.getMeasureScore().setValue(groupDef.getScore()) + ↓ +6. Return MeasureReport with scores +``` + +### Orchestration Point + +**MeasureEvaluationResultHandler.java** (lines 88-89): +```java +logger.debug("Scoring MeasureDef using MeasureReportDefScorer for measure: {}", measureDef.url()); +measureReportDefScorer.score(measureDef.url(), measureDef); +``` + +- Scoring happens ONCE after evaluation, before report building +- Scores are stored on Def objects as mutable state +- Builders copy (not recalculate) scores to FHIR resources + +## Score Calculation Examples + +### Proportion Scoring + +```java +// MeasureScoreCalculator.calculateProportionScore() +return (numerator - numeratorExclusion) / + (denominator - denominatorExclusion - denominatorException); + +// Example: +// Numerator: 2, Denominator: 6 +// Score: 2/6 = 0.3333333333333333 +``` + +### Ratio Scoring + +```java +// Similar to proportion but with different population semantics +// Can include continuous variable observations +// numeratorObservation / denominatorObservation +``` + +### Continuous Variable Scoring + +```java +// Uses aggregation result from measure observation population +final QuantityDef quantityDef = scoreContinuousVariable(measureObsPop); +measureObsPop.setAggregationResult(quantityDef); +return quantityDef != null ? quantityDef.value() : null; +``` + +### Cohort Scoring + +```java +// No score - cohort measures just count patients +return null; +``` + +## Testing Patterns + +### Dual Assertion Structure + +Tests verify both internal Def state and FHIR Report output: + +```java +.then() + // MeasureDef assertions (pre-scoring) - verify internal state + .def() + .hasNoErrors() + .hasMeasureScoring(MeasureScoring.PROPORTION) // Measure-level + .firstGroup() + .hasNoGroupLevelScoring() // No group override + .hasEffectiveScoring(MeasureScoring.PROPORTION) // Resolved scoring + .population("numerator").hasCount(2).up() + .hasScore(0.3333333333333333) // Double score + .up() + .up() + // MeasureReport assertions (post-scoring) - verify FHIR resource output + .report() + .firstGroup() + .hasNoGroupScoringExt() // No extension + .population("numerator").hasCount(2).up() + .hasScore("0.3333333333333333") // String score + .up() + .report(); +``` + +### Test Assertion Methods (as of 2025-12-16) + +**SelectedMeasureDef (measure-level):** +- `hasMeasureScoring(MeasureScoring)`: Assert measure-level scoring is set +- `hasNoMeasureScoring()`: Assert no measure-level scoring (group-level expected) + +**SelectedMeasureDefGroup (group-level, internal state):** +- `hasScore(Double)`: Assert calculated score value (existing) +- `hasNullScore()`: Assert score is null (pre-scoring) (existing) +- `hasMeasureScoring(MeasureScoring)`: Assert GroupDef.measureScoring field (existing) +- `hasGroupLevelScoring(MeasureScoring)`: Assert group has its own scoring +- `hasNoGroupLevelScoring()`: Assert group has no override (uses measure scoring) +- `hasEffectiveScoring(MeasureScoring)`: Assert the resolved scoring type (measure or group) + +**SelectedMeasureReportGroup (FHIR report output):** +- `hasScore(String)`: Assert FHIR report score (existing) +- `hasGroupScoringExt(MeasureScoring)`: Assert cqfm-scoring extension present with specific code +- `hasGroupScoringExt(String)`: Assert extension present with code string +- `hasNoGroupScoringExt()`: Assert no extension (standard measure-level scoring case) + +### Error Message Format + +All assertion messages use `.formatted()` with expected/actual values: + +```java +// Good - uses .formatted() +"Expected measure-level scoring: %s, actual: %s".formatted(expected, actual) + +// Bad - uses concatenation (avoid) +"Expected measure-level scoring: " + expected + ", actual: " + actual +``` + +## Common Patterns + +### Pattern 1: Measure-Level Scoring (90% of measures) + +```java +// Measure resource +{ + "resourceType": "Measure", + "scoring": { + "coding": [{ + "code": "proportion" + }] + }, + "group": [{ + // No group-level scoring + "population": [...] + }] +} + +// Test assertions +.def() + .hasMeasureScoring(MeasureScoring.PROPORTION) + .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.PROPORTION) + +.report() + .firstGroup() + .hasNoGroupScoringExt() +``` + +### Pattern 2: Group-Level Scoring (10% of measures) + +```java +// Measure resource +{ + "resourceType": "Measure", + // No measure-level scoring + "group": [{ + "extension": [{ + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring", + "valueCodeableConcept": { + "coding": [{ + "code": "proportion" + }] + } + }], + "population": [...] + }] +} + +// Test assertions +.def() + .hasNoMeasureScoring() + .firstGroup() + .hasGroupLevelScoring(MeasureScoring.PROPORTION) + .hasEffectiveScoring(MeasureScoring.PROPORTION) + +.report() + .firstGroup() + .hasGroupScoringExt(MeasureScoring.PROPORTION) +``` + +## Migration Notes (Historical Context) + +### Phase 1: Initial MeasureDef/GroupDef Implementation (2025-12-03) +- Added `score` field to `GroupDef` +- Added `setScoreAndAdaptToImprovementNotation()` method + +### Phase 2: MeasureReportDefScorer Integration (2025-12-16) +- Created version-agnostic `MeasureReportDefScorer` +- Removed old scoring logic from builders +- Builders now use `copyScoresFromDef()` instead of recalculating +- Deleted `Dstu3MeasureReportScorer` (no longer needed) +- Retained `R4MeasureReportScorer` for external callers only + +### Phase 3: Test Assertion Enhancement (2026-02-04) +- Added measure-level scoring assertions to `SelectedMeasureDef` +- Added group-level scoring assertions to `SelectedMeasureDefGroup` +- Added extension assertions to `SelectedMeasureReportGroup` +- Updated all assertion messages to use `.formatted()` with expected/actual values +- Updated 37+ tests across multiple test files + +## File Locations + +### Core Implementation +- `cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureDef.java` +- `cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java` +- `cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorer.java` +- `cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluationResultHandler.java` + +### R4 Implementation +- `cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java` +- `cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportScorer.java` (deprecated for internal use) +- `cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtils.java` + +### Test Assertions +- `cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDef.java` +- `cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDefGroup.java` +- `cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/report/SelectedMeasureReportGroup.java` + +### Example Tests +- `cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeProportionTest.java` +- `cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeRatioContVariableTest.java` +- `cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java` + +## Key Takeaways + +1. **Mutually Exclusive**: Measures have EITHER measure-level OR group-level scoring, never both +2. **Single Scorer**: `MeasureReportDefScorer` is the only internal scorer (as of 2025-12-16) +3. **Mutation Pattern**: Scoring mutates `GroupDef` objects, then builders copy to FHIR +4. **Extension Marker**: `cqfm-scoring` extension on report groups indicates group-level scoring +5. **Resolution Logic**: Measure scoring takes precedence over group scoring in resolution +6. **Testing**: Always test both Def state (internal) and Report output (FHIR) +7. **Error Messages**: Use `.formatted()` with expected/actual values for clarity diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java index 584205ad94..15b9b783a2 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/GroupDef.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cr.measure.common; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import jakarta.annotation.Nullable; import java.util.Collections; import java.util.List; @@ -13,7 +14,10 @@ public class GroupDef { private final ConceptDef code; private final List stratifiers; private final List populations; + + @Nullable private final MeasureScoring measureScoring; + private final boolean isGroupImpNotation; private final CodeDef populationBasis; private final CodeDef improvementNotation; @@ -28,7 +32,7 @@ public GroupDef( ConceptDef code, List stratifiers, List populations, - MeasureScoring measureScoring, + @Nullable MeasureScoring measureScoring, boolean isGroupImprovementNotation, CodeDef improvementNotation, CodeDef populationBasis) { @@ -149,6 +153,17 @@ private Map> index(List groups; private final List sdes; private final List errors; public static MeasureDef fromIdAndUrl(IIdType idType, @Nullable String url) { - return new MeasureDef(idType, url, null, List.of(), List.of()); + return new MeasureDef(idType, url, null, null, List.of(), List.of()); } - public MeasureDef(IIdType idType, @Nullable String url, String version, List groups, List sdes) { + public MeasureDef( + IIdType idType, + @Nullable String url, + String version, + @Nullable MeasureScoring measureScoring, + List groups, + List sdes) { + this.idType = idType; this.url = url; this.version = version; + this.measureScoring = measureScoring; this.groups = groups; this.sdes = sdes; @@ -53,6 +65,15 @@ public String version() { return this.version; } + public boolean hasMeasureScoring() { + return this.measureScoring != null; + } + + @Nullable + public MeasureScoring measureScoring() { + return this.measureScoring; + } + public List sdes() { return this.sdes; } @@ -93,6 +114,7 @@ public String toString() { .add("idType='" + idType.getValueAsString() + "'") .add("url='" + url + "'") .add("version='" + version + "'") + .add("measureScoring='" + measureScoring + "'") .add("groups=" + groups.size()) .add("sdes=" + sdes.size()) .add("errors=" + errors) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java index 011963c36e..a7ff13a47f 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureEvaluator.java @@ -220,6 +220,7 @@ protected void validateRatioContinuousVariable(GroupDef groupDef) { } protected void evaluateProportion( + MeasureScoring measureOrGroupScoring, GroupDef groupDef, String subjectType, String subjectId, @@ -228,7 +229,7 @@ protected void evaluateProportion( boolean applyScoring) { // check populations R4MeasureScoringTypePopulations.validateScoringTypePopulations( - groupDef.populations().stream().map(PopulationDef::type).toList(), groupDef.measureScoring()); + groupDef.populations().stream().map(PopulationDef::type).toList(), measureOrGroupScoring); PopulationDef initialPopulation = groupDef.getSingle(INITIALPOPULATION); PopulationDef numerator = groupDef.getSingle(NUMERATOR); @@ -624,10 +625,18 @@ protected void evaluateGroup( evaluateStratifiers(subjectId, groupDef.stratifiers(), evaluationResult, groupDef); - var scoring = groupDef.measureScoring(); - switch (scoring) { + final MeasureScoring measureOrGroupScoring = groupDef.getMeasureOrGroupScoring(measureDef); + + switch (measureOrGroupScoring) { case PROPORTION, RATIO: - evaluateProportion(groupDef, subjectType, subjectId, reportType, evaluationResult, applyScoring); + evaluateProportion( + measureOrGroupScoring, + groupDef, + subjectType, + subjectId, + reportType, + evaluationResult, + applyScoring); break; case CONTINUOUSVARIABLE: evaluateContinuousVariable( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorer.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorer.java index 3f8ac065c3..8f959eec59 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorer.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorer.java @@ -90,29 +90,27 @@ public class MeasureReportDefScorer { public void score(String measureUrl, MeasureDef measureDef) { // Def-first iteration: iterate over MeasureDef.groups() for (GroupDef groupDef : measureDef.groups()) { - scoreGroup(measureUrl, groupDef); + scoreGroup(measureDef, groupDef); } } - /** - * Score a single group including all its stratifiers - MUTATES GroupDef and StratumDef objects. - * - * @param measureUrl the measure URL for error reporting - * @param groupDef the group definition to score (will be mutated with setScore()) - */ - public void scoreGroup(String measureUrl, GroupDef groupDef) { - MeasureScoring measureScoring = checkMissingScoringType(measureUrl, groupDef.measureScoring()); + public void scoreGroup(MeasureDef measureDef, GroupDef groupDef) { + + var measureUrl = measureDef.url(); + var measureOrGroupScoring = groupDef.getMeasureOrGroupScoring(measureDef); + + checkMissingScoringType(measureUrl, measureOrGroupScoring); // Calculate group-level score - Double groupScore = calculateGroupScore(measureUrl, groupDef, measureScoring); + Double groupScore = calculateGroupScore(measureUrl, groupDef, measureOrGroupScoring); // MUTATE: Set score on GroupDef - groupDef.setScoreAndAdaptToImprovementNotation(groupScore); + groupDef.setScoreAndAdaptToImprovementNotation(groupScore, measureOrGroupScoring); // Score all stratifiers using Def-first iteration // Modified from R4MeasureReportScorer to iterate over Def classes instead of FHIR components for (StratifierDef stratifierDef : groupDef.stratifiers()) { - scoreStratifier(measureUrl, groupDef, stratifierDef, measureScoring); + scoreStratifier(measureUrl, groupDef, stratifierDef, measureOrGroupScoring); } } @@ -121,8 +119,7 @@ public void scoreGroup(String measureUrl, GroupDef groupDef) { */ private Double calculateGroupScore(String measureUrl, GroupDef groupDef, MeasureScoring measureScoring) { switch (measureScoring) { - case PROPORTION: - case RATIO: + case PROPORTION, RATIO: // Special case: RATIO with separate numerator/denominator observations if (measureScoring == MeasureScoring.RATIO && groupDef.hasPopulationType(MeasurePopulationType.MEASUREOBSERVATION)) { @@ -263,12 +260,11 @@ private Double getStratumScoreOrNull( String measureUrl, GroupDef groupDef, StratumDef stratumDef, MeasureScoring measureScoring) { switch (measureScoring) { - case PROPORTION: - case RATIO: + case PROPORTION, RATIO: // Check for special RATIO continuous variable case if (measureScoring.equals(MeasureScoring.RATIO) && groupDef.hasPopulationType(MeasurePopulationType.MEASUREOBSERVATION)) { - return scoreRatioMeasureObservationStratum(measureUrl, stratumDef); + return scoreRatioMeasureObservationStratum(stratumDef); } else { return scoreProportionRatioStratum(groupDef, stratumDef); } @@ -286,12 +282,11 @@ private Double getStratumScoreOrNull( * Handles continuous variable ratio scoring where numerator and denominator have separate observations. * Uses pre-computed cache to eliminate redundant lookups during scoring. * - * @param measureUrl the measure URL for error reporting * @param stratumDef the stratum definition * @return the calculated score or null */ @Nullable - private Double scoreRatioMeasureObservationStratum(String measureUrl, StratumDef stratumDef) { + private Double scoreRatioMeasureObservationStratum(StratumDef stratumDef) { if (stratumDef == null) { return null; @@ -311,8 +306,7 @@ private Double scoreRatioMeasureObservationStratum(String measureUrl, StratumDef PopulationDef numPopDef = stratumPopulationDefNum.populationDef(); PopulationDef denPopDef = stratumPopulationDefDen.populationDef(); - return scoreRatioContVariableStratum( - measureUrl, stratumPopulationDefNum, stratumPopulationDefDen, numPopDef, denPopDef); + return scoreRatioContVariableStratum(stratumPopulationDefNum, stratumPopulationDefDen, numPopDef, denPopDef); } /** @@ -372,7 +366,6 @@ private Double scoreContinuousVariableStratum(String measureUrl, GroupDef groupD * Score ratio continuous variable for a stratum. * Copied from R4MeasureReportScorer#scoreRatioContVariableStratum. * - * @param measureUrl the measure URL for error reporting * @param measureObsNumStratum stratum population for numerator measure observation * @param measureObsDenStratum stratum population for denominator measure observation * @param numPopDef numerator population definition @@ -380,7 +373,6 @@ private Double scoreContinuousVariableStratum(String measureUrl, GroupDef groupD * @return the ratio score or null */ private Double scoreRatioContVariableStratum( - String measureUrl, StratumPopulationDef measureObsNumStratum, StratumPopulationDef measureObsDenStratum, PopulationDef numPopDef, @@ -502,7 +494,7 @@ private static Collection getResultsForStratum( .filter(entry -> stratumSubjectsUnqualified.contains(entry.getKey())) .map(Map.Entry::getValue) .flatMap(Collection::stream) - .collect(Collectors.toList()); + .toList(); } /** @@ -561,7 +553,7 @@ private static Collection getResultsForStratumByResourceIds( } return false; }) - .collect(Collectors.toList()); + .toList(); } /** @@ -601,21 +593,19 @@ private static QuantityDef calculateContinuousVariableAggregateQuantity( } private static void setAggregateResultIfPopNonNull(@Nullable PopulationDef populationDef, QuantityDef quantityDef) { - Optional.ofNullable(populationDef).ifPresent(nonNullPopulationDef -> { - nonNullPopulationDef.setAggregationResult(quantityDef); - }); + Optional.ofNullable(populationDef) + .ifPresent(nonNullPopulationDef -> nonNullPopulationDef.setAggregationResult(quantityDef)); } /** * Validate scoring type is present. * Reused from BaseMeasureReportScorer pattern. */ - protected MeasureScoring checkMissingScoringType(String measureUrl, MeasureScoring measureScoring) { + private void checkMissingScoringType(String measureUrl, MeasureScoring measureScoring) { if (measureScoring == null) { throw new InvalidRequestException( "Measure does not have a scoring methodology defined. Add a \"scoring\" property to the measure definition or the group definition for measure: " + measureUrl); } - return measureScoring; } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureDefBuilder.java index 4d5871c862..5f887bce12 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureDefBuilder.java @@ -5,6 +5,7 @@ import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.IMPROVEMENT_NOTATION_SYSTEM_INCREASE; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -53,7 +54,8 @@ public MeasureDef build(Measure measure) { } // scoring - MeasureScoring measureScoring = + @Nullable + final MeasureScoring measureScoring = MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode()); // populationBasis var measureBasis = getMeasureBasis(measure); @@ -115,7 +117,8 @@ public MeasureDef build(Measure measure) { groups.add(groupDef); } - return new MeasureDef(measure.getIdElement(), measure.getUrl(), measure.getVersion(), groups, sdes); + return new MeasureDef( + measure.getIdElement(), measure.getUrl(), measure.getVersion(), measureScoring, groups, sdes); } private ConceptDef conceptToConceptDef(CodeableConcept codeable) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index ef932042a6..10d5ff1515 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -57,7 +57,7 @@ public MeasureDef build(Measure measure) { checkId(measure); // scoring - var measureScoring = R4MeasureUtils.getMeasureScoring(measure); + @Nullable final MeasureScoring measureScoring = R4MeasureUtils.getMeasureScoring(measure); // populationBasis var measureBasis = getMeasureBasis(measure); // improvement Notation @@ -73,18 +73,30 @@ public MeasureDef build(Measure measure) { return new MeasureDef( // We don't need either the version of the "Measure" qualifier here - measure.getIdElement(), measure.getUrl(), measure.getVersion(), groups, getSdeDefs(measure)); + measure.getIdElement(), + measure.getUrl(), + measure.getVersion(), + measureScoring, + groups, + getSdeDefs(measure)); } private GroupDef buildGroupDef( Measure measure, MeasureGroupComponent group, - MeasureScoring measureScoring, + @Nullable MeasureScoring measureScoring, CodeDef measureImpNotation, CodeDef measureBasis) { // group Measure Scoring var groupScoring = getGroupMeasureScoring(measure, group); + + if (measureScoring != null && groupScoring != null) { + throw new InvalidRequestException( + "Scoring should be at the measure level or the group level, but not both for measure: %s" + .formatted(measure.getUrl())); + } + // populationBasis var groupBasis = getGroupPopulationBasis(group); // improvement Notation @@ -114,7 +126,7 @@ private GroupDef buildGroupDef( conceptToConceptDef(group.getCode()), stratifiers, mergePopulations(populationsWithCriteriaReference, optPopulationDefDateOfCompliance.orElse(null)), - R4MeasureUtils.computeScoring(measure.getUrl(), measureScoring, groupScoring), + groupScoring, hasGroupImpNotation, getImprovementNotation(measureImpNotation, groupImpNotation), populationBasisDef); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java index 564565dd11..4522197396 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilder.java @@ -51,7 +51,6 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.SdeDef; -import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; import org.opencds.cqf.fhir.cr.measure.common.StratumDef; import org.opencds.cqf.fhir.cr.measure.common.StratumValueWrapper; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; @@ -158,17 +157,7 @@ private void buildGroup( groupDefSizeDiff = 1; } - if ((measureGroup.getPopulation().size()) != (groupDef.populations().size() - groupDefSizeDiff)) { - throw new InvalidRequestException( - "The MeasureGroup has a different number of populations defined than the GroupDef for Measure: " - + bc.measure().getUrl()); - } - - if (measureGroup.getStratifier().size() != (groupDef.stratifiers().size())) { - throw new InvalidRequestException( - "The MeasureGroup has a different number of stratifiers defined than the GroupDef for Measure: " - + bc.measure().getUrl()); - } + validateGroup(bc, measureGroup, groupDef, groupDefSizeDiff); reportGroup.setCode(measureGroup.getCode()); reportGroup.setId(measureGroup.getId()); @@ -185,28 +174,38 @@ private void buildGroup( buildPopulation(bc, measurePop, reportPop, defPop, groupDef); } + final MeasureScoring groupMeasureScoring = groupDef.measureScoring(); + + if (groupMeasureScoring != null) { + + if (bc.measureDef().hasMeasureScoring()) { + throw new InternalErrorException("Cannot have both measure and group scoring"); + } + + reportGroup.addExtension(R4MeasureReportUtils.createGroupScoringExtension(groupMeasureScoring)); + } + // add extension to group for totalDenominator and totalNumerator - if (groupDef.measureScoring().equals(MeasureScoring.PROPORTION) - || groupDef.measureScoring().equals(MeasureScoring.RATIO) - || groupDef.measureScoring().equals(MeasureScoring.CONTINUOUSVARIABLE)) { + if ((groupMeasureScoring == MeasureScoring.PROPORTION + || groupMeasureScoring == MeasureScoring.RATIO + || groupMeasureScoring == MeasureScoring.CONTINUOUSVARIABLE) + && bc.report().getType().equals(MeasureReport.MeasureReportType.INDIVIDUAL)) { // add extension to group for - if (bc.report().getType().equals(MeasureReport.MeasureReportType.INDIVIDUAL)) { - var docPopDef = groupDef.findPopulationByType(DATEOFCOMPLIANCE); - if (docPopDef != null - && docPopDef.getAllSubjectResources() != null - && !docPopDef.getAllSubjectResources().isEmpty()) { - var docValue = docPopDef.getAllSubjectResources().iterator().next(); - if (docValue != null) { - assert docValue instanceof Interval; - Interval docInterval = (Interval) docValue; - - var helper = new R4DateHelper(); - reportGroup - .addExtension() - .setUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) - .setValue(helper.buildMeasurementPeriod((docInterval))); - } + var docPopDef = groupDef.findPopulationByType(DATEOFCOMPLIANCE); + if (docPopDef != null + && docPopDef.getAllSubjectResources() != null + && !docPopDef.getAllSubjectResources().isEmpty()) { + var docValue = docPopDef.getAllSubjectResources().iterator().next(); + if (docValue != null) { + assert docValue instanceof Interval; + Interval docInterval = (Interval) docValue; + + var helper = new R4DateHelper(); + reportGroup + .addExtension() + .setUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) + .setValue(helper.buildMeasurementPeriod((docInterval))); } } } @@ -219,6 +218,24 @@ private void buildGroup( } } + private void validateGroup( + R4MeasureReportBuilderContext bc, + MeasureGroupComponent measureGroup, + GroupDef groupDef, + int groupDefSizeDiff) { + if ((measureGroup.getPopulation().size()) != (groupDef.populations().size() - groupDefSizeDiff)) { + throw new InvalidRequestException( + "The MeasureGroup has a different number of populations defined than the GroupDef for Measure: " + + bc.measure().getUrl()); + } + + if (measureGroup.getStratifier().size() != (groupDef.stratifiers().size())) { + throw new InvalidRequestException( + "The MeasureGroup has a different number of stratifiers defined than the GroupDef for Measure: " + + bc.measure().getUrl()); + } + } + private void addMeasureDescription(MeasureReportGroupComponent reportGroup, MeasureGroupComponent measureGroup) { if (measureGroup.hasDescription()) { reportGroup.addExtension( @@ -290,7 +307,7 @@ static ListResource createList(String id) { } private ListResource createIdList(String id, Collection ids) { - return this.createReferenceList(id, ids.stream().map(Reference::new).collect(Collectors.toList())); + return this.createReferenceList(id, ids.stream().map(Reference::new).toList()); } private ListResource createReferenceList(String id, Collection references) { @@ -529,13 +546,7 @@ private DomainResource createPatientObservation( private Observation createObservation(R4MeasureReportBuilderContext bc, String id, String populationId) { var measure = bc.measure(); MeasureInfo measureInfo = new MeasureInfo() - .withMeasure( - measure.hasUrl() - ? measure.getUrl() - : (measure.hasId() - ? MeasureInfo.MEASURE_PREFIX - + measure.getIdElement().getIdPart() - : "")) + .withMeasure(measure.hasUrl() ? measure.getUrl() : getIdStringOrBlank(measure)) .withPopulationId(populationId); Observation obs = new Observation(); @@ -546,12 +557,10 @@ private Observation createObservation(R4MeasureReportBuilderContext bc, String i return obs; } - private Observation createMeasureObservation(R4MeasureReportBuilderContext bc, String id, String observationName) { - Observation obs = this.createObservation(bc, id, observationName); - CodeableConcept cc = new CodeableConcept(); - cc.setText(observationName); - obs.setCode(cc); - return obs; + private String getIdStringOrBlank(Measure measure) { + return measure.hasId() + ? MeasureInfo.MEASURE_PREFIX + measure.getIdElement().getIdPart() + : ""; } /** @@ -633,7 +642,7 @@ private void copyStratifierScores(MeasureReportGroupComponent reportGroup, Group for (var stratumDef : stratifierDef.getStratum()) { // Find matching report stratum by comparing value strings var reportStratum = reportStratifier.getStratum().stream() - .filter(rs -> matchesStratumValue(rs, stratumDef, stratifierDef)) + .filter(rs -> matchesStratumValue(rs, stratumDef)) .findFirst() .orElse(null); @@ -659,11 +668,9 @@ private void copyStratifierScores(MeasureReportGroupComponent reportGroup, Group * * @param reportStratum the MeasureReport stratum * @param stratumDef the StratumDef - * @param stratifierDef the parent StratifierDef (for context) * @return true if values match */ - private boolean matchesStratumValue( - MeasureReport.StratifierGroupComponent reportStratum, StratumDef stratumDef, StratifierDef stratifierDef) { - return R4MeasureReportUtils.matchesStratumValue(reportStratum, stratumDef, stratifierDef); + private boolean matchesStratumValue(MeasureReport.StratifierGroupComponent reportStratum, StratumDef stratumDef) { + return R4MeasureReportUtils.matchesStratumValue(reportStratum, stratumDef); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java index ab43f6ff78..fb565203c8 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java @@ -56,7 +56,7 @@ public class R4PopulationBasisValidator implements PopulationBasisValidator { public void validateGroupPopulations(MeasureDef measureDef, GroupDef groupDef, EvaluationResult evaluationResult) { groupDef.populations() .forEach(population -> - validateGroupPopulationBasisType(measureDef.url(), groupDef, population, evaluationResult)); + validateGroupPopulationBasisType(measureDef, groupDef, population, evaluationResult)); } @Override @@ -67,10 +67,10 @@ public void validateStratifiers(MeasureDef measureDef, GroupDef groupDef, Evalua } private void validateGroupPopulationBasisType( - String url, GroupDef groupDef, PopulationDef populationDef, EvaluationResult evaluationResult) { + MeasureDef measureDef, GroupDef groupDef, PopulationDef populationDef, EvaluationResult evaluationResult) { // PROPORTION - var scoring = groupDef.measureScoring(); + var scoring = groupDef.getMeasureOrGroupScoring(measureDef); // Numerator var populationExpression = populationDef.expression(); if (populationExpression == null || populationExpression.isBlank()) { @@ -101,7 +101,7 @@ private void validateGroupPopulationBasisType( populationExpression, scoring, groupPopulationBasisCode, - url, + measureDef.url(), prettyClassNames(resultClasses), prettyClassNames(resultMatchingClasses))); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtils.java index 8ddda86067..7025633547 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtils.java @@ -2,20 +2,22 @@ import jakarta.annotation.Nullable; import java.util.Objects; +import java.util.Set; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; import org.hl7.fhir.r4.model.MeasureReport.StratifierGroupComponent; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Type; -import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationAggregateMethod; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; +import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; -import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; import org.opencds.cqf.fhir.cr.measure.common.StratumDef; +import org.opencds.cqf.fhir.cr.measure.common.StratumValueDef; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants; @@ -44,11 +46,10 @@ private R4MeasureReportUtils() { * *

Based on logic from R4MeasureReportScorer and R4MeasureReportBuilder. * - * @param stratifierDef the StratifierDef containing type information * @param stratumDef the StratumDef containing value information * @return text representation of the stratum value, or null if not determinable */ - public static String getStratumDefText(StratifierDef stratifierDef, StratumDef stratumDef) { + public static String getStratumDefText(StratumDef stratumDef) { var valueDefs = stratumDef.valueDefs(); // Early return if no values @@ -58,30 +59,7 @@ public static String getStratumDefText(StratifierDef stratifierDef, StratumDef s // Fast path for non-component stratifiers (single value) if (!stratumDef.isComponent()) { - var valuePair = valueDefs.iterator().next(); - var value = valuePair.value(); - - // Handle CodeableConcept values for non-component - if (value.getValueClass().equals(CodeableConcept.class) - && value.getValue() instanceof CodeableConcept codeableConcept) { - return codeableConcept.getText(); - } - - // Handle VALUE or CRITERIA type stratifiers with non-CodeableConcept values - var stratifierType = stratifierDef.getStratifierType(); - - if (MeasureStratifierType.VALUE == stratifierType) { - // VALUE-type stratifiers with non-CodeableConcept values - return value.getValueAsString(); - } else if (MeasureStratifierType.NON_SUBJECT_VALUE == stratifierType) { - // NON_SUBJECT_VALUE-type stratifiers with non-CodeableConcept values - return value.getValueAsString(); - } else if (MeasureStratifierType.CRITERIA == stratifierType) { - // CRITERIA-type stratifiers with non-CodeableConcept values - return value.getValueAsString(); - } - - return null; + return getStratumDefTextNonComponent(valueDefs); } // Process component stratifiers (multiple values) @@ -107,6 +85,23 @@ public static String getStratumDefText(StratifierDef stratifierDef, StratumDef s return stratumText; } + @Nullable + private static String getStratumDefTextNonComponent(Set valueDefs) { + var valuePair = valueDefs.iterator().next(); + var value = valuePair.value(); + + // Handle CodeableConcept values for non-component + if (value.getValueClass().equals(CodeableConcept.class) + && value.getValue() instanceof CodeableConcept codeableConcept) { + return codeableConcept.getText(); + } + + // VALUE-type stratifiers with non-CodeableConcept values + // NON_SUBJECT_VALUE-type stratifiers with non-CodeableConcept values + // CRITERIA-type stratifiers with non-CodeableConcept values + return value.getValueAsString(); + } + /** * Check if a MeasureReport stratum matches a StratumDef by comparing text representations. * @@ -116,14 +111,12 @@ public static String getStratumDefText(StratifierDef stratifierDef, StratumDef s * * @param reportStratum the MeasureReport StratifierGroupComponent (stratum) * @param stratumDef the StratumDef to match against - * @param stratifierDef the parent StratifierDef (for context) * @return true if the stratum values match, false otherwise */ - public static boolean matchesStratumValue( - StratifierGroupComponent reportStratum, StratumDef stratumDef, StratifierDef stratifierDef) { + public static boolean matchesStratumValue(StratifierGroupComponent reportStratum, StratumDef stratumDef) { // Use the same logic as R4MeasureReportScorer: compare CodeableConcept.text String reportText = reportStratum.hasValue() ? reportStratum.getValue().getText() : null; - String defText = getStratumDefText(stratifierDef, stratumDef); + String defText = getStratumDefText(stratumDef); return Objects.equals(reportText, defText); } @@ -187,6 +180,17 @@ public static void addAggregationResultMethodAndCriteriaRef( } } + public static Extension createGroupScoringExtension(MeasureScoring groupMeasureScoring) { + + return new Extension() + .setUrl(MeasureConstants.CQFM_SCORING_EXT_URL) + .setValue(new CodeableConcept() + .addCoding(new Coding() + .setSystem(MeasureConstants.CQFM_SCORING_SYSTEM_URL) + .setCode(groupMeasureScoring.toCode()) + .setDisplay(groupMeasureScoring.getDisplay()))); + } + private static void addAggregateMethodInner( MeasureReportGroupPopulationComponent measurePopulation, ContinuousVariableObservationAggregateMethod aggregateMethod) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureUtils.java index 0c31865cdc..d72133d449 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureUtils.java @@ -50,6 +50,7 @@ private R4MeasureUtils() { * @return the MeasureScoring enum value * @throws InvalidRequestException if scoring code is invalid */ + @Nullable public static MeasureScoring getMeasureScoring(Measure measure) { var scoringCode = measure.getScoring().getCodingFirstRep().getCode(); return getMeasureScoring(measure.getUrl(), scoringCode); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorerTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorerTest.java index 63cdd5b609..1d6f50ed51 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorerTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/common/MeasureReportDefScorerTest.java @@ -2,10 +2,12 @@ import static org.junit.jupiter.api.Assertions.*; +import jakarta.annotation.Nonnull; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import org.hl7.fhir.r4.model.IdType; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; @@ -45,10 +47,12 @@ void testScoreGroup_SetsScoreOnGroupDef() { createImprovementNotationCode("increase"), encounterBasis); + final MeasureDef measureDef = getMeasureDef(groupDef); + // Score is null before scoring assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(measureDef, groupDef); // VERIFY: aggregationResult is null for all populations (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -93,7 +97,7 @@ void testScoreGroup_ProportionWithExclusions() { createImprovementNotationCode("increase"), stringBasis); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for all populations (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -133,7 +137,7 @@ void testScoreGroup_ZeroDenominator_SetsNullScore() { createImprovementNotationCode("increase"), dateBasis); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for all populations (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -230,7 +234,7 @@ void testScoreStratifier_SetsScoresOnStratumDefs() { assertNull(femaleStratum.getScore()); // Execute - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for all populations (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -265,7 +269,7 @@ void testScoreGroup_RatioMeasure() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (basic RATIO without MEASUREOBSERVATION) assertNull(numeratorPop.getAggregationResult()); @@ -323,7 +327,7 @@ void testScoreGroup_ContinuousVariable_SumAggregation() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for non-observation populations assertNull(initialPopulation.getAggregationResult()); @@ -383,7 +387,7 @@ void testScoreGroup_ContinuousVariable_AvgAggregation() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for non-observation populations assertNull(initialPopulation.getAggregationResult()); @@ -443,7 +447,7 @@ void testScoreGroup_ContinuousVariable_MinAggregation() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for non-observation populations assertNull(initialPopulation.getAggregationResult()); @@ -503,7 +507,7 @@ void testScoreGroup_ContinuousVariable_MaxAggregation() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for non-observation populations assertNull(initialPopulation.getAggregationResult()); @@ -573,8 +577,7 @@ void testGetMeasureScore_ZeroScore_IncreaseNotation() { createImprovementNotationCode("increase"), encounterBasis); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); - + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); assertNull(denominatorPop.getAggregationResult()); @@ -606,7 +609,7 @@ void testGetMeasureScore_NegativeScore_ReturnsNull() { stringBasis); // Manually set negative score to simulate strange value scenario - groupDef.setScoreAndAdaptToImprovementNotation(-0.5); + groupDef.setScoreAndAdaptToImprovementNotation(-0.5, MeasureScoring.PROPORTION); // VERIFY: getMeasureScore returns null for negative scores assertNull(groupDef.getScore()); @@ -634,7 +637,7 @@ void testGetMeasureScore_PositiveScore_IncreaseNotation() { createImprovementNotationCode("increase"), dateBasis); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -666,7 +669,7 @@ void testGetMeasureScore_PositiveScore_DecreaseNotation() { createImprovementNotationCode("decrease"), // DECREASE notation booleanBasis); - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -746,7 +749,7 @@ void testScoreGroup_BooleanBasis_CountsUniqueSubjects() { assertEquals(3, denominatorPop.getCount()); // 3 subjects (patient1, patient2, patient3) // Execute scoring - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -825,7 +828,7 @@ void testScoreGroup_EncounterBasis_CountsAllResources() { assertEquals(9, denominatorPop.getCount()); // 9 encounters (3 + 2 + 4) // Execute scoring - scorer.scoreGroup("http://example.com/Measure/test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -864,7 +867,7 @@ void testScoreGroup_CohortMeasure_NoScoreSet() { assertNull(groupDef.getScore()); // Execute - scorer.scoreGroup("http://example.com/Measure/cohort-test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (COHORT measure) assertNull(initialPopulation.getAggregationResult()); @@ -893,8 +896,7 @@ void testScoreGroup_MissingScoringType_ThrowsException() { booleanBasis); // Execute and verify exception - Exception exception = assertThrows( - Exception.class, () -> scorer.scoreGroup("http://example.com/Measure/missing-scoring", groupDef)); + Exception exception = assertThrows(Exception.class, () -> scorer.scoreGroup(getMeasureDef(groupDef), groupDef)); // Verify exception message contains useful information assertTrue(exception.getMessage().contains("scoring")); @@ -990,7 +992,7 @@ void testScoreGroup_RatioWithObservations_GroupLevel() { assertNull(groupDef.getScore()); // Execute - scorer.scoreGroup("http://example.com/Measure/ratio-obs-test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: Non-observation populations should have null aggregationResult assertNull(initialPopulation.getAggregationResult()); @@ -1056,7 +1058,7 @@ void testScoreGroup_RatioWithObservations_ZeroCount() { createImprovementNotationCode("increase"), booleanBasis); - scorer.scoreGroup("http://example.com/Measure/ratio-zero-obs", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: All have null aggregationResult when count is zero assertNull(initialPopulation.getAggregationResult()); @@ -1116,7 +1118,7 @@ void testScoreGroup_RatioWithObservations_NullNumeratorPopulation() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/ratio-null-num", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: Denominator aggregation result IS set even though numerator is null assertNotNull(denominatorMeasureObs.getAggregationResult()); @@ -1175,7 +1177,7 @@ void testScoreGroup_RatioWithObservations_NullDenominatorPopulation() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/ratio-null-den", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: Numerator aggregation result IS set even though denominator is null assertNotNull(numeratorMeasureObs.getAggregationResult()); @@ -1216,7 +1218,7 @@ void testScoreGroup_RatioWithObservations_BothPopulationsNull() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/ratio-both-null", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: No aggregation results are set (both populations null) assertNull(measureObs.getAggregationResult()); @@ -1278,7 +1280,7 @@ void testScoreGroup_RatioWithObservations_NullAggregateForNumerator() { createImprovementNotationCode("increase"), booleanBasis); - scorer.scoreGroup("http://example.com/Measure/ratio-null-num-agg", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: Numerator aggregation result is null (no observations) assertNull(numeratorMeasureObs.getAggregationResult()); @@ -1374,7 +1376,7 @@ void testScoreGroup_RatioWithObservations_AvgAggregation() { assertNull(groupDef.getScore()); - scorer.scoreGroup("http://example.com/Measure/ratio-avg-test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: Non-observation populations have null aggregationResult assertNull(initialPopulation.getAggregationResult()); @@ -1556,7 +1558,7 @@ void testScoreStratifier_RatioWithObservations_StratumLevel() { assertNull(femaleStratum.getScore()); // Execute - scorer.scoreGroup("http://example.com/Measure/ratio-obs-stratified", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: Group-level MEASUREOBSERVATION populations have aggregationResult assertEquals(5, numeratorMeasureObs.getCount()); @@ -1731,7 +1733,7 @@ void testScoreStratifier_QualifiedVsUnqualifiedSubjectIds() { assertNull(maleStratum.getScore()); // Execute - scorer.scoreGroup("http://example.com/Measure/qualified-ids-test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null (PROPORTION measure) assertNull(numeratorPop.getAggregationResult()); @@ -1793,6 +1795,7 @@ void testPopulationDef_SetAggregationResultWithNullQuantityDef() { // Set to a value first measureObsPop.setAggregationResult(100.0); + assertNotNull(measureObsPop.getAggregationResult()); assertEquals(100.0, measureObsPop.getAggregationResult(), 0.001); // Set to null using QuantityDef overload @@ -1888,7 +1891,7 @@ void testScoreGroup_ContinuousVariable_MedianAggregation() { assertNull(groupDef.getScore()); assertNull(measureObsPop.getAggregationResult()); - scorer.scoreGroup("http://example.com/Measure/median-test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for non-observation populations assertNull(initialPopulation.getAggregationResult()); @@ -1952,7 +1955,7 @@ void testScoreGroup_ContinuousVariable_CountAggregation() { assertNull(groupDef.getScore()); assertNull(measureObsPop.getAggregationResult()); - scorer.scoreGroup("http://example.com/Measure/count-test", groupDef); + scorer.scoreGroup(getMeasureDef(groupDef), groupDef); // VERIFY: aggregationResult is null for non-observation populations assertNull(initialPopulation.getAggregationResult()); @@ -1965,4 +1968,9 @@ void testScoreGroup_ContinuousVariable_CountAggregation() { // VERIFY: COUNT aggregation = 7.0 assertEquals(7.0, groupDef.getScore(), 0.001); } + + @Nonnull + private static MeasureDef getMeasureDef(GroupDef groupDef) { + return new MeasureDef(new IdType("measure1"), "url", null, null, List.of(groupDef), null); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilderTest.java index ce3ab8033d..380c469fd4 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureReportBuilderTest.java @@ -14,17 +14,12 @@ import org.hl7.fhir.dstu3.model.Measure.MeasureGroupPopulationComponent; import org.hl7.fhir.dstu3.model.Measure.MeasureGroupStratifierComponent; import org.junit.jupiter.api.Test; -import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; import org.opencds.cqf.fhir.cr.measure.common.CodeDef; import org.opencds.cqf.fhir.cr.measure.common.ConceptDef; -import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; -import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; -import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierComponentDef; -import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; import org.opencds.cqf.fhir.cr.measure.common.StratumDef; import org.opencds.cqf.fhir.cr.measure.common.StratumPopulationDef; import org.opencds.cqf.fhir.cr.measure.common.StratumValueDef; @@ -52,7 +47,8 @@ void testScoreCopying_GroupScore() { MeasureDef measureDef = dstu3MeasureDefBuilder.build(measure); // Manually set a score on the first group - measureDef.groups().get(0).setScoreAndAdaptToImprovementNotation(0.80); + var groupDef = measureDef.groups().get(0); + groupDef.setScoreAndAdaptToImprovementNotation(0.80, groupDef.getMeasureOrGroupScoring(measureDef)); // When: Build the MeasureReport var dstu3MeasureReportBuilder = new Dstu3MeasureReportBuilder(); @@ -77,7 +73,8 @@ void testScoreCopying_NullScore() { MeasureDef measureDef = dstu3MeasureDefBuilder.build(measure); // Explicitly set null score (or just don't set it) - measureDef.groups().get(0).setScoreAndAdaptToImprovementNotation(null); + var groupDef = measureDef.groups().get(0); + groupDef.setScoreAndAdaptToImprovementNotation(null, groupDef.getMeasureOrGroupScoring(measureDef)); // When: Build the MeasureReport var dstu3MeasureReportBuilder = new Dstu3MeasureReportBuilder(); @@ -98,7 +95,8 @@ void testScoreCopying_NegativeScore() { MeasureDef measureDef = dstu3MeasureDefBuilder.build(measure); // Set negative score - measureDef.groups().get(0).setScoreAndAdaptToImprovementNotation(-1.0); + var groupDef = measureDef.groups().get(0); + groupDef.setScoreAndAdaptToImprovementNotation(-1.0, groupDef.getMeasureOrGroupScoring(measureDef)); // When: Build the MeasureReport var dstu3MeasureReportBuilder = new Dstu3MeasureReportBuilder(); @@ -260,99 +258,4 @@ private static Measure buildMeasureWithStratifier(String id, String url) { return measure; } - - private static MeasureDef buildMeasureDefWithQualifiedStratumIds(String id, String url) { - CodeDef booleanBasis = new CodeDef("http://hl7.org/fhir/fhir-types", "boolean"); - - // Create populations with UNQUALIFIED subject IDs (e.g., "patient-1") - ConceptDef numCode = new ConceptDef( - List.of(new CodeDef("http://terminology.hl7.org/CodeSystem/measure-population", "numerator")), - "numerator"); - PopulationDef numeratorPop = - new PopulationDef("num-1", numCode, MeasurePopulationType.NUMERATOR, "Numerator", booleanBasis, null); - // Add resources with UNQUALIFIED IDs - numeratorPop.addResource("patient-1", true); - numeratorPop.addResource("patient-2", true); - numeratorPop.addResource("patient-3", true); - numeratorPop.addResource("patient-4", true); - - ConceptDef denCode = new ConceptDef( - List.of(new CodeDef("http://terminology.hl7.org/CodeSystem/measure-population", "denominator")), - "denominator"); - PopulationDef denominatorPop = new PopulationDef( - "den-1", denCode, MeasurePopulationType.DENOMINATOR, "Denominator", booleanBasis, null); - // Add resources with UNQUALIFIED IDs - denominatorPop.addResource("patient-1", true); - denominatorPop.addResource("patient-2", true); - denominatorPop.addResource("patient-3", true); - denominatorPop.addResource("patient-4", true); - denominatorPop.addResource("patient-5", true); - - // Create stratum populations with QUALIFIED subject IDs (e.g., "Patient/patient-1") - StratumPopulationDef stratumNumPop = new StratumPopulationDef( - numeratorPop, - Set.of( - "Patient/patient-1", - "Patient/patient-2", - "Patient/patient-3", - "Patient/patient-4"), // QUALIFIED IDs - Set.of(), - List.of(), - MeasureStratifierType.VALUE, - booleanBasis); - - StratumPopulationDef stratumDenPop = new StratumPopulationDef( - denominatorPop, - Set.of( - "Patient/patient-1", - "Patient/patient-2", - "Patient/patient-3", - "Patient/patient-4", - "Patient/patient-5"), // QUALIFIED IDs - Set.of(), - List.of(), - MeasureStratifierType.VALUE, - booleanBasis); - - // Create stratum with CodeableConcept value (text-based matching) - StratifierComponentDef genderComponent = new StratifierComponentDef( - "gender-component", - new ConceptDef(List.of(new CodeDef("http://hl7.org/fhir/administrative-gender", "female")), "female"), - "Gender"); - - StratumDef stratum = new StratumDef( - List.of(stratumNumPop, stratumDenPop), - Set.of(new StratumValueDef( - new StratumValueWrapper(new org.hl7.fhir.dstu3.model.CodeableConcept().setText("female")), - genderComponent)), - Set.of( - "Patient/patient-1", - "Patient/patient-2", - "Patient/patient-3", - "Patient/patient-4", - "Patient/patient-5"), // QUALIFIED IDs - null); // MeasureObservationStratumCache - - // Create stratifier - StratifierDef stratifierDef = new StratifierDef( - "gender-stratifier", - new ConceptDef(List.of(), "Gender Stratifier"), - "Gender", - MeasureStratifierType.VALUE); - stratifierDef.addAllStratum(List.of(stratum)); - - // Create group - GroupDef groupDef = new GroupDef( - "group-1", - new ConceptDef(List.of(), "Test Group"), - List.of(stratifierDef), - List.of(numeratorPop, denominatorPop), - MeasureScoring.PROPORTION, - false, - new CodeDef("http://terminology.hl7.org/CodeSystem/measure-improvement-notation", "increase"), - booleanBasis); - - return new MeasureDef( - new org.hl7.fhir.dstu3.model.IdType("Measure", id), url, null, List.of(groupDef), List.of()); - } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java index 4eb96eb2ac..6c4bd39b45 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/ContinuousVariableResourceMeasureObservationTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationAggregateMethod; +import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; @SuppressWarnings("squid:S2699") @@ -40,7 +41,10 @@ void continuousVariableResourceMeasureObservationBooleanBasisAvg() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("initial-population") .hasCount(11) .hasNoAggregationResult() @@ -67,6 +71,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisAvg() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(11) .hasNoAggregationResultsExtensionValue() @@ -195,7 +200,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisAvg() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("initial-population") .hasCount(12) .hasNoAggregationResult() @@ -218,7 +226,9 @@ void continuousVariableResourceMeasureObservationEncounterBasisAvg() { .up() // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() + .logReportJson() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(12) .hasNoAggregationResultsExtensionValue() @@ -266,7 +276,10 @@ void continuousVariableResourceMeasureObservationBooleanBasisCount() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("initial-population") .hasCount(11) .up() @@ -288,6 +301,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisCount() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(11) .up() @@ -387,7 +401,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisCount() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(10) .up() @@ -397,6 +414,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisCount() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(12) .up() @@ -483,7 +501,10 @@ void continuousVariableResourceMeasureObservationBooleanBasisMedian() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("initial-population") .hasCount(11) .up() @@ -505,6 +526,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisMedian() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(11) .up() @@ -604,7 +626,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisMedian() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(10) .up() @@ -614,6 +639,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisMedian() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(12) .up() @@ -700,7 +726,10 @@ void continuousVariableResourceMeasureObservationBooleanBasisMin() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(11) .up() @@ -710,6 +739,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisMin() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(11) .up() @@ -809,7 +839,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisMin() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(10) .up() @@ -819,6 +852,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisMin() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(12) .up() @@ -905,7 +939,10 @@ void continuousVariableResourceMeasureObservationBooleanBasisMax() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(11) .up() @@ -915,6 +952,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisMax() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(11) .up() @@ -1014,7 +1052,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisMax() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(10) .up() @@ -1024,6 +1065,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisMax() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(12) .up() @@ -1110,7 +1152,10 @@ void continuousVariableResourceMeasureObservationBooleanBasisSum() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(11) .up() @@ -1120,6 +1165,7 @@ void continuousVariableResourceMeasureObservationBooleanBasisSum() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(11) .up() @@ -1219,7 +1265,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisSum() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("measure-population") .hasSubjectCount(10) .up() @@ -1229,6 +1278,7 @@ void continuousVariableResourceMeasureObservationEncounterBasisSum() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(12) .up() @@ -1312,7 +1362,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisSpecificSubjectId // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("initial-population") .hasNoAggregationResult() .up() @@ -1386,7 +1439,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisAnotherSpecificSu // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("initial-population") .hasNoAggregationResult() .up() @@ -1460,7 +1516,10 @@ void continuousVariableResourceMeasureObservationEncounterBasisAnotherSpecificSu // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.CONTINUOUSVARIABLE) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.CONTINUOUSVARIABLE) .population("initial-population") .hasNoAggregationResult() .up() diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java index 9fe902c647..a301347067 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java @@ -65,17 +65,21 @@ class MeasureDefBuilderTest { public MeasureDef measureDefBuilder( String group1Basis, - String group1Scoring, + @Nullable String group1Scoring, CodeableConcept group1ImpNotation, List group1Stratifiers, String group2Basis, - String group2Scoring, + @Nullable String group2Scoring, CodeableConcept group2ImpNotation, List group2Stratifiers, String measureBasis, - String measureScoring, + @Nullable String measureScoring, CodeableConcept measureImpNotation) { + if (measureScoring != null && group1Scoring != null || group2Scoring != null) { + throw new IllegalArgumentException("Cannot have both a measure and group score at the same time"); + } + R4MeasureDefBuilder defBuilder = new R4MeasureDefBuilder(); Measure measure = (org.hl7.fhir.r4.model.Measure) parser.parseResource(MeasureDefBuilderTest.class.getResourceAsStream("TemplateMeasure.json")); @@ -106,7 +110,7 @@ public MeasureDef measureDefBuilder( var group2 = measure.getGroup().stream() .filter(t -> t.getId().equals("group-2")) .findFirst() - .get(); + .orElse(null); if (group2Basis != null) { group2.addExtension(new Extension() .setUrl(MeasureConstants.POPULATION_BASIS_URL) @@ -145,19 +149,24 @@ public MeasureDef measureDefBuilder( public void validateMeasureDef( MeasureDef measureDef, + MeasureScoring measureScoring, boolean group1IsBooleanBasis, String group1Basis, boolean group1IsGroupImpNotation, String group1ImpNotationValue, - MeasureScoring group1MeasureScoring, + MeasureScoring group1DirectMeasureScoring, + MeasureScoring group1CombinedMeasureScoring, List group1Stratifiers, boolean group2IsBooleanBasis, String group2Basis, boolean group2IsGroupImpNotation, String group2ImpNotationValue, - MeasureScoring group2MeasureScoring, + MeasureScoring group2DirectMeasureScoring, + MeasureScoring group2CombinedMeasureScoring, List group2Stratifiers) { + assertEquals(measureScoring, measureDef.measureScoring()); + var groupsById = measureDef.groups().stream().collect(Collectors.toMap(GroupDef::id, entry -> entry)); var group1 = groupsById.get("group-1"); @@ -168,7 +177,8 @@ public void validateMeasureDef( assertEquals(group1IsGroupImpNotation, group1.isGroupImprovementNotation()); assertEquals(group1ImpNotationValue, group1.getImprovementNotation().code()); // Scoring - assertEquals(group1MeasureScoring, group1.measureScoring()); + assertEquals(group1DirectMeasureScoring, group1.measureScoring()); + assertEquals(group1CombinedMeasureScoring, group1.getMeasureOrGroupScoring(measureDef)); validateStratifiers(group1Stratifiers, group1); var group2 = groupsById.get("group-2"); @@ -180,7 +190,8 @@ public void validateMeasureDef( assertEquals(group2IsGroupImpNotation, group2.isGroupImprovementNotation()); assertEquals(group2ImpNotationValue, group2.getImprovementNotation().code()); // Scoring - assertEquals(group2MeasureScoring, group2.measureScoring()); + assertEquals(group2DirectMeasureScoring, group2.measureScoring()); + assertEquals(group2CombinedMeasureScoring, group2.getMeasureOrGroupScoring(measureDef)); validateStratifiers(group2Stratifiers, group2); } @@ -190,16 +201,19 @@ void basisMeasure() { validateMeasureDef( def, + MeasureScoring.RATIO, true, "boolean", false, "decrease", + null, MeasureScoring.RATIO, null, true, "boolean", false, "decrease", + null, MeasureScoring.RATIO, null); } @@ -207,31 +221,24 @@ void basisMeasure() { @Test void basisMeasureAndGroup() { var def = measureDefBuilder( - "Encounter", - "cohort", - increase, - null, - "Encounter", - "cohort", - increase, - null, - "boolean", - "ratio", - decrease); + "Encounter", "cohort", increase, null, "Encounter", "ratio", increase, null, "boolean", null, decrease); validateMeasureDef( def, + null, false, "Encounter", true, "increase", MeasureScoring.COHORT, + MeasureScoring.COHORT, null, false, "Encounter", true, "increase", - MeasureScoring.COHORT, + MeasureScoring.RATIO, + MeasureScoring.RATIO, null); } @@ -242,17 +249,20 @@ void basisOnlyGroup() { validateMeasureDef( def, + null, false, "Encounter", true, "increase", MeasureScoring.COHORT, + MeasureScoring.COHORT, null, false, "Encounter", true, "increase", MeasureScoring.COHORT, + MeasureScoring.COHORT, null); } @@ -263,17 +273,20 @@ void basisDifferentGroup() { validateMeasureDef( def, + null, false, "Encounter", true, "decrease", MeasureScoring.COHORT, + MeasureScoring.COHORT, null, true, "boolean", true, "increase", MeasureScoring.COHORT, + MeasureScoring.COHORT, null); } @@ -283,17 +296,20 @@ void basisNotDefined() { validateMeasureDef( def, + null, true, "boolean", true, "decrease", MeasureScoring.COHORT, + MeasureScoring.COHORT, null, true, "boolean", true, "increase", MeasureScoring.COHORT, + MeasureScoring.COHORT, null); } @@ -304,17 +320,20 @@ void basisPartiallyDefinedGroup() { validateMeasureDef( def, + null, true, "boolean", true, "decrease", MeasureScoring.COHORT, + MeasureScoring.COHORT, null, false, "Encounter", true, "increase", MeasureScoring.COHORT, + MeasureScoring.COHORT, null); } @@ -342,17 +361,32 @@ private record ScoringMeasureScoringAndGroupParams( String measureScoring, @Nullable String group1Scoring, @Nullable String group2Scoring, - MeasureScoring expectedGroup1MeasureScoring, - MeasureScoring expectedGroup2MeasureScoring) {} + MeasureScoring expectedMeasureScoring, + MeasureScoring expectedDirectGroup1Scoring, + MeasureScoring expectedCombinedGroup1Scoring, + MeasureScoring expectedDirectGroup2Scoring, + MeasureScoring expectedCombinedGroup2Scoring) {} private static Stream scoringMeasureScoringAndGroupParams() { return Stream.of( new ScoringMeasureScoringAndGroupParams( - "cohort", "ratio", "proportion", MeasureScoring.RATIO, MeasureScoring.PROPORTION), - new ScoringMeasureScoringAndGroupParams( - "cohort", null, null, MeasureScoring.COHORT, MeasureScoring.COHORT), + "cohort", + null, + null, + MeasureScoring.COHORT, + null, + MeasureScoring.COHORT, + null, + MeasureScoring.COHORT), new ScoringMeasureScoringAndGroupParams( - null, "ratio", "proportion", MeasureScoring.RATIO, MeasureScoring.PROPORTION)); + null, + "cohort", + "ratio", + null, + MeasureScoring.COHORT, + MeasureScoring.COHORT, + MeasureScoring.RATIO, + MeasureScoring.RATIO)); } @ParameterizedTest(name = "{index} => testCase={0}") @@ -373,17 +407,20 @@ void scoringMeasureScoringAndGroup(ScoringMeasureScoringAndGroupParams testCase) validateMeasureDef( def, + testCase.expectedMeasureScoring(), true, "boolean", false, "decrease", - testCase.expectedGroup1MeasureScoring(), + testCase.expectedDirectGroup1Scoring(), + testCase.expectedCombinedGroup1Scoring(), null, true, "boolean", false, "decrease", - testCase.expectedGroup2MeasureScoring(), + testCase.expectedDirectGroup2Scoring(), + testCase.expectedCombinedGroup2Scoring(), null); } @@ -430,17 +467,20 @@ void groupAndMeasureImprovementNotation() { validateMeasureDef( def, + null, true, "boolean", true, "increase", MeasureScoring.RATIO, + MeasureScoring.RATIO, null, true, "boolean", true, "increase", MeasureScoring.PROPORTION, + MeasureScoring.PROPORTION, null); } @@ -451,17 +491,20 @@ void groupImprovementNotation() { validateMeasureDef( def, + null, true, "boolean", true, "increase", MeasureScoring.RATIO, + MeasureScoring.RATIO, null, true, "boolean", true, "increase", MeasureScoring.PROPORTION, + MeasureScoring.PROPORTION, null); } @@ -471,17 +514,20 @@ void noImprovementNotation() { validateMeasureDef( def, + null, true, "boolean", false, "increase", MeasureScoring.RATIO, + MeasureScoring.RATIO, null, true, "boolean", false, "increase", MeasureScoring.PROPORTION, + MeasureScoring.PROPORTION, null); } @@ -541,17 +587,20 @@ void basicStratifiers(BasicStratifiersParams testCase) { validateMeasureDef( def, + null, true, "boolean", false, "increase", MeasureScoring.RATIO, + MeasureScoring.RATIO, testCase.outputStratifiersGroup1(), true, "boolean", false, "increase", MeasureScoring.PROPORTION, + MeasureScoring.PROPORTION, testCase.outputStratifiersGroup2()); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeProportionTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeProportionTest.java index 58db05147d..ce6cabdca1 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeProportionTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeProportionTest.java @@ -2,6 +2,7 @@ import org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; /** @@ -29,7 +30,10 @@ void proportionBooleanPopulation() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.PROPORTION) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.PROPORTION) .population("initial-population") .hasCount(10) .up() @@ -54,6 +58,7 @@ void proportionBooleanPopulation() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(10) .up() @@ -88,7 +93,10 @@ void proportionBooleanIndividual() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.PROPORTION) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.PROPORTION) .population("initial-population") .hasCount(1) .up() @@ -113,6 +121,7 @@ void proportionBooleanIndividual() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(1) .up() @@ -146,7 +155,10 @@ void proportionResourcePopulation() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.PROPORTION) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.PROPORTION) .population("initial-population") .hasCount(11) .up() @@ -171,6 +183,7 @@ void proportionResourcePopulation() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(11) .up() @@ -217,7 +230,10 @@ void proportionResourceIndividual() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.PROPORTION) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.PROPORTION) .population("initial-population") .hasCount(2) .up() @@ -242,6 +258,7 @@ void proportionResourceIndividual() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCount(2) .up() @@ -288,7 +305,10 @@ void proportionBooleanGroupScoringDef() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasNoMeasureScoring() .firstGroup() + .hasGroupLevelScoring(MeasureScoring.PROPORTION) + .hasEffectiveScoring(MeasureScoring.PROPORTION) .population("initial-population") .hasCount(10) .up() @@ -313,6 +333,7 @@ void proportionBooleanGroupScoringDef() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasGroupScoringExt(MeasureScoring.PROPORTION) .population("initial-population") .hasCount(10) .up() diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeRatioContVariableTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeRatioContVariableTest.java index e01017adef..b7f7d2fbab 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeRatioContVariableTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureScoringTypeRatioContVariableTest.java @@ -8,7 +8,9 @@ import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationAggregateMethod; import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType; +import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; +import org.opencds.cqf.fhir.cr.measure.r4.Measure.When; /** * Summary of generated Patients and their Encounter durations. @@ -77,7 +79,10 @@ void ratioContinuousVariableResourceBasisSum() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -131,6 +136,7 @@ void ratioContinuousVariableResourceBasisSum() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasNoAggregationResultsExtensionValue() @@ -202,7 +208,10 @@ void ratioContinuousVariableResourceBasisCount() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasNoAggregationResult() @@ -272,6 +281,7 @@ void ratioContinuousVariableResourceBasisCount() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasNoAggregationResultsExtensionValue() @@ -342,7 +352,10 @@ void ratioContinuousVariableResourceBasisAvg() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -393,6 +406,7 @@ void ratioContinuousVariableResourceBasisAvg() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -463,7 +477,10 @@ void ratioContinuousVariableResourceBasisMin() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -514,6 +531,7 @@ void ratioContinuousVariableResourceBasisMin() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -584,7 +602,10 @@ void ratioContinuousVariableResourceBasisMax() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -635,6 +656,7 @@ void ratioContinuousVariableResourceBasisMax() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -705,7 +727,10 @@ void ratioContinuousVariableResourceBasisMedian() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -826,7 +851,10 @@ void ratioContinuousVariableBooleanBasisSum() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasCount(10) .hasNoAggregationResult() @@ -877,6 +905,7 @@ void ratioContinuousVariableBooleanBasisSum() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasCount(10) @@ -947,7 +976,10 @@ void ratioContinuousVariableResourceBasisMix() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -999,6 +1031,7 @@ void ratioContinuousVariableResourceBasisMix() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -1337,12 +1370,11 @@ void ratioContinuousVariableNoDenDef() { */ @Test void ratioContinuousVariableBadDenDef() { - var expectedException = assertThrows(InvalidRequestException.class, () -> given.when() + final When when = given.when() .measureId("RatioContVarResourceSumError2") .subject("Patient/patient-9") - .evaluate() - .then() - .report()); + .evaluate(); + var expectedException = assertThrows(InvalidRequestException.class, when::then); assertTrue(expectedException.getMessage().contains("no matching criteria reference was found for extension")); } @@ -1362,7 +1394,10 @@ void ratioContinuousVariableStratifierResource() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -1479,6 +1514,7 @@ void ratioContinuousVariableStratifierResource() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasCount(11) @@ -1615,7 +1651,10 @@ void ratioContinuousVariableStratifierBoolean() { // MeasureDef assertions (pre-scoring) - verify internal state after processing .def() .hasNoErrors() + .hasMeasureScoring(MeasureScoring.RATIO) .firstGroup() + .hasNoGroupLevelScoring() + .hasEffectiveScoring(MeasureScoring.RATIO) .population("initial-population") .hasType(MeasurePopulationType.INITIALPOPULATION) .hasCount(10) @@ -1728,6 +1767,7 @@ void ratioContinuousVariableStratifierBoolean() { // MeasureReport assertions (post-scoring) - verify FHIR resource output .report() .firstGroup() + .hasNoGroupScoringExt() .population("initial-population") .hasCode(MeasurePopulationType.INITIALPOPULATION) .hasCount(10) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasurementPeriodBuilderTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasurementPeriodBuilderTests.java index c655cf95b9..86c074ab1e 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasurementPeriodBuilderTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasurementPeriodBuilderTests.java @@ -99,7 +99,7 @@ void uberSimple_UsesMeasurementPeriodToExcludeResource() { // "end": "2020-01-16T21:00:00Z" // } // test is one hour after resource period - var when = GIVEN_REPO + GIVEN_REPO .when() .measureId("UberSimple") .periodStart(ZonedDateTime.of(LocalDateTime.of(2020, Month.JANUARY, 16, 22, 0, 0), ZoneOffset.UTC)) @@ -107,9 +107,8 @@ void uberSimple_UsesMeasurementPeriodToExcludeResource() { .reportType("subject") .subject("Patient/female-1914") .evaluate() - .then(); - - when.hasReportType("Individual") + .then() + .hasReportType("Individual") .hasPeriodStart(Date.from( LocalDateTime.of(2020, Month.JANUARY, 16, 22, 0, 0).toInstant(ZoneOffset.UTC))) .hasPeriodEnd(Date.from( @@ -120,7 +119,10 @@ void uberSimple_UsesMeasurementPeriodToExcludeResource() { .hasCount(1) .up() .population("numerator") - .hasCount(0); + .hasCount(0) + .up() + .up() + .logReportJson(); } @Test diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java index 169a9de37a..6c1b0ee343 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java @@ -59,7 +59,7 @@ void happyPathEmptySdes() { var measureReport = r4MeasureReportBuilder.build( buildMeasure(MEASURE_ID_1, MEASURE_URL_1, 2, 0), - buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, 2, 0, true, Set.of(buildInterval())), + buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, null, 2, 0, true, Set.of(buildInterval())), MeasureReportType.INDIVIDUAL, null, List.of()); @@ -77,7 +77,7 @@ void happyPathEmptySdesAllResourcesAsNull() { var measureReport = r4MeasureReportBuilder.build( buildMeasure(MEASURE_ID_2, MEASURE_URL_2, 2, 0), - buildMeasureDef(MEASURE_ID_2, MEASURE_URL_2, 2, 0, true, null), + buildMeasureDef(MEASURE_ID_2, MEASURE_URL_2, null, 2, 0, true, null), MeasureReportType.INDIVIDUAL, null, List.of()); @@ -98,7 +98,7 @@ void happyPathEmptySdesAllNullResources() { var measureReport = r4MeasureReportBuilder.build( buildMeasure(MEASURE_ID_1, MEASURE_URL_1, 2, 0), - buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, 2, 0, true, nulls), + buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, null, 2, 0, true, nulls), MeasureReportType.INDIVIDUAL, null, List.of()); @@ -116,7 +116,7 @@ void happyPathNonEmptySdes() { var measureReport = r4MeasureReportBuilder.build( buildMeasure(MEASURE_ID_1, MEASURE_URL_1, 2, 3), - buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, 2, 3, true, Set.of()), + buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, null, 2, 3, true, Set.of()), MeasureReportType.INDIVIDUAL, null, List.of()); @@ -141,7 +141,7 @@ void happyPathNonEmptySdesCreateObservations() { var measureReport = r4MeasureReportBuilder.build( buildMeasure(MEASURE_ID_1, null, 2, 3), - buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, 2, 3, false, Set.of()), + buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, null, 2, 3, false, Set.of()), MeasureReportType.INDIVIDUAL, null, List.of()); @@ -164,7 +164,7 @@ void happyPathNonEmptySdesCreateObservations() { void errorMismatchedGroupsSizes_tooMany() { var r4MeasureReportBuilder = new R4MeasureReportBuilder(); final Measure measure = buildMeasure(MEASURE_ID_1, MEASURE_URL_1, 1, 2); - final MeasureDef measureDef = buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, 2, 2, true, Set.of()); + final MeasureDef measureDef = buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, null, 2, 2, true, Set.of()); final List subjectIds = List.of(); try { @@ -181,7 +181,7 @@ void errorMismatchedGroupsSizes_tooMany() { void errorMismatchedGroupsSizes_tooFew() { var r4MeasureReportBuilder = new R4MeasureReportBuilder(); final Measure measure = buildMeasure(MEASURE_ID_1, MEASURE_URL_1, 2, 2); - final MeasureDef measureDef = buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, 1, 2, true, Set.of()); + final MeasureDef measureDef = buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, null, 1, 2, true, Set.of()); final List subjectIds = List.of(); try { @@ -201,7 +201,7 @@ void invalidPopulationResource() { try { r4MeasureReportBuilder.build( buildMeasure(null, MEASURE_URL_1, 2, 2), - buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, 2, 2, true, Set.of(new Patient())), + buildMeasureDef(MEASURE_ID_1, MEASURE_URL_1, null, 2, 2, true, Set.of(new Patient())), MeasureReportType.INDIVIDUAL, null, List.of()); @@ -214,6 +214,7 @@ void invalidPopulationResource() { private static MeasureDef buildMeasureDef( String id, String url, + @Nullable MeasureScoring measureScoring, int numGroups, int numSdes, boolean isKeyResource, @@ -222,6 +223,7 @@ private static MeasureDef buildMeasureDef( new IdType(ResourceType.Measure.name(), id), url, null, + measureScoring, IntStream.range(0, numGroups) .mapToObj(num -> buildGroupDef("group_" + num, evaluatedResources)) .toList(), @@ -351,7 +353,7 @@ void aggregateMethodExtensionNotAddedForNA() { null, List.of(buildStratifierDef()), List.of(populationDefNA), - MeasureScoring.CONTINUOUSVARIABLE, + null, false, null, new CodeDef(MeasureConstants.POPULATION_BASIS_URL, "boolean")); @@ -360,6 +362,7 @@ void aggregateMethodExtensionNotAddedForNA() { new IdType(ResourceType.Measure.name(), MEASURE_ID_1), MEASURE_URL_1, null, + MeasureScoring.CONTINUOUSVARIABLE, List.of(groupDef), List.of()); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java index 46cb837ff0..5f8991eaec 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java @@ -44,7 +44,7 @@ class R4PopulationBasisValidatorTest { private static final String FAKE_MEASURE_URL = "fakeMeasureUrl"; // Not ENTIRELY realistic since the GroupDefs are ultimately sourced from a MeasureDef, but for this simplistic // test, it works - private static final MeasureDef MEASURE_DEF = new MeasureDef(null, FAKE_MEASURE_URL, null, null, null); + private static final MeasureDef MEASURE_DEF = new MeasureDef(null, FAKE_MEASURE_URL, null, null, null, null); private static final String EXPRESSION_INITIALPOPULATION = "InitialPopulation"; private static final String EXPRESSION_DENOMINATOR = "Denominator"; private static final String EXPRESSION_NUMERATOR = "Numerator"; diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDef.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDef.java index 747248faf6..570fb5d31a 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDef.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDef.java @@ -7,6 +7,7 @@ import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; +import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; /** * Fluent assertion API for MeasureDef objects. @@ -177,6 +178,38 @@ public SelectedMeasureDef

hasMeasureVersion(String version) { return this; } + /** + * Assert that the measure has a measure-level scoring type. + * + * @param scoring expected MeasureScoring type at measure level + * @return this SelectedMeasureDef for chaining + */ + public SelectedMeasureDef

hasMeasureScoring(MeasureScoring scoring) { + assertNotNull(value(), "MeasureDef is null"); + assertTrue( + value().hasMeasureScoring(), + "Expected measure-level scoring to be present, but MeasureDef.measureScoring is null"); + assertEquals( + scoring, + value().measureScoring(), + "Expected measure-level scoring: %s, actual: %s".formatted(scoring, value().measureScoring())); + return this; + } + + /** + * Assert that the measure does NOT have measure-level scoring defined. + * This means scoring must be defined at the group level. + * + * @return this SelectedMeasureDef for chaining + */ + public SelectedMeasureDef

hasNoMeasureScoring() { + assertNotNull(value(), "MeasureDef is null"); + assertFalse( + value().hasMeasureScoring(), + "Expected no measure-level scoring (null), but found: %s".formatted(value().measureScoring())); + return this; + } + /** * Get the underlying MeasureDef for advanced assertions. * diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDefGroup.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDefGroup.java index b38b385f8a..fb094aa4b1 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDefGroup.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/def/SelectedMeasureDefGroup.java @@ -4,8 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; +import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; @@ -181,6 +183,88 @@ public SelectedMeasureDefGroup

hasMeasureScoring(MeasureScoring scoring) { return this; } + /** + * Assert the effective scoring type used to calculate this group's score. + * This is the scoring type that was actually used by the scorer - either the + * measure's scoring (if defined) or the group's scoring (if measure has none). + *

+ * This method requires access to the parent MeasureDef to determine the resolution. + * + * @param measureDef the parent MeasureDef + * @param expectedScoring the expected effective scoring type + * @return this SelectedMeasureDefGroup for chaining + */ + public SelectedMeasureDefGroup

hasEffectiveScoring(MeasureDef measureDef, MeasureScoring expectedScoring) { + assertNotNull(value(), "GroupDef is null"); + assertNotNull(measureDef, "MeasureDef is null"); + MeasureScoring effectiveScoring = value().getMeasureOrGroupScoring(measureDef); + assertEquals( + expectedScoring, + effectiveScoring, + "Expected effective scoring (measure's or group's): %s, actual: %s" + .formatted(expectedScoring, effectiveScoring)); + return this; + } + + /** + * Assert the effective scoring type when the parent is a SelectedMeasureDef. + * This is a convenience method that extracts the MeasureDef from the parent. + *

+ * Can only be used when the parent is SelectedMeasureDef (i.e., after .def().firstGroup()). + * + * @param expectedScoring the expected effective scoring type + * @return this SelectedMeasureDefGroup for chaining + * @throws IllegalStateException if parent is not SelectedMeasureDef + */ + @SuppressWarnings("unchecked") + public SelectedMeasureDefGroup

hasEffectiveScoring(MeasureScoring expectedScoring) { + assertNotNull(value(), "GroupDef is null"); + + // Try to get MeasureDef from parent if it's SelectedMeasureDef + if (up() instanceof SelectedMeasureDef) { + SelectedMeasureDef selectedMeasureDef = (SelectedMeasureDef) up(); + MeasureDef measureDef = selectedMeasureDef.measureDef(); + return hasEffectiveScoring(measureDef, expectedScoring); + } + + throw new IllegalStateException( + "Cannot use hasEffectiveScoring() without MeasureDef parameter when parent is not SelectedMeasureDef. " + + "Use hasEffectiveScoring(MeasureDef, MeasureScoring) instead."); + } + + /** + * Assert that the group does NOT have its own group-level scoring override. + * This means the group uses the measure's scoring type. + * + * @return this SelectedMeasureDefGroup for chaining + */ + public SelectedMeasureDefGroup

hasNoGroupLevelScoring() { + assertNotNull(value(), "GroupDef is null"); + assertFalse( + value().hasMeasureScoring(), + "Expected no group-level scoring (null), but found: %s".formatted(value().measureScoring())); + return this; + } + + /** + * Assert that the group HAS its own group-level scoring. + * This means the measure does NOT have measure-level scoring. + * + * @param scoring the expected group-level scoring type + * @return this SelectedMeasureDefGroup for chaining + */ + public SelectedMeasureDefGroup

hasGroupLevelScoring(MeasureScoring scoring) { + assertNotNull(value(), "GroupDef is null"); + assertTrue( + value().hasMeasureScoring(), + "Expected group-level scoring to be present, but GroupDef.measureScoring is null"); + assertEquals( + scoring, + value().measureScoring(), + "Expected group-level scoring: %s, actual: %s".formatted(scoring, value().measureScoring())); + return this; + } + /** * Assert the population basis. * diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/report/SelectedMeasureReportGroup.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/report/SelectedMeasureReportGroup.java index abbc6c997c..6c8afd68b3 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/report/SelectedMeasureReportGroup.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/selected/report/SelectedMeasureReportGroup.java @@ -16,6 +16,8 @@ import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupStratifierComponent; import org.hl7.fhir.r4.model.Period; +import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; +import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Selected; import org.opencds.cqf.fhir.cr.measure.r4.Measure.Selector; import org.opencds.cqf.fhir.cr.measure.r4.MeasureValidationUtils; @@ -57,6 +59,55 @@ public SelectedMeasureReportGroup hasNoImprovementNotationExt() { return this; } + /** + * Assert that the group has a cqfm-scoring extension with the specified code. + * This extension is present when the group has its own scoring type (i.e., when + * the measure does NOT have measure-level scoring). + *

+ * Extension URL: http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring + * + * @param code the expected scoring code (e.g., "proportion", "ratio") + * @return this SelectedMeasureReportGroup for chaining + */ + public SelectedMeasureReportGroup hasGroupScoringExt(String code) { + var scoringExt = value().getExtensionByUrl(MeasureConstants.CQFM_SCORING_EXT_URL); + assertNotNull(scoringExt, "Expected cqfm-scoring extension but none found"); + var codeConcept = (CodeableConcept) scoringExt.getValue(); + String actualCode = codeConcept.getCodingFirstRep().getCode(); + assertTrue( + codeConcept.hasCoding(MeasureConstants.CQFM_SCORING_SYSTEM_URL, code), + "Expected cqfm-scoring extension code: %s, actual: %s".formatted(code, actualCode)); + return this; + } + + /** + * Assert that the group has a cqfm-scoring extension with the specified MeasureScoring type. + * This is a convenience method that extracts the code from the enum. + * + * @param scoring the expected MeasureScoring type + * @return this SelectedMeasureReportGroup for chaining + */ + public SelectedMeasureReportGroup hasGroupScoringExt(MeasureScoring scoring) { + return hasGroupScoringExt(scoring.toCode()); + } + + /** + * Assert that the group does NOT have a cqfm-scoring extension. + * This is the normal case when the measure has measure-level scoring. + * + * @return this SelectedMeasureReportGroup for chaining + */ + public SelectedMeasureReportGroup hasNoGroupScoringExt() { + var scoringExt = value().getExtensionByUrl(MeasureConstants.CQFM_SCORING_EXT_URL); + String actualCode = scoringExt != null + ? ((CodeableConcept) scoringExt.getValue()).getCodingFirstRep().getCode() + : null; + assertNull( + scoringExt, + "Expected no cqfm-scoring extension (null), but found extension with code: %s".formatted(actualCode)); + return this; + } + public SelectedMeasureReportGroup hasDateOfCompliance() { assertEquals( CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL, diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtilsTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtilsTest.java index ad1a4b0c5b..717f9c4850 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtilsTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureReportUtilsTest.java @@ -20,7 +20,6 @@ import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.Test; -import org.opencds.cqf.fhir.cr.measure.MeasureStratifierType; import org.opencds.cqf.fhir.cr.measure.common.CodeDef; import org.opencds.cqf.fhir.cr.measure.common.ConceptDef; import org.opencds.cqf.fhir.cr.measure.common.ContinuousVariableObservationAggregateMethod; @@ -29,7 +28,6 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.PopulationDef; import org.opencds.cqf.fhir.cr.measure.common.StratifierComponentDef; -import org.opencds.cqf.fhir.cr.measure.common.StratifierDef; import org.opencds.cqf.fhir.cr.measure.common.StratumDef; import org.opencds.cqf.fhir.cr.measure.common.StratumValueDef; import org.opencds.cqf.fhir.cr.measure.common.StratumValueWrapper; @@ -116,38 +114,6 @@ void testAddAggregationResultAndMethod_AndCriteriaReference_FromBigDecimal_WithV assertEquals("avg", ((StringType) methodExt.getValue()).getValue()); } - @Test - void testAddAggregationResultAndMethod_AndCriteriaReference_FromBigDecimal_WithNullValue() { - MeasureReportGroupPopulationComponent population = new MeasureReportGroupPopulationComponent(); - - R4MeasureReportUtils.addAggregationResultMethodAndCriteriaRef( - population, ContinuousVariableObservationAggregateMethod.SUM, null, ""); - - // Assert neither extension is set when value is null - Extension resultExt = population.getExtensionByUrl(MeasureConstants.EXT_AGGREGATION_METHOD_RESULT); - assertNull(resultExt, "No result extension should be added when value is null"); - Extension methodExt = population.getExtensionByUrl(EXT_CQFM_AGGREGATE_METHOD_URL); - assertNull(methodExt, "No method extension should be added when value is null"); - } - - @Test - void testAddAggregationResultAndMethod_AndCriteriaReference_FromBigDecimal_WithZeroValue() { - MeasureReportGroupPopulationComponent population = new MeasureReportGroupPopulationComponent(); - - R4MeasureReportUtils.addAggregationResultMethodAndCriteriaRef( - population, ContinuousVariableObservationAggregateMethod.COUNT, 0.0, ""); - - // Assert both extensions are set - Extension resultExt = population.getExtensionByUrl(MeasureConstants.EXT_AGGREGATION_METHOD_RESULT); - assertNotNull(resultExt); - // Use compareTo for BigDecimal comparison to ignore scale differences (0 vs 0.0) - assertEquals(0, BigDecimal.ZERO.compareTo(((DecimalType) resultExt.getValue()).getValue())); - - Extension methodExt = population.getExtensionByUrl(EXT_CQFM_AGGREGATE_METHOD_URL); - assertNotNull(methodExt); - assertEquals("count", ((StringType) methodExt.getValue()).getValue()); - } - @Test void testAddAggregationResultAndMethod_FromBigDecimal_WithNullMethodAndCriteriaReference() { MeasureReportGroupPopulationComponent population = new MeasureReportGroupPopulationComponent(); @@ -780,14 +746,11 @@ private GroupDef createGroupDef(boolean isGroupImprovementNotation, String impro @Test void testGetStratumDefText_EmptyValueDefs_ReturnsNull() { // Arrange - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", new ConceptDef(List.of(), "Age"), "AgeExpression", MeasureStratifierType.VALUE); - StratumDef stratumDef = new StratumDef(Collections.emptyList(), Collections.emptySet(), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertNull(result, "Should return null when valueDefs is empty"); @@ -795,10 +758,6 @@ void testGetStratumDefText_EmptyValueDefs_ReturnsNull() { @Test void testGetStratumDefText_NonComponent_CodeableConcept_ReturnsText() { - // Arrange - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", new ConceptDef(List.of(), "Age"), "AgeExpression", MeasureStratifierType.VALUE); - CodeableConcept codeableConcept = new CodeableConcept(); codeableConcept.setText("Age Group 18-64"); codeableConcept.addCoding(new Coding("http://example.com", "age-group-1", "18-64")); @@ -810,7 +769,7 @@ void testGetStratumDefText_NonComponent_CodeableConcept_ReturnsText() { new StratumDef(Collections.emptyList(), Set.of(valueDef), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertEquals("Age Group 18-64", result, "Should return CodeableConcept text for non-component stratifier"); @@ -818,10 +777,6 @@ void testGetStratumDefText_NonComponent_CodeableConcept_ReturnsText() { @Test void testGetStratumDefText_NonComponent_CodeableConcept_NoText_ReturnsNull() { - // Arrange - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", new ConceptDef(List.of(), "Age"), "AgeExpression", MeasureStratifierType.VALUE); - CodeableConcept codeableConcept = new CodeableConcept(); codeableConcept.addCoding(new Coding("http://example.com", "age-group-1", "18-64")); // No text set @@ -833,7 +788,7 @@ void testGetStratumDefText_NonComponent_CodeableConcept_NoText_ReturnsNull() { new StratumDef(Collections.emptyList(), Set.of(valueDef), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertNull(result, "Should return null when CodeableConcept has no text"); @@ -841,10 +796,6 @@ void testGetStratumDefText_NonComponent_CodeableConcept_NoText_ReturnsNull() { @Test void testGetStratumDefText_NonComponent_ValueType_IntegerType_ReturnsString() { - // Arrange - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", new ConceptDef(List.of(), "Age"), "AgeExpression", MeasureStratifierType.VALUE); - IntegerType intValue = new IntegerType(42); StratumValueWrapper valueWrapper = new StratumValueWrapper(intValue); StratumValueDef valueDef = new StratumValueDef(valueWrapper, null); @@ -853,7 +804,7 @@ void testGetStratumDefText_NonComponent_ValueType_IntegerType_ReturnsString() { new StratumDef(Collections.emptyList(), Set.of(valueDef), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertEquals("42", result, "Should return value as string for VALUE type with IntegerType"); @@ -862,9 +813,6 @@ void testGetStratumDefText_NonComponent_ValueType_IntegerType_ReturnsString() { @Test void testGetStratumDefText_NonComponent_ValueType_StringType_ReturnsString() { // Arrange - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", new ConceptDef(List.of(), "Gender"), "GenderExpression", MeasureStratifierType.VALUE); - StringType stringValue = new StringType("Male"); StratumValueWrapper valueWrapper = new StratumValueWrapper(stringValue); StratumValueDef valueDef = new StratumValueDef(valueWrapper, null); @@ -873,7 +821,7 @@ void testGetStratumDefText_NonComponent_ValueType_StringType_ReturnsString() { new StratumDef(Collections.emptyList(), Set.of(valueDef), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertEquals("Male", result, "Should return value as string for VALUE type with StringType"); @@ -881,13 +829,6 @@ void testGetStratumDefText_NonComponent_ValueType_StringType_ReturnsString() { @Test void testGetStratumDefText_NonComponent_CriteriaType_ReturnsString() { - // Arrange - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", - new ConceptDef(List.of(), "High Risk"), - "HighRiskExpression", - MeasureStratifierType.CRITERIA); - StringType boolValue = new StringType("true"); StratumValueWrapper valueWrapper = new StratumValueWrapper(boolValue); StratumValueDef valueDef = new StratumValueDef(valueWrapper, null); @@ -896,7 +837,7 @@ void testGetStratumDefText_NonComponent_CriteriaType_ReturnsString() { new StratumDef(Collections.emptyList(), Set.of(valueDef), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertEquals("true", result, "Should return value as string for CRITERIA type"); @@ -910,13 +851,6 @@ void testGetStratumDefText_Component_CodeableConcept_ReturnsComponentText() { StratifierComponentDef componentDef = new StratifierComponentDef("component-1", componentCodeDef, "AgeComponentExpression"); - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", - new ConceptDef(List.of(), "Age and Gender"), - "AgeGenderExpression", - MeasureStratifierType.VALUE, - List.of(componentDef)); - CodeableConcept codeableConcept1 = new CodeableConcept(); codeableConcept1.setText("18-64"); @@ -933,7 +867,7 @@ void testGetStratumDefText_Component_CodeableConcept_ReturnsComponentText() { new StratumDef(Collections.emptyList(), Set.of(valueDef1, valueDef2), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertEquals( @@ -950,13 +884,6 @@ void testGetStratumDefText_Component_NonCodeableConcept_ReturnsValueAsString() { StratifierComponentDef componentDef2 = new StratifierComponentDef("component-2", new ConceptDef(List.of(), "Gender"), "GenderExpression"); - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", - new ConceptDef(List.of(), "Age and Gender"), - "AgeGenderExpression", - MeasureStratifierType.VALUE, - List.of(componentDef1, componentDef2)); - IntegerType intValue = new IntegerType(42); StratumValueWrapper valueWrapper1 = new StratumValueWrapper(intValue); StratumValueDef valueDef1 = new StratumValueDef(valueWrapper1, componentDef1); @@ -969,7 +896,7 @@ void testGetStratumDefText_Component_NonCodeableConcept_ReturnsValueAsString() { new StratumDef(Collections.emptyList(), Set.of(valueDef1, valueDef2), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert // Component with non-CodeableConcept returns immediately @@ -992,13 +919,6 @@ void testGetStratumDefText_Component_MultipleCodeableConcepts_ReturnsLastNonNull StratifierComponentDef componentDef2 = new StratifierComponentDef("component-2", componentCodeDef2, "GenderExpression"); - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", - new ConceptDef(List.of(), "Age and Gender"), - "AgeGenderExpression", - MeasureStratifierType.VALUE, - List.of(componentDef1, componentDef2)); - CodeableConcept codeableConcept1 = new CodeableConcept(); codeableConcept1.setText("18-64"); @@ -1015,7 +935,7 @@ void testGetStratumDefText_Component_MultipleCodeableConcepts_ReturnsLastNonNull new StratumDef(Collections.emptyList(), Set.of(valueDef1, valueDef2), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); assertNotNull(result); // Assert @@ -1027,10 +947,6 @@ void testGetStratumDefText_Component_MultipleCodeableConcepts_ReturnsLastNonNull @Test void testGetStratumDefText_Component_CodeableConceptWithNullComponentDef_ReturnsNull() { - // Arrange - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", new ConceptDef(List.of(), "Age"), "AgeExpression", MeasureStratifierType.VALUE); - CodeableConcept codeableConcept1 = new CodeableConcept(); codeableConcept1.addCoding(new Coding("http://example.com", "age-1", "18-64")); @@ -1047,7 +963,7 @@ void testGetStratumDefText_Component_CodeableConceptWithNullComponentDef_Returns new StratumDef(Collections.emptyList(), Set.of(valueDef1, valueDef2), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertNull(result, "Should return null when component has CodeableConcept but componentDef is null"); @@ -1064,13 +980,6 @@ void testGetStratumDefText_Component_CodeableConceptWithNullCode_ReturnsNull() { StratifierComponentDef componentDef2 = new StratifierComponentDef("component-2", componentCodeDef2, "GenderExpression"); - StratifierDef stratifierDef = new StratifierDef( - "stratifier-1", - new ConceptDef(List.of(), "Age and Gender"), - "AgeGenderExpression", - MeasureStratifierType.VALUE, - List.of(componentDef1, componentDef2)); - CodeableConcept codeableConcept1 = new CodeableConcept(); codeableConcept1.addCoding(new Coding("http://example.com", "age-1", "18-64")); @@ -1087,7 +996,7 @@ void testGetStratumDefText_Component_CodeableConceptWithNullCode_ReturnsNull() { new StratumDef(Collections.emptyList(), Set.of(valueDef1, valueDef2), Collections.emptyList(), null); // Act - String result = R4MeasureReportUtils.getStratumDefText(stratifierDef, stratumDef); + String result = R4MeasureReportUtils.getStratumDefText(stratumDef); // Assert assertNull(result, "Should return null when component code text is null");