From 570b67b18e916d87639ad63ade150a6b8500147f Mon Sep 17 00:00:00 2001 From: Ruchi Dhamankar Date: Wed, 29 Nov 2023 00:03:38 +0530 Subject: [PATCH] FINERACT-1992: Multi Delinquency event for installment range change --- .../DelinquencyWritePlatformServiceImpl.java | 34 ++++- ...tePlatformServiceRangeChangeEventTest.java | 139 ++++++++++++++++++ 2 files changed, 167 insertions(+), 6 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index 2e88c76ad..1ebe04761 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -398,6 +398,11 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat loanDelinquencyTagPrev.setLiftedOnDate(transactionDate); loanDelinquencyTagHistory.add(loanDelinquencyTagPrev); changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange()); + // event when loan goes out of delinquency we do not calculate at + // installment level and remove all installment tags, so event needs to raised here. + if (loan.isEnableInstallmentLevelDelinquency()) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); + } } } else { if (optLoanDelinquencyTag.isPresent()) { @@ -422,7 +427,11 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat } if (loanDelinquencyTagHistory.size() > 0) { this.loanDelinquencyTagRepository.saveAllAndFlush(loanDelinquencyTagHistory); - businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); + // if installment level delinquency is enabled event will be raised at installment level calculation, no + // need to raise the event here + if (!loan.isEnableInstallmentLevelDelinquency()) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); + } } return changes; } @@ -441,20 +450,29 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat private void applyDelinquencyDetailsForLoanInstallments(final Loan loan, final DelinquencyBucket delinquencyBucket, final Map installmentsCollectionData) { + boolean isDelinquencyRangeChangedForAnyOfInstallment = false; for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { if (installmentsCollectionData.containsKey(installment.getId())) { - setInstallmentDelinquencyDetails(loan, installment, delinquencyBucket, installmentsCollectionData.get(installment.getId())); + boolean isDelinquencySetForInstallment = setInstallmentDelinquencyDetails(loan, installment, delinquencyBucket, + installmentsCollectionData.get(installment.getId())); + isDelinquencyRangeChangedForAnyOfInstallment = isDelinquencyRangeChangedForAnyOfInstallment + || isDelinquencySetForInstallment; } } // remove tags for non existing installments that got deleted due to re-schedule removeDelinquencyTagsForNonExistingInstallments(loan.getId()); + // raise event if there is any change at installment level delinquency + if (isDelinquencyRangeChangedForAnyOfInstallment) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); + } + } - private void setInstallmentDelinquencyDetails(final Loan loan, final LoanRepaymentScheduleInstallment installment, + private boolean setInstallmentDelinquencyDetails(final Loan loan, final LoanRepaymentScheduleInstallment installment, final DelinquencyBucket delinquencyBucket, final CollectionData installmentDelinquencyData) { DelinquencyRange delinquencyRangeForInstallment = getInstallmentDelinquencyRange(delinquencyBucket, installmentDelinquencyData.getDelinquentDays()); - setDelinquencyDetailsForInstallment(loan, installment, installmentDelinquencyData, delinquencyRangeForInstallment); + return setDelinquencyDetailsForInstallment(loan, installment, installmentDelinquencyData, delinquencyRangeForInstallment); } private DelinquencyRange getInstallmentDelinquencyRange(final DelinquencyBucket delinquencyBucket, Long overDueDays) { @@ -480,10 +498,11 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat return delinquencyRangeForInstallment; } - private void setDelinquencyDetailsForInstallment(final Loan loan, final LoanRepaymentScheduleInstallment installment, + private boolean setDelinquencyDetailsForInstallment(final Loan loan, final LoanRepaymentScheduleInstallment installment, CollectionData installmentDelinquencyData, final DelinquencyRange delinquencyRangeForInstallment) { List installmentDelinquencyTags = new ArrayList<>(); LocalDate delinquencyCalculationDate = DateUtils.getBusinessLocalDate(); + boolean isDelinquencyRangeChanged = false; LoanInstallmentDelinquencyTag previousInstallmentDelinquencyTag = loanInstallmentDelinquencyTagRepository .findByLoanAndInstallment(loan, installment).orElse(null); @@ -494,6 +513,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat if (previousInstallmentDelinquencyTag != null) { // event installment out of delinquency loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag); + isDelinquencyRangeChanged = true; } } else { LoanInstallmentDelinquencyTag installmentDelinquency = null; @@ -506,6 +526,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat installmentDelinquencyData.getDelinquentAmount()); loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag); // event installment delinquency range change + isDelinquencyRangeChanged = true; } else { previousInstallmentDelinquencyTag.setOutstandingAmount(installmentDelinquencyData.getDelinquentAmount()); installmentDelinquency = previousInstallmentDelinquencyTag; @@ -516,6 +537,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat delinquencyCalculationDate, null, installmentDelinquencyData.getDelinquentDate(), installmentDelinquencyData.getDelinquentAmount()); // event installment delinquent + isDelinquencyRangeChanged = true; } if (installmentDelinquency != null) { @@ -527,7 +549,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat if (installmentDelinquencyTags.size() > 0) { loanInstallmentDelinquencyTagRepository.saveAllAndFlush(installmentDelinquencyTags); } - + return isDelinquencyRangeChanged; } private void cleanLoanInstallmentsDelinquencyTags(Loan loan) { diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java index 977cea5e6..e40c8d541 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyIterable; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -47,6 +48,7 @@ import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMapping import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; +import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag; import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository; @@ -136,6 +138,7 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket); when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true); + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(false); when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty()); when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData); @@ -212,6 +215,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { public void givenLoanAccountWithOverdueInstallmentAndEnableInstallmentThenDelinquencyRangeIsSetForInstallmentTest() { ArgumentCaptor> loanInstallmentDelinquencyTagsArgumentCaptor = ArgumentCaptor .forClass(List.class); + ArgumentCaptor loanDelinquencyRangeChangeEvent = ArgumentCaptor + .forClass(LoanDelinquencyRangeChangeBusinessEvent.class); // given Loan loanForProcessing = Mockito.mock(Loan.class); LoanProduct loanProduct = Mockito.mock(LoanProduct.class); @@ -253,6 +258,7 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket); when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments); + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true); when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty()); when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData); when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(0))) @@ -270,6 +276,11 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { assertEquals(1, installmentDelinquencyTags.get(0).getInstallment().getInstallmentNumber()); assertEquals(1, installmentDelinquencyTags.get(0).getDelinquencyRange().getId()); assertEquals(installmentPrincipalAmount, installmentDelinquencyTags.get(0).getOutstandingAmount()); + + // verify range change event is raised + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanDelinquencyRangeChangeEvent.capture()); + Loan loanPayloadForEvent = loanDelinquencyRangeChangeEvent.getValue().get(); + assertEquals(loanForProcessing, loanPayloadForEvent); } @Test @@ -278,6 +289,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { .forClass(List.class); ArgumentCaptor loanInstallmentDelinquencyTagArgumentCaptorForDelete = ArgumentCaptor .forClass(LoanInstallmentDelinquencyTag.class); + + ArgumentCaptor loanDelinquencyRangeChangeEvent = ArgumentCaptor + .forClass(LoanDelinquencyRangeChangeBusinessEvent.class); // given Loan loanForProcessing = Mockito.mock(Loan.class); LoanProduct loanProduct = Mockito.mock(LoanProduct.class); @@ -322,6 +336,7 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket); when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments); + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true); when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty()); when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData); when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(0))) @@ -345,5 +360,129 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { assertNotNull(deletedInstallmentDelinquencyTag); assertEquals(previousInstallmentTag, deletedInstallmentDelinquencyTag); + // verify range change event is raised + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanDelinquencyRangeChangeEvent.capture()); + Loan loanPayloadForEvent = loanDelinquencyRangeChangeEvent.getValue().get(); + assertEquals(loanForProcessing, loanPayloadForEvent); + + } + + @Test + public void givenLoanAccountWithOverdueInstallmentsAndEnableInstallmentThenDelinquencyRangeChangesEventWhenOneOfInstallmentIsOutOfDelinquencyTest() { + ArgumentCaptor loanInstallmentDelinquencyTagArgumentCaptorForDelete = ArgumentCaptor + .forClass(LoanInstallmentDelinquencyTag.class); + + ArgumentCaptor loanDelinquencyRangeChangeEvent = ArgumentCaptor + .forClass(LoanDelinquencyRangeChangeBusinessEvent.class); + // given + Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2); + range1.setId(1L); + DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30); + range2.setId(2L); + List listDelinquencyRanges = Arrays.asList(range1, range2); + DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket"); + delinquencyBucket.setRanges(listDelinquencyRanges); + + final Long daysDiff = 2L; + final LocalDate fromDate = DateUtils.getBusinessLocalDate().minusMonths(1).minusDays(daysDiff); + final LocalDate dueDate = DateUtils.getBusinessLocalDate().minusDays(daysDiff); + final BigDecimal installmentPrincipalAmount = BigDecimal.valueOf(100); + final BigDecimal zeroAmount = BigDecimal.ZERO; + + LoanRepaymentScheduleInstallment installment_1 = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, fromDate, dueDate, + installmentPrincipalAmount, zeroAmount, zeroAmount, zeroAmount, false, new HashSet<>(), zeroAmount); + installment_1.setId(1L); + + LoanRepaymentScheduleInstallment installment_2 = new LoanRepaymentScheduleInstallment(loanForProcessing, 2, fromDate, dueDate, + installmentPrincipalAmount, zeroAmount, zeroAmount, zeroAmount, false, new HashSet<>(), zeroAmount); + installment_2.setId(2L); + + List repaymentScheduleInstallments = Arrays.asList(installment_1, installment_2); + + LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29); + LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L, + loanForProcessing); + CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, BigDecimal.ZERO, null, null, + null, null, null, null); + + CollectionData installmentCollectionData_1 = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, + installmentPrincipalAmount, null, null, null, null, null, null); + + CollectionData installmentCollectionData_2 = new CollectionData(BigDecimal.ZERO, 0L, null, 0L, null, installmentPrincipalAmount, + null, null, null, null, null, null); + + Map installmentsCollection = new HashMap<>(); + installmentsCollection.put(1L, installmentCollectionData_1); + installmentsCollection.put(2L, installmentCollectionData_2); + + LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection); + + LoanInstallmentDelinquencyTag previousInstallmentTag_1 = new LoanInstallmentDelinquencyTag(); + previousInstallmentTag_1.setDelinquencyRange(range2); + + LoanInstallmentDelinquencyTag previousInstallmentTag = new LoanInstallmentDelinquencyTag(); + previousInstallmentTag.setDelinquencyRange(range1); + + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket); + when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true); + when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments); + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true); + when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty()); + when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData); + when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(0))) + .thenReturn(Optional.of(previousInstallmentTag_1)); + when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(1))) + .thenReturn(Optional.of(previousInstallmentTag)); + + // when + underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData); + + // then + verify(loanDelinquencyTagRepository, times(1)).saveAllAndFlush(anyIterable()); + verify(loanInstallmentDelinquencyTagRepository, times(1)).delete(loanInstallmentDelinquencyTagArgumentCaptorForDelete.capture()); + + LoanInstallmentDelinquencyTag deletedInstallmentDelinquencyTag = loanInstallmentDelinquencyTagArgumentCaptorForDelete.getValue(); + assertNotNull(deletedInstallmentDelinquencyTag); + assertEquals(previousInstallmentTag, deletedInstallmentDelinquencyTag); + + // verify range change event is raised + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanDelinquencyRangeChangeEvent.capture()); + Loan loanPayloadForEvent = loanDelinquencyRangeChangeEvent.getValue().get(); + assertEquals(loanForProcessing, loanPayloadForEvent); + + } + + @Test + public void givenLoanAccountEnableInstallmentLevelDelinquencyWhenLoanIsOutOfDelinquencyEventIsRaisedTest() { + ArgumentCaptor loanDelinquencyRangeChangeEvent = ArgumentCaptor + .forClass(LoanDelinquencyRangeChangeBusinessEvent.class); + // given + Loan loanForProcessing = Mockito.mock(Loan.class); + DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2); + range1.setId(1L); + DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30); + range2.setId(2L); + List listDelinquencyRanges = Arrays.asList(range1, range2); + DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket"); + delinquencyBucket.setRanges(listDelinquencyRanges); + + LoanDelinquencyTagHistory prevTagForLoan = new LoanDelinquencyTagHistory(); + prevTagForLoan.setDelinquencyRange(range1); + + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true); + when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.of(prevTagForLoan)); + + // when + underTest.removeDelinquencyTagToLoan(loanForProcessing); + + verify(loanInstallmentDelinquencyTagRepository, times(1)).deleteAllLoanInstallmentsTags(anyLong()); + // verify range change event is raised + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanDelinquencyRangeChangeEvent.capture()); + Loan loanPayloadForEvent = loanDelinquencyRangeChangeEvent.getValue().get(); + assertEquals(loanForProcessing, loanPayloadForEvent); + } }