Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f179bc1
feat!: Add 2026 CMS ID Validator
kekey1 Apr 23, 2026
4b1904c
refactor: Convert string hashmaps to objects for cmsid validation
kekey1 Apr 23, 2026
398c81e
refactor: Clean up some unneeded comments
kekey1 Apr 23, 2026
690c03c
Merge branch 'staging' into OCD-4928
kekey1 Apr 28, 2026
185414b
feat: Get listing details when calculating CMS ID
kekey1 Apr 29, 2026
462ed2f
Merge branch 'staging' into OCD-4928
kekey1 Apr 29, 2026
b8824ea
wip: getting closer with up-to-date cmsid logic
kekey1 Apr 29, 2026
2a53cdf
feat: Complete filling out up-to-date required criteria
kekey1 Apr 30, 2026
44a26a3
Merge branch 'staging' into OCD-4928
kekey1 May 1, 2026
afcbb32
refactor: Use correct logic to check for code sets present
kekey1 May 1, 2026
8c6ad0a
feat: Attempt to make getting listing details for cms id more efficient
kekey1 May 1, 2026
625675c
wip: How to account for required criteria met but not up-to-date in %s
kekey1 May 1, 2026
9e926be
fix: Calculate %s correctly; doesn't count if criteria not up-to-date
kekey1 May 5, 2026
fe57e51
refactor: Add some logging
kekey1 May 5, 2026
ebb13db
fix: Limit thread pool size to 2 for cert id search
kekey1 May 5, 2026
0c18f62
Merge branch 'staging' into OCD-4928
kekey1 May 12, 2026
f17fea3
Merge branch 'staging' into OCD-4928
kekey1 May 13, 2026
d6ab365
refactor: Move upToDate calculation to listing details cert results
kekey1 May 14, 2026
44be9f6
Merge branch 'staging' into OCD-4928
kekey1 May 19, 2026
4feee56
feat!: Indicate CMS ID resources unavailable depending on flag state
kekey1 May 26, 2026
0ceb8d8
Merge branch 'staging' into OCD-4928
kekey1 May 27, 2026
95f5882
Merge branch 'OCD-4928' into OCD-5300
kekey1 May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import gov.healthit.chpl.user.cognito.authentication.CognitoAuthenticationChallengeException;
import gov.healthit.chpl.user.cognito.authentication.CognitoPasswordResetRequiredException;
import gov.healthit.chpl.util.ErrorMessageUtil;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.log4j.Log4j2;

Expand All @@ -64,6 +65,12 @@ public ResponseEntity<ErrorResponse> exception(NotImplementedException e) {
return new ResponseEntity<ErrorResponse>(new ErrorResponse(e.getMessage()), HttpStatus.NOT_IMPLEMENTED);
}

@ExceptionHandler(UnavailableException.class)
public ResponseEntity<ErrorResponse> exception(UnavailableException e) {
//TODO - what is the right response code? Could also do like Unauthorized or "Not Allowed"
return new ResponseEntity<ErrorResponse>(new ErrorResponse(e.getMessage()), HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS);
}

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> exception(AccessDeniedException e) {
return new ResponseEntity<ErrorResponse>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package gov.healthit.chpl.web.controller;

import static gov.healthit.chpl.util.LambdaExceptionUtil.rethrowFunction;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.ff4j.FF4j;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
Expand All @@ -16,6 +19,7 @@
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import gov.healthit.chpl.FeatureList;
import gov.healthit.chpl.certificationId.CertificationIdCreateBody;
import gov.healthit.chpl.certificationId.CertificationIdLookupResults;
import gov.healthit.chpl.certificationId.CertificationIdManager;
Expand All @@ -35,22 +39,28 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.UnavailableException;
import lombok.extern.log4j.Log4j2;

@Log4j2
@Tag(name = "certification-ids", description = "All certification ID operations.")
@RestController
public class CertificationIdController {

private CertificationIdSearchService certIdSearchService;
private CertificationIdManager certificationIdManager;
private CertificationIdYearCalculator certIdYearCalculator;
private FF4j ff4j;

@Autowired
public CertificationIdController(CertificationIdSearchService certIdSearchService,
CertificationIdManager certificationIdManager,
CertificationIdYearCalculator certIdYearCalculator) {
CertificationIdYearCalculator certIdYearCalculator,
FF4j ff4j) {
this.certIdSearchService = certIdSearchService;
this.certificationIdManager = certificationIdManager;
this.certIdYearCalculator = certIdYearCalculator;
this.ff4j = ff4j;
}

@Operation(summary = "Generate the CMS EHR Certification ID Report and email the results to the logged-in user.",
Expand All @@ -65,7 +75,11 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
@DeprecatedApi(friendlyUrl = "/certification_ids/report-request", httpMethod = "POST",
message = "This endpoint is deprecated and will be removed. Please use certification-ids/report-request",
removalDate = "2026-10-01")
public @ResponseBody ChplOneTimeTrigger triggerCmsIdReportDeprecated() throws SchedulerException, ValidationException {
public @ResponseBody ChplOneTimeTrigger triggerCmsIdReportDeprecated()
throws SchedulerException, ValidationException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}
ChplOneTimeTrigger jobTrigger = certificationIdManager.triggerCmsIdReport();
return jobTrigger;
}
Expand All @@ -78,7 +92,10 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
})
@RequestMapping(value = "/certification-ids/report-request",
method = RequestMethod.POST, produces = "application/json; charset=utf-8")
public @ResponseBody ChplOneTimeTrigger triggerCmsIdReport() throws SchedulerException, ValidationException {
public @ResponseBody ChplOneTimeTrigger triggerCmsIdReport() throws SchedulerException, ValidationException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}
ChplOneTimeTrigger jobTrigger = certificationIdManager.triggerCmsIdReport();
return jobTrigger;
}
Expand All @@ -98,8 +115,12 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
message = "This endpoint is deprecated and will be removed. Please use certification-ids/search",
removalDate = "2026-10-01")
public @ResponseBody CertificationIdResults searchCertificationIdDeprecated(
@RequestParam(required = false) List<Long> ids) throws InvalidArgumentsException,
CertificationIdException {
@RequestParam(required = false) List<Long> ids)
throws InvalidArgumentsException, EntityRetrievalException, CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}

return certIdSearchService.findCertificationByListingIds(ids, null, false);
}

Expand All @@ -114,17 +135,15 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
MediaType.APPLICATION_JSON_VALUE
})
public @ResponseBody List<CertificationIdResults> searchCertificationId(
@RequestParam(required = true) List<Long> listingIds) throws InvalidArgumentsException,
CertificationIdException {
@RequestParam(required = true) List<Long> listingIds) throws InvalidArgumentsException, EntityRetrievalException,
CertificationIdException, UnavailableException, Exception {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}

List<String> certificationYears = certIdYearCalculator.getValidCertIdYearsToday();
return certificationYears.stream()
.map(certYear -> {
try {
return certIdSearchService.findCertificationByListingIds(listingIds, certYear, false);
} catch (InvalidArgumentsException | CertificationIdException ex) {
throw new RuntimeException(ex);
}
})
.map(rethrowFunction(certYear -> certIdSearchService.findCertificationByListingIds(listingIds, certYear, false)))
.collect(Collectors.toList());
}

Expand All @@ -143,9 +162,11 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
@DeprecatedApi(friendlyUrl = "/certification_ids", httpMethod = "POST",
message = "This endpoint is deprecated and will be removed. Please POST to /certification-ids",
removalDate = "2026-10-01")
public @ResponseBody CertificationIdResults createCertificationIdDeprecated(
@RequestParam(required = true) List<Long> ids) throws InvalidArgumentsException,
CertificationIdException {
public @ResponseBody CertificationIdResults createCertificationIdDeprecated(@RequestParam(required = true) List<Long> ids)
throws InvalidArgumentsException, EntityRetrievalException, CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}
return certIdSearchService.findCertificationByListingIds(ids, null, true);
}

Expand All @@ -160,9 +181,12 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
@RequestMapping(value = "/certification-ids", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = {
MediaType.APPLICATION_JSON_VALUE
})
public @ResponseBody CertificationIdResults createCertificationId(
@RequestBody CertificationIdCreateBody createBody) throws InvalidArgumentsException,
CertificationIdException {
public @ResponseBody CertificationIdResults createCertificationId(@RequestBody CertificationIdCreateBody createBody)
throws InvalidArgumentsException, EntityRetrievalException, CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}

return certIdSearchService.findCertificationByListingIds(createBody.getListingIds(), createBody.getYear(), true);
}

Expand All @@ -186,7 +210,11 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
@RequestParam(required = false, defaultValue = "false") Boolean includeCriteria,
@RequestParam(required = false, defaultValue = "false") Boolean includeCqms)
throws InvalidArgumentsException,
EntityRetrievalException, CertificationIdException {
EntityRetrievalException, CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}

return certIdSearchService.findCertificationIdByCertificationId(certificationId, includeCriteria, includeCqms);
}

Expand All @@ -206,7 +234,11 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
@RequestParam(required = false, defaultValue = "false") Boolean includeCriteria,
@RequestParam(required = false, defaultValue = "false") Boolean includeCqms)
throws InvalidArgumentsException,
EntityRetrievalException, CertificationIdException {
EntityRetrievalException, CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}

return certIdSearchService.findCertificationIdByCertificationId(certificationId, includeCriteria, includeCqms);
}

Expand All @@ -225,7 +257,10 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
removalDate = "2026-10-01")
public @ResponseBody CertificationIdVerifyResults verifyCertificationIdsDeprecated(
@RequestBody final CertificationIdVerificationBodyDeprecated body) throws InvalidArgumentsException,
CertificationIdException {
CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}
return this.verify(body.getIds());
}

Expand All @@ -240,7 +275,11 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
})
public @ResponseBody CertificationIdVerifyResults verifyCertificationIds(
@RequestBody CertificationIdVerificationBody body) throws InvalidArgumentsException,
CertificationIdException {
CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}

return this.verify(body.getCertificationIds());
}

Expand All @@ -258,7 +297,10 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
removalDate = "2026-10-01")
public @ResponseBody CertificationIdVerifyResults verifyCertificationIdsDeprecated(
@RequestParam("ids") final List<String> certificationIds) throws InvalidArgumentsException,
CertificationIdException {
CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}
return this.verify(certificationIds);
}

Expand All @@ -272,7 +314,10 @@ public CertificationIdController(CertificationIdSearchService certIdSearchServic
})
public @ResponseBody CertificationIdVerifyResults verifyCertificationIds(
@RequestParam("certificationIds") List<String> certificationIds) throws InvalidArgumentsException,
CertificationIdException {
CertificationIdException, UnavailableException {
if (ff4j.check(FeatureList.CMS_DISABLED)) {
throw new UnavailableException("This endpoint is not currently available.");
}
return this.verify(certificationIds);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public final class FeatureList {
private FeatureList() {
}

public static final String CMS_DISABLED = "cms-disabled";
public static final String DEMOGRAPHIC_CHANGE_REQUEST = "demographic-change-request";
public static final String INSIGHTS_DISPLAY = "insights-display";
public static final String SERVICE_BASE_URL_LIST_CHANGE_REQUEST = "sbul-change-request";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import gov.healthit.chpl.certificationCriteria.CertificationCriterion;
import gov.healthit.chpl.domain.CertificationResult;
import gov.healthit.chpl.domain.CertifiedProductSearchDetails;
import gov.healthit.chpl.exception.EntityRetrievalException;
import gov.healthit.chpl.standard.BaselineStandardService;
import gov.healthit.chpl.standard.Standard;
import gov.healthit.chpl.standard.StandardDAO;
Expand Down Expand Up @@ -104,14 +103,9 @@ private Boolean isStandardInList(Standard standardToCheck, List<Standard> standa
}

private boolean doesCriterionHaveAnyStandards(CertificationCriterion criterion, Logger logger) {
try {
return standardDao.getAllStandardCriteriaMap().stream()
.filter(map -> map.getCriterion().getId().equals(criterion.getId()))
.findAny()
.isPresent();
} catch (EntityRetrievalException e) {
logger.error("Could not retrieve Standards for Criterion.", e);
return false;
}
return standardDao.getAllStandardCriteriaMap().stream()
.filter(map -> map.getCriterion().getId().equals(criterion.getId()))
.findAny()
.isPresent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import gov.healthit.chpl.certificationCriteria.CertificationCriterion;
import gov.healthit.chpl.domain.CertificationResult;
import gov.healthit.chpl.domain.CertifiedProductSearchDetails;
import gov.healthit.chpl.exception.EntityRetrievalException;
import gov.healthit.chpl.standard.Standard;
import gov.healthit.chpl.standard.StandardDAO;
import gov.healthit.chpl.standard.StandardGroupService;
Expand Down Expand Up @@ -189,14 +188,9 @@ private Boolean isStandardInList(Standard standardToCheck, List<Standard> standa
}

private List<Standard> getAllStandardsForCriterion(CertificationCriterion criterion, Logger logger) {
try {
return standardDao.getAllStandardCriteriaMap().stream()
.filter(map -> map.getCriterion().getId().equals(criterion.getId()))
.map(map -> map.getStandard())
.toList();
} catch (EntityRetrievalException e) {
logger.error("Could not retrieve Standards for Criterion.", e);
return List.of();
}
return standardDao.getAllStandardCriteriaMap().stream()
.filter(map -> map.getCriterion().getId().equals(criterion.getId()))
.map(map -> map.getStandard())
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import gov.healthit.chpl.certificationCriteria.CertificationCriterion;
import gov.healthit.chpl.certificationCriteria.CertificationCriterionEntity;
import gov.healthit.chpl.dao.impl.BaseDAOImpl;
import gov.healthit.chpl.dto.CertifiedProductDetailsDTO;
import gov.healthit.chpl.exception.EntityCreationException;
import gov.healthit.chpl.exception.EntityRetrievalException;
import jakarta.persistence.Query;
Expand Down Expand Up @@ -118,9 +117,9 @@ public List<CertificationIdAndCertifiedProductDTO> getAllCertificationIdsWithPro
return results;
}

public CertificationIdDTO getByListings(List<CertifiedProductDetailsDTO> listings, String year)
public CertificationIdDTO getByListings(List<Long> listingIds, String year)
throws EntityRetrievalException {
CertificationIdEntity entity = getEntityByListings(listings, year);
CertificationIdEntity entity = getEntityByListings(listingIds, year);
if (entity == null) {
return null;
}
Expand Down Expand Up @@ -241,11 +240,8 @@ private CertificationIdEntity getEntityByCertificationId(String certificationId)
return entity;
}

private CertificationIdEntity getEntityByListings(List<CertifiedProductDetailsDTO> listings, String year)
private CertificationIdEntity getEntityByListings(List<Long> listingIds, String year)
throws EntityRetrievalException {
List<Long> productIds = listings.stream()
.map(listing -> listing.getId())
.toList();
CertificationIdEntity entity = null;

// Lookup the EHR Certification ID record by:
Expand Down Expand Up @@ -276,8 +272,8 @@ private CertificationIdEntity getEntityByListings(List<CertifiedProductDetailsDT
+ "ORDER BY creationDate DESC ",
CertificationIdEntity.class);

query.setParameter("productIds", productIds);
query.setParameter("productCount", Long.valueOf(productIds.size()));
query.setParameter("productIds", listingIds);
query.setParameter("productCount", Long.valueOf(listingIds.size()));
query.setParameter("year", year);
List<CertificationIdEntity> results = query.getResultList();
if (!CollectionUtils.isEmpty(results) && results.size() > 1) {
Expand Down Expand Up @@ -338,7 +334,6 @@ private String generateCertificationIdString(String year) throws EntityCreationE

private String getYearPartOfNewCertIdString(String year) {
LocalDate now = LocalDate.now();
//TODO: Remove with //OCD-4928
if (now.isBefore(certIdYearCalculator.getInitialCmsIdTransitionToAnnualFormatDay())) {
return "00" + year.substring(year.length() - 2);
}
Expand Down
Loading
Loading