Skip to content

Commit e98de24

Browse files
authored
Feature ab2d 1070 Add ability to cache contract-to-bene data (#204)
* AB2D-1070 introduce ability to cache contract-to-bene data First search the local database for a given contract for a specific month If no records are found, only then do we need to call the remote BDF contract-to-bene api. introduce an externally configurable property contract2bene.caching.threshold if the data returned from the remote api call is greater than the threshold value store the beneficiary data locally for faster retrieval for the next time * add junit test cases * review comments
1 parent 4d20d0d commit e98de24

File tree

12 files changed

+404
-20
lines changed

12 files changed

+404
-20
lines changed

common/src/main/java/gov/cms/ab2d/common/model/Contract.java

-7
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,4 @@ public class Contract {
4646
private Set<Coverage> coverages = new HashSet<>();
4747

4848

49-
public void addBeneficiary(Beneficiary beneficiary) {
50-
Coverage coverage = new Coverage();
51-
coverage.setContract(this);
52-
coverage.setBeneficiary(beneficiary);
53-
coverages.add(coverage);
54-
beneficiary.getCoverages().add(coverage);
55-
}
5649
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package gov.cms.ab2d.common.repository;
2+
3+
import gov.cms.ab2d.common.model.Beneficiary;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.Optional;
8+
9+
@Repository
10+
public interface BeneficiaryRepository extends JpaRepository<Beneficiary, Long> {
11+
12+
13+
Optional<Beneficiary> findByPatientId(String patientId);
14+
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package gov.cms.ab2d.common.repository;
2+
3+
import gov.cms.ab2d.common.model.Beneficiary;
4+
import gov.cms.ab2d.common.model.Contract;
5+
import gov.cms.ab2d.common.model.Coverage;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.stereotype.Repository;
9+
10+
import java.util.List;
11+
import java.util.Optional;
12+
13+
@Repository
14+
public interface CoverageRepository extends JpaRepository<Coverage, Long> {
15+
16+
17+
@Query(" SELECT c.beneficiary.patientId " +
18+
" FROM Coverage c " +
19+
" WHERE c.contract.id = :contractId " +
20+
" AND c.partDMonth = :month ")
21+
List<String> findActivePatientIds(Long contractId, int month);
22+
23+
Optional<Coverage> findByContractAndBeneficiaryAndPartDMonth(Contract contract, Beneficiary bene, int month);
24+
25+
}

worker/src/main/java/gov/cms/ab2d/worker/adapter/bluebutton/ContractAdapterImpl.java

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package gov.cms.ab2d.worker.adapter.bluebutton;
22

33
import gov.cms.ab2d.bfd.client.BFDClient;
4+
import gov.cms.ab2d.common.repository.ContractRepository;
45
import gov.cms.ab2d.filter.FilterOutByDate;
56
import gov.cms.ab2d.filter.FilterOutByDate.DateRange;
67
import gov.cms.ab2d.worker.adapter.bluebutton.GetPatientsByContractResponse.PatientDTO;
8+
import gov.cms.ab2d.worker.service.BeneficiaryService;
79
import lombok.RequiredArgsConstructor;
810
import lombok.extern.slf4j.Slf4j;
911
import org.apache.commons.lang3.StringUtils;
@@ -12,6 +14,7 @@
1214
import org.hl7.fhir.dstu3.model.Identifier;
1315
import org.hl7.fhir.dstu3.model.Patient;
1416
import org.hl7.fhir.dstu3.model.ResourceType;
17+
import org.springframework.beans.factory.annotation.Value;
1518
import org.springframework.stereotype.Component;
1619

1720
import java.text.ParseException;
@@ -24,25 +27,44 @@
2427

2528

2629
@Slf4j
27-
//@Primary - once the BFD API starts returning data, change this to primary bean so spring injects this instead of the stub.
30+
//@Primary // - once the BFD API starts returning data, change this to primary bean so spring injects this instead of the stub.
2831
@Component
2932
@RequiredArgsConstructor
3033
public class ContractAdapterImpl implements ContractAdapter {
3134

3235
private static final String BENEFICIARY_ID = "https://bluebutton.cms.gov/resources/variables/bene_id";
3336

37+
@Value("${contract2bene.caching.threshold:1000}")
38+
private int cachingThreshold;
3439

3540
private final BFDClient bfdClient;
36-
41+
private final ContractRepository contractRepo;
42+
private final BeneficiaryService beneficiaryService;
3743

3844

3945
@Override
4046
public GetPatientsByContractResponse getPatients(final String contractNumber, final int currentMonth) {
4147

4248
var patientDTOs = new ArrayList<PatientDTO>();
4349

50+
var contract = contractRepo.findContractByContractNumber(contractNumber).get();
51+
var contractId = contract.getId();
52+
4453
for (var month = 1; month <= currentMonth; month++) {
45-
var bfdPatientsIds = getPatientIdsForMonth(contractNumber, month);
54+
55+
Set<String> bfdPatientsIds = beneficiaryService.findPatientIdsInDb(contractId, month);
56+
if (bfdPatientsIds.isEmpty()) {
57+
// patient ids were not found in local DB given the contractId and currentMonth
58+
// call BFD to fetch the data
59+
60+
bfdPatientsIds = getPatientIdsForMonth(contractNumber, month);
61+
62+
//if number of benes for this month exceeds cachingThreshold, cache it
63+
var beneficiaryCount = bfdPatientsIds.size();
64+
if (beneficiaryCount > cachingThreshold) {
65+
beneficiaryService.storeBeneficiaries(contract.getId(), bfdPatientsIds, month);
66+
}
67+
}
4668

4769
var monthDateRange = toDateRange(month);
4870

@@ -51,7 +73,7 @@ public GetPatientsByContractResponse getPatients(final String contractNumber, fi
5173
var optPatient = findPatient(patientDTOs, bfdPatientId);
5274
if (optPatient.isPresent()) {
5375
// patient id was already active on this contract in previous month(s)
54-
// So just add this month to the patient's datesUnderContract
76+
// So just add this month to the patient's dateRangesUnderContract
5577

5678
var patientDTO = optPatient.get();
5779
if (monthDateRange != null) {
@@ -61,7 +83,7 @@ public GetPatientsByContractResponse getPatients(final String contractNumber, fi
6183
} else {
6284
// new patient id.
6385
// Create a new PatientDTO for this patient
64-
// And then add this month to the patient's datesUnderContract
86+
// And then add this month to the patient's dateRangesUnderContract
6587

6688
var patientDTO = PatientDTO.builder()
6789
.patientId(bfdPatientId)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package gov.cms.ab2d.worker.service;
2+
3+
import java.util.Set;
4+
5+
public interface BeneficiaryService {
6+
7+
Set<String> findPatientIdsInDb(Long contractId, int month);
8+
9+
10+
void storeBeneficiaries(Long contractId, Set<String> bfdPatientsIds, int month);
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package gov.cms.ab2d.worker.service;
2+
3+
import gov.cms.ab2d.common.model.Beneficiary;
4+
import gov.cms.ab2d.common.model.Contract;
5+
import gov.cms.ab2d.common.model.Coverage;
6+
import gov.cms.ab2d.common.repository.BeneficiaryRepository;
7+
import gov.cms.ab2d.common.repository.ContractRepository;
8+
import gov.cms.ab2d.common.repository.CoverageRepository;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Propagation;
13+
import org.springframework.transaction.annotation.Transactional;
14+
15+
import java.util.LinkedHashSet;
16+
import java.util.Optional;
17+
import java.util.Set;
18+
19+
@Slf4j
20+
@Service
21+
@RequiredArgsConstructor
22+
public class BeneficiaryServiceImpl implements BeneficiaryService {
23+
24+
private final BeneficiaryRepository beneRepo;
25+
private final ContractRepository contractRepo;
26+
private final CoverageRepository coverageRepo;
27+
28+
29+
/**
30+
* Given a contractId and a month,
31+
* search for bene information in the local db
32+
*
33+
* @param contractId
34+
* @param month
35+
* @return
36+
*/
37+
@Override
38+
public Set<String> findPatientIdsInDb(Long contractId, int month) {
39+
var patientIds = coverageRepo.findActivePatientIds(contractId, month);
40+
return new LinkedHashSet<>(patientIds);
41+
}
42+
43+
44+
45+
@Override
46+
@Transactional(propagation = Propagation.REQUIRES_NEW)
47+
public void storeBeneficiaries(Long contractId, Set<String> patientIds, int month) {
48+
final Contract contract = contractRepo.findById(contractId).get();
49+
patientIds.forEach(patientId -> storeBeneficiaryCoverage(contract, patientId, month));
50+
}
51+
52+
53+
private void storeBeneficiaryCoverage(Contract contract, String patientId, int month) {
54+
final Beneficiary beneficiary = getBeneficiary(patientId);
55+
final Coverage coverage = createCoverage(contract, beneficiary, month);
56+
contract.getCoverages().add(coverage);
57+
beneficiary.getCoverages().add(coverage);
58+
}
59+
60+
61+
private Beneficiary getBeneficiary(String patientId) {
62+
final Optional<Beneficiary> optPatient = beneRepo.findByPatientId(patientId);
63+
if (optPatient.isPresent()) {
64+
return optPatient.get();
65+
} else {
66+
return createBeneficiary(patientId);
67+
}
68+
69+
}
70+
private Beneficiary createBeneficiary(String patientId) {
71+
Beneficiary beneficiary = new Beneficiary();
72+
beneficiary.setPatientId(patientId);
73+
return beneRepo.save(beneficiary);
74+
}
75+
76+
private Coverage createCoverage(Contract contract, Beneficiary beneficiary, int month) {
77+
Coverage coverage = new Coverage();
78+
coverage.setContract(contract);
79+
coverage.setBeneficiary(beneficiary);
80+
coverage.setPartDMonth(month);
81+
return coverageRepo.save(coverage);
82+
}
83+
84+
85+
}

worker/src/main/resources/application.properties

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ report.progress.log.frequency=10000
2020
## fail the job if >= 1% of the records fail
2121
failure.threshold=1
2222

23+
contract2bene.caching.threshold=1000
24+
2325
## ---------------------------------------------------------------------------- PCP THREAD-POOL CONFIG
2426
## These properties apply to "patientProcessorThreadPool".
2527
pcp.queue.capacity=${AB2D_PCP_QUEUE_CAPACITY:#{150}}

worker/src/test/java/gov/cms/ab2d/worker/adapter/bluebutton/ContractAdapterTest.java

+66-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
44
import gov.cms.ab2d.bfd.client.BFDClient;
5+
import gov.cms.ab2d.common.model.Contract;
6+
import gov.cms.ab2d.common.repository.ContractRepository;
7+
import gov.cms.ab2d.worker.service.BeneficiaryService;
58
import org.hl7.fhir.dstu3.model.Bundle;
69
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
710
import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent;
@@ -14,30 +17,33 @@
1417
import org.mockito.Mock;
1518
import org.mockito.Mockito;
1619
import org.mockito.junit.jupiter.MockitoExtension;
20+
import org.springframework.test.util.ReflectionTestUtils;
1721

22+
import java.time.Instant;
1823
import java.time.Month;
1924
import java.util.Calendar;
25+
import java.util.Optional;
26+
import java.util.Set;
2027

2128
import static org.hamcrest.CoreMatchers.endsWith;
2229
import static org.hamcrest.CoreMatchers.is;
2330
import static org.hamcrest.CoreMatchers.notNullValue;
2431
import static org.hamcrest.MatcherAssert.assertThat;
2532
import static org.junit.jupiter.api.Assertions.assertThrows;
2633
import static org.mockito.ArgumentMatchers.anyInt;
34+
import static org.mockito.ArgumentMatchers.anyLong;
2735
import static org.mockito.ArgumentMatchers.anyString;
28-
import static org.mockito.Mockito.never;
29-
import static org.mockito.Mockito.times;
30-
import static org.mockito.Mockito.verify;
31-
import static org.mockito.Mockito.when;
36+
import static org.mockito.Mockito.*;
3237

3338

3439
@ExtendWith(MockitoExtension.class)
3540
class ContractAdapterTest {
3641

3742
private static final String BENEFICIARY_ID = "https://bluebutton.cms.gov/resources/variables/bene_id";
3843

39-
@Mock
40-
private BFDClient client;
44+
@Mock BFDClient client;
45+
@Mock ContractRepository contractRepository;
46+
@Mock BeneficiaryService beneficiaryService;
4147

4248
private ContractAdapter cut;
4349
private String contractNumber = "S0000";
@@ -47,9 +53,15 @@ class ContractAdapterTest {
4753

4854
@BeforeEach
4955
void setUp() {
50-
cut = new ContractAdapterImpl(client);
56+
cut = new ContractAdapterImpl(client, contractRepository, beneficiaryService);
57+
5158
bundle = createBundle();
52-
when(client.requestPartDEnrolleesFromServer(anyString(), anyInt())).thenReturn(bundle);
59+
lenient().when(client.requestPartDEnrolleesFromServer(anyString(), anyInt())).thenReturn(bundle);
60+
61+
Contract contract = new Contract();
62+
contract.setId(Long.valueOf(Instant.now().getNano()));
63+
contract.setContractNumber(contractNumber);
64+
when(contractRepository.findContractByContractNumber(anyString())).thenReturn(Optional.of(contract));
5365
}
5466

5567
@Test
@@ -277,6 +289,52 @@ void GivenDuplicatePatientRowsFromBFD_ShouldEliminateDuplicates() {
277289
}
278290

279291

292+
@Test
293+
@DisplayName("given patientid rows in db for a specific contract & month, should not call BFD contract-2-bene api")
294+
void GivenPatientInLocalDb_ShouldNotCallBfdContractToBeneAPI() {
295+
when(beneficiaryService.findPatientIdsInDb(anyLong(), anyInt())).thenReturn(Set.of("ccw_patient_005"));
296+
cut.getPatients(contractNumber, Month.JANUARY.getValue());
297+
298+
verify(client, never()).requestPartDEnrolleesFromServer(anyString(), anyInt());
299+
verify(beneficiaryService, never()).storeBeneficiaries(anyLong(), anySet(), anyInt());
300+
}
301+
302+
303+
@Test
304+
@DisplayName("given patient count > cachingThreshold, should cache beneficiary data")
305+
void GivenPatientCountGreaterThanCachingThreshold_ShouldCacheBeneficiaryData() {
306+
var entries = bundle.getEntry();
307+
entries.add(createBundleEntry("ccw_patient_001"));
308+
entries.add(createBundleEntry("ccw_patient_002"));
309+
entries.add(createBundleEntry("ccw_patient_003"));
310+
entries.add(createBundleEntry("ccw_patient_004"));
311+
entries.add(createBundleEntry("ccw_patient_005"));
312+
313+
ReflectionTestUtils.setField(cut, "cachingThreshold", 2);
314+
cut.getPatients(contractNumber, Month.JANUARY.getValue());
315+
316+
verify(client).requestPartDEnrolleesFromServer(anyString(), anyInt());
317+
verify(beneficiaryService).storeBeneficiaries(anyLong(), anySet(), anyInt());
318+
}
319+
320+
@Test
321+
@DisplayName("given patient count < cachingThreshold, should not cache beneficiary data")
322+
void GivenPatientCountLessThanCachingThreshold_ShouldNotCacheBeneficiaryData() {
323+
var entries = bundle.getEntry();
324+
entries.add(createBundleEntry("ccw_patient_001"));
325+
entries.add(createBundleEntry("ccw_patient_002"));
326+
entries.add(createBundleEntry("ccw_patient_003"));
327+
entries.add(createBundleEntry("ccw_patient_004"));
328+
entries.add(createBundleEntry("ccw_patient_005"));
329+
330+
ReflectionTestUtils.setField(cut, "cachingThreshold", 10);
331+
cut.getPatients(contractNumber, Month.JANUARY.getValue());
332+
333+
verify(client).requestPartDEnrolleesFromServer(anyString(), anyInt());
334+
verify(beneficiaryService, never()).storeBeneficiaries(anyLong(), anySet(), anyInt());
335+
}
336+
337+
280338
@Test
281339
@DisplayName("when call to BFD API throws Invalid Request exception, throws Exception")
282340
void whenBfdCallThrowsInvalidRequestException_ShouldThrowRuntimeException() {

worker/src/test/java/gov/cms/ab2d/worker/processor/JobProcessorIntegrationTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.junit.jupiter.api.io.TempDir;
2626
import org.mockito.Mock;
2727
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.beans.factory.annotation.Qualifier;
2829
import org.springframework.boot.test.context.SpringBootTest;
2930
import org.springframework.integration.test.context.SpringIntegrationTest;
3031
import org.springframework.test.util.ReflectionTestUtils;
@@ -70,6 +71,7 @@ class JobProcessorIntegrationTest {
7071
@Autowired
7172
private JobOutputRepository jobOutputRepository;
7273
@Autowired
74+
@Qualifier("contractAdapterStub")
7375
private ContractAdapter contractAdapterStub;
7476
@Autowired
7577
private OptOutRepository optOutRepository;

0 commit comments

Comments
 (0)