Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
61cde0b
Eob updates for previous and next links on patient searchs
marshallweekley-eng Jun 4, 2026
3ecfdc4
merge in master
marshallweekley-eng Jun 4, 2026
e4f04ca
update merged in change
marshallweekley-eng Jun 4, 2026
d273b39
correct imports
marshallweekley-eng Jun 4, 2026
10b17dc
correct java docs
marshallweekley-eng Jun 4, 2026
e8cd995
Add Offset logic to handle supporting _offset and startIndex params
marshallweekley-eng Jun 5, 2026
4b523ed
update unit test
marshallweekley-eng Jun 5, 2026
71c451c
update unit test
marshallweekley-eng Jun 5, 2026
89e16f0
update unit test
marshallweekley-eng Jun 5, 2026
ab1204e
Merge remote-tracking branch 'origin/master' into BFD-4707
marshallweekley-eng Jun 5, 2026
ca13d69
update sonarqube code smells
marshallweekley-eng Jun 5, 2026
565dfe2
correct formatting
marshallweekley-eng Jun 5, 2026
8d4af19
correct formatting
marshallweekley-eng Jun 5, 2026
b72eb08
correct formatting
marshallweekley-eng Jun 5, 2026
7d5a0ee
update url link generation method for sonarqube
marshallweekley-eng Jun 5, 2026
cdf0aa8
correct comment
marshallweekley-eng Jun 8, 2026
de71556
update to use spring url parse rather than string checking
marshallweekley-eng Jun 8, 2026
bb1fdc3
Correct tests and snapshot
marshallweekley-eng Jun 8, 2026
efd9ec7
Merge remote-tracking branch 'origin/master' into BFD-4707
marshallweekley-eng Jun 8, 2026
019db95
Updates for better flow to determine need for links
marshallweekley-eng Jun 9, 2026
04fb825
Merge remote-tracking branch 'origin/master' into BFD-4707
marshallweekley-eng Jun 9, 2026
1d52e4d
revert
marshallweekley-eng Jun 9, 2026
ac7a03d
revert formatting
marshallweekley-eng Jun 9, 2026
dd83ba5
revert formatting
marshallweekley-eng Jun 9, 2026
57503f6
Fix formatting of JPQL query in EobSearchIT.java
marshallweekley-eng Jun 9, 2026
116be7b
update limit method to be integer and not boolean based
marshallweekley-eng Jun 10, 2026
475bc17
fix formatting
marshallweekley-eng Jun 10, 2026
e7d5c7c
update to using the primitive integer type in resolveLimitWithExtra
marshallweekley-eng Jun 10, 2026
8edf926
fix formatting
marshallweekley-eng Jun 10, 2026
fa4b741
Change null check to use an optional
marshallweekley-eng Jun 11, 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 @@ -2,6 +2,7 @@

import static gov.cms.bfd.server.ng.util.MetricTimer.SAMHSA_FILTER_MODE;

import ca.uhn.fhir.rest.api.server.RequestDetails;
import gov.cms.bfd.server.ng.ClaimFilterOptions;
import gov.cms.bfd.server.ng.ClaimSecurityStatus;
import gov.cms.bfd.server.ng.SamhsaFilterMode;
Expand Down Expand Up @@ -68,9 +69,13 @@ public Optional<ExplanationOfBenefit> find(final Long fhirId, ClaimFilterOptions
*
* @param criteria filter criteria
* @param options claim filter options
* @param requestDetails Hapi FHIR request details
* @return bundle
*/
public Bundle searchByBene(ClaimSearchCriteria criteria, ClaimFilterOptions options) {
public Bundle searchByBene(
ClaimSearchCriteria criteria,
ClaimFilterOptions options,
Optional<RequestDetails> requestDetails) {

var beneSk = criteria.beneSk();
var beneXrefSk = beneficiaryRepository.getXrefSkFromBeneSk(beneSk);
Expand Down Expand Up @@ -98,11 +103,18 @@ public Bundle searchByBene(ClaimSearchCriteria criteria, ClaimFilterOptions opti
() ->
filterSamhsaClaims(claims, options.getSamhsaFilterMode())
.skip(repositoryCriteria.resolveOffset())
.limit(repositoryCriteria.resolveLimit())
.limit(repositoryCriteria.resolveLimitWithExtra(1))
.map(claim -> transformToFhir(claim, options)),
_ -> Tags.of(SAMHSA_FILTER_MODE, options.getSamhsaFilterMode().name()));

var bundle = FhirUtil.bundleOrDefault(filteredClaims, loadProgressRepository::lastUpdated);
var bundle =
FhirUtil.bundleOrDefault(
filteredClaims,
loadProgressRepository::lastUpdated,
requestDetails,
// we want the raw limit
Optional.of(repositoryCriteria.resolveLimit()),
Optional.of(repositoryCriteria.resolveOffset()));
recordResultSize(bundle, options.getSamhsaFilterMode());
return bundle;
}
Expand All @@ -119,12 +131,21 @@ private Stream<? extends ClaimBase> filterSamhsaClaims(
// Process claims in parallel
// Note: DO NOT call toList() until the very end as materializing the list multiple times could
// negatively impact perf.
var claimStream =
claims.parallelStream().sorted(Comparator.comparing(ClaimBase::getClaimUniqueId));
var claimStream = claims.parallelStream();
return switch (samhsaFilterMode) {
case INCLUDE -> claimStream;
case ONLY_SAMHSA -> claimStream.filter(this::claimHasSamhsa);
case EXCLUDE -> claimStream.filter(claim -> !claimHasSamhsa(claim));
case INCLUDE -> claimStream.sorted(Comparator.comparing(ClaimBase::getClaimUniqueId));
// it is faster to filter unordered so if we are filtering we should do it unordered first
// before the id ordering
case ONLY_SAMHSA ->
claimStream
.unordered()
.filter(this::claimHasSamhsa)
.sorted(Comparator.comparing(ClaimBase::getClaimUniqueId));
case EXCLUDE ->
claimStream
.unordered()
.filter(claim -> !claimHasSamhsa(claim))
.sorted(Comparator.comparing(ClaimBase::getClaimUniqueId));
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package gov.cms.bfd.server.ng.eob;

import ca.uhn.fhir.rest.annotation.Count;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
Expand Down Expand Up @@ -82,12 +77,13 @@ public ExplanationOfBenefit find(
* @param serviceDate service date
* @param lastUpdated last updated
* @param startIndex start index
* @param offset offset
* @param tag tags to filter by
* @param type claim type to filter by
* @param source claim source to filter by
* @param security security to filter SAMHSA by
* @param requestDetails request details object
* @param request HTTP request details
* @param requestDetails HAPI FHIR request details
* @return bundle
*/
@Search
Expand All @@ -98,12 +94,13 @@ public Bundle searchByPatient(
@OptionalParam(name = ExplanationOfBenefit.SP_RES_LAST_UPDATED)
final DateRangeParam lastUpdated,
@OptionalParam(name = START_INDEX) final NumberParam startIndex,
@Offset final Integer offset,
@OptionalParam(name = Constants.PARAM_TAG) final TokenAndListParam tag,
@OptionalParam(name = TYPE) final TokenAndListParam type,
@OptionalParam(name = Constants.PARAM_SOURCE) final TokenAndListParam source,
@OptionalParam(name = Constants.PARAM_SECURITY) final TokenAndListParam security,
final RequestDetails requestDetails,
final HttpServletRequest request) {
final HttpServletRequest request,
final RequestDetails requestDetails) {

var includeTaxNumbers =
FhirInputConverter.parseBooleanHeader(requestDetails, INCLUDE_TAX_NUMBERS_HEADER);
Expand All @@ -123,12 +120,14 @@ public Bundle searchByPatient(
FhirInputConverter.toDateTimeRange(serviceDate),
FhirInputConverter.toDateTimeRange(lastUpdated),
Optional.ofNullable(count),
FhirInputConverter.toIntOptional(startIndex),
// we will support both offset and startIndex for now, but they can't be used together.
// If both are provided, offset will take precedence
offset != null ? Optional.of(offset) : FhirInputConverter.toIntOptional(startIndex),
Comment thread
marshallweekley-eng marked this conversation as resolved.
Outdated
tagCriteria,
claimTypeCodes,
FhirInputConverter.parseSourceParameter(source));

return eobHandler.searchByBene(criteria, options);
return eobHandler.searchByBene(criteria, options, Optional.of(requestDetails));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,17 @@ public Integer resolveOffset() {
* @return limit
*/
public Integer resolveLimit() {
return limit.orElse(5000);
return resolveLimitWithExtra(0);
}

/**
* Returns the limit or the default.
*
* @param extra extra to add for pagination checking than the requested limit
* @return limit
*/
public Integer resolveLimitWithExtra(int extra) {
return limit.orElse(5000) + extra;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package gov.cms.bfd.server.ng.util;

import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Resource;
import org.springframework.web.util.UriComponentsBuilder;

/** FHIR-related utility methods. */
public class FhirUtil {
private FhirUtil() {}

private static final Pattern IS_INTEGER = Pattern.compile("\\d+");
private static final String OFFSET_PARAM = "_offset";
private static final String START_INDEX_PARAM = "startIndex";

/**
* Adds a data absent reason of the coding is empty.
Expand Down Expand Up @@ -50,37 +57,82 @@ public static String getHcpcsSystem(String code) {

/**
* Creates a bundle from the resource, returning a default bundle with lastUpdated populated if
* empty.
* adds previous link if an offset exists add a next link if the stream contains at least one more
* than the limit empty.
*
* @param resources resources
* @param batchLastUpdated last updated
* @param requestDetails request details
* @param limit record count
* @param offset start index
* @return bundle
*/
public static Bundle bundleOrDefault(
Stream<? extends Resource> resources, Supplier<ZonedDateTime> batchLastUpdated) {
var bundle = getBundle(resources);
Stream<? extends Resource> resources,
Supplier<ZonedDateTime> batchLastUpdated,
Optional<RequestDetails> requestDetails,
Optional<Integer> limit,
Optional<Integer> offset) {
var bundle = getBundle(resources, requestDetails, limit, offset);

if (bundle.getEntry().isEmpty()) {
return defaultBundle(batchLastUpdated);
}
return bundle;
}

/**
* Creates a bundle from the resource, returning a default bundle with lastUpdated populated if
* empty.
*
* @param resources resources
* @param batchLastUpdated last updated
* @return bundle
*/
public static Bundle bundleOrDefault(
Stream<? extends Resource> resources, Supplier<ZonedDateTime> batchLastUpdated) {
return bundleOrDefault(
resources, batchLastUpdated, Optional.empty(), Optional.empty(), Optional.empty());
}

/**
* Creates a bundle from the resource, returning a default bundle with lastUpdated populated if
* empty.
*
* @param resource resource
* @param batchLastUpdated last updated
* @param requestDetails request details
* @param limit record count
* @param offset start index
* @return bundle
*/
public static Bundle bundleOrDefault(
Optional<Resource> resource, Supplier<ZonedDateTime> batchLastUpdated) {
Optional<Resource> resource,
Supplier<ZonedDateTime> batchLastUpdated,
Optional<RequestDetails> requestDetails,
Optional<Integer> limit,
Optional<Integer> offset) {
return resource
.map(value -> bundleOrDefault(Stream.of(value), batchLastUpdated))
.map(
value ->
bundleOrDefault(Stream.of(value), batchLastUpdated, requestDetails, limit, offset))
.orElseGet(() -> defaultBundle(batchLastUpdated));
}

/**
* Creates a bundle from the resource, returning a default bundle with lastUpdated populated if
* empty.
*
* @param resource resource
* @param batchLastUpdated last updated
* @return bundle
*/
public static Bundle bundleOrDefault(
Optional<Resource> resource, Supplier<ZonedDateTime> batchLastUpdated) {
return bundleOrDefault(
resource, batchLastUpdated, Optional.empty(), Optional.empty(), Optional.empty());
}

/**
* Builds the bundle and includes full urls to every entry in the bundle.
*
Expand Down Expand Up @@ -112,9 +164,47 @@ public static Bundle bundleWithFullUrls(
return bundle;
}

private static Bundle getBundle(Stream<? extends Resource> resources) {
return new Bundle()
.setEntry(resources.map(r -> new Bundle.BundleEntryComponent().setResource(r)).toList());
private static Bundle getBundle(
Stream<? extends Resource> resources,
Optional<RequestDetails> requestDetails,
Optional<Integer> limit,
Optional<Integer> offset) {

record Page(List<Bundle.BundleEntryComponent> items, boolean hasMore) {}

var page =
resources
.map(r -> new Bundle.BundleEntryComponent().setResource(r))
.collect(
Collectors.teeing(
Collectors.toList(),
Collectors.counting(),
// collecting and counting to see if we do have a next
// limits the stream to only return the requested limit
(list, count) ->
new Page(
Comment thread
marshallweekley-eng marked this conversation as resolved.
trimEntriesToLimit(list, count, limit),
determineHasMore(count, limit))));

var bundle = new Bundle().setEntry(page.items());

// if we do not have a request we cannot build the links
requestDetails.ifPresent(
details -> applyBundleLinks(bundle, details, page.hasMore(), offset, limit));

return bundle;
}

private static List<Bundle.BundleEntryComponent> trimEntriesToLimit(
List<Bundle.BundleEntryComponent> entries, long count, Optional<Integer> limit) {
if (limit.isPresent() && count > limit.get().longValue()) {
return entries.subList(0, limit.get());
}
return entries;
}

private static boolean determineHasMore(long count, Optional<Integer> limit) {
return limit.isPresent() && count > limit.get().longValue();
}

/**
Expand All @@ -128,4 +218,44 @@ public static Bundle defaultBundle(Supplier<ZonedDateTime> batchLastUpdated) {
bundle.setMeta(new Meta().setLastUpdated(DateUtil.toDate(batchLastUpdated.get())));
return bundle;
}

private static void applyBundleLinks(
Bundle bundle,
RequestDetails requestDetails,
boolean hasMore,
Optional<Integer> offset,
Optional<Integer> limit) {
// check if a link is needed
if (hasMore) {
var nextOffset = Math.max(0, offset.orElse(0) + limit.orElse(0));
bundle
.addLink()
.setRelation(Constants.LINK_NEXT)
.setUrl(buildLinkURL(requestDetails, nextOffset));
}
// check if a previous link is needed
if (offset.isPresent() && offset.get() > 0) {
// get previous offset
var previousOffset = Math.max(0, offset.get() - limit.orElse(0));
bundle
.addLink()
.setRelation(Constants.LINK_PREVIOUS)
.setUrl(buildLinkURL(requestDetails, previousOffset));
}
}

private static String buildLinkURL(RequestDetails requestDetails, Integer offset) {
var uriBuilder = UriComponentsBuilder.fromUriString(requestDetails.getCompleteUrl());

// Remove offset and startIndex parameters
uriBuilder.replaceQueryParam(OFFSET_PARAM);
uriBuilder.replaceQueryParam(START_INDEX_PARAM);

// Add the new offset if it's not 0
if (offset != 0) {
uriBuilder.queryParam(OFFSET_PARAM, offset);
}

return uriBuilder.build().toUriString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ private List<ExplanationOfBenefit> getClaimsByBene(long beneSk, ClaimFilterOptio
Collections.emptyList(),
List.of(),
Collections.emptyList());
var claims = eobHandler.searchByBene(criteria, options);
var claims = eobHandler.searchByBene(criteria, options, Optional.empty());
return getEobFromBundle(claims);
}

Expand Down
Loading