From 65b1c45a5fc988cbb8ba4bcc0f7821310ea3bbd4 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Wed, 28 Feb 2024 14:05:45 +0100 Subject: [PATCH] FINERACT-1981: Fix down-payment handling on overpaid loan (Advanced payment allocation loan) --- .../loanaccount/domain/LoanTransaction.java | 11 +++-- .../DefaultLoanLifecycleStateMachine.java | 4 +- .../domain/LoanAccountDomainServiceJpa.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.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 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 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