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 5c9de4a17b4..9d51055d1e1 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.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 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 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 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 String getEnumStyledName() { 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 974d0d0ac23..ab8b8ae1e73 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.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.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 MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class); + @Mock private ConfigurationDomainService configurationDomainService; @Mock @@ -75,21 +86,34 @@ public void tearDown() { 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 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 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 void givenLoanWithNoInstallmentOverdueAfterConfiguredDaysWhenStepExecutio 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 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 void givenLoanWithInstallmentOverdueAfterConfiguredDaysButInstallmentPaid 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 void givenLoanWithInstallmentOverdueAfterConfiguredDaysButInstallmentPaid List 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 void givenLoanWithInstallmentOverdueAfterConfiguredDaysInLoanProductWhenS // 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 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 void givenLoanWithInstallmentOverdueAfterConfiguredDaysInLoanProductWhenS 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 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 28a349db605..752217f4b49 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.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;