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 65b1c45a5 FINERACT-1981: Fix down-payment handling on overpaid loan
(Advanced payment allocation loan)
65b1c45a5 is described below
commit 65b1c45a5fc988cbb8ba4bcc0f7821310ea3bbd4
Author: Adam Saghy <[email protected]>
AuthorDate: Wed Feb 28 14:05:45 2024 +0100
FINERACT-1981: Fix down-payment handling on overpaid loan (Advanced payment
allocation loan)
---
.../loanaccount/domain/LoanTransaction.java | 11 ++++--
.../domain/DefaultLoanLifecycleStateMachine.java | 4 +-
.../domain/LoanAccountDomainServiceJpa.java | 2 +-
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 4 +-
.../DefaultLoanLifecycleStateMachineTest.java | 43 +++++++++++++++++++++-
5 files changed, 55 insertions(+), 9 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 7df92edab..f14835273 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -148,9 +148,14 @@ public class LoanTransaction extends
AbstractAuditableWithUTCDateTimeCustom {
}
public static LoanTransaction disbursement(final Office office, final
Money amount, final PaymentDetail paymentDetail,
- final LocalDate disbursementDate, final ExternalId externalId) {
- return new LoanTransaction(null, office,
LoanTransactionType.DISBURSEMENT, paymentDetail, amount.getAmount(),
disbursementDate,
- externalId);
+ final LocalDate disbursementDate, final ExternalId externalId,
final Money loanTotalOverpaid) {
+ // We need to set the overpayment amount because it could happen the
transaction got saved before the proper
+ // portion calculation and side effect would be reverse-replay
+ Money overPaymentPortion = amount.isGreaterThan(loanTotalOverpaid) ?
loanTotalOverpaid : amount;
+ LoanTransaction disbursement = new LoanTransaction(null, office,
LoanTransactionType.DISBURSEMENT, paymentDetail,
+ amount.getAmount(), disbursementDate, externalId);
+ disbursement.setOverPayments(overPaymentPortion);
+ return disbursement;
}
public static LoanTransaction repayment(final Office office, final Money
amount, final PaymentDetail paymentDetail,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
index 5b12b72ba..bf2e53f49 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java
@@ -82,7 +82,9 @@ public class DefaultLoanLifecycleStateMachine implements
LoanLifecycleStateMachi
}
break;
case LOAN_DISBURSED:
- if (anyOfAllowedWhenComingFrom(from, LoanStatus.APPROVED,
LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) {
+ if (anyOfAllowedWhenComingFrom(from, LoanStatus.APPROVED,
LoanStatus.CLOSED_OBLIGATIONS_MET)) {
+ newState = activeTransition();
+ } else if (from.isOverpaid() &&
loan.getTotalOverpaidAsMoney().isZero()) {
newState = activeTransition();
}
break;
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
index cc795819b..8039a6479 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
@@ -535,7 +535,7 @@ public class LoanAccountDomainServiceJpa implements
LoanAccountDomainService {
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Money amount = Money.of(loan.getCurrency(), transactionAmount);
LoanTransaction disbursementTransaction =
LoanTransaction.disbursement(loan.getOffice(), amount, paymentDetail,
transactionDate,
- txnExternalId);
+ txnExternalId, loan.getTotalOverpaidAsMoney());
// Subtract Previous loan outstanding balance from netDisbursalAmount
loan.deductFromNetDisbursalAmount(transactionAmount);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index a34a3071f..5c9759407 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -436,7 +436,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
disbursementTransaction =
LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail,
- actualDisbursementDate, txnExternalId);
+ actualDisbursementDate, txnExternalId,
loan.getTotalOverpaidAsMoney());
disbursementTransaction.updateLoan(loan);
loan.addLoanTransaction(disbursementTransaction);
}
@@ -752,7 +752,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
LoanTransaction disbursementTransaction =
LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail,
- actualDisbursementDate, txnExternalId);
+ actualDisbursementDate, txnExternalId,
loan.getTotalOverpaidAsMoney());
disbursementTransaction.updateLoan(loan);
loan.addLoanTransaction(disbursementTransaction);
businessEventNotifierService
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java
index c901e7d34..c9c5e0c5d 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java
@@ -23,12 +23,21 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import
org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
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.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@@ -39,11 +48,22 @@ class DefaultLoanLifecycleStateMachineTest {
private DefaultLoanLifecycleStateMachine underTest;
+ private MockedStatic<MoneyHelper> moneyHelperStatic;
+
@BeforeEach
public void setUp() {
+
+ moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
+ moneyHelperStatic.when(() ->
MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
underTest = new
DefaultLoanLifecycleStateMachine(businessEventNotifierService);
}
+ @AfterEach
+ public void deregister() {
+ ThreadLocalContextUtil.reset();
+ moneyHelperStatic.close();
+ }
+
@Test
public void testTransitionShouldWorkProperlyForLoanCreation() {
// given
@@ -113,14 +133,33 @@ class DefaultLoanLifecycleStateMachineTest {
@Test
public void
testTransitionShouldWorkProperlyForLoanDisbursementWhenLoanIsOverpaid() {
// given
- Loan loan = createLoanWithStatus(LoanStatus.OVERPAID);
+ Money overpayment = Money.of(new MonetaryCurrency("USD", 2, null),
BigDecimal.ZERO);
+ Loan loan = Mockito.mock(Loan.class);
+
Mockito.when(loan.getPlainStatus()).thenReturn(LoanStatus.OVERPAID.getValue());
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID);
+ Mockito.when(loan.getTotalOverpaidAsMoney()).thenReturn(overpayment);
// when
underTest.transition(LoanEvent.LOAN_DISBURSED, loan);
// then
- assertThat(loan.getStatus()).isEqualTo(LoanStatus.ACTIVE);
+ verify(loan,
Mockito.times(1)).setLoanStatus(LoanStatus.ACTIVE.getValue());
verify(businessEventNotifierService).notifyPostBusinessEvent(any(LoanStatusChangedBusinessEvent.class));
}
+ @Test
+ public void
testTransitionShouldWorkProperlyForLoanDisbursementWhenLoanIsOverpaidAndRemainsOverpaid()
{
+ // given
+ Money overpayment = Money.of(new MonetaryCurrency("USD", 2, null),
BigDecimal.TEN);
+ Loan loan = Mockito.mock(Loan.class);
+
Mockito.when(loan.getPlainStatus()).thenReturn(LoanStatus.OVERPAID.getValue());
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID);
+ Mockito.when(loan.getTotalOverpaidAsMoney()).thenReturn(overpayment);
+ // when
+ underTest.transition(LoanEvent.LOAN_DISBURSED, loan);
+ // then
+ verify(loan,
Mockito.never()).setLoanStatus(LoanStatus.ACTIVE.getValue());
+ verify(businessEventNotifierService,
Mockito.never()).notifyPostBusinessEvent(any(LoanStatusChangedBusinessEvent.class));
+ }
+
@Test
public void testTransitionShouldWorkProperlyForLoanApprovalUndo() {
// given