From 4aedf42aa77fa3889352b08e255f4ae75643e20c Mon Sep 17 00:00:00 2001 From: Katy Ekey Date: Tue, 21 Jul 2026 11:48:47 -0400 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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()))