From 4aedf42aa77fa3889352b08e255f4ae75643e20c Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 21 Jul 2026 11:48:47 -0400 Subject: [PATCH 01/27] feat: Exclude removed criteria from RWT eligibility checks [#OCD-5321] --- .../realworldtesting/RealWorldTestingCriteriaService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingCriteriaService.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingCriteriaService.java index 5d1557bc26..ac4ffcac0c 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingCriteriaService.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingCriteriaService.java @@ -50,6 +50,7 @@ private Integer getYearOrMostRecentPastYear(Integer requestedYear) throws Invali private List getRwtEligibleCriteria(List eligibleCriteriaKeys) { return eligibleCriteriaKeys.stream() .map(key -> certificationCriterionService.get(key)) + .filter(criterion -> !criterion.isRemoved()) .collect(Collectors.toList()); } From 5a73bfacf40a3fc8d364301fec92fdc2629b646d Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Wed, 22 Jul 2026 09:13:30 -0400 Subject: [PATCH 02/27] feat-flag!: Include different RWT criteria in report based on flag state [#OCD-5321] --- .../src/main/resources/environment.properties | 2 +- .../java/gov/healthit/chpl/FeatureList.java | 1 + .../domain/RealWorldTestingReport.java | 69 ++------- .../RealWorldTestingReportService.java | 29 ++-- .../job/RealWorldTestingReportEmailJob.java | 32 +---- ...alWorldTestingSummaryReportCreatorJob.java | 38 ++++- .../RealWorldTestingCsvPresenter.java | 132 ++++++++++++++++++ .../CertificationCriterionService.java | 21 +++ .../RealWorldTestingEligibility.java | 4 +- .../RealWorldTestingEligiblityService.java | 8 +- 10 files changed, 230 insertions(+), 106 deletions(-) create mode 100644 chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java diff --git a/chpl/chpl-resources/src/main/resources/environment.properties b/chpl/chpl-resources/src/main/resources/environment.properties index d853ab650f..4141afdde2 100644 --- a/chpl/chpl-resources/src/main/resources/environment.properties +++ b/chpl/chpl-resources/src/main/resources/environment.properties @@ -366,10 +366,10 @@ realWorldTestingCriteriaKeys={2021: 'criterion.170_315_b_1_old,\ criterion.170_315_g_7,\ criterion.170_315_g_8,\ criterion.170_315_g_9_cures,\ + criterion.170_315_g_10,\ criterion.170_315_g_31,\ criterion.170_315_g_32,\ criterion.170_315_g_33,\ - criterion.170_315_g_10,\ criterion.170_315_h_1,\ criterion.170_315_h_2,\ criterion.170_315_j_20,\ diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/FeatureList.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/FeatureList.java index 0482168218..dd8d398a15 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/FeatureList.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/FeatureList.java @@ -6,6 +6,7 @@ private FeatureList() { public static final String CMS_DISABLED = "cms-disabled"; public static final String DEMOGRAPHIC_CHANGE_REQUEST = "demographic-change-request"; + public static final String HTI_5_ERD = "hti-5-erd"; public static final String INSIGHTS_DISPLAY = "insights-display"; public static final String SERVICE_BASE_URL_LIST_CHANGE_REQUEST = "sbul-change-request"; public static final String RWT_CHANGE_REQUEST = "rwt-change-request"; diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/domain/RealWorldTestingReport.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/domain/RealWorldTestingReport.java index 5072e08ee1..475f3c0fe2 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/domain/RealWorldTestingReport.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/domain/RealWorldTestingReport.java @@ -1,17 +1,13 @@ package gov.healthit.chpl.realworldtesting.domain; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.BooleanUtils; - -import gov.healthit.chpl.util.DateUtil; +import gov.healthit.chpl.certificationCriteria.CertificationCriterion; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder @@ -34,57 +30,16 @@ public class RealWorldTestingReport { private LocalDate rwtResultsCheckDate; private String rwtPlansMessage; private String rwtResultsMessage; - private Boolean attestsG7; - private Boolean attestsG9; - private Boolean attestsG10; - - public List toListOfStrings() { - List results = new ArrayList(); - results.add(acbName); - results.add(chplProductNumber); - results.add(currentStatus); - results.add(DateUtil.format(certificationDate)); - results.add(productName); - results.add(productId == null ? null : productId.toString()); - results.add(developerName); - results.add(developerId == null ? null : developerId.toString()); - results.add(!CollectionUtils.isEmpty(developerUsers) ? developerUsers.stream().collect(Collectors.joining("; ")) : ""); - results.add(rwtEligibilityYear == null ? null : rwtEligibilityYear.toString()); - results.add(BooleanUtils.isTrue(ics) ? "Yes" : ""); - results.add(rwtPlansUrl); - results.add(rwtPlansCheckDate == null ? null : rwtPlansCheckDate.toString()); - results.add(rwtResultsUrl); - results.add(rwtResultsCheckDate == null ? null : rwtResultsCheckDate.toString()); - results.add(rwtPlansMessage); - results.add(rwtResultsMessage); - results.add(BooleanUtils.isTrue(attestsG7) ? "TRUE" : "FALSE"); - results.add(BooleanUtils.isTrue(attestsG9) ? "TRUE" : "FALSE"); - results.add(BooleanUtils.isTrue(attestsG10) ? "TRUE" : "FALSE"); - return results; - } + private List criterionAndSvapData; - public static List getHeaders() { - List results = new ArrayList(); - results.add("ONC-ACB Name"); - results.add("CHPL ID"); - results.add("Current Status"); - results.add("Certification Date"); - results.add("Product Name"); - results.add("Product DBID"); - results.add("Developer Name"); - results.add("Developer DBID"); - results.add("Developer Users"); - results.add("Initial RWT Year"); - results.add("ICS"); - results.add("RWT Plans URL"); - results.add("RWT Plans Submission Confirmed"); - results.add("RWT Results URL"); - results.add("RWT Results Submission Confirmed"); - results.add("RWT Plans Message"); - results.add("RWT Results Message"); - results.add("170.315 (g)(7)"); - results.add("170.315 (g)(9)"); - results.add("170.315 (g)(10)"); - return results; + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CriterionAndSvapData { + private CertificationCriterion criterion; + private boolean isAttested; + private boolean isGCriterion; + private boolean usesSvap; } } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingReportService.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingReportService.java index f56c77c553..a10d3cec91 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingReportService.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingReportService.java @@ -14,9 +14,9 @@ import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; -import gov.healthit.chpl.certificationCriteria.CertificationCriterion; import gov.healthit.chpl.certifiedproduct.service.CertificationStatusEventsService; import gov.healthit.chpl.dao.CertifiedProductDAO; +import gov.healthit.chpl.domain.CertificationResult; import gov.healthit.chpl.domain.CertificationStatusEvent; import gov.healthit.chpl.domain.Developer; import gov.healthit.chpl.domain.auth.User; @@ -25,8 +25,8 @@ import gov.healthit.chpl.exception.EntityRetrievalException; import gov.healthit.chpl.permissions.ResourcePermissionsFactory; import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport; +import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; import gov.healthit.chpl.service.CertificationCriterionService; -import gov.healthit.chpl.service.CertificationCriterionService.Criteria2015; import gov.healthit.chpl.service.realworldtesting.RealWorldTestingEligibility; import gov.healthit.chpl.service.realworldtesting.RealWorldTestingEligiblityReason; import gov.healthit.chpl.service.realworldtesting.RealWorldTestingEligiblityService; @@ -41,23 +41,23 @@ public class RealWorldTestingReportService { private ErrorMessageUtil errorMsg; private Environment env; private CertificationStatusEventsService certificationStatusEventsService; + private CertificationCriterionService criteriaService; private RealWorldTestingEligiblityServiceFactory rwtEligServiceFactory; private ResourcePermissionsFactory resourcePermissionsFactory; private List withdrawnStatuses; - private CertificationCriterion g7, g9, g10; @Autowired public RealWorldTestingReportService(CertifiedProductDAO certifiedProductDAO, ErrorMessageUtil errorMsg, Environment env, CertificationStatusEventsService certificationStatusEventsService, CertificationCriterionService criteriaService, - CertificationCriterionService certificationCriterionService, RealWorldTestingEligiblityServiceFactory rwtEligServiceFactory, ResourcePermissionsFactory resourcePermissionsFactory) { this.certifiedProductDAO = certifiedProductDAO; this.errorMsg = errorMsg; this.env = env; + this.criteriaService = criteriaService; this.certificationStatusEventsService = certificationStatusEventsService; this.rwtEligServiceFactory = rwtEligServiceFactory; this.resourcePermissionsFactory = resourcePermissionsFactory; @@ -67,9 +67,6 @@ public RealWorldTestingReportService(CertifiedProductDAO certifiedProductDAO, CertificationStatusType.WithdrawnByDeveloperUnderReview, CertificationStatusType.Retired, CertificationStatusType.TerminatedByOnc); - g7 = criteriaService.get(Criteria2015.G_7); - g9 = criteriaService.get(Criteria2015.G_9_CURES); - g10 = criteriaService.get(Criteria2015.G_10); } public List getRealWorldTestingReports(List acbIds, Logger logger) { @@ -146,9 +143,7 @@ private RealWorldTestingReport getRealWorldTestingReport(CertifiedProductDetails .rwtPlansCheckDate(listing.getRwtPlansCheckDate()) .rwtResultsUrl(listing.getRwtResultsUrl()) .rwtResultsCheckDate(listing.getRwtResultsCheckDate()) - .attestsG7(attestsCriteria(rwtElig, g7)) - .attestsG9(attestsCriteria(rwtElig, g9)) - .attestsG10(attestsCriteria(rwtElig, g10)) + .criterionAndSvapData(buildCriterionAndSvapData(rwtElig.getAttestedCertificationResults())) .build(); if (rwtElig.getEligibilityYear() != null) { @@ -158,11 +153,15 @@ private RealWorldTestingReport getRealWorldTestingReport(CertifiedProductDetails } } - private boolean attestsCriteria(RealWorldTestingEligibility eligibility, CertificationCriterion criterion) { - return eligibility.getAttestedCriteria().stream() - .filter(crit -> crit.getId().equals(criterion.getId())) - .findAny() - .isPresent(); + private List buildCriterionAndSvapData(List certResults) { + return certResults.stream() + .map(certResult -> CriterionAndSvapData.builder() + .criterion(certResult.getCriterion()) + .isAttested(certResult.getSuccess()) + .isGCriterion(criteriaService.isGCriterion(certResult.getCriterion())) + .usesSvap(!CollectionUtils.isEmpty(certResult.getSvaps())) + .build()) + .collect(Collectors.toList()); } private List getDeveloperUsers(Developer developer) { diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java index a82c19067e..700497acd4 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java @@ -1,10 +1,7 @@ package gov.healthit.chpl.scheduler.job; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.nio.charset.Charset; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; @@ -12,8 +9,6 @@ import java.util.Objects; import java.util.stream.Collectors; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVPrinter; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @@ -32,6 +27,7 @@ import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReportSummary; import gov.healthit.chpl.realworldtesting.manager.RealWorldTestingReportService; import gov.healthit.chpl.scheduler.job.realworldtesting.RealWorldTestingReportSummaryCalculator; +import gov.healthit.chpl.scheduler.presenter.RealWorldTestingCsvPresenter; import gov.healthit.chpl.util.NullSafeEvaluator; import lombok.AllArgsConstructor; import lombok.Getter; @@ -50,6 +46,9 @@ public class RealWorldTestingReportEmailJob implements Job { @Autowired private Environment env; + @Autowired + private RealWorldTestingCsvPresenter csvPresenter; + @Autowired private ChplHtmlEmailBuilder chplHtmlEmailBuilder; @@ -103,7 +102,7 @@ private String createHtmlMessage(JobExecutionContext context, List rows) { LOGGER.info("Generating CSV attachment"); File outputFile = getOutputFile(env.getProperty("rwt.report.filename") + LocalDate.now().toString()); - outputFile = writeToFile(rows, outputFile); + writeToFile(rows, outputFile); LOGGER.info("Completed Generating CSV attachment"); return outputFile; } @@ -120,25 +119,8 @@ private File getOutputFile(String reportFilename) { return temp; } - private File writeToFile(List rows, File outputFile) { - try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(outputFile), - Charset.forName("UTF-8").newEncoder()); - CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.EXCEL)) { - writer.write('\ufeff'); - csvPrinter.printRecord(RealWorldTestingReport.getHeaders()); - rows.stream() - .forEach(row -> { - try { - csvPrinter.printRecord(row.toListOfStrings()); - } catch (Exception e) { - LOGGER.error(e); - } - - }); - } catch (Exception e) { - LOGGER.error(e); - } - return outputFile; + private void writeToFile(List rows, File outputFile) { + csvPresenter.presentAsFile(rows, outputFile); } private String getAcbNamesAsBrSeparatedList(JobExecutionContext jobContext) { diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java index 60be011a8f..a3a72072b4 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java @@ -8,6 +8,7 @@ import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Pair; +import org.ff4j.FF4j; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; @@ -18,6 +19,8 @@ import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.context.support.SpringBeanAutowiringSupport; +import gov.healthit.chpl.FeatureList; +import gov.healthit.chpl.certificationCriteria.CertificationCriterion; import gov.healthit.chpl.developer.search.ActiveListingSearchOptions; import gov.healthit.chpl.developer.search.DeveloperSearchRequest; import gov.healthit.chpl.developer.search.DeveloperSearchResult; @@ -30,6 +33,8 @@ import gov.healthit.chpl.report.realworldtesting.RealWorldTestingResultsSummaryReportDao; import gov.healthit.chpl.report.realworldtesting.RealWorldTestingSummaryByAcbReport; import gov.healthit.chpl.report.realworldtesting.RealWorldTestingSummaryByDeveloperReport; +import gov.healthit.chpl.service.CertificationCriterionService; +import gov.healthit.chpl.service.CertificationCriterionService.Criteria2015; import gov.healthit.chpl.util.DateUtil; import lombok.extern.log4j.Log4j2; @@ -54,10 +59,22 @@ public class RealWorldTestingSummaryReportCreatorJob extends QuartzJob { @Autowired private PlatformTransactionManager transactionManager; + @Autowired + private FF4j ff4j; + + @Autowired + private CertificationCriterionService criteriaService; + + private CertificationCriterion g7, g9, g10; + @Override public void execute(JobExecutionContext context) throws JobExecutionException { SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this); LOGGER.info("********* Starting the Real World Testing Summary Report Creator job *********"); + g7 = criteriaService.get(Criteria2015.G_7); + g9 = criteriaService.get(Criteria2015.G_9_CURES); + g10 = criteriaService.get(Criteria2015.G_10); + try { List activeAcbIds = certificationBodyManager.getAllActive().stream() .map(acb -> acb.getId()) @@ -66,9 +83,8 @@ public void execute(JobExecutionContext context) throws JobExecutionException { List rwtPlansReports = rwtReportService.getRealWorldTestingReports(activeAcbIds, LOGGER).stream() .collect(Collectors.toList()); - //RWT Results requirement is only enforced for listings with g7, g9, or g10 so we only want to report on those List rwtResultReports = rwtPlansReports.stream() - .filter(report -> report.getAttestsG7() || report.getAttestsG9() || report.getAttestsG10()) + .filter(report -> isRwtResultsRequired(report)) .collect(Collectors.toList()); TransactionOperations transactionOperations = new TransactionTemplate(transactionManager, @@ -86,6 +102,24 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } + private boolean isRwtResultsRequired(RealWorldTestingReport rwtReport) { + //RWT Results requirement is only enforced for listings with g7, g9, or g10 pre HTI-5 + //and listings with any g-criteria after HTI-5 so we only want to report on those + if (ff4j.check(FeatureList.HTI_5_ERD)) { + return rwtReport.getCriterionAndSvapData().stream() + .filter(item -> item.isAttested() && (item.isGCriterion() || item.isUsesSvap())) + .findAny() + .isPresent(); + } else { + return rwtReport.getCriterionAndSvapData().stream() + .filter(item -> item.getCriterion().getId().equals(g7.getId()) + || item.getCriterion().getId().equals(g9.getId()) + || item.getCriterion().getId().equals(g10.getId())) + .findAny() + .isPresent(); + } + } + private void processRwtResultsCountsByAcb(List reportRows) { Integer rwtEligibilityYear = LocalDate.now().getYear() - 1; diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java new file mode 100644 index 0000000000..5964bb9c66 --- /dev/null +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java @@ -0,0 +1,132 @@ +package gov.healthit.chpl.scheduler.presenter; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.lang3.BooleanUtils; +import org.ff4j.FF4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import gov.healthit.chpl.FeatureList; +import gov.healthit.chpl.certificationCriteria.CertificationCriterion; +import gov.healthit.chpl.certificationCriteria.CertificationCriterionComparator; +import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport; +import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; +import gov.healthit.chpl.service.CertificationCriterionService; +import gov.healthit.chpl.service.CertificationCriterionService.Criteria2015; +import gov.healthit.chpl.service.realworldtesting.RealWorldTestingCriteriaService; +import gov.healthit.chpl.util.DateUtil; +import gov.healthit.chpl.util.Util; +import lombok.extern.log4j.Log4j2; + +@Component +@Log4j2(topic = "realWorldTestingReportEmailJobLogger") +public class RealWorldTestingCsvPresenter { + private List rwtResultsRequiredCriteria; + + @Autowired + public RealWorldTestingCsvPresenter(RealWorldTestingCriteriaService rwtCriteriaService, + CertificationCriterionService criteriaService, + CertificationCriterionComparator criteriaComparator, + FF4j ff4j) { + Integer currentYear = Calendar.getInstance().get(Calendar.YEAR); + if (ff4j.check(FeatureList.HTI_5_ERD)) { + rwtResultsRequiredCriteria = rwtCriteriaService.getEligibleCriteria(currentYear); + } else { + rwtResultsRequiredCriteria = Stream.of(criteriaService.get(Criteria2015.G_7), + criteriaService.get(Criteria2015.G_9_CURES), + criteriaService.get(Criteria2015.G_10)).collect(Collectors.toList()); + } + rwtResultsRequiredCriteria.stream().sorted(criteriaComparator); + } + + public void presentAsFile(List rwtReports, File outputFile) { + try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(outputFile), + Charset.forName("UTF-8").newEncoder()); + CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.EXCEL)) { + writer.write('\ufeff'); + csvPrinter.printRecord(generateHeaderValues()); + for (RealWorldTestingReport rwtReport : rwtReports) { + csvPrinter.printRecord(rwtReport); + } + } catch (IOException ex) { + LOGGER.error("Could not write file " + outputFile.getName(), ex); + } + } + + protected List generateHeaderValues() { + List results = new ArrayList(); + results.add("ONC-ACB Name"); + results.add("CHPL ID"); + results.add("Current Status"); + results.add("Certification Date"); + results.add("Product Name"); + results.add("Product DBID"); + results.add("Developer Name"); + results.add("Developer DBID"); + results.add("Developer Users"); + results.add("Initial RWT Year"); + results.add("ICS"); + results.add("RWT Plans URL"); + results.add("RWT Plans Submission Confirmed"); + results.add("RWT Results URL"); + results.add("RWT Results Submission Confirmed"); + results.add("RWT Plans Message"); + results.add("RWT Results Message"); + rwtResultsRequiredCriteria.stream() + .forEach(criterion -> results.add(Util.formatCriteriaNumber(criterion))); + return results; + } + + protected List generateRowValue(RealWorldTestingReport rwtReport) { + List results = new ArrayList(); + results.add(rwtReport.getAcbName()); + results.add(rwtReport.getChplProductNumber()); + results.add(rwtReport.getCurrentStatus()); + results.add(DateUtil.format(rwtReport.getCertificationDate())); + results.add(rwtReport.getProductName()); + results.add(rwtReport.getProductId() == null ? null : rwtReport.getProductId().toString()); + results.add(rwtReport.getDeveloperName()); + results.add(rwtReport.getDeveloperId() == null ? null : rwtReport.getDeveloperId().toString()); + results.add(!CollectionUtils.isEmpty(rwtReport.getDeveloperUsers()) + ? rwtReport.getDeveloperUsers().stream().collect(Collectors.joining("; ")) + : ""); + results.add(rwtReport.getRwtEligibilityYear() == null ? null : rwtReport.getRwtEligibilityYear().toString()); + results.add(BooleanUtils.isTrue(rwtReport.getIcs()) ? "Yes" : ""); + results.add(rwtReport.getRwtPlansUrl()); + results.add(rwtReport.getRwtPlansCheckDate() == null ? null : rwtReport.getRwtPlansCheckDate().toString()); + results.add(rwtReport.getRwtResultsUrl()); + results.add(rwtReport.getRwtResultsCheckDate() == null ? null : rwtReport.getRwtResultsCheckDate().toString()); + results.add(rwtReport.getRwtPlansMessage()); + results.add(rwtReport.getRwtResultsMessage()); + rwtResultsRequiredCriteria.stream() + .forEach(criterion -> results.add(determineCriteriaReportValue(rwtReport.getCriterionAndSvapData(), criterion))); + return results; + } + + private String determineCriteriaReportValue(List criteriaAndSvapData, CertificationCriterion criterion) { + CriterionAndSvapData criterionAndSvapData = criteriaAndSvapData.stream() + .filter(item -> item.getCriterion().getId().equals(criterion.getId())) + .findAny() + .orElse(null); + if (criterionAndSvapData == null) { + return "FALSE"; + } else if (criterionAndSvapData.isGCriterion()) { + return criterionAndSvapData.isAttested() ? "TRUE" : "FALSE"; + } else { + return criterionAndSvapData.isAttested() && criterionAndSvapData.isUsesSvap() ? "TRUE" : "FALSE"; + } + } +} diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/CertificationCriterionService.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/CertificationCriterionService.java index 80d268f065..d4347a9e9c 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/CertificationCriterionService.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/CertificationCriterionService.java @@ -344,6 +344,27 @@ public List getUscdiCriteria() { get(Criteria2015.G_10)); } + public boolean isGCriterion(CertificationCriterion criterion) { + if (criterion.getId().equals(get(Criteria2015.G_1).getId()) + || criterion.getId().equals(get(Criteria2015.G_2).getId()) + || criterion.getId().equals(get(Criteria2015.G_3).getId()) + || criterion.getId().equals(get(Criteria2015.G_4).getId()) + || criterion.getId().equals(get(Criteria2015.G_5).getId()) + || criterion.getId().equals(get(Criteria2015.G_6_OLD).getId()) + || criterion.getId().equals(get(Criteria2015.G_6_CURES).getId()) + || criterion.getId().equals(get(Criteria2015.G_7).getId()) + || criterion.getId().equals(get(Criteria2015.G_8).getId()) + || criterion.getId().equals(get(Criteria2015.G_9_OLD).getId()) + || criterion.getId().equals(get(Criteria2015.G_9_CURES).getId()) + || criterion.getId().equals(get(Criteria2015.G_10).getId()) + || criterion.getId().equals(get(Criteria2015.G_31).getId()) + || criterion.getId().equals(get(Criteria2015.G_32).getId()) + || criterion.getId().equals(get(Criteria2015.G_33).getId())) { + return true; + } + return false; + } + public static class Criteria2015 { public static final String A_1 = "criterion.170_315_a_1"; public static final String A_2 = "criterion.170_315_a_2"; diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligibility.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligibility.java index 339ec019ec..619e23f03d 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligibility.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligibility.java @@ -3,7 +3,7 @@ import java.io.Serializable; import java.util.List; -import gov.healthit.chpl.certificationCriteria.CertificationCriterion; +import gov.healthit.chpl.domain.CertificationResult; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -18,5 +18,5 @@ public class RealWorldTestingEligibility implements Serializable { private RealWorldTestingEligiblityReason reason; private Integer eligibilityYear; - private List attestedCriteria; + private List attestedCertificationResults; } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java index 0781940b55..45d6c9a3fa 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java @@ -19,6 +19,7 @@ import gov.healthit.chpl.activity.history.query.RealWorldTestingEligibilityQuery; import gov.healthit.chpl.certificationCriteria.CertificationCriterion; import gov.healthit.chpl.dao.CertifiedProductDAO; +import gov.healthit.chpl.domain.CertificationResult; import gov.healthit.chpl.domain.CertificationStatusEvent; import gov.healthit.chpl.domain.CertifiedProduct; import gov.healthit.chpl.domain.CertifiedProductSearchDetails; @@ -113,9 +114,9 @@ private Optional getRwtEligBasedOnStandardRequireme private RealWorldTestingEligibility getRwtEligibility(Optional listing, RealWorldTestingEligiblityReason reason, Integer currentRwtEligYear) { - List attestedCriteria = new ArrayList(); + List attestedCertificationResults = new ArrayList(); if (listing != null && listing.isPresent()) { - attestedCriteria = listing.get().getCertificationResults().stream() + attestedCertificationResults = listing.get().getCertificationResults().stream() //We might be getting this listing in it's original state from saved JSON. //For most of CHPL before mid-2023, we saved a certification result on the listing for each criteria //and used the "success" field to determine if that listing attested to that criterion. @@ -123,13 +124,12 @@ private RealWorldTestingEligibility getRwtEligibility(Optional BooleanUtils.isTrue(certResult.getSuccess())) - .map(certResult -> certResult.getCriterion()) .collect(Collectors.toList()); } return RealWorldTestingEligibility.builder() .reason(reason) .eligibilityYear(currentRwtEligYear) - .attestedCriteria(attestedCriteria) + .attestedCertificationResults(attestedCertificationResults) .build(); } From aa2e672ccc9ceb5ea7aeb4ff32ae410320869ca4 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Wed, 22 Jul 2026 16:00:22 -0400 Subject: [PATCH 03/27] flat-feat!: Dynamically add RWT cols to attestation check-in report [#OCD-5321] --- .../job/RealWorldTestingReportEmailJob.java | 2 +- ...alWorldTestingSummaryReportCreatorJob.java | 2 +- .../AttestationCheckinReportDAO.java | 3 - .../AttestationCheckinReportEntity.java | 16 +-- .../developer/attestation/CheckInReport.java | 77 +----------- .../attestation/CheckInReportCsvWriter.java | 116 +++++++++++++++++- .../CheckInReportDataCollector.java | 20 --- .../CheckInReportRwtResultsDataCollector.java | 78 ++++++++++++ .../DeveloperAttestationCheckInReportJob.java | 10 +- .../RealWorldTestingCsvPresenter.java | 2 +- 10 files changed, 206 insertions(+), 120 deletions(-) create mode 100644 chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java index 700497acd4..4c42deba92 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java @@ -151,7 +151,7 @@ private String getEmailSummaryParagraph(List rows) { RealWorldTestingReportSummary summary = RealWorldTestingReportSummaryCalculator.calculateSummariesByEligibityYear(rows, year); paragraph.append("Real World Testing for ").append(summary.getRwtEligibilityYear()).append(":
") .append("
    ") - .append("
  • ").append(NullSafeEvaluator.eval(() -> summary.getTotalListings(), 0)).append(" Total listing for this period
  • ") + .append("
  • ").append(NullSafeEvaluator.eval(() -> summary.getTotalListings(), 0)).append(" Total listing(s) for this period
  • ") .append("
  • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithdrawn(), 0)).append(" Withdrawn, no longer eligible
  • ") .append("
  • ").append(NullSafeEvaluator.eval(() -> summary.getTotalActive(), 0)).append(" Active
  • ") .append("
  • ").append(NullSafeEvaluator.eval(() -> summary.getTotalEligibleViaIcs(), 0)).append(" Eligible via ICS
  • ") diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java index a3a72072b4..7b44caed80 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java @@ -104,7 +104,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { private boolean isRwtResultsRequired(RealWorldTestingReport rwtReport) { //RWT Results requirement is only enforced for listings with g7, g9, or g10 pre HTI-5 - //and listings with any g-criteria after HTI-5 so we only want to report on those + //and listings with any g-criteria or non-g-criteria and svap after HTI-5 so we only want to report on those if (ff4j.check(FeatureList.HTI_5_ERD)) { return rwtReport.getCriterionAndSvapData().stream() .filter(item -> item.isAttested() && (item.isGCriterion() || item.isUsesSvap())) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java index 81ad423118..d764dd7cdc 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java @@ -67,9 +67,6 @@ public void save(List checkinReports) { .realWorldTestingValidation(report.getRealWorldTestingValidation()) .apiValidation(report.getApiValidation()) .warnings(report.getWarnings()) - .attestsG7(report.getAttestsG7()) - .attestsG9(report.getAttestsG9()) - .attestsG10(report.getAttestsG10()) .build(); create(entity); }); diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java index e05251ac25..fa37be8a51 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java @@ -129,17 +129,8 @@ public class AttestationCheckinReportEntity extends EntityAudit { @Column(name = "warnings") private String warnings; - @Column(name = "attests_g7") - private Boolean attestsG7; - - @Column(name = "attests_g9") - private Boolean attestsG9; - - @Column(name = "attests_g10") - private Boolean attestsG10; - public CheckInReport toDomain() { - return CheckInReport.builder() + CheckInReport checkInReport = CheckInReport.builder() .developerName(this.getDeveloperName()) .developerCode(this.getDeveloperCode()) .developerId(this.getDeveloperId()) @@ -170,9 +161,8 @@ public CheckInReport toDomain() { .realWorldTestingValidation(this.getRealWorldTestingValidation()) .apiValidation(this.getApiValidation()) .warnings(this.getWarnings()) - .attestsG7(this.getAttestsG7()) - .attestsG9(this.getAttestsG9()) - .attestsG10(this.getAttestsG10()) .build(); + //TODO add criteria + return checkInReport; } } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java index 9d417a4c3a..afa8129c85 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java @@ -3,11 +3,10 @@ import java.time.LocalDateTime; import java.util.List; -import org.apache.commons.lang3.BooleanUtils; - import gov.healthit.chpl.changerequest.domain.ChangeRequest; import gov.healthit.chpl.domain.CertificationBody; import gov.healthit.chpl.domain.Developer; +import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -48,81 +47,9 @@ public class CheckInReport { private String realWorldTestingValidation; private String apiValidation; private String warnings; - private Boolean attestsG7; - private Boolean attestsG9; - private Boolean attestsG10; + private List criterionAndSvapData; private ChangeRequest mostRecentAttestationChangeRequest; private Developer developer; private List certificationBodies; - - public List toListOfStrings() { - return List.of(developerName, - developerCode, - developerId.toString(), - submittedDate != null ? submittedDate.toString() : "", - published ? "Yes" : "No", - currentStatusName != null ? currentStatusName : "", - lastStatusChangeDate != null ? lastStatusChangeDate.toString() : "", - relevantAcbs != null ? relevantAcbs : "", - attestationPeriod != null ? attestationPeriod : "", - informationBlockingResponse != null ? informationBlockingResponse : "", - informationBlockingNoncompliantResponse != null ? informationBlockingNoncompliantResponse : "", - assurancesResponse != null ? assurancesResponse : "", - assurancesNoncompliantResponse != null ? assurancesNoncompliantResponse : "", - communicationsResponse != null ? communicationsResponse : "", - communicationsNoncompliantResponse != null ? communicationsNoncompliantResponse : "", - apiResponse != null ? apiResponse : "", - apiNoncompliantResponse != null ? apiNoncompliantResponse : "", - rwtResponse != null ? rwtResponse : "", - rwtNoncompliantResponse != null ? rwtNoncompliantResponse : "", - signature != null ? signature : "", - signatureEmail != null ? signatureEmail : "", - totalSurveillances != null ? totalSurveillances.toString() : "0", - totalSurveillanceNonconformities != null ? totalSurveillanceNonconformities.toString() : "0", - openSurveillanceNonconformities != null ? openSurveillanceNonconformities.toString() : "0", - totalDirectReviewNonconformities != null ? totalDirectReviewNonconformities.toString() : "0", - openDirectReviewNonconformities != null ? openDirectReviewNonconformities.toString() : "0", - assurancesValidation != null ? assurancesValidation : "", - apiValidation != null ? apiValidation : "", - realWorldTestingValidation != null ? realWorldTestingValidation : "", - BooleanUtils.isTrue(attestsG7) ? "TRUE" : "FALSE", - BooleanUtils.isTrue(attestsG9) ? "TRUE" : "FALSE", - BooleanUtils.isTrue(attestsG10) ? "TRUE" : "FALSE"); - } - - public static List getHeaders() { - return List.of("Developer Name", - "Developer Code", - "Developer DBID", - "Change Request Submitted Date", - "Attestations Published?", - "Change Request Current Status", - "Change Request Last Status Change Date", - "ONC-ACBs", - "Attestations Period", - "Information Blocking Response", - "Information Blocking Optional Response", - "Assurances Response", - "Assurances Optional Response", - "Communications Response", - "Communications Optional Response", - "Application Programming Interfaces Response", - "Application Programming Interfaces Optional Response", - "Real World Testing Response", - "Real World Testing Optional Response", - "Submitted by Name", - "Submitted by Email", - "Total Surveillance", - "Total Surveillance Non-conformities", - "Open Surveillance Non-conformities", - "Total Direct Review Non-conformities", - "Open Direct Review Non-conformities", - "Has listing(s) with Assurances criteria (b)(10)", - "Has listing(s) with API criteria", - "Has listing(s) with RWT criteria", - "170.315 (g)(7)", - "170.315 (g)(9)", - "170.315 (g)(10)"); - } } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java index 9f8a32428c..14f37659ea 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java @@ -6,24 +6,50 @@ import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.time.LocalDate; +import java.util.Calendar; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; +import org.ff4j.FF4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import gov.healthit.chpl.FeatureList; +import gov.healthit.chpl.certificationCriteria.CertificationCriterion; +import gov.healthit.chpl.certificationCriteria.CertificationCriterionComparator; +import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; +import gov.healthit.chpl.service.CertificationCriterionService; +import gov.healthit.chpl.service.CertificationCriterionService.Criteria2015; +import gov.healthit.chpl.service.realworldtesting.RealWorldTestingCriteriaService; +import gov.healthit.chpl.util.Util; import lombok.extern.log4j.Log4j2; @Component @Log4j2 public class CheckInReportCsvWriter { + private List rwtResultsRequiredCriteria; private String reportFileName; @Autowired - public CheckInReportCsvWriter(@Value("${developer.attestation.checkin.report.filename}") String reportFileName) { + public CheckInReportCsvWriter(RealWorldTestingCriteriaService rwtCriteriaService, + CertificationCriterionService criteriaService, + CertificationCriterionComparator criteriaComparator, + FF4j ff4j, + @Value("${developer.attestation.checkin.report.filename}") String reportFileName) { this.reportFileName = reportFileName; + Integer currentYear = Calendar.getInstance().get(Calendar.YEAR); + if (ff4j.check(FeatureList.HTI_5_ERD)) { + rwtResultsRequiredCriteria = rwtCriteriaService.getEligibleCriteria(currentYear); + } else { + rwtResultsRequiredCriteria = Stream.of(criteriaService.get(Criteria2015.G_7), + criteriaService.get(Criteria2015.G_9_CURES), + criteriaService.get(Criteria2015.G_10)).collect(Collectors.toList()); + } + rwtResultsRequiredCriteria.stream().sorted(criteriaComparator); } public File generateFile(List rows) { @@ -36,11 +62,11 @@ public File generateFile(List rows) { Charset.forName("UTF-8").newEncoder()); CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.EXCEL)) { writer.write('\ufeff'); - csvPrinter.printRecord(CheckInReport.getHeaders()); + csvPrinter.printRecord(getHeaders()); rows.stream() .forEach(row -> { try { - csvPrinter.printRecord(row.toListOfStrings()); + csvPrinter.printRecord(toListOfStrings(row)); } catch (Exception e) { LOGGER.error(e); } @@ -51,6 +77,90 @@ public File generateFile(List rows) { return outputFile; } + private List getHeaders() { + List headers = List.of("Developer Name", + "Developer Code", + "Developer DBID", + "Change Request Submitted Date", + "Attestations Published?", + "Change Request Current Status", + "Change Request Last Status Change Date", + "ONC-ACBs", + "Attestations Period", + "Information Blocking Response", + "Information Blocking Optional Response", + "Assurances Response", + "Assurances Optional Response", + "Communications Response", + "Communications Optional Response", + "Application Programming Interfaces Response", + "Application Programming Interfaces Optional Response", + "Real World Testing Response", + "Real World Testing Optional Response", + "Submitted by Name", + "Submitted by Email", + "Total Surveillance", + "Total Surveillance Non-conformities", + "Open Surveillance Non-conformities", + "Total Direct Review Non-conformities", + "Open Direct Review Non-conformities", + "Has listing(s) with Assurances criteria (b)(10)", + "Has listing(s) with API criteria", + "Has listing(s) with RWT criteria"); + rwtResultsRequiredCriteria.stream() + .forEach(criterion -> headers.add(Util.formatCriteriaNumber(criterion))); + return headers; + } + + private List toListOfStrings(CheckInReport checkInReport) { + List csvRow = List.of(checkInReport.getDeveloperName(), + checkInReport.getDeveloperCode(), + checkInReport.getDeveloperId().toString(), + checkInReport.getSubmittedDate() != null ? checkInReport.getSubmittedDate().toString() : "", + checkInReport.getPublished() ? "Yes" : "No", + checkInReport.getCurrentStatusName() != null ? checkInReport.getCurrentStatusName() : "", + checkInReport.getLastStatusChangeDate() != null ? checkInReport.getLastStatusChangeDate().toString() : "", + checkInReport.getRelevantAcbs() != null ? checkInReport.getRelevantAcbs() : "", + checkInReport.getAttestationPeriod() != null ? checkInReport.getAttestationPeriod() : "", + checkInReport.getInformationBlockingResponse() != null ? checkInReport.getInformationBlockingResponse() : "", + checkInReport.getInformationBlockingNoncompliantResponse() != null ? checkInReport.getInformationBlockingNoncompliantResponse() : "", + checkInReport.getAssurancesResponse() != null ? checkInReport.getAssurancesResponse() : "", + checkInReport.getAssurancesNoncompliantResponse() != null ? checkInReport.getAssurancesNoncompliantResponse() : "", + checkInReport.getCommunicationsResponse() != null ? checkInReport.getCommunicationsResponse() : "", + checkInReport.getCommunicationsNoncompliantResponse() != null ? checkInReport.getCommunicationsNoncompliantResponse() : "", + checkInReport.getApiResponse() != null ? checkInReport.getApiResponse() : "", + checkInReport.getApiNoncompliantResponse() != null ? checkInReport.getApiNoncompliantResponse() : "", + checkInReport.getRwtResponse() != null ? checkInReport.getRwtResponse() : "", + checkInReport.getRwtNoncompliantResponse() != null ? checkInReport.getRwtNoncompliantResponse() : "", + checkInReport.getSignature() != null ? checkInReport.getSignature() : "", + checkInReport.getSignatureEmail() != null ? checkInReport.getSignatureEmail() : "", + checkInReport.getTotalSurveillances() != null ? checkInReport.getTotalSurveillances().toString() : "0", + checkInReport.getTotalSurveillanceNonconformities() != null ? checkInReport.getTotalSurveillanceNonconformities().toString() : "0", + checkInReport.getOpenSurveillanceNonconformities() != null ? checkInReport.getOpenSurveillanceNonconformities().toString() : "0", + checkInReport.getTotalDirectReviewNonconformities() != null ? checkInReport.getTotalDirectReviewNonconformities().toString() : "0", + checkInReport.getOpenDirectReviewNonconformities() != null ? checkInReport.getOpenDirectReviewNonconformities().toString() : "0", + checkInReport.getAssurancesValidation() != null ? checkInReport.getAssurancesValidation() : "", + checkInReport.getApiValidation() != null ? checkInReport.getApiValidation() : "", + checkInReport.getRealWorldTestingValidation() != null ? checkInReport.getRealWorldTestingValidation() : ""); + rwtResultsRequiredCriteria.stream() + .forEach(criterion -> csvRow.add(determineCriteriaReportValue(checkInReport.getCriterionAndSvapData(), criterion))); + return csvRow; + } + + private String determineCriteriaReportValue(List criteriaAndSvapData, CertificationCriterion criterion) { + CriterionAndSvapData criterionAndSvapData = criteriaAndSvapData.stream() + .filter(item -> item.getCriterion().getId().equals(criterion.getId())) + .findAny() + .orElse(null); + if (criterionAndSvapData == null) { + return "FALSE"; + } else if (criterionAndSvapData.isGCriterion()) { + return criterionAndSvapData.isAttested() ? "TRUE" : "FALSE"; + } else { + return criterionAndSvapData.isAttested() && criterionAndSvapData.isUsesSvap() ? "TRUE" : "FALSE"; + } + } + private File getOutputFile() { File temp = null; try { diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java index 7099e920d6..c27bc1d350 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java @@ -11,7 +11,6 @@ import gov.healthit.chpl.attestation.domain.AttestationSubmission; import gov.healthit.chpl.attestation.manager.AttestationManager; import gov.healthit.chpl.attestation.service.AttestationCertificationBodyService; -import gov.healthit.chpl.certificationCriteria.CertificationCriterion; import gov.healthit.chpl.changerequest.dao.DeveloperCertificationBodyMapDAO; import gov.healthit.chpl.changerequest.domain.ChangeRequest; import gov.healthit.chpl.changerequest.domain.ChangeRequestAttestationSubmission; @@ -24,8 +23,6 @@ import gov.healthit.chpl.search.ListingSearchService; import gov.healthit.chpl.search.domain.ListingSearchResult; import gov.healthit.chpl.search.domain.SearchRequest; -import gov.healthit.chpl.service.CertificationCriterionService; -import gov.healthit.chpl.service.CertificationCriterionService.Criteria2015; import gov.healthit.chpl.service.realworldtesting.RealWorldTestingCriteriaService; import gov.healthit.chpl.util.CertificationStatusUtil; import lombok.extern.log4j.Log4j2; @@ -43,8 +40,6 @@ public class CheckInReportDataCollector { private CheckInReportSourceService checkInReportSourceService; private CheckInReportValidation checkInReportValidation; private AttestationCertificationBodyService attestationCertificationBodyService; - private CertificationCriterion g7, g9, g10; - private List activeStatuses = CertificationStatusUtil.getActiveStatusNames(); public CheckInReportDataCollector(AttestationManager attestationManager, @@ -52,7 +47,6 @@ public CheckInReportDataCollector(AttestationManager attestationManager, DeveloperCertificationBodyMapDAO developerCertificationBodyMapDAO, ListingSearchService listingSearchService, DirectReviewSearchService directReviewSearchService, - CertificationCriterionService certificationCriterionService, RealWorldTestingCriteriaService realWorldTestingCriteriaService, CheckInReportSourceService checkInReportSourceService, CheckInReportValidation checkInReportValidation, @@ -66,9 +60,6 @@ public CheckInReportDataCollector(AttestationManager attestationManager, this.checkInReportSourceService = checkInReportSourceService; this.checkInReportValidation = checkInReportValidation; this.attestationCertificationBodyService = attestationCertificationBodyService; - this.g7 = certificationCriterionService.get(Criteria2015.G_7); - this.g9 = certificationCriterionService.get(Criteria2015.G_9_CURES); - this.g10 = certificationCriterionService.get(Criteria2015.G_10); } public List collect(List acbIds) throws EntityRetrievalException { @@ -174,9 +165,6 @@ private CheckInReport addComplianceInformation(CheckInReport checkInReport, Deve checkInReport.setOpenSurveillanceNonconformities(getOpenSurveillanceNonconformities(allActiveListingsForDeveloper)); checkInReport.setTotalDirectReviewNonconformities(getTotalDirectReviewNonconformities(developer, LOGGER)); checkInReport.setOpenDirectReviewNonconformities(getOpenDirectReviewNonconformities(developer, LOGGER)); - checkInReport.setAttestsG7(getAnyListingForDeveloperAttestsCriterion(allActiveListingsForDeveloper, g7)); - checkInReport.setAttestsG9(getAnyListingForDeveloperAttestsCriterion(allActiveListingsForDeveloper, g9)); - checkInReport.setAttestsG10(getAnyListingForDeveloperAttestsCriterion(allActiveListingsForDeveloper, g10)); return checkInReport; } @@ -224,14 +212,6 @@ private Long getOpenSurveillanceNonconformities(List allAct .collect(Collectors.summingLong(Long::longValue)); } - private Boolean getAnyListingForDeveloperAttestsCriterion(List allActiveListingsForDeveloper, CertificationCriterion criterionToCheck) { - return allActiveListingsForDeveloper.stream() - .flatMap(listing -> listing.getCriteriaMet().stream()) - .filter(criterion -> criterion.getId().equals(criterionToCheck.getId())) - .findAny() - .isPresent(); - } - private Long getTotalDirectReviewNonconformities(Developer developer, Logger logger) { return directReviewSearchService.getDeveloperDirectReviews(developer.getId(), logger).stream() .flatMap(dr -> dr.getNonConformities().stream()) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java new file mode 100644 index 0000000000..abae966700 --- /dev/null +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java @@ -0,0 +1,78 @@ +package gov.healthit.chpl.scheduler.job.developer.attestation; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import gov.healthit.chpl.certificationCriteria.CertificationCriterion; +import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; +import gov.healthit.chpl.search.ListingSearchService; +import gov.healthit.chpl.search.domain.ListingSearchResult; +import gov.healthit.chpl.search.domain.SearchRequest; +import gov.healthit.chpl.service.CertificationCriterionService; +import gov.healthit.chpl.util.CertificationStatusUtil; +import lombok.extern.log4j.Log4j2; + +@Component +@Log4j2(topic = "developerAttestationCheckinReportJobLogger") +public class CheckInReportRwtResultsDataCollector { + private static final Integer MAX_PAGE_SIZE = 100; + + private ListingSearchService listingSearchService; + private CertificationCriterionService criteriaService; + private List activeStatuses = CertificationStatusUtil.getActiveStatusNames(); + + @Autowired + public CheckInReportRwtResultsDataCollector(ListingSearchService listingSearchService, + CertificationCriterionService criteriaService) { + this.listingSearchService = listingSearchService; + this.criteriaService = criteriaService; + } + + public List collect(Long developerId) { + List allActiveListingsForDeveloper = getActiveListingsForDeveloper(developerId); + return buildCriteriaAndSvapData(allActiveListingsForDeveloper); + } + + private List getActiveListingsForDeveloper(Long developerId) { + LOGGER.info("Getting all active listings"); + SearchRequest searchRequest = SearchRequest.builder() + .certificationStatuses(activeStatuses.stream().collect(Collectors.toSet())) + .developerId(developerId) + .pageSize(MAX_PAGE_SIZE) + .pageNumber(0) + .build(); + return listingSearchService.getAllPagesOfSearchResults(searchRequest, LOGGER); + } + + private List buildCriteriaAndSvapData(List allActiveListingsForDeveloper) { + List allAttestedCriterionIds = allActiveListingsForDeveloper.stream() + .flatMap(listingSearchResult -> listingSearchResult.getCriteriaMet().stream()) + .map(item -> item.getId()) + .distinct() + .collect(Collectors.toList()); + + return allAttestedCriterionIds.stream() + .map(attestedCriterionId -> buildCriterionAndSvapData(criteriaService.get(attestedCriterionId), allActiveListingsForDeveloper)) + .collect(Collectors.toList()); + } + + private CriterionAndSvapData buildCriterionAndSvapData(CertificationCriterion criterion, List listings) { + return CriterionAndSvapData.builder() + .criterion(criterion) + .isAttested(true) + .isGCriterion(criteriaService.isGCriterion(criterion)) + .usesSvap(doesAnyListingUseSvapWithCriterion(criterion, listings)) + .build(); + } + + private boolean doesAnyListingUseSvapWithCriterion(CertificationCriterion criterion, List listings) { + return listings.stream() + .filter(listing -> listing.getCriteriaMet().stream().filter(criterionMet -> criterionMet.getId().equals(criterion.getId())).findAny().isPresent()) + .filter(listingWithCriterion -> listingWithCriterion.getSvaps().stream().filter(svap -> svap.getCriterion().getId().equals(criterion.getId())).findAny().isPresent()) + .findAny() + .isPresent(); + } +} diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java index d22c64ad58..5613bd7097 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java @@ -38,7 +38,10 @@ public class DeveloperAttestationCheckInReportJob extends QuartzJob { private AttestationCheckinReportDAO attestationCheckinReportDAO; @Autowired - private CheckInReportSummaryDataCollector checkInReportSummaryDataCollection; + private CheckInReportSummaryDataCollector checkInReportSummaryDataCollector; + + @Autowired + private CheckInReportRwtResultsDataCollector checkInReportRwtResultsDataCollector; @Autowired private CheckInReportCsvWriter checkInReportCsvWriter; @@ -102,7 +105,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { List reportRows = getCheckInReports(acbs); - CheckInReportSummary reportSummary = checkInReportSummaryDataCollection.collect(reportRows); + CheckInReportSummary reportSummary = checkInReportSummaryDataCollector.collect(reportRows); File csv = checkInReportCsvWriter.generateFile(reportRows); chplEmailFactory.emailBuilder() .recipient(context.getMergedJobDataMap().getString("email")) @@ -133,7 +136,8 @@ public void execute(JobExecutionContext context) throws JobExecutionException { private List getCheckInReports(List acbs) { return attestationCheckinReportDAO.getCheckinReports(attestationCheckinReportDAO.getMaxReportDate()).stream() .filter(cr -> isCheckinreportValidOForAcbs(cr, acbs)) - .toList(); + .peek(checkInReport -> checkInReport.setCriterionAndSvapData(checkInReportRwtResultsDataCollector.collect(checkInReport.getDeveloperId()))) + .collect(Collectors.toList()); } private boolean isCheckinreportValidOForAcbs(CheckInReport report, List acbs) { diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java index 5964bb9c66..c11f70889a 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java @@ -59,7 +59,7 @@ public void presentAsFile(List rwtReports, File outputFi writer.write('\ufeff'); csvPrinter.printRecord(generateHeaderValues()); for (RealWorldTestingReport rwtReport : rwtReports) { - csvPrinter.printRecord(rwtReport); + csvPrinter.printRecord(generateRowValue(rwtReport)); } } catch (IOException ex) { LOGGER.error("Could not write file " + outputFile.getName(), ex); From dedc04affa0cf57f534c55a9032ecf7c98b27ee3 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Thu, 23 Jul 2026 12:59:53 -0400 Subject: [PATCH 04/27] Revert "flat-feat!: Dynamically add RWT cols to attestation check-in report" [#OCD-5321] This reverts commit aa2e672ccc9ceb5ea7aeb4ff32ae410320869ca4. --- .../job/RealWorldTestingReportEmailJob.java | 2 +- ...alWorldTestingSummaryReportCreatorJob.java | 2 +- .../AttestationCheckinReportDAO.java | 3 + .../AttestationCheckinReportEntity.java | 16 ++- .../developer/attestation/CheckInReport.java | 77 +++++++++++- .../attestation/CheckInReportCsvWriter.java | 116 +----------------- .../CheckInReportDataCollector.java | 20 +++ .../CheckInReportRwtResultsDataCollector.java | 78 ------------ .../DeveloperAttestationCheckInReportJob.java | 10 +- .../RealWorldTestingCsvPresenter.java | 2 +- 10 files changed, 120 insertions(+), 206 deletions(-) delete mode 100644 chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java index 4c42deba92..700497acd4 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java @@ -151,7 +151,7 @@ private String getEmailSummaryParagraph(List rows) { RealWorldTestingReportSummary summary = RealWorldTestingReportSummaryCalculator.calculateSummariesByEligibityYear(rows, year); paragraph.append("Real World Testing for ").append(summary.getRwtEligibilityYear()).append(":
    ") .append("
      ") - .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalListings(), 0)).append(" Total listing(s) for this period
    • ") + .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalListings(), 0)).append(" Total listing for this period
    • ") .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithdrawn(), 0)).append(" Withdrawn, no longer eligible
    • ") .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalActive(), 0)).append(" Active
    • ") .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalEligibleViaIcs(), 0)).append(" Eligible via ICS
    • ") diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java index 7b44caed80..a3a72072b4 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java @@ -104,7 +104,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { private boolean isRwtResultsRequired(RealWorldTestingReport rwtReport) { //RWT Results requirement is only enforced for listings with g7, g9, or g10 pre HTI-5 - //and listings with any g-criteria or non-g-criteria and svap after HTI-5 so we only want to report on those + //and listings with any g-criteria after HTI-5 so we only want to report on those if (ff4j.check(FeatureList.HTI_5_ERD)) { return rwtReport.getCriterionAndSvapData().stream() .filter(item -> item.isAttested() && (item.isGCriterion() || item.isUsesSvap())) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java index d764dd7cdc..81ad423118 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportDAO.java @@ -67,6 +67,9 @@ public void save(List checkinReports) { .realWorldTestingValidation(report.getRealWorldTestingValidation()) .apiValidation(report.getApiValidation()) .warnings(report.getWarnings()) + .attestsG7(report.getAttestsG7()) + .attestsG9(report.getAttestsG9()) + .attestsG10(report.getAttestsG10()) .build(); create(entity); }); diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java index fa37be8a51..e05251ac25 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/AttestationCheckinReportEntity.java @@ -129,8 +129,17 @@ public class AttestationCheckinReportEntity extends EntityAudit { @Column(name = "warnings") private String warnings; + @Column(name = "attests_g7") + private Boolean attestsG7; + + @Column(name = "attests_g9") + private Boolean attestsG9; + + @Column(name = "attests_g10") + private Boolean attestsG10; + public CheckInReport toDomain() { - CheckInReport checkInReport = CheckInReport.builder() + return CheckInReport.builder() .developerName(this.getDeveloperName()) .developerCode(this.getDeveloperCode()) .developerId(this.getDeveloperId()) @@ -161,8 +170,9 @@ public CheckInReport toDomain() { .realWorldTestingValidation(this.getRealWorldTestingValidation()) .apiValidation(this.getApiValidation()) .warnings(this.getWarnings()) + .attestsG7(this.getAttestsG7()) + .attestsG9(this.getAttestsG9()) + .attestsG10(this.getAttestsG10()) .build(); - //TODO add criteria - return checkInReport; } } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java index afa8129c85..9d417a4c3a 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReport.java @@ -3,10 +3,11 @@ import java.time.LocalDateTime; import java.util.List; +import org.apache.commons.lang3.BooleanUtils; + import gov.healthit.chpl.changerequest.domain.ChangeRequest; import gov.healthit.chpl.domain.CertificationBody; import gov.healthit.chpl.domain.Developer; -import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -47,9 +48,81 @@ public class CheckInReport { private String realWorldTestingValidation; private String apiValidation; private String warnings; - private List criterionAndSvapData; + private Boolean attestsG7; + private Boolean attestsG9; + private Boolean attestsG10; private ChangeRequest mostRecentAttestationChangeRequest; private Developer developer; private List certificationBodies; + + public List toListOfStrings() { + return List.of(developerName, + developerCode, + developerId.toString(), + submittedDate != null ? submittedDate.toString() : "", + published ? "Yes" : "No", + currentStatusName != null ? currentStatusName : "", + lastStatusChangeDate != null ? lastStatusChangeDate.toString() : "", + relevantAcbs != null ? relevantAcbs : "", + attestationPeriod != null ? attestationPeriod : "", + informationBlockingResponse != null ? informationBlockingResponse : "", + informationBlockingNoncompliantResponse != null ? informationBlockingNoncompliantResponse : "", + assurancesResponse != null ? assurancesResponse : "", + assurancesNoncompliantResponse != null ? assurancesNoncompliantResponse : "", + communicationsResponse != null ? communicationsResponse : "", + communicationsNoncompliantResponse != null ? communicationsNoncompliantResponse : "", + apiResponse != null ? apiResponse : "", + apiNoncompliantResponse != null ? apiNoncompliantResponse : "", + rwtResponse != null ? rwtResponse : "", + rwtNoncompliantResponse != null ? rwtNoncompliantResponse : "", + signature != null ? signature : "", + signatureEmail != null ? signatureEmail : "", + totalSurveillances != null ? totalSurveillances.toString() : "0", + totalSurveillanceNonconformities != null ? totalSurveillanceNonconformities.toString() : "0", + openSurveillanceNonconformities != null ? openSurveillanceNonconformities.toString() : "0", + totalDirectReviewNonconformities != null ? totalDirectReviewNonconformities.toString() : "0", + openDirectReviewNonconformities != null ? openDirectReviewNonconformities.toString() : "0", + assurancesValidation != null ? assurancesValidation : "", + apiValidation != null ? apiValidation : "", + realWorldTestingValidation != null ? realWorldTestingValidation : "", + BooleanUtils.isTrue(attestsG7) ? "TRUE" : "FALSE", + BooleanUtils.isTrue(attestsG9) ? "TRUE" : "FALSE", + BooleanUtils.isTrue(attestsG10) ? "TRUE" : "FALSE"); + } + + public static List getHeaders() { + return List.of("Developer Name", + "Developer Code", + "Developer DBID", + "Change Request Submitted Date", + "Attestations Published?", + "Change Request Current Status", + "Change Request Last Status Change Date", + "ONC-ACBs", + "Attestations Period", + "Information Blocking Response", + "Information Blocking Optional Response", + "Assurances Response", + "Assurances Optional Response", + "Communications Response", + "Communications Optional Response", + "Application Programming Interfaces Response", + "Application Programming Interfaces Optional Response", + "Real World Testing Response", + "Real World Testing Optional Response", + "Submitted by Name", + "Submitted by Email", + "Total Surveillance", + "Total Surveillance Non-conformities", + "Open Surveillance Non-conformities", + "Total Direct Review Non-conformities", + "Open Direct Review Non-conformities", + "Has listing(s) with Assurances criteria (b)(10)", + "Has listing(s) with API criteria", + "Has listing(s) with RWT criteria", + "170.315 (g)(7)", + "170.315 (g)(9)", + "170.315 (g)(10)"); + } } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java index 14f37659ea..9f8a32428c 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportCsvWriter.java @@ -6,50 +6,24 @@ import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.time.LocalDate; -import java.util.Calendar; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; -import org.ff4j.FF4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import gov.healthit.chpl.FeatureList; -import gov.healthit.chpl.certificationCriteria.CertificationCriterion; -import gov.healthit.chpl.certificationCriteria.CertificationCriterionComparator; -import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; -import gov.healthit.chpl.service.CertificationCriterionService; -import gov.healthit.chpl.service.CertificationCriterionService.Criteria2015; -import gov.healthit.chpl.service.realworldtesting.RealWorldTestingCriteriaService; -import gov.healthit.chpl.util.Util; import lombok.extern.log4j.Log4j2; @Component @Log4j2 public class CheckInReportCsvWriter { - private List rwtResultsRequiredCriteria; private String reportFileName; @Autowired - public CheckInReportCsvWriter(RealWorldTestingCriteriaService rwtCriteriaService, - CertificationCriterionService criteriaService, - CertificationCriterionComparator criteriaComparator, - FF4j ff4j, - @Value("${developer.attestation.checkin.report.filename}") String reportFileName) { + public CheckInReportCsvWriter(@Value("${developer.attestation.checkin.report.filename}") String reportFileName) { this.reportFileName = reportFileName; - Integer currentYear = Calendar.getInstance().get(Calendar.YEAR); - if (ff4j.check(FeatureList.HTI_5_ERD)) { - rwtResultsRequiredCriteria = rwtCriteriaService.getEligibleCriteria(currentYear); - } else { - rwtResultsRequiredCriteria = Stream.of(criteriaService.get(Criteria2015.G_7), - criteriaService.get(Criteria2015.G_9_CURES), - criteriaService.get(Criteria2015.G_10)).collect(Collectors.toList()); - } - rwtResultsRequiredCriteria.stream().sorted(criteriaComparator); } public File generateFile(List rows) { @@ -62,11 +36,11 @@ public File generateFile(List rows) { Charset.forName("UTF-8").newEncoder()); CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.EXCEL)) { writer.write('\ufeff'); - csvPrinter.printRecord(getHeaders()); + csvPrinter.printRecord(CheckInReport.getHeaders()); rows.stream() .forEach(row -> { try { - csvPrinter.printRecord(toListOfStrings(row)); + csvPrinter.printRecord(row.toListOfStrings()); } catch (Exception e) { LOGGER.error(e); } @@ -77,90 +51,6 @@ public File generateFile(List rows) { return outputFile; } - private List getHeaders() { - List headers = List.of("Developer Name", - "Developer Code", - "Developer DBID", - "Change Request Submitted Date", - "Attestations Published?", - "Change Request Current Status", - "Change Request Last Status Change Date", - "ONC-ACBs", - "Attestations Period", - "Information Blocking Response", - "Information Blocking Optional Response", - "Assurances Response", - "Assurances Optional Response", - "Communications Response", - "Communications Optional Response", - "Application Programming Interfaces Response", - "Application Programming Interfaces Optional Response", - "Real World Testing Response", - "Real World Testing Optional Response", - "Submitted by Name", - "Submitted by Email", - "Total Surveillance", - "Total Surveillance Non-conformities", - "Open Surveillance Non-conformities", - "Total Direct Review Non-conformities", - "Open Direct Review Non-conformities", - "Has listing(s) with Assurances criteria (b)(10)", - "Has listing(s) with API criteria", - "Has listing(s) with RWT criteria"); - rwtResultsRequiredCriteria.stream() - .forEach(criterion -> headers.add(Util.formatCriteriaNumber(criterion))); - return headers; - } - - private List toListOfStrings(CheckInReport checkInReport) { - List csvRow = List.of(checkInReport.getDeveloperName(), - checkInReport.getDeveloperCode(), - checkInReport.getDeveloperId().toString(), - checkInReport.getSubmittedDate() != null ? checkInReport.getSubmittedDate().toString() : "", - checkInReport.getPublished() ? "Yes" : "No", - checkInReport.getCurrentStatusName() != null ? checkInReport.getCurrentStatusName() : "", - checkInReport.getLastStatusChangeDate() != null ? checkInReport.getLastStatusChangeDate().toString() : "", - checkInReport.getRelevantAcbs() != null ? checkInReport.getRelevantAcbs() : "", - checkInReport.getAttestationPeriod() != null ? checkInReport.getAttestationPeriod() : "", - checkInReport.getInformationBlockingResponse() != null ? checkInReport.getInformationBlockingResponse() : "", - checkInReport.getInformationBlockingNoncompliantResponse() != null ? checkInReport.getInformationBlockingNoncompliantResponse() : "", - checkInReport.getAssurancesResponse() != null ? checkInReport.getAssurancesResponse() : "", - checkInReport.getAssurancesNoncompliantResponse() != null ? checkInReport.getAssurancesNoncompliantResponse() : "", - checkInReport.getCommunicationsResponse() != null ? checkInReport.getCommunicationsResponse() : "", - checkInReport.getCommunicationsNoncompliantResponse() != null ? checkInReport.getCommunicationsNoncompliantResponse() : "", - checkInReport.getApiResponse() != null ? checkInReport.getApiResponse() : "", - checkInReport.getApiNoncompliantResponse() != null ? checkInReport.getApiNoncompliantResponse() : "", - checkInReport.getRwtResponse() != null ? checkInReport.getRwtResponse() : "", - checkInReport.getRwtNoncompliantResponse() != null ? checkInReport.getRwtNoncompliantResponse() : "", - checkInReport.getSignature() != null ? checkInReport.getSignature() : "", - checkInReport.getSignatureEmail() != null ? checkInReport.getSignatureEmail() : "", - checkInReport.getTotalSurveillances() != null ? checkInReport.getTotalSurveillances().toString() : "0", - checkInReport.getTotalSurveillanceNonconformities() != null ? checkInReport.getTotalSurveillanceNonconformities().toString() : "0", - checkInReport.getOpenSurveillanceNonconformities() != null ? checkInReport.getOpenSurveillanceNonconformities().toString() : "0", - checkInReport.getTotalDirectReviewNonconformities() != null ? checkInReport.getTotalDirectReviewNonconformities().toString() : "0", - checkInReport.getOpenDirectReviewNonconformities() != null ? checkInReport.getOpenDirectReviewNonconformities().toString() : "0", - checkInReport.getAssurancesValidation() != null ? checkInReport.getAssurancesValidation() : "", - checkInReport.getApiValidation() != null ? checkInReport.getApiValidation() : "", - checkInReport.getRealWorldTestingValidation() != null ? checkInReport.getRealWorldTestingValidation() : ""); - rwtResultsRequiredCriteria.stream() - .forEach(criterion -> csvRow.add(determineCriteriaReportValue(checkInReport.getCriterionAndSvapData(), criterion))); - return csvRow; - } - - private String determineCriteriaReportValue(List criteriaAndSvapData, CertificationCriterion criterion) { - CriterionAndSvapData criterionAndSvapData = criteriaAndSvapData.stream() - .filter(item -> item.getCriterion().getId().equals(criterion.getId())) - .findAny() - .orElse(null); - if (criterionAndSvapData == null) { - return "FALSE"; - } else if (criterionAndSvapData.isGCriterion()) { - return criterionAndSvapData.isAttested() ? "TRUE" : "FALSE"; - } else { - return criterionAndSvapData.isAttested() && criterionAndSvapData.isUsesSvap() ? "TRUE" : "FALSE"; - } - } - private File getOutputFile() { File temp = null; try { diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java index c27bc1d350..7099e920d6 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportDataCollector.java @@ -11,6 +11,7 @@ import gov.healthit.chpl.attestation.domain.AttestationSubmission; import gov.healthit.chpl.attestation.manager.AttestationManager; import gov.healthit.chpl.attestation.service.AttestationCertificationBodyService; +import gov.healthit.chpl.certificationCriteria.CertificationCriterion; import gov.healthit.chpl.changerequest.dao.DeveloperCertificationBodyMapDAO; import gov.healthit.chpl.changerequest.domain.ChangeRequest; import gov.healthit.chpl.changerequest.domain.ChangeRequestAttestationSubmission; @@ -23,6 +24,8 @@ import gov.healthit.chpl.search.ListingSearchService; import gov.healthit.chpl.search.domain.ListingSearchResult; import gov.healthit.chpl.search.domain.SearchRequest; +import gov.healthit.chpl.service.CertificationCriterionService; +import gov.healthit.chpl.service.CertificationCriterionService.Criteria2015; import gov.healthit.chpl.service.realworldtesting.RealWorldTestingCriteriaService; import gov.healthit.chpl.util.CertificationStatusUtil; import lombok.extern.log4j.Log4j2; @@ -40,6 +43,8 @@ public class CheckInReportDataCollector { private CheckInReportSourceService checkInReportSourceService; private CheckInReportValidation checkInReportValidation; private AttestationCertificationBodyService attestationCertificationBodyService; + private CertificationCriterion g7, g9, g10; + private List activeStatuses = CertificationStatusUtil.getActiveStatusNames(); public CheckInReportDataCollector(AttestationManager attestationManager, @@ -47,6 +52,7 @@ public CheckInReportDataCollector(AttestationManager attestationManager, DeveloperCertificationBodyMapDAO developerCertificationBodyMapDAO, ListingSearchService listingSearchService, DirectReviewSearchService directReviewSearchService, + CertificationCriterionService certificationCriterionService, RealWorldTestingCriteriaService realWorldTestingCriteriaService, CheckInReportSourceService checkInReportSourceService, CheckInReportValidation checkInReportValidation, @@ -60,6 +66,9 @@ public CheckInReportDataCollector(AttestationManager attestationManager, this.checkInReportSourceService = checkInReportSourceService; this.checkInReportValidation = checkInReportValidation; this.attestationCertificationBodyService = attestationCertificationBodyService; + this.g7 = certificationCriterionService.get(Criteria2015.G_7); + this.g9 = certificationCriterionService.get(Criteria2015.G_9_CURES); + this.g10 = certificationCriterionService.get(Criteria2015.G_10); } public List collect(List acbIds) throws EntityRetrievalException { @@ -165,6 +174,9 @@ private CheckInReport addComplianceInformation(CheckInReport checkInReport, Deve checkInReport.setOpenSurveillanceNonconformities(getOpenSurveillanceNonconformities(allActiveListingsForDeveloper)); checkInReport.setTotalDirectReviewNonconformities(getTotalDirectReviewNonconformities(developer, LOGGER)); checkInReport.setOpenDirectReviewNonconformities(getOpenDirectReviewNonconformities(developer, LOGGER)); + checkInReport.setAttestsG7(getAnyListingForDeveloperAttestsCriterion(allActiveListingsForDeveloper, g7)); + checkInReport.setAttestsG9(getAnyListingForDeveloperAttestsCriterion(allActiveListingsForDeveloper, g9)); + checkInReport.setAttestsG10(getAnyListingForDeveloperAttestsCriterion(allActiveListingsForDeveloper, g10)); return checkInReport; } @@ -212,6 +224,14 @@ private Long getOpenSurveillanceNonconformities(List allAct .collect(Collectors.summingLong(Long::longValue)); } + private Boolean getAnyListingForDeveloperAttestsCriterion(List allActiveListingsForDeveloper, CertificationCriterion criterionToCheck) { + return allActiveListingsForDeveloper.stream() + .flatMap(listing -> listing.getCriteriaMet().stream()) + .filter(criterion -> criterion.getId().equals(criterionToCheck.getId())) + .findAny() + .isPresent(); + } + private Long getTotalDirectReviewNonconformities(Developer developer, Logger logger) { return directReviewSearchService.getDeveloperDirectReviews(developer.getId(), logger).stream() .flatMap(dr -> dr.getNonConformities().stream()) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java deleted file mode 100644 index abae966700..0000000000 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/CheckInReportRwtResultsDataCollector.java +++ /dev/null @@ -1,78 +0,0 @@ -package gov.healthit.chpl.scheduler.job.developer.attestation; - -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import gov.healthit.chpl.certificationCriteria.CertificationCriterion; -import gov.healthit.chpl.realworldtesting.domain.RealWorldTestingReport.CriterionAndSvapData; -import gov.healthit.chpl.search.ListingSearchService; -import gov.healthit.chpl.search.domain.ListingSearchResult; -import gov.healthit.chpl.search.domain.SearchRequest; -import gov.healthit.chpl.service.CertificationCriterionService; -import gov.healthit.chpl.util.CertificationStatusUtil; -import lombok.extern.log4j.Log4j2; - -@Component -@Log4j2(topic = "developerAttestationCheckinReportJobLogger") -public class CheckInReportRwtResultsDataCollector { - private static final Integer MAX_PAGE_SIZE = 100; - - private ListingSearchService listingSearchService; - private CertificationCriterionService criteriaService; - private List activeStatuses = CertificationStatusUtil.getActiveStatusNames(); - - @Autowired - public CheckInReportRwtResultsDataCollector(ListingSearchService listingSearchService, - CertificationCriterionService criteriaService) { - this.listingSearchService = listingSearchService; - this.criteriaService = criteriaService; - } - - public List collect(Long developerId) { - List allActiveListingsForDeveloper = getActiveListingsForDeveloper(developerId); - return buildCriteriaAndSvapData(allActiveListingsForDeveloper); - } - - private List getActiveListingsForDeveloper(Long developerId) { - LOGGER.info("Getting all active listings"); - SearchRequest searchRequest = SearchRequest.builder() - .certificationStatuses(activeStatuses.stream().collect(Collectors.toSet())) - .developerId(developerId) - .pageSize(MAX_PAGE_SIZE) - .pageNumber(0) - .build(); - return listingSearchService.getAllPagesOfSearchResults(searchRequest, LOGGER); - } - - private List buildCriteriaAndSvapData(List allActiveListingsForDeveloper) { - List allAttestedCriterionIds = allActiveListingsForDeveloper.stream() - .flatMap(listingSearchResult -> listingSearchResult.getCriteriaMet().stream()) - .map(item -> item.getId()) - .distinct() - .collect(Collectors.toList()); - - return allAttestedCriterionIds.stream() - .map(attestedCriterionId -> buildCriterionAndSvapData(criteriaService.get(attestedCriterionId), allActiveListingsForDeveloper)) - .collect(Collectors.toList()); - } - - private CriterionAndSvapData buildCriterionAndSvapData(CertificationCriterion criterion, List listings) { - return CriterionAndSvapData.builder() - .criterion(criterion) - .isAttested(true) - .isGCriterion(criteriaService.isGCriterion(criterion)) - .usesSvap(doesAnyListingUseSvapWithCriterion(criterion, listings)) - .build(); - } - - private boolean doesAnyListingUseSvapWithCriterion(CertificationCriterion criterion, List listings) { - return listings.stream() - .filter(listing -> listing.getCriteriaMet().stream().filter(criterionMet -> criterionMet.getId().equals(criterion.getId())).findAny().isPresent()) - .filter(listingWithCriterion -> listingWithCriterion.getSvaps().stream().filter(svap -> svap.getCriterion().getId().equals(criterion.getId())).findAny().isPresent()) - .findAny() - .isPresent(); - } -} diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java index 5613bd7097..d22c64ad58 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/developer/attestation/DeveloperAttestationCheckInReportJob.java @@ -38,10 +38,7 @@ public class DeveloperAttestationCheckInReportJob extends QuartzJob { private AttestationCheckinReportDAO attestationCheckinReportDAO; @Autowired - private CheckInReportSummaryDataCollector checkInReportSummaryDataCollector; - - @Autowired - private CheckInReportRwtResultsDataCollector checkInReportRwtResultsDataCollector; + private CheckInReportSummaryDataCollector checkInReportSummaryDataCollection; @Autowired private CheckInReportCsvWriter checkInReportCsvWriter; @@ -105,7 +102,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { List reportRows = getCheckInReports(acbs); - CheckInReportSummary reportSummary = checkInReportSummaryDataCollector.collect(reportRows); + CheckInReportSummary reportSummary = checkInReportSummaryDataCollection.collect(reportRows); File csv = checkInReportCsvWriter.generateFile(reportRows); chplEmailFactory.emailBuilder() .recipient(context.getMergedJobDataMap().getString("email")) @@ -136,8 +133,7 @@ public void execute(JobExecutionContext context) throws JobExecutionException { private List getCheckInReports(List acbs) { return attestationCheckinReportDAO.getCheckinReports(attestationCheckinReportDAO.getMaxReportDate()).stream() .filter(cr -> isCheckinreportValidOForAcbs(cr, acbs)) - .peek(checkInReport -> checkInReport.setCriterionAndSvapData(checkInReportRwtResultsDataCollector.collect(checkInReport.getDeveloperId()))) - .collect(Collectors.toList()); + .toList(); } private boolean isCheckinreportValidOForAcbs(CheckInReport report, List acbs) { diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java index c11f70889a..5964bb9c66 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java @@ -59,7 +59,7 @@ public void presentAsFile(List rwtReports, File outputFi writer.write('\ufeff'); csvPrinter.printRecord(generateHeaderValues()); for (RealWorldTestingReport rwtReport : rwtReports) { - csvPrinter.printRecord(generateRowValue(rwtReport)); + csvPrinter.printRecord(rwtReport); } } catch (IOException ex) { LOGGER.error("Could not write file " + outputFile.getName(), ex); From d3c9e2bef697bea4e1c495bb70e53adbbba48d50 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Thu, 23 Jul 2026 13:00:34 -0400 Subject: [PATCH 05/27] fix: Use correct grammar in RWT email body [#OCD-5321] --- .../chpl/scheduler/job/RealWorldTestingReportEmailJob.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java index 700497acd4..4c42deba92 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java @@ -151,7 +151,7 @@ private String getEmailSummaryParagraph(List rows) { RealWorldTestingReportSummary summary = RealWorldTestingReportSummaryCalculator.calculateSummariesByEligibityYear(rows, year); paragraph.append("Real World Testing for ").append(summary.getRwtEligibilityYear()).append(":
      ") .append("
        ") - .append("
      • ").append(NullSafeEvaluator.eval(() -> summary.getTotalListings(), 0)).append(" Total listing for this period
      • ") + .append("
      • ").append(NullSafeEvaluator.eval(() -> summary.getTotalListings(), 0)).append(" Total listing(s) for this period
      • ") .append("
      • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithdrawn(), 0)).append(" Withdrawn, no longer eligible
      • ") .append("
      • ").append(NullSafeEvaluator.eval(() -> summary.getTotalActive(), 0)).append(" Active
      • ") .append("
      • ").append(NullSafeEvaluator.eval(() -> summary.getTotalEligibleViaIcs(), 0)).append(" Eligible via ICS
      • ") From fb3b235caf809088d2b8cedec0395cde0fe01ce0 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Thu, 23 Jul 2026 13:12:32 -0400 Subject: [PATCH 06/27] feat: Append "SVAP" to non-G-criteria column headings [#OCD-5321] --- .../presenter/RealWorldTestingCsvPresenter.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java index 5964bb9c66..f7564ca045 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java @@ -34,6 +34,7 @@ @Component @Log4j2(topic = "realWorldTestingReportEmailJobLogger") public class RealWorldTestingCsvPresenter { + private CertificationCriterionService criteriaService; private List rwtResultsRequiredCriteria; @Autowired @@ -41,6 +42,7 @@ public RealWorldTestingCsvPresenter(RealWorldTestingCriteriaService rwtCriteriaS CertificationCriterionService criteriaService, CertificationCriterionComparator criteriaComparator, FF4j ff4j) { + this.criteriaService = criteriaService; Integer currentYear = Calendar.getInstance().get(Calendar.YEAR); if (ff4j.check(FeatureList.HTI_5_ERD)) { rwtResultsRequiredCriteria = rwtCriteriaService.getEligibleCriteria(currentYear); @@ -86,7 +88,13 @@ protected List generateHeaderValues() { results.add("RWT Plans Message"); results.add("RWT Results Message"); rwtResultsRequiredCriteria.stream() - .forEach(criterion -> results.add(Util.formatCriteriaNumber(criterion))); + .forEach(criterion -> { + if (criteriaService.isGCriterion(criterion)) { + results.add(Util.formatCriteriaNumber(criterion) + "SVAP"); + } else { + results.add(Util.formatCriteriaNumber(criterion)); + } + }); return results; } From fbf37d5e87423c6f69f61faaa6be1dfbecf7127b Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Thu, 23 Jul 2026 13:48:06 -0400 Subject: [PATCH 07/27] feat: Include SVAP in RWT eligibility calculations [#OCD-5321] --- .../RealWorldTestingEligiblityService.java | 24 ++++++++++++++++--- ...lWorldTestingEligiblityServiceFactory.java | 15 +++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java index 45d6c9a3fa..de5e83f9ff 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityService.java @@ -11,9 +11,12 @@ import java.util.Optional; import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.logging.log4j.Logger; +import org.ff4j.FF4j; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.activity.history.ListingActivityUtil; import gov.healthit.chpl.activity.history.explorer.RealWorldTestingEligibilityActivityExplorer; import gov.healthit.chpl.activity.history.query.RealWorldTestingEligibilityQuery; @@ -26,6 +29,7 @@ import gov.healthit.chpl.dto.ActivityDTO; import gov.healthit.chpl.dto.CertifiedProductDTO; import gov.healthit.chpl.exception.EntityRetrievalException; +import gov.healthit.chpl.service.CertificationCriterionService; import gov.healthit.chpl.util.CertificationStatusUtil; import gov.healthit.chpl.util.DateUtil; import lombok.AllArgsConstructor; @@ -42,19 +46,27 @@ public class RealWorldTestingEligiblityService { private RealWorldTestingEligibilityActivityExplorer realWorldTestingEligibilityActivityExplorer; private ListingActivityUtil listingActivityUtil; private CertifiedProductDAO certifiedProductDAO; + private CertificationCriterionService criteriaService; + private FF4j ff4j; private Map memo = new HashMap(); public RealWorldTestingEligiblityService(RealWorldTestingCriteriaService realWorldTestingCriteriaService, RealWorldTestingEligibilityActivityExplorer realWorldTestingEligibilityActivityExplorer, ListingActivityUtil listingActivityUtil, - CertifiedProductDAO certifiedProductDAO, LocalDate rwtProgramStartDate, Integer rwtProgramFirstEligibilityYear) { + CertifiedProductDAO certifiedProductDAO, + LocalDate rwtProgramStartDate, + Integer rwtProgramFirstEligibilityYear, + CertificationCriterionService criteriaService, + FF4j ff4j) { this.realWorldTestingCriteriaService = realWorldTestingCriteriaService; this.realWorldTestingEligibilityActivityExplorer = realWorldTestingEligibilityActivityExplorer; this.listingActivityUtil = listingActivityUtil; this.certifiedProductDAO = certifiedProductDAO; this.rwtProgramStartDate = rwtProgramStartDate; this.rwtProgramFirstEligibilityYear = rwtProgramFirstEligibilityYear; + this.criteriaService = criteriaService; + this.ff4j = ff4j; } public RealWorldTestingEligibility getRwtEligibilityYearForListing(Long listingId, Logger logger) { @@ -207,7 +219,6 @@ private boolean isListingRwtEligible(CertifiedProductSearchDetails listing, Loca return isListingStatusActiveAsOfEligibilityDate(listing, asOfDate) && isCertificationDateBeforeEligibilityDate(listing, asOfDate) && doesListingAttestToEligibleCriteria(listing, asOfDate.getYear()); - } private boolean doesListingAttestToEligibleCriteria(CertifiedProductSearchDetails listing, Integer year) { @@ -215,13 +226,20 @@ private boolean doesListingAttestToEligibleCriteria(CertifiedProductSearchDetail return listing.getCertificationResults().stream() .filter(result -> result.getSuccess() && eligibleCriteria.stream() - .filter(crit -> crit.getId().equals(result.getCriterion().getId())) + .filter(crit -> crit.getId().equals(result.getCriterion().getId()) && isGCriteriaOrUsesSvap(result)) .findAny() .isPresent()) .findAny() .isPresent(); } + private boolean isGCriteriaOrUsesSvap(CertificationResult certResult) { + if (ff4j.check(FeatureList.HTI_5_ERD)) { + return criteriaService.isGCriterion(certResult.getCriterion()) || !CollectionUtils.isEmpty(certResult.getSvaps()); + } + return true; + } + private boolean isCertificationDateBeforeEligibilityDate(CertifiedProductSearchDetails listing, LocalDate eligibilityDate) { if (Objects.isNull(listing) || Objects.isNull(listing.getCertificationDate())) { return false; diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityServiceFactory.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityServiceFactory.java index 28a9160c99..dc40a67988 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityServiceFactory.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/service/realworldtesting/RealWorldTestingEligiblityServiceFactory.java @@ -2,6 +2,7 @@ import java.time.LocalDate; +import org.ff4j.FF4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -9,6 +10,7 @@ import gov.healthit.chpl.activity.history.ListingActivityUtil; import gov.healthit.chpl.activity.history.explorer.RealWorldTestingEligibilityActivityExplorer; import gov.healthit.chpl.dao.CertifiedProductDAO; +import gov.healthit.chpl.service.CertificationCriterionService; @Component public class RealWorldTestingEligiblityServiceFactory { @@ -19,24 +21,31 @@ public class RealWorldTestingEligiblityServiceFactory { @Value("#{T(java.time.LocalDate).parse('${rwtProgramStartDate}')}") private LocalDate rwtProgramStartDate; + private CertificationCriterionService criteriaService; private RealWorldTestingCriteriaService realWorldTestingCriteriaService; private RealWorldTestingEligibilityActivityExplorer realWorldTestingEligibilityActivityExplorer; private ListingActivityUtil listingActivityUtil; private CertifiedProductDAO certifiedProductDAO; + private FF4j ff4j; @Autowired - public RealWorldTestingEligiblityServiceFactory(RealWorldTestingCriteriaService realWorldTestingCriteriaService, + public RealWorldTestingEligiblityServiceFactory(CertificationCriterionService criteriaService, + RealWorldTestingCriteriaService realWorldTestingCriteriaService, RealWorldTestingEligibilityActivityExplorer realWorldTestingEligibilityActivityExplorer, ListingActivityUtil listingActivityUtil, - CertifiedProductDAO certifiedProductDAO) { + CertifiedProductDAO certifiedProductDAO, + FF4j ff4j) { + this.criteriaService = criteriaService; this.realWorldTestingCriteriaService = realWorldTestingCriteriaService; this.realWorldTestingEligibilityActivityExplorer = realWorldTestingEligibilityActivityExplorer; this.listingActivityUtil = listingActivityUtil; this.certifiedProductDAO = certifiedProductDAO; + this.ff4j = ff4j; } public RealWorldTestingEligiblityService getInstance() { return new RealWorldTestingEligiblityService(realWorldTestingCriteriaService, realWorldTestingEligibilityActivityExplorer, - listingActivityUtil, certifiedProductDAO, rwtProgramStartDate, rwtProgramFirstEligibilityYear); + listingActivityUtil, certifiedProductDAO, rwtProgramStartDate, rwtProgramFirstEligibilityYear, + criteriaService, ff4j); } } From f7f2f14653d511dcfb8ecd8f79bd639d5f2e0e5b Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Thu, 23 Jul 2026 14:09:20 -0400 Subject: [PATCH 08/27] fix: Correctly print CSV row [#OCD-5321] --- .../chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java index f7564ca045..9c711930f9 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java @@ -61,7 +61,7 @@ public void presentAsFile(List rwtReports, File outputFi writer.write('\ufeff'); csvPrinter.printRecord(generateHeaderValues()); for (RealWorldTestingReport rwtReport : rwtReports) { - csvPrinter.printRecord(rwtReport); + csvPrinter.printRecord(generateRowValue(rwtReport)); } } catch (IOException ex) { LOGGER.error("Could not write file " + outputFile.getName(), ex); From cb7b8625622050f0e6acb92f6cc6d7573e5809c7 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Fri, 24 Jul 2026 08:59:05 -0400 Subject: [PATCH 09/27] fix: Name column correctly [#OCD-5321] --- .../scheduler/presenter/RealWorldTestingCsvPresenter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java index 9c711930f9..2e0c6884d5 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java @@ -90,9 +90,9 @@ protected List generateHeaderValues() { rwtResultsRequiredCriteria.stream() .forEach(criterion -> { if (criteriaService.isGCriterion(criterion)) { - results.add(Util.formatCriteriaNumber(criterion) + "SVAP"); - } else { results.add(Util.formatCriteriaNumber(criterion)); + } else { + results.add(Util.formatCriteriaNumber(criterion) + "SVAP"); } }); return results; From 7124b2c0c9d42f5ddbad3446692f16f0255c17db Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Fri, 24 Jul 2026 08:59:19 -0400 Subject: [PATCH 10/27] fix: Correctly comment the HTI-5 requirement [#OCD-5321] --- .../job/RealWorldTestingSummaryReportCreatorJob.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java index a3a72072b4..22039946f5 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java @@ -103,8 +103,8 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } private boolean isRwtResultsRequired(RealWorldTestingReport rwtReport) { - //RWT Results requirement is only enforced for listings with g7, g9, or g10 pre HTI-5 - //and listings with any g-criteria after HTI-5 so we only want to report on those + //RWT Results requirement is only enforced for listings with g7, g9, or g10 pre HTI-5. + //Post HTI-5 listings with any g-criteria or non-g-criteria using svap have the RWT Results requirement. if (ff4j.check(FeatureList.HTI_5_ERD)) { return rwtReport.getCriterionAndSvapData().stream() .filter(item -> item.isAttested() && (item.isGCriterion() || item.isUsesSvap())) From f753646598ac934b9e2773e8aff4e75011b1b8eb Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Mon, 27 Jul 2026 11:36:37 -0400 Subject: [PATCH 11/27] feat-flag!: Exclude RWT Plans from Questionable URL report calculations [#OCD-5385] --- .../questionableurl/QuestionableUrlReportDao.java | 2 +- .../urlStatus/QuestionableUrlReportGenerator.java | 14 +++++++++++++- .../job/urlStatus/UrlStatusDataCollector.java | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/report/questionableurl/QuestionableUrlReportDao.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/report/questionableurl/QuestionableUrlReportDao.java index 3fc9ca4a0d..4d9caffbe3 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/report/questionableurl/QuestionableUrlReportDao.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/report/questionableurl/QuestionableUrlReportDao.java @@ -71,7 +71,7 @@ private boolean isForListingUrl(QuestionableUrlDetailReport report) { || report.getUrlType().equals(UrlType.EXPORT_DOCUMENTATION.getName()) || (!ff4j.check(FeatureList.HTI_5_ERD) && report.getUrlType().equals(UrlType.FULL_USABILITY_REPORT.getName())) || report.getUrlType().equals(UrlType.MANDATORY_DISCLOSURE.getName()) - || report.getUrlType().equals(UrlType.REAL_WORLD_TESTING_PLANS.getName()) + || (!ff4j.check(FeatureList.HTI_5_ERD) && report.getUrlType().equals(UrlType.REAL_WORLD_TESTING_PLANS.getName())) || report.getUrlType().equals(UrlType.REAL_WORLD_TESTING_RESULTS.getName()) || report.getUrlType().equals(UrlType.RISK_MANAGEMENT_SUMMARY_INFORMATION.getName()) || report.getUrlType().equals(UrlType.SERVICE_BASE_URL_LIST.getName()) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/QuestionableUrlReportGenerator.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/QuestionableUrlReportGenerator.java index e710eaab95..c2628b2ea1 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/QuestionableUrlReportGenerator.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/QuestionableUrlReportGenerator.java @@ -126,7 +126,17 @@ public void execute(JobExecutionContext jobContext) throws JobExecutionException } break; case MANDATORY_DISCLOSURE: + LOGGER.info("[" + i + "] Getting Listings with bad " + questionableUrlResult.getUrlType().getName() + + " website " + questionableUrlResult.getUrl()); + questionableUrls.addAll(urlLookupDao.getListingsWithUrl(questionableUrlResult)); + break; case REAL_WORLD_TESTING_PLANS: + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + LOGGER.info("[" + i + "] Getting Listings with bad " + questionableUrlResult.getUrlType().getName() + + " website " + questionableUrlResult.getUrl()); + questionableUrls.addAll(urlLookupDao.getListingsWithUrl(questionableUrlResult)); + } + break; case REAL_WORLD_TESTING_RESULTS: case STANDARDS_VERSION_ADVANCEMENT_PROCESS_NOTICE: LOGGER.info("[" + i + "] Getting Listings with bad " + questionableUrlResult.getUrlType().getName() @@ -343,7 +353,9 @@ private String createHtmlEmailBody(List urlResults, JobExecutio brokenUrlSummaryHtml += "
      • " + UrlType.DOCUMENTATION.getName() + ": " + brokenDocumentationUrlUrls + "
      • "; brokenUrlSummaryHtml += "
      • " + UrlType.USE_CASES.getName() + ": " + brokenUseCasesUrls + "
      • "; brokenUrlSummaryHtml += "
      • " + UrlType.RISK_MANAGEMENT_SUMMARY_INFORMATION.getName() + ": " + brokenRiskManagementSummaryInfoUrls + "
      • "; - brokenUrlSummaryHtml += "
      • " + UrlType.REAL_WORLD_TESTING_PLANS.getName() + ": " + brokenRwtPlansUrls + "
      • "; + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + brokenUrlSummaryHtml += "
      • " + UrlType.REAL_WORLD_TESTING_PLANS.getName() + ": " + brokenRwtPlansUrls + "
      • "; + } brokenUrlSummaryHtml += "
      • " + UrlType.REAL_WORLD_TESTING_RESULTS.getName() + ": " + brokenRwtResultsUrls + "
      • "; brokenUrlSummaryHtml += "
      • " + UrlType.STANDARDS_VERSION_ADVANCEMENT_PROCESS_NOTICE.getName() + ": " + brokenSvapNoticeUrls + "
      • "; brokenUrlSummaryHtml += "
      "; diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/UrlStatusDataCollector.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/UrlStatusDataCollector.java index fe8bd66732..c949f24636 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/UrlStatusDataCollector.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/urlStatus/UrlStatusDataCollector.java @@ -114,6 +114,7 @@ private List getUrlTypesToExclude() { List exclusions = Stream.of(UrlType.SERVICE_BASE_URL_LIST).collect(Collectors.toList()); if (ff4j.check(FeatureList.HTI_5_ERD)) { exclusions.add(UrlType.FULL_USABILITY_REPORT); + exclusions.add(UrlType.REAL_WORLD_TESTING_PLANS); } return exclusions; } From c6a88105c4619e09b8d2bb8c1450cecc18bca2d4 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Mon, 27 Jul 2026 13:04:34 -0400 Subject: [PATCH 12/27] feat-flag!: Remove access to RWT Plans URL/Check Date in API response [#OCD-5385] --- ...IgnorableResponseFieldAnnotationIntrospector.java | 9 ++++++++- .../chpl/domain/CertifiedProductSearchDetails.java | 3 +++ .../realworldtesting/DeprecatedRwtPlansData.java | 12 ++++++++++++ .../main/java/gov/healthit/chpl/util/JSONUtils.java | 10 +++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/DeprecatedRwtPlansData.java diff --git a/chpl/chpl-api/src/main/java/gov/healthit/chpl/IgnorableResponseFieldAnnotationIntrospector.java b/chpl/chpl-api/src/main/java/gov/healthit/chpl/IgnorableResponseFieldAnnotationIntrospector.java index 31ecbd3a18..8c8ef30cb0 100644 --- a/chpl/chpl-api/src/main/java/gov/healthit/chpl/IgnorableResponseFieldAnnotationIntrospector.java +++ b/chpl/chpl-api/src/main/java/gov/healthit/chpl/IgnorableResponseFieldAnnotationIntrospector.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; import gov.healthit.chpl.api.deprecatedUsage.DeprecatedResponseField; +import gov.healthit.chpl.realworldtesting.DeprecatedRwtPlansData; import gov.healthit.chpl.sed.DeprecatedSedSummaryData; import gov.healthit.chpl.sed.DeprecatedSedTestTaskData; import lombok.extern.log4j.Log4j2; @@ -29,7 +30,7 @@ public boolean hasIgnoreMarker(MapperConfig config, AnnotatedMember m) { if (super.hasIgnoreMarker(config, m)) { return true; } else { - return isDeprecatedAndIgnorable(m) || isSedAndIgnorable(m); + return isDeprecatedAndIgnorable(m) || isSedAndIgnorable(m) || isRwtAndIgnorable(m); } } @@ -45,4 +46,10 @@ private boolean isSedAndIgnorable(AnnotatedMember m) { boolean isSedTestTask = _findAnnotation(m, DeprecatedSedTestTaskData.class) != null; return isHti5Erd && (isSedSummary || isSedTestTask); } + + private boolean isRwtAndIgnorable(AnnotatedMember m) { + boolean isHti5Erd = ff4j.check(FeatureList.HTI_5_ERD); + boolean isRwtPlans = _findAnnotation(m, DeprecatedRwtPlansData.class) != null; + return isHti5Erd && isRwtPlans; + } } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchDetails.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchDetails.java index 0e3b508d56..c148981ecf 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchDetails.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchDetails.java @@ -27,6 +27,7 @@ import gov.healthit.chpl.domain.compliance.DirectReview; import gov.healthit.chpl.domain.surveillance.Surveillance; import gov.healthit.chpl.entity.CertificationStatusType; +import gov.healthit.chpl.realworldtesting.DeprecatedRwtPlansData; import gov.healthit.chpl.sed.CertifiedProductSed; import gov.healthit.chpl.sed.DeprecatedSedSummaryData; import gov.healthit.chpl.targeteduser.CertifiedProductTargetedUser; @@ -250,9 +251,11 @@ public class CertifiedProductSearchDetails implements Serializable { @Schema(description = "All data related to safety-enhanced design for this listing.") private CertifiedProductSed sed = new CertifiedProductSed(); + @DeprecatedRwtPlansData @Schema(description = "URL where the listings Real World Testing Plan is located") private String rwtPlansUrl; + @DeprecatedRwtPlansData @Schema(description = "Date the listings Real World Testing Plan was submitted") @JsonDeserialize(using = LocalDateDeserializer.class) @JsonSerialize(using = LocalDateSerializer.class) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/DeprecatedRwtPlansData.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/DeprecatedRwtPlansData.java new file mode 100644 index 0000000000..3d9aa2365f --- /dev/null +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/DeprecatedRwtPlansData.java @@ -0,0 +1,12 @@ +package gov.healthit.chpl.realworldtesting; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface DeprecatedRwtPlansData { + +} diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/util/JSONUtils.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/util/JSONUtils.java index e534941286..7c539bf6f4 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/util/JSONUtils.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/util/JSONUtils.java @@ -10,6 +10,7 @@ import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.activity.ActivityExclude; +import gov.healthit.chpl.realworldtesting.DeprecatedRwtPlansData; import gov.healthit.chpl.sed.DeprecatedSedSummaryData; import gov.healthit.chpl.sed.DeprecatedSedTestTaskData; import lombok.extern.log4j.Log4j2; @@ -44,7 +45,8 @@ public boolean hasIgnoreMarker(MapperConfig config, AnnotatedMember m) { return super.hasIgnoreMarker(config, m) || m.hasAnnotation(Deprecated.class) || m.hasAnnotation(ActivityExclude.class) - || isSedAndIgnorable(m); + || isSedAndIgnorable(m) + || isRwtAndIgnorable(m); } private boolean isSedAndIgnorable(AnnotatedMember m) { @@ -53,6 +55,12 @@ private boolean isSedAndIgnorable(AnnotatedMember m) { boolean isSedTestTask = _findAnnotation(m, DeprecatedSedTestTaskData.class) != null; return isHti5Erd && (isSedSummary || isSedTestTask); } + + private boolean isRwtAndIgnorable(AnnotatedMember m) { + boolean isHti5Erd = ff4j.check(FeatureList.HTI_5_ERD); + boolean isRwtPlans = _findAnnotation(m, DeprecatedRwtPlansData.class) != null; + return isHti5Erd && isRwtPlans; + } }) .build(); this.writerExcludingIgnoredFields = mapperExcludingIgnoredFields.writer(); From 8e6216bbae795aa35b19f54f4330bd8755bd9683 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Mon, 27 Jul 2026 13:39:00 -0400 Subject: [PATCH 13/27] feat-flag!: Ignore RWT Plans columns in listing upload, csv download [#OCD-5385] --- .../csv/ListingCsvDataWriter.java | 59 +++++++++++-------- .../csv/ListingCsvHeadingWriter.java | 18 ++++-- .../chpl/upload/listing/HeadingPostHti5.java | 2 - .../handler/ListingDetailsUploadHandler.java | 4 +- 4 files changed, 49 insertions(+), 34 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvDataWriter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvDataWriter.java index a5379107e8..1057c403a9 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvDataWriter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvDataWriter.java @@ -73,19 +73,25 @@ public class ListingCsvDataWriter { private static final int DEVELOPER_CONTACT_COL_START = 18; private static final int SVAP_NOTICE_URL_COL = 21; private static final int RWT_COL_START = 22; - private static final int TARGETED_USERS_COL = 26; - private static final int QMS_STD_START_COL = 27; - private static final int ICS_START_COL = 30; - private static final int ACCESSIBILITY_STANDARDS_START_COL = 32; - private static final int K1_URL_COL = 34; - private static final int CQM_START_COL = 35; + private static final int TARGETED_USERS_COL = 24; + private static final int TARGETED_USERS_COL_PRE_HTI5 = 26; + private static final int QMS_STD_START_COL = 25; + private static final int QMS_STD_START_COL_PRE_HTI5 = 27; + private static final int ICS_START_COL = 28; + private static final int ICS_START_COL_PRE_HTI5 = 30; + private static final int ACCESSIBILITY_STANDARDS_START_COL = 30; + private static final int ACCESSIBILITY_STANDARDS_START_COL_PRE_HTI5 = 32; + private static final int K1_URL_COL = 32; + private static final int K1_URL_COL_PRE_HTI5 = 34; + private static final int CQM_START_COL = 33; + private static final int CQM_START_COL_PRE_HTI5 = 35; private static final int SED_REPORT_URL_COL = 38; private static final int SED_INTENDED_USERS_COL = 39; private static final int SED_TESTING_DATE_COL = 40; private static final int PARTICIPANT_START_COL = 41; private static final int TASK_START_COL = 50; private static final int CRITERIA_START_COL_PRE_HTI5 = 65; - private static final int CRITERIA_START_COL = 38; + private static final int CRITERIA_START_COL = 36; private static final int ADDITIONAL_SOFTWARE_COL_COUNT = 5; private static final int UCD_PROCESS_COL_COUNT = 2; private static final int SED_TESTING_COL_COUNT = 2; @@ -125,7 +131,7 @@ public List> getCsvData(CertifiedProductSearchDetails listing, int addQmsStandards(csvDataMatrix, listing.getQmsStandards()); addIcs(csvDataMatrix, listing.getIcs()); addAccessibilityStandards(csvDataMatrix, listing.getAccessibilityStandards()); - csvDataMatrix[0][K1_URL_COL] = listing.getMandatoryDisclosures(); + csvDataMatrix[0][(!ff4j.check(FeatureList.HTI_5_ERD) ? K1_URL_COL_PRE_HTI5 : K1_URL_COL)] = listing.getMandatoryDisclosures(); addCqms(csvDataMatrix, listing.getCqmResults()); if (!ff4j.check(FeatureList.HTI_5_ERD)) { csvDataMatrix[0][SED_REPORT_URL_COL] = listing.getSedReportFileLocation(); @@ -333,47 +339,50 @@ private void addDeveloperContact(String[][] csvDataMatrix, Developer developer) private void addRwtData(String[][] csvDataMatrix, CertifiedProductSearchDetails listing) { int col = RWT_COL_START; - csvDataMatrix[0][col++] = !StringUtils.isEmpty(listing.getRwtPlansUrl()) ? listing.getRwtPlansUrl() : ""; - csvDataMatrix[0][col++] = listing.getRwtPlansCheckDate() != null ? dateFormat.format(DateUtil.toDate(listing.getRwtPlansCheckDate())) : ""; + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + csvDataMatrix[0][col++] = !StringUtils.isEmpty(listing.getRwtPlansUrl()) ? listing.getRwtPlansUrl() : ""; + csvDataMatrix[0][col++] = listing.getRwtPlansCheckDate() != null ? dateFormat.format(DateUtil.toDate(listing.getRwtPlansCheckDate())) : ""; + } csvDataMatrix[0][col++] = !StringUtils.isEmpty(listing.getRwtResultsUrl()) ? listing.getRwtResultsUrl() : ""; csvDataMatrix[0][col++] = listing.getRwtResultsCheckDate() != null ? dateFormat.format(DateUtil.toDate(listing.getRwtResultsCheckDate())) : ""; } private void addTargetedUsers(String[][] csvDataMatrix, List targetedUsers) { + int currCol = !ff4j.check(FeatureList.HTI_5_ERD) ? TARGETED_USERS_COL_PRE_HTI5 : TARGETED_USERS_COL; for (int i = 0; i < targetedUsers.size(); i++) { CertifiedProductTargetedUser targetedUser = targetedUsers.get(i); - csvDataMatrix[i][TARGETED_USERS_COL] = targetedUser.getTargetedUserName(); + csvDataMatrix[i][currCol] = targetedUser.getTargetedUserName(); } } private void addQmsStandards(String[][] csvDataMatrix, List qmsStandards) { for (int i = 0; i < qmsStandards.size(); i++) { - int col = QMS_STD_START_COL; + int currCol = !ff4j.check(FeatureList.HTI_5_ERD) ? QMS_STD_START_COL_PRE_HTI5 : QMS_STD_START_COL; CertifiedProductQmsStandard qmsStandard = qmsStandards.get(i); - csvDataMatrix[i][col++] = qmsStandard.getQmsStandardName(); - csvDataMatrix[i][col++] = qmsStandard.getApplicableCriteria(); - csvDataMatrix[i][col++] = qmsStandard.getQmsModification(); + csvDataMatrix[i][currCol++] = qmsStandard.getQmsStandardName(); + csvDataMatrix[i][currCol++] = qmsStandard.getApplicableCriteria(); + csvDataMatrix[i][currCol++] = qmsStandard.getQmsModification(); } } private void addIcs(String[][] csvDataMatrix, InheritedCertificationStatus ics) { if (ics != null && !CollectionUtils.isEmpty(ics.getParents())) { - int col = ICS_START_COL; - csvDataMatrix[0][col++] = "1"; + int currCol = !ff4j.check(FeatureList.HTI_5_ERD) ? ICS_START_COL_PRE_HTI5 : ICS_START_COL; + csvDataMatrix[0][currCol++] = "1"; for (int i = 0; i < ics.getParents().size(); i++) { CertifiedProduct parent = ics.getParents().get(i); - csvDataMatrix[i][col++] = parent.getChplProductNumber(); + csvDataMatrix[i][currCol++] = parent.getChplProductNumber(); } } } private void addAccessibilityStandards(String[][] csvDataMatrix, List accStds) { if (!CollectionUtils.isEmpty(accStds)) { - int col = ACCESSIBILITY_STANDARDS_START_COL; - csvDataMatrix[0][col++] = "1"; + int currCol = !ff4j.check(FeatureList.HTI_5_ERD) ? ACCESSIBILITY_STANDARDS_START_COL_PRE_HTI5 : ACCESSIBILITY_STANDARDS_START_COL; + csvDataMatrix[0][currCol++] = "1"; for (int i = 0; i < accStds.size(); i++) { CertifiedProductAccessibilityStandard accStd = accStds.get(i); - csvDataMatrix[i][col] = accStd.getAccessibilityStandardName(); + csvDataMatrix[i][currCol] = accStd.getAccessibilityStandardName(); } } } @@ -384,11 +393,11 @@ private void addCqms(String[][] csvDataMatrix, List cqmResults .collect(Collectors.toList()); for (int i = 0; i < attestedCqms.size(); i++) { CQMResultDetails cqm = attestedCqms.get(i); - int col = CQM_START_COL; - csvDataMatrix[i][col++] = cqm.getCmsId(); - csvDataMatrix[i][col++] = + int currCol = !ff4j.check(FeatureList.HTI_5_ERD) ? CQM_START_COL_PRE_HTI5 : CQM_START_COL; + csvDataMatrix[i][currCol++] = cqm.getCmsId(); + csvDataMatrix[i][currCol++] = cqm.getSuccessVersions().stream().collect(Collectors.joining(";")); - csvDataMatrix[i][col++] = + csvDataMatrix[i][currCol++] = cqm.getCriteria().stream() .map(crit -> Util.formatCriteriaNumber(crit.getCriterion())) .collect(Collectors.joining(";")); diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvHeadingWriter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvHeadingWriter.java index 1b807cb26b..77c753e99a 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvHeadingWriter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/certifiedproduct/csv/ListingCsvHeadingWriter.java @@ -65,9 +65,17 @@ public List getCsvHeadings(CertifiedProductSearchDetails listing) { HeadingPostHti5.DEVELOPER_EMAIL.getHeading(), HeadingPostHti5.DEVELOPER_PHONE.getHeading(), HeadingPostHti5.DEVELOPER_CONTACT_NAME.getHeading(), - HeadingPostHti5.SVAP_NOTICE_URL.getHeading(), - HeadingPostHti5.RWT_PLANS_URL.getHeading(), - HeadingPostHti5.RWT_PLANS_CHECK_DATE.getHeading(), + HeadingPostHti5.SVAP_NOTICE_URL.getHeading()) + .collect(Collectors.toList()); + + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + headings.addAll(Stream.of( + HeadingPreHti5.RWT_PLANS_URL.getHeading(), + HeadingPreHti5.RWT_PLANS_CHECK_DATE.getHeading()) + .collect(Collectors.toList())); + } + + headings.addAll(Stream.of( HeadingPostHti5.RWT_RESULTS_URL.getHeading(), HeadingPostHti5.RWT_RESULTS_CHECK_DATE.getHeading(), HeadingPostHti5.TARGETED_USERS.getHeading(), @@ -81,8 +89,8 @@ public List getCsvHeadings(CertifiedProductSearchDetails listing) { HeadingPostHti5.K_1_URL.getHeading(), HeadingPostHti5.CQM_NUMBER.getHeading(), HeadingPostHti5.CQM_VERSION.getHeading(), - HeadingPostHti5.CQM_CRITERIA.getHeading() - ).collect(Collectors.toList()); + HeadingPostHti5.CQM_CRITERIA.getHeading()) + .collect(Collectors.toList())); if (!ff4j.check(FeatureList.HTI_5_ERD)) { headings.addAll(Stream.of( diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/HeadingPostHti5.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/HeadingPostHti5.java index d36f33cb9a..fbc634839d 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/HeadingPostHti5.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/HeadingPostHti5.java @@ -36,8 +36,6 @@ public enum HeadingPostHti5 implements Heading { ACCESSIBILITY_STANDARD("Accessibility Standard"), K_1_URL("170.523(k)(1) URL", "170.523 (k)(1) URL"), SVAP_NOTICE_URL("SVAP Notice URL"), - RWT_PLANS_URL("RWT Plans URL", "RWT_Plans_URL"), - RWT_PLANS_CHECK_DATE("RWT Plans Check Date"), RWT_RESULTS_URL("RWT Results URL", "RWT_Results_URL"), RWT_RESULTS_CHECK_DATE("RWT Results Check Date"), CQM_NUMBER("CQM Number", "CQM_NUMBER"), diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/handler/ListingDetailsUploadHandler.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/handler/ListingDetailsUploadHandler.java index a1191776b0..359f3620b3 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/handler/ListingDetailsUploadHandler.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/upload/listing/handler/ListingDetailsUploadHandler.java @@ -235,11 +235,11 @@ private String parseSvapNoticeUrl(CSVRecord headingRecord, List listi } private String parseRwtPlansUrl(CSVRecord headingRecord, List listingRecords) { - return uploadUtil.parseSingleRowField(HeadingPostHti5.RWT_PLANS_URL, headingRecord, listingRecords); + return uploadUtil.parseSingleRowField(HeadingPreHti5.RWT_PLANS_URL, headingRecord, listingRecords); } private String parseRwtPlansCheckDate(CSVRecord headingRecord, List listingRecords) { - return uploadUtil.parseSingleRowField(HeadingPostHti5.RWT_PLANS_CHECK_DATE, headingRecord, listingRecords); + return uploadUtil.parseSingleRowField(HeadingPreHti5.RWT_PLANS_CHECK_DATE, headingRecord, listingRecords); } private String parseRwtResultsUrl(CSVRecord headingRecord, List listingRecords) { From 3dadb9319ae5ef132e7462378c67cd465dfe4aaf Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Mon, 27 Jul 2026 13:45:02 -0400 Subject: [PATCH 14/27] flag-feat: Do not save RWT Plans on listing create, update [#OCD-5385] --- .../gov/healthit/chpl/dao/CertifiedProductDAO.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java index 09cb8cdd22..b07c355e85 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java @@ -80,8 +80,10 @@ public Long create(CertifiedProductSearchDetails listing) throws EntityCreationE entity.setIcs(listing.getIcs() == null || listing.getIcs().getInherits() == null ? Boolean.FALSE : listing.getIcs().getInherits()); entity.setAccessibilityCertified(listing.getAccessibilityCertified()); entity.setSvapNoticeUrl(listing.getSvapNoticeUrl()); - entity.setRwtPlansUrl(listing.getRwtPlansUrl()); - entity.setRwtPlansCheckDate(listing.getRwtPlansCheckDate()); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + entity.setRwtPlansUrl(listing.getRwtPlansUrl()); + entity.setRwtPlansCheckDate(listing.getRwtPlansCheckDate()); + } entity.setRwtResultsUrl(listing.getRwtResultsUrl()); entity.setRwtResultsCheckDate(listing.getRwtResultsCheckDate()); entity.setChplProductNumber(null); @@ -122,8 +124,10 @@ public CertifiedProductDTO update(CertifiedProductDTO dto) throws EntityRetrieva entity.setCertificationBodyId(dto.getCertificationBodyId()); entity.setCertificationEditionId(dto.getCertificationEditionId()); entity.setProductVersionId(dto.getProductVersionId()); - entity.setRwtPlansUrl(dto.getRwtPlansUrl()); - entity.setRwtPlansCheckDate(dto.getRwtPlansCheckDate()); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + entity.setRwtPlansUrl(dto.getRwtPlansUrl()); + entity.setRwtPlansCheckDate(dto.getRwtPlansCheckDate()); + } entity.setRwtResultsUrl(dto.getRwtResultsUrl()); entity.setRwtResultsCheckDate(dto.getRwtResultsCheckDate()); entity.setSvapNoticeUrl(dto.getSvapNoticeUrl()); From 13ebcd4f55370cbc528725cb08f1ed2de8114bcd Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 09:43:02 -0400 Subject: [PATCH 15/27] feat: Exclude RWT Plans fields from "basic" API response [#OCD-5385] --- .../chpl/domain/CertifiedProductSearchBasicDetails.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchBasicDetails.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchBasicDetails.java index d3d6c1b842..8671838c18 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchBasicDetails.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/domain/CertifiedProductSearchBasicDetails.java @@ -12,11 +12,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; -import tools.jackson.databind.annotation.JsonDeserialize; -import tools.jackson.databind.annotation.JsonSerialize; import gov.healthit.chpl.domain.compliance.DirectReview; import gov.healthit.chpl.domain.surveillance.Surveillance; +import gov.healthit.chpl.realworldtesting.DeprecatedRwtPlansData; import gov.healthit.chpl.sed.CertifiedProductSed; import gov.healthit.chpl.targeteduser.CertifiedProductTargetedUser; import gov.healthit.chpl.util.DateUtil; @@ -26,6 +25,8 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; @Data @Builder @@ -192,9 +193,11 @@ public class CertifiedProductSearchBasicDetails implements Serializable { @Schema(description = "Indicates whether the direct reviews were available when the call was made") private boolean directReviewsAvailable; + @DeprecatedRwtPlansData @Schema(description = "URL where the listing's Real World Testing Plan is located") private String rwtPlansUrl; + @DeprecatedRwtPlansData @Schema(description = "Date the listing's Real World Testing Plan was submitted") @JsonDeserialize(using = LocalDateDeserializer.class) @JsonSerialize(using = LocalDateSerializer.class) From 69d0412eed4a5be5b3b3e6b070814a939578fae6 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 09:52:23 -0400 Subject: [PATCH 16/27] feat-flag: Remove RWT Plans from RWT email body [#OCD-5385] --- .../job/RealWorldTestingReportEmailJob.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java index a82c19067e..681b392cd7 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingReportEmailJob.java @@ -14,6 +14,7 @@ import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; +import org.ff4j.FF4j; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @@ -21,6 +22,7 @@ import org.springframework.core.env.Environment; import org.springframework.web.context.support.SpringBeanAutowiringSupport; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.dao.CertificationBodyDAO; import gov.healthit.chpl.email.ChplEmailFactory; import gov.healthit.chpl.email.ChplHtmlEmailBuilder; @@ -56,6 +58,9 @@ public class RealWorldTestingReportEmailJob implements Job { @Autowired private ChplEmailFactory chplEmailFactory; + @Autowired + private FF4j ff4j; + private List acbIds = new ArrayList(); @Override @@ -172,10 +177,12 @@ private String getEmailSummaryParagraph(List rows) { .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalListings(), 0)).append(" Total listing for this period
    • ") .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithdrawn(), 0)).append(" Withdrawn, no longer eligible
    • ") .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalActive(), 0)).append(" Active
    • ") - .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalEligibleViaIcs(), 0)).append(" Eligible via ICS
    • ") - .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithPlansUrl(), 0)).append(" Have RWT plans URL
    • ") - .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithPlansUrlValidated(), 0)).append(" RWT plans validated
    • ") - .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithResultsUrl(), 0)).append(" Have RWT results URL
    • ") + .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalEligibleViaIcs(), 0)).append(" Eligible via ICS
    • "); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + paragraph.append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithPlansUrl(), 0)).append(" Have RWT plans URL
    • ") + .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithPlansUrlValidated(), 0)).append(" RWT plans validated
    • "); + } + paragraph.append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithResultsUrl(), 0)).append(" Have RWT results URL
    • ") .append("
    • ").append(NullSafeEvaluator.eval(() -> summary.getTotalWithResultsUrlValidated(), 0)).append(" RWT results validated
    • ") .append("
    ").append("
    "); }); From e626b52d426585dc65a8502b4f5ac4ab6a7b3524 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 10:10:46 -0400 Subject: [PATCH 17/27] feat-flag: Do not allow RWT Plans change request type to be created [#OCD-5385] --- chpl/chpl-resources/src/main/resources/errors.properties | 1 + .../chpl/changerequest/manager/ChangeRequestManager.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/chpl/chpl-resources/src/main/resources/errors.properties b/chpl/chpl-resources/src/main/resources/errors.properties index 04016c54b7..c2def18062 100644 --- a/chpl/chpl-resources/src/main/resources/errors.properties +++ b/chpl/chpl-resources/src/main/resources/errors.properties @@ -990,6 +990,7 @@ changeRequest.listingUrl.serviceBaseUrlList.sameUrl=No change to the Service Bas changeRequest.listingUrl.serviceBaseUrlList.missing=The Service Base URL List may not be blank for %s. changeRequest.missingDetails=Details about what is being changed are required. changeRequest.listingUrl.rwtUrl.featureDisabled=Creating a Change Request to update a Real World Testing URL is not enabled. +changeRequest.listingUrl.rwtPlans.notAllowed=Creating a change request for an RWT Plans URL is not allowed. changeRequest.listingUrl.rwtUrl.missing=The Real World Testing URL may not be blank for %s. changeRequest.listingUrl.checkDateRequired=The Check Date is required when accepting a Real World Testing URL change request. changeRequest.listingUrl.checkDateFuture=The Check Date may not be in the future when accepting a Real World Testing URL change request. diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java index ed5074a5f9..df6bba14f2 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java @@ -190,6 +190,10 @@ public ChangeRequest createChangeRequest(ChangeRequest changeRequest) if (!ff4j.check(FeatureList.SERVICE_BASE_URL_LIST_CHANGE_REQUEST) && changeRequest.getChangeRequestType().isSbul()) { throw new InvalidArgumentsException(msgUtil.getMessage("changeRequest.listingUrl.serviceBaseUrlList.featureDisabled")); + } else if (ff4j.check(FeatureList.RWT_CHANGE_REQUEST) + && ff4j.check(FeatureList.HTI_5_ERD) + && changeRequest.getChangeRequestType().isRwtPlans()) { + throw new InvalidArgumentsException(msgUtil.getMessage("changeRequest.listingUrl.rwtPlans.notAllowed")); } else if (!ff4j.check(FeatureList.RWT_CHANGE_REQUEST) && isRwtChangeRequestType(changeRequest.getChangeRequestType())) { throw new InvalidArgumentsException(msgUtil.getMessage("changeRequest.listingUrl.rwtUrl.featureDisabled")); From 301a4b840a7aa49e7cfe38903169132b90c6141b Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 10:13:37 -0400 Subject: [PATCH 18/27] feat-flag: Exclude RWT Plans in change request types response [#OCD-5385] --- .../chpl/changerequest/manager/ChangeRequestManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java index df6bba14f2..52c91604db 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/changerequest/manager/ChangeRequestManager.java @@ -165,7 +165,8 @@ public List getChangeRequestTypes() { || (type.getName().equals(ChangeRequestType.SBUL_TYPE) && ff4j.check(FeatureList.SERVICE_BASE_URL_LIST_CHANGE_REQUEST)) || (type.getName().equals(ChangeRequestType.RWT_PLANS_TYPE) - && ff4j.check(FeatureList.RWT_CHANGE_REQUEST)) + && ff4j.check(FeatureList.RWT_CHANGE_REQUEST) + && !ff4j.check(FeatureList.HTI_5_ERD)) || (type.getName().equals(ChangeRequestType.RWT_RESULTS_TYPE) && ff4j.check(FeatureList.RWT_CHANGE_REQUEST)) ) From 03b6995c3b70e8c7cdfc8b35e8755018455e2508 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 11:48:53 -0400 Subject: [PATCH 19/27] feat-flag: Do not include RWT Plans in questionable url data gathering [#OCD-5385] --- .../main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java index b07c355e85..ce3ee3a85a 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/dao/CertifiedProductDAO.java @@ -627,7 +627,9 @@ public List getSummaryByUrl(final String url, final queryStr += " AND cp.sedReportFileLocation = :url "; break; case REAL_WORLD_TESTING_PLANS: - queryStr += " AND cp.rwtPlansUrl = :url "; + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + queryStr += " AND cp.rwtPlansUrl = :url "; + } break; case REAL_WORLD_TESTING_RESULTS: queryStr += " AND cp.rwtResultsUrl = :url "; From aaf7d9e222ce5cb01dd376729eccc8381d24de96 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 11:50:34 -0400 Subject: [PATCH 20/27] feat-flag: Stop recording questionable activity for RWT Plans changes [#OCD-5385] --- .../ListingQuestionableActivityService.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/questionableactivity/service/ListingQuestionableActivityService.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/questionableactivity/service/ListingQuestionableActivityService.java index b9bf8e9d81..ea1a684736 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/questionableactivity/service/ListingQuestionableActivityService.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/questionableactivity/service/ListingQuestionableActivityService.java @@ -4,10 +4,12 @@ import java.util.List; import java.util.Optional; +import org.ff4j.FF4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.dao.CertifiedProductDAO; import gov.healthit.chpl.domain.CertifiedProductSearchDetails; import gov.healthit.chpl.dto.ActivityDTO; @@ -49,16 +51,19 @@ public class ListingQuestionableActivityService { private List listingActivities; private CertifiedProductDAO certifiedProductDAO; private Environment env; + private FF4j ff4j; private List triggerTypes; @Autowired ListingQuestionableActivityService(QuestionableActivityDAO questionableActivityDao, List listingActivities, - CertifiedProductDAO certifiedProductDAO, Environment env) { + CertifiedProductDAO certifiedProductDAO, Environment env, + FF4j ff4j) { this.questionableActivityDao = questionableActivityDao; this.listingActivities = listingActivities; this.certifiedProductDAO = certifiedProductDAO; this.env = env; + this.ff4j = ff4j; triggerTypes = questionableActivityDao.getAllTriggers(); } @@ -73,14 +78,20 @@ public void processQuestionableActivity(CertifiedProductSearchDetails origListin processListingActivity(UpdatedCertificationStatusHistoryActivity.class.getName(), origListing, newListing, activity, activityReason); processListingActivity(FutureCertificationStatusAddedActivity.class.getName(), origListing, newListing, activity, activityReason); processListingActivity(UpdatedTestingLabActivity.class.getName(), origListing, newListing, activity, activityReason); - processListingActivity(DeletedRwtPlanActivity.class.getName(), origListing, newListing, activity, activityReason); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + processListingActivity(DeletedRwtPlanActivity.class.getName(), origListing, newListing, activity, activityReason); + } processListingActivity(DeletedRwtResultsActivity.class.getName(), origListing, newListing, activity, activityReason); - processListingActivity(AddedRwtPlanNonEligibleListingActivity.class.getName(), origListing, newListing, activity, activityReason); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + processListingActivity(AddedRwtPlanNonEligibleListingActivity.class.getName(), origListing, newListing, activity, activityReason); + } processListingActivity(AddedRwtResultsNonEligibleListingActivity.class.getName(), origListing, newListing, activity, activityReason); processListingActivity(UpdatedPromotingInteroperabilityActivity.class.getName(), origListing, newListing, activity, activityReason); processListingActivity(DeletedMeasuresActivity.class.getName(), origListing, newListing, activity, activityReason); processListingActivity(RwtResultsUpdatedOutsideNormalPeriod.class.getName(), origListing, newListing, activity, activityReason); - processListingActivity(RwtPlansUpdatedOutsideNormalPeriod.class.getName(), origListing, newListing, activity, activityReason); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + processListingActivity(RwtPlansUpdatedOutsideNormalPeriod.class.getName(), origListing, newListing, activity, activityReason); + } processListingActivity(AttestRemovedCriteriaActivity.class.getName(), origListing, newListing, activity, activityReason); processListingActivity(NonActiveCertificateEdited.class.getName(), origListing, newListing, activity, activityReason); From 62c013058d64153acefc0e21810275578c63fb68 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 11:50:59 -0400 Subject: [PATCH 21/27] feat-flag: Do not include RWT Plans cr attachment in cr report email [#OCD-5385] --- .../job/changerequest/ChangeRequestReportEmailJob.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/changerequest/ChangeRequestReportEmailJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/changerequest/ChangeRequestReportEmailJob.java index c8b7fc49c9..d3b95f531e 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/changerequest/ChangeRequestReportEmailJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/changerequest/ChangeRequestReportEmailJob.java @@ -231,7 +231,9 @@ private List getAttachments() { attachments.add(tempSbulFile); } if (ff4j.check(FeatureList.RWT_CHANGE_REQUEST)) { - attachments.add(tempRwtPlansFile); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + attachments.add(tempRwtPlansFile); + } attachments.add(tempRwtResultsFile); } return attachments; From 31315efcd2a9d948aa2e2c39a22af1ba92475492 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 12:07:49 -0400 Subject: [PATCH 22/27] feat-flag: Do not count Plans in RWT summary overnight job [#OCD-5385] --- .../job/RealWorldTestingSummaryReportCreatorJob.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java index 60be011a8f..6ff6cf0a8b 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingSummaryReportCreatorJob.java @@ -8,6 +8,7 @@ import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Pair; +import org.ff4j.FF4j; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; @@ -18,6 +19,7 @@ import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.context.support.SpringBeanAutowiringSupport; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.developer.search.ActiveListingSearchOptions; import gov.healthit.chpl.developer.search.DeveloperSearchRequest; import gov.healthit.chpl.developer.search.DeveloperSearchResult; @@ -54,6 +56,9 @@ public class RealWorldTestingSummaryReportCreatorJob extends QuartzJob { @Autowired private PlatformTransactionManager transactionManager; + @Autowired + private FF4j ff4j; + @Override public void execute(JobExecutionContext context) throws JobExecutionException { SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this); @@ -176,6 +181,9 @@ && isListingValidAsOfDate(row.getCertificationDate(), reportDate)) } private void processRwtPlanCountsByAcb(List reportRows) { + if (ff4j.check(FeatureList.HTI_5_ERD)) { + return; + } Integer rwtEligibilityYear = LocalDate.now().getYear() + 1; if (!isDateInPlansSubmissionWindow(LocalDate.now(), rwtEligibilityYear)) { From f925ed194ef61f16cf7ed0fbd22b350d15ffa673 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 13:48:39 -0400 Subject: [PATCH 23/27] feat-flag: Ignore RWT Plans in special RWT upload file [#OCD-5385] --- .../job/RealWorldTestingUploadJob.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingUploadJob.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingUploadJob.java index 2e9081a409..01f4c48f50 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingUploadJob.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/RealWorldTestingUploadJob.java @@ -14,12 +14,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.ff4j.FF4j; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.web.context.support.SpringBeanAutowiringSupport; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.auth.user.JWTAuthenticatedUser; import gov.healthit.chpl.certifiedproduct.CertifiedProductDetailsManager; import gov.healthit.chpl.domain.CertifiedProductSearchDetails; @@ -51,6 +53,9 @@ public class RealWorldTestingUploadJob extends QuartzJob { @Autowired private Environment env; + @Autowired + private FF4j ff4j; + @Autowired private ChplEmailFactory chplEmailFactory; @@ -68,19 +73,23 @@ public void execute(JobExecutionContext context) throws JobExecutionException { // Run some basic validation on upload records that do not have any errors // yet... - rwts.stream().filter(rwt -> rwt.getValidationErrors().size() == 0) - .forEach(rwt -> rwt.getValidationErrors().addAll(validateRwtUpload(rwt))); + rwts.stream() + .filter(rwt -> rwt.getType().equals(RealWorldTestingType.RESULTS) || (!ff4j.check(FeatureList.HTI_5_ERD) && rwt.getType().equals(RealWorldTestingType.PLANS))) + .filter(rwt -> rwt.getValidationErrors().size() == 0) + .forEach(rwt -> rwt.getValidationErrors().addAll(validateRwtUpload(rwt))); // Determine if there multiple Plans or Results for the same listing. These will // not get // get processed, and will have an error added to the upload record. rwts = markMultipleChangesForSamePlanTypeAndListing(rwts); - // Process the plans - List rwtPlans = rwts.stream().filter( - rwt -> rwt.getValidationErrors().size() == 0 && rwt.getType().equals(RealWorldTestingType.PLANS)) - .collect(Collectors.toList()); - saveRealWorldTestingUploads(rwtPlans); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + // Process the plans + List rwtPlans = rwts.stream().filter( + rwt -> rwt.getValidationErrors().size() == 0 && rwt.getType().equals(RealWorldTestingType.PLANS)) + .collect(Collectors.toList()); + saveRealWorldTestingUploads(rwtPlans); + } // Process the results List rwtResults = rwts.stream().filter( @@ -218,13 +227,15 @@ private Integer getThreadCountForJob() throws NumberFormatException { private List markMultipleChangesForSamePlanTypeAndListing( List rwts) { - // Find any listings that have duplicate updates - multiple Plans - rwts.stream() + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + // Find any listings that have duplicate updates - multiple Plans + rwts.stream() .filter(rwt -> rwt.getValidationErrors().size() == 0 && rwt.getType().equals(RealWorldTestingType.PLANS)) .collect(Collectors.groupingBy(RealWorldTestingUpload::getChplProductNumber)).entrySet().stream() .filter(lst -> lst.getValue().size() > 1).forEach(lst -> lst.getValue().stream().forEach( rwt -> rwt.getValidationErrors().add("Multiple Plans found for this CHPL Product Number"))); + } rwts.stream() .filter(rwt -> rwt.getValidationErrors().size() == 0 From 6a67f7e35ead39ff95675ffd9a27cf541009c4fc Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 13:54:24 -0400 Subject: [PATCH 24/27] feat-flag: Ignore RWT Plans subscriptions and don't create new ones [#OCD-5385] --- .../chpl/subscription/SubscriptionObserver.java | 11 +++++++++-- .../chpl/subscription/dao/SubscriptionDao.java | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/SubscriptionObserver.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/SubscriptionObserver.java index 1fee8372bf..872664f389 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/SubscriptionObserver.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/SubscriptionObserver.java @@ -5,9 +5,11 @@ import java.util.Optional; import org.apache.commons.collections4.CollectionUtils; +import org.ff4j.FF4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.dto.ActivityDTO; import gov.healthit.chpl.subscription.dao.SubscriptionDao; import gov.healthit.chpl.subscription.dao.SubscriptionObservationDao; @@ -28,6 +30,7 @@ public class SubscriptionObserver { private SubscriptionDao subscriptionDao; private SubscriptionObservationDao observationDao; private SubscriptionLookupUtil lookupUtil; + private FF4j ff4j; private List processors; private List allSubjects; @@ -35,10 +38,12 @@ public class SubscriptionObserver { @Autowired public SubscriptionObserver(SubscriptionDao subscriptionDao, SubscriptionObservationDao observationDao, - SubscriptionLookupUtil lookupUtil) { + SubscriptionLookupUtil lookupUtil, + FF4j ff4j) { this.subscriptionDao = subscriptionDao; this.observationDao = observationDao; this.lookupUtil = lookupUtil; + this.ff4j = ff4j; this.allSubjects = subscriptionDao.getAllSubjects(); createSubscriptionSubjectProcessors(); @@ -73,7 +78,9 @@ private void createSubscriptionSubjectProcessors() { this.processors.add(new CertificationStatusChangedActivityProcessor(getSubject(lookupUtil.getCertificationStatusChangedSubjectId()))); this.processors.add(new CertificationCriteriaAddedActivityProcessor(getSubject(lookupUtil.getCertificationCriteriaAddedSubjectId()))); this.processors.add(new CertificationCriteriaRemovedActivityProcessor(getSubject(lookupUtil.getCertificationCriteriaRemovedSubjectId()))); - this.processors.add(new RwtPlansUrlChangedActivityProcessor(getSubject(lookupUtil.getRwtPlansUrlChangedSubjectId()))); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + this.processors.add(new RwtPlansUrlChangedActivityProcessor(getSubject(lookupUtil.getRwtPlansUrlChangedSubjectId()))); + } this.processors.add(new RwtResultsUrlChangedActivityProcessor(getSubject(lookupUtil.getRwtResultsUrlChangedSubjectId()))); this.processors.add(new ServiceBaseUrlListChangedActivityProcessor(getSubject(lookupUtil.getServiceBaseUrlListChangedSubjectId()))); } diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/dao/SubscriptionDao.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/dao/SubscriptionDao.java index 0eef9a79d5..bf462cf2f0 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/dao/SubscriptionDao.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/subscription/dao/SubscriptionDao.java @@ -5,8 +5,10 @@ import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; +import org.ff4j.FF4j; import org.springframework.stereotype.Repository; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.dao.impl.BaseDAOImpl; import gov.healthit.chpl.subscription.domain.Subscription; import gov.healthit.chpl.subscription.domain.SubscriptionConsolidationMethod; @@ -37,9 +39,12 @@ public class SubscriptionDao extends BaseDAOImpl { + "AND subscriber.deleted = false "; private SubscriptionLookupUtil lookupUtil; + private FF4j ff4j; - public SubscriptionDao(SubscriptionLookupUtil lookupUtil) { + public SubscriptionDao(SubscriptionLookupUtil lookupUtil, + FF4j ff4j) { this.lookupUtil = lookupUtil; + this.ff4j = ff4j; } public List getAllSubscriptionObjectTypes() { @@ -103,6 +108,8 @@ public void createSubscription(UUID subscriberId, Long subscribedObjectTypeId, L List subjectsForObjectType = getAllSubjectsForObjectType(subscribedObjectTypeId); subjectsForObjectType.stream() + .filter(subject -> !lookupUtil.getRwtPlansUrlChangedSubjectId().equals(subject.getId()) + || (!ff4j.check(FeatureList.HTI_5_ERD) && lookupUtil.getRwtPlansUrlChangedSubjectId().equals(subject.getId()))) .forEach(subject -> createSubscriptionIfNotExists( subscriberId, subscribedObjectId, subject.getId(), lookupUtil.getDailyConsolidationMethodId())); From 878c90c0b93fabe11b672f5d3335d0790cc18b2c Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 28 Jul 2026 16:41:43 -0400 Subject: [PATCH 25/27] feat-flag: Remove RWT Plans from listing search result data [#OCD-5385] --- .../gov/healthit/chpl/search/domain/ListingSearchResult.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/search/domain/ListingSearchResult.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/search/domain/ListingSearchResult.java index 49c2a274e5..897cf8711f 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/search/domain/ListingSearchResult.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/search/domain/ListingSearchResult.java @@ -15,6 +15,7 @@ import gov.healthit.chpl.domain.CertificationEdition; import gov.healthit.chpl.domain.IdNamePair; +import gov.healthit.chpl.realworldtesting.DeprecatedRwtPlansData; import gov.healthit.chpl.util.CertificationStatusUtil; import gov.healthit.chpl.util.LocalDateDeserializer; import gov.healthit.chpl.util.LocalDateSerializer; @@ -80,6 +81,7 @@ public class ListingSearchResult implements Serializable { private CertificationCriterionSearchResultWithStringField serviceBaseUrlList; private CertificationCriterionSearchResultWithStringField riskManagementSummaryInformation; private Set svaps; + @DeprecatedRwtPlansData private String rwtPlansUrl; private String rwtResultsUrl; private String svapNoticeUrl; From 088d2bfdf60c6ecd0652ddf52508eaba5579bf1b Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 4 Aug 2026 13:41:19 -0400 Subject: [PATCH 26/27] feat-flag: Remove RWT Plans from RWT Report attachment [#OCD-5385] --- .../RealWorldTestingCsvPresenter.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java index 2e0c6884d5..409dbc3ce4 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/presenter/RealWorldTestingCsvPresenter.java @@ -36,6 +36,7 @@ public class RealWorldTestingCsvPresenter { private CertificationCriterionService criteriaService; private List rwtResultsRequiredCriteria; + private FF4j ff4j; @Autowired public RealWorldTestingCsvPresenter(RealWorldTestingCriteriaService rwtCriteriaService, @@ -43,6 +44,7 @@ public RealWorldTestingCsvPresenter(RealWorldTestingCriteriaService rwtCriteriaS CertificationCriterionComparator criteriaComparator, FF4j ff4j) { this.criteriaService = criteriaService; + this.ff4j = ff4j; Integer currentYear = Calendar.getInstance().get(Calendar.YEAR); if (ff4j.check(FeatureList.HTI_5_ERD)) { rwtResultsRequiredCriteria = rwtCriteriaService.getEligibleCriteria(currentYear); @@ -81,11 +83,15 @@ protected List generateHeaderValues() { results.add("Developer Users"); results.add("Initial RWT Year"); results.add("ICS"); - results.add("RWT Plans URL"); - results.add("RWT Plans Submission Confirmed"); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + results.add("RWT Plans URL"); + results.add("RWT Plans Submission Confirmed"); + } results.add("RWT Results URL"); results.add("RWT Results Submission Confirmed"); - results.add("RWT Plans Message"); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + results.add("RWT Plans Message"); + } results.add("RWT Results Message"); rwtResultsRequiredCriteria.stream() .forEach(criterion -> { @@ -113,11 +119,15 @@ protected List generateRowValue(RealWorldTestingReport rwtReport) { : ""); results.add(rwtReport.getRwtEligibilityYear() == null ? null : rwtReport.getRwtEligibilityYear().toString()); results.add(BooleanUtils.isTrue(rwtReport.getIcs()) ? "Yes" : ""); - results.add(rwtReport.getRwtPlansUrl()); - results.add(rwtReport.getRwtPlansCheckDate() == null ? null : rwtReport.getRwtPlansCheckDate().toString()); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + results.add(rwtReport.getRwtPlansUrl()); + results.add(rwtReport.getRwtPlansCheckDate() == null ? null : rwtReport.getRwtPlansCheckDate().toString()); + } results.add(rwtReport.getRwtResultsUrl()); results.add(rwtReport.getRwtResultsCheckDate() == null ? null : rwtReport.getRwtResultsCheckDate().toString()); - results.add(rwtReport.getRwtPlansMessage()); + if (!ff4j.check(FeatureList.HTI_5_ERD)) { + results.add(rwtReport.getRwtPlansMessage()); + } results.add(rwtReport.getRwtResultsMessage()); rwtResultsRequiredCriteria.stream() .forEach(criterion -> results.add(determineCriteriaReportValue(rwtReport.getCriterionAndSvapData(), criterion))); From 7a0737aeeafab43f07c420fc26f5e91badc6dff3 Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Wed, 5 Aug 2026 09:41:46 -0400 Subject: [PATCH 27/27] feat-flag: Ignore Plans updates in RWT Upload file [#OCD-5385] --- .../manager/RealWorldTestingManager.java | 14 +++++++++++++- .../manager/RealWorldTestingManagerTest.java | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManager.java b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManager.java index 780a34a517..01a168142f 100644 --- a/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManager.java +++ b/chpl/chpl-service/src/main/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManager.java @@ -14,6 +14,7 @@ import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; import org.apache.commons.lang3.StringUtils; +import org.ff4j.FF4j; import org.quartz.JobDataMap; import org.quartz.SchedulerException; import org.springframework.beans.factory.annotation.Autowired; @@ -22,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import gov.healthit.chpl.FeatureList; import gov.healthit.chpl.auth.user.JWTAuthenticatedUser; import gov.healthit.chpl.domain.schedule.ChplJob; import gov.healthit.chpl.domain.schedule.ChplOneTimeTrigger; @@ -51,15 +53,18 @@ public class RealWorldTestingManager { private RealWorldTestingByDeveloperDao rwtByDeveloperDao; private SchedulerManager schedulerManager; private ErrorMessageUtil errorMessageUtil; + private FF4j ff4j; @Autowired public RealWorldTestingManager(RealWorldTestingByDeveloperDao rwtByDeveloperDao, SchedulerManager schedulerManager, - ErrorMessageUtil errorMessageUtil) { + ErrorMessageUtil errorMessageUtil, + FF4j ff4j) { this.rwtByDeveloperDao = rwtByDeveloperDao; this.schedulerManager = schedulerManager; this.errorMessageUtil = errorMessageUtil; + this.ff4j = ff4j; } public List getPlansUrls(Long developerId) { @@ -160,6 +165,13 @@ private List parseCsvFile(MultipartFile file) throws Val .map(rec -> createRwtUploadFromCsvRecord(rec)) .collect(Collectors.toList()); + //only keep RESULTS upload records after HTI-5 + if (ff4j.check(FeatureList.HTI_5_ERD)) { + rwts = rwts.stream() + .filter(rwt -> rwt.getType().equals(RealWorldTestingType.RESULTS)) + .collect(Collectors.toList()); + } + return rwts; } catch (final IOException ioEx) { throw new ValidationException("Could not get input stream for uploaded file " + file.getName()); diff --git a/chpl/chpl-service/src/test/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManagerTest.java b/chpl/chpl-service/src/test/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManagerTest.java index 2f787fe23f..aa45bf9b77 100644 --- a/chpl/chpl-service/src/test/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManagerTest.java +++ b/chpl/chpl-service/src/test/java/gov/healthit/chpl/realworldtesting/manager/RealWorldTestingManagerTest.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.UUID; +import org.ff4j.FF4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; @@ -48,7 +49,9 @@ public void setup() throws SchedulerException, ValidationException, UserRetrieva .thenReturn("This is an error message."); realWorldTestingManager = new RealWorldTestingManager(Mockito.mock(RealWorldTestingByDeveloperDao.class), - schedulerManager, errorMessageUtil); + schedulerManager, + errorMessageUtil, + Mockito.mock(FF4j.class)); } @Test