This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 99ba46386 FINERACT-1971: RepaymentOverDueBusinessEvent should not be 
sent when balance is 0
99ba46386 is described below

commit 99ba46386bc62099ebaa6f8897fe25e0823cace4
Author: Jose Alberto Hernandez <albe...@black-box.lan>
AuthorDate: Tue Apr 9 22:27:56 2024 -0600

    FINERACT-1971: RepaymentOverDueBusinessEvent should not be sent when 
balance is 0
---
 .../CheckLoanRepaymentOverdueBusinessStep.java     |  46 +++++---
 .../CheckLoanRepaymentOverdueBusinessStepTest.java | 124 +++++++++++++++++++--
 .../InitiateExternalAssetOwnerTransferTest.java    |   3 +
 3 files changed, 150 insertions(+), 23 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java
index 5c9de4a17..9d51055d1 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java
@@ -18,7 +18,9 @@
  */
 package org.apache.fineract.cob.loan;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.Arrays;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -28,6 +30,7 @@ import 
org.apache.fineract.infrastructure.event.business.domain.loan.repayment.L
 import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
 import org.springframework.stereotype.Component;
 
 @Slf4j
@@ -40,25 +43,31 @@ public class CheckLoanRepaymentOverdueBusinessStep 
implements LoanCOBBusinessSte
 
     @Override
     public Loan execute(Loan loan) {
-        log.debug("start processing loan repayment overdue business step for 
loan with Id [{}]", loan.getId());
-        Long numberOfDaysAfterDueDateToRaiseEvent = 
configurationDomainService.retrieveRepaymentOverdueDays();
-        if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() != null) {
-            if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() > 0) {
-                numberOfDaysAfterDueDateToRaiseEvent = 
loan.getLoanProduct().getOverDueDaysForRepaymentEvent().longValue();
+        List<LoanStatus> nonDisbursedStatuses = 
Arrays.asList(LoanStatus.INVALID, LoanStatus.SUBMITTED_AND_PENDING_APPROVAL,
+                LoanStatus.APPROVED);
+        if (!nonDisbursedStatuses.contains(loan.getStatus())
+                && 
loan.getLoanSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0) {
+            log.debug("start processing loan repayment overdue business step 
for loan with Id [{}]", loan.getId());
+            Long numberOfDaysAfterDueDateToRaiseEvent = 
configurationDomainService.retrieveRepaymentOverdueDays();
+            if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() != 
null) {
+                if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() > 
0) {
+                    numberOfDaysAfterDueDateToRaiseEvent = 
loan.getLoanProduct().getOverDueDaysForRepaymentEvent().longValue();
+                }
             }
-        }
-        final LocalDate currentDate = DateUtils.getBusinessLocalDate();
-        final List<LoanRepaymentScheduleInstallment> 
loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments();
-        for (LoanRepaymentScheduleInstallment repaymentSchedule : 
loanRepaymentScheduleInstallments) {
-            if (!repaymentSchedule.isObligationsMet()) {
-                LocalDate installmentDueDate = repaymentSchedule.getDueDate();
-                if 
(installmentDueDate.plusDays(numberOfDaysAfterDueDateToRaiseEvent).equals(currentDate))
 {
-                    businessEventNotifierService.notifyPostBusinessEvent(new 
LoanRepaymentOverdueBusinessEvent(repaymentSchedule));
-                    break;
+            final LocalDate currentDate = DateUtils.getBusinessLocalDate();
+            final List<LoanRepaymentScheduleInstallment> 
loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments();
+            for (LoanRepaymentScheduleInstallment repaymentSchedule : 
loanRepaymentScheduleInstallments) {
+                if (!repaymentSchedule.isObligationsMet()) {
+                    LocalDate installmentDueDate = 
repaymentSchedule.getDueDate();
+                    if (isOverDueEventNeededToBeSent(loan, 
numberOfDaysAfterDueDateToRaiseEvent, currentDate, repaymentSchedule,
+                            installmentDueDate)) {
+                        
businessEventNotifierService.notifyPostBusinessEvent(new 
LoanRepaymentOverdueBusinessEvent(repaymentSchedule));
+                        break;
+                    }
                 }
             }
+            log.debug("end processing loan repayment overdue business step for 
loan with Id [{}]", loan.getId());
         }
-        log.debug("end processing loan repayment overdue business step for 
loan with Id [{}]", loan.getId());
         return loan;
     }
 
@@ -71,4 +80,11 @@ public class CheckLoanRepaymentOverdueBusinessStep 
implements LoanCOBBusinessSte
     public String getHumanReadableName() {
         return "Check loan repayment overdue";
     }
+
+    private static boolean isOverDueEventNeededToBeSent(Loan loan, Long 
numberOfDaysBeforeDueDateToRaiseEvent, LocalDate currentDate,
+            LoanRepaymentScheduleInstallment repaymentScheduleInstallment, 
LocalDate repaymentDate) {
+        return 
repaymentDate.plusDays(numberOfDaysBeforeDueDateToRaiseEvent).equals(currentDate)
+                && 
repaymentScheduleInstallment.getTotalOutstanding(loan.getCurrency()).isGreaterThanZero();
+    }
+
 }
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java
index 974d0d0ac..ab8b8ae1e 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.util.Arrays;
@@ -40,21 +41,31 @@ import 
org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import 
org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentOverdueBusinessEvent;
 import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
 import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
+import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 @ExtendWith(MockitoExtension.class)
 public class CheckLoanRepaymentOverdueBusinessStepTest {
 
+    private static final MockedStatic<MoneyHelper> MONEY_HELPER = 
Mockito.mockStatic(MoneyHelper.class);
+
     @Mock
     private ConfigurationDomainService configurationDomainService;
     @Mock
@@ -75,21 +86,34 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
         ThreadLocalContextUtil.reset();
     }
 
+    @BeforeAll
+    public static void init() {
+        
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
+    }
+
+    @AfterAll
+    public static void destruct() {
+        MONEY_HELPER.close();
+    }
+
     @Test
     public void 
givenLoanWithInstallmentOverdueAfterConfiguredDaysWhenStepExecutionThenBusinessEventIsRaised()
 {
         ArgumentCaptor<LoanRepaymentOverdueBusinessEvent> 
loanRepaymentDueBusinessEventArgumentCaptor = ArgumentCaptor
                 .forClass(LoanRepaymentOverdueBusinessEvent.class);
         // given
         
when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(1L);
-        LocalDate loanInstallmentRepaymentDueDate = 
DateUtils.getBusinessLocalDate().minusDays(1);
         Loan loanForProcessing = Mockito.mock(Loan.class);
         LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
-        LoanRepaymentScheduleInstallment repaymentInstallment = new 
LoanRepaymentScheduleInstallment(loanForProcessing, 1,
-                LocalDate.now(ZoneId.systemDefault()), 
loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), 
BigDecimal.valueOf(0.0),
-                BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new 
HashSet<>(), BigDecimal.valueOf(0.0));
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
+        MonetaryCurrency currency = new MonetaryCurrency("CODE", 1, 1);
+        LoanRepaymentScheduleInstallment repaymentInstallment = 
buildInstallment(loanForProcessing, currency, BigDecimal.valueOf(100),
+                BigDecimal.valueOf(0), BigDecimal.valueOf(0), 
BigDecimal.valueOf(0), BigDecimal.valueOf(100), -1);
         List<LoanRepaymentScheduleInstallment> 
loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment);
         when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
         when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
+        when(loanForProcessing.getCurrency()).thenReturn(currency);
         
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
 
         // when
@@ -108,11 +132,14 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
         LocalDate loanInstallmentRepaymentDueDateBefore5Days = 
DateUtils.getBusinessLocalDate().minusDays(5);
         Loan loanForProcessing = Mockito.mock(Loan.class);
         LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
         List<LoanRepaymentScheduleInstallment> 
loanRepaymentScheduleInstallments = Arrays
                 .asList(new 
LoanRepaymentScheduleInstallment(loanForProcessing, 1, 
LocalDate.now(ZoneId.systemDefault()),
                         loanInstallmentRepaymentDueDateBefore5Days, 
BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0),
                         BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), 
false, new HashSet<>(), BigDecimal.valueOf(0.0)));
         when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
         when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null);
         
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
         // when
@@ -130,6 +157,7 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
         LocalDate loanInstallmentRepaymentDueDate = 
DateUtils.getBusinessLocalDate().minusDays(1);
         Loan loanForProcessing = Mockito.mock(Loan.class);
         LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
         LoanRepaymentScheduleInstallment repaymentInstallmentPaidOff = new 
LoanRepaymentScheduleInstallment(loanForProcessing, 1,
                 LocalDate.now(ZoneId.systemDefault()), 
loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), 
BigDecimal.valueOf(0.0),
                 BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new 
HashSet<>(), BigDecimal.valueOf(0.0));
@@ -138,6 +166,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
 
         List<LoanRepaymentScheduleInstallment> 
loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallmentPaidOff);
         when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
         when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null);
         
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
 
@@ -155,16 +185,20 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
         // given
         // global configuration
         
when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(2L);
-        LocalDate loanInstallmentRepaymentDueDate = 
DateUtils.getBusinessLocalDate().minusDays(1);
         Loan loanForProcessing = Mockito.mock(Loan.class);
         LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
-        LoanRepaymentScheduleInstallment repaymentInstallment = new 
LoanRepaymentScheduleInstallment(loanForProcessing, 1,
-                LocalDate.now(ZoneId.systemDefault()), 
loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), 
BigDecimal.valueOf(0.0),
-                BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new 
HashSet<>(), BigDecimal.valueOf(0.0));
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
+        MonetaryCurrency currency = new MonetaryCurrency("CODE", 1, 1);
+        LoanRepaymentScheduleInstallment repaymentInstallment = 
buildInstallment(loanForProcessing, currency, BigDecimal.valueOf(100),
+                BigDecimal.valueOf(0), BigDecimal.valueOf(0), 
BigDecimal.valueOf(0), BigDecimal.valueOf(100), -1);
         List<LoanRepaymentScheduleInstallment> 
loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment);
         when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
+        when(loanForProcessing.getStatus()).thenReturn(LoanStatus.ACTIVE);
         // product configuration overrides global configuration
         when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(1);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
+        when(loanForProcessing.getCurrency()).thenReturn(currency);
         
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
 
         // when
@@ -175,4 +209,78 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
         assertEquals(repaymentInstallment, loanPayloadForEvent);
         assertEquals(processedLoan, loanForProcessing);
     }
+
+    @Test
+    public void 
givenActiveLoanWithZeroOutstandingWhenStepExecutionThenNoBusinessEventIsRaised()
 {
+        // given
+        Loan loanForProcessing = Mockito.mock(Loan.class);
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
+        when(loanForProcessing.getStatus()).thenReturn(LoanStatus.ACTIVE);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
+        // when
+        Loan processedLoan = underTest.execute(loanForProcessing);
+        // then - No Business Event raised
+        verify(businessEventNotifierService, 
times(0)).notifyPostBusinessEvent(any());
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    @Test
+    public void 
givenActiveLoanWithNonZeroOutstandingWhenStepExecutionThenBusinessEventIsRaised()
 {
+        // given
+        
when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(2L);
+        LocalDate loanInstallmentRepaymentDueDateBefore5Days = 
DateUtils.getBusinessLocalDate().minusDays(1);
+        Loan loanForProcessing = Mockito.mock(Loan.class);
+        LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
+        MonetaryCurrency currency = new MonetaryCurrency("CODE", 1, 1);
+        List<LoanRepaymentScheduleInstallment> 
loanRepaymentScheduleInstallments = Arrays
+                .asList(new 
LoanRepaymentScheduleInstallment(loanForProcessing, 1, 
LocalDate.now(ZoneId.systemDefault()),
+                        loanInstallmentRepaymentDueDateBefore5Days, 
BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0),
+                        BigDecimal.valueOf(1.0), BigDecimal.valueOf(0.0), 
false, new HashSet<>(), BigDecimal.valueOf(0.0)));
+        when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
+        when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(1);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.ONE);
+        when(loanForProcessing.getCurrency()).thenReturn(currency);
+        
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
+        // when
+        Loan processedLoan = underTest.execute(loanForProcessing);
+        // then - Business Event raised
+        verify(businessEventNotifierService, 
times(1)).notifyPostBusinessEvent(any());
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    @Test
+    public void 
givenSubmittedLoanWhenStepExecutionThenNoBusinessEventIsRaised() {
+        // given
+        Loan loanForProcessing = Mockito.mock(Loan.class);
+        
when(loanForProcessing.getStatus()).thenReturn(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL);
+        // when
+        Loan processedLoan = underTest.execute(loanForProcessing);
+        // then - No Business Event raised
+        verify(businessEventNotifierService, 
times(0)).notifyPostBusinessEvent(any());
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    @Test
+    public void 
givenApprovedLoanWhenStepExecutionThenNoBusinessEventIsRaised() {
+        // given
+        Loan loanForProcessing = Mockito.mock(Loan.class);
+        when(loanForProcessing.getStatus()).thenReturn(LoanStatus.APPROVED);
+        // when
+        Loan processedLoan = underTest.execute(loanForProcessing);
+        // then - No Business Event raised
+        verify(businessEventNotifierService, 
times(0)).notifyPostBusinessEvent(any());
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    private LoanRepaymentScheduleInstallment buildInstallment(Loan loan, 
MonetaryCurrency currency, BigDecimal principalAmount,
+            BigDecimal freeAmount, BigDecimal interestAmount, BigDecimal 
penaltyAmount, BigDecimal totalAmount, int minusDays) {
+        LoanRepaymentScheduleInstallment installment = 
Mockito.mock(LoanRepaymentScheduleInstallment.class);
+        when(installment.getTotalOutstanding(any())).thenAnswer(a -> 
Money.of(currency, totalAmount));
+        when(installment.getDueDate()).thenAnswer(a -> 
DateUtils.getBusinessLocalDate().plusDays(minusDays));
+        return installment;
+    }
+
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
index 28a349db6..752217f4b 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
@@ -88,11 +88,14 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @SuppressWarnings("rawtypes")
 @ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class 
})
 public class InitiateExternalAssetOwnerTransferTest {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(InitiateExternalAssetOwnerTransferTest.class);
     private static ResponseSpecification RESPONSE_SPEC;
     private static RequestSpecification REQUEST_SPEC;
     private static Account ASSET_ACCOUNT;

Reply via email to