FINERACT-1981: Fix down-payment handling on overpaid loan (Advanced payment allocation loan)

This commit is contained in:
Adam Saghy
2024-02-28 15:53:14 +01:00
parent cd6258bd91
commit 65b1c45a5f
5 changed files with 55 additions and 9 deletions
@@ -148,9 +148,14 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
} }
public static LoanTransaction disbursement(final Office office, final Money amount, final PaymentDetail paymentDetail, public static LoanTransaction disbursement(final Office office, final Money amount, final PaymentDetail paymentDetail,
final LocalDate disbursementDate, final ExternalId externalId) { final LocalDate disbursementDate, final ExternalId externalId, final Money loanTotalOverpaid) {
return new LoanTransaction(null, office, LoanTransactionType.DISBURSEMENT, paymentDetail, amount.getAmount(), disbursementDate, // We need to set the overpayment amount because it could happen the transaction got saved before the proper
externalId); // 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, public static LoanTransaction repayment(final Office office, final Money amount, final PaymentDetail paymentDetail,
@@ -82,7 +82,9 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
} }
break; break;
case LOAN_DISBURSED: 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(); newState = activeTransition();
} }
break; break;
@@ -535,7 +535,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final List<Long> existingReversedTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Money amount = Money.of(loan.getCurrency(), transactionAmount); final Money amount = Money.of(loan.getCurrency(), transactionAmount);
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amount, paymentDetail, transactionDate, LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amount, paymentDetail, transactionDate,
txnExternalId); txnExternalId, loan.getTotalOverpaidAsMoney());
// Subtract Previous loan outstanding balance from netDisbursalAmount // Subtract Previous loan outstanding balance from netDisbursalAmount
loan.deductFromNetDisbursalAmount(transactionAmount); loan.deductFromNetDisbursalAmount(transactionAmount);
@@ -436,7 +436,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail, disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail,
actualDisbursementDate, txnExternalId); actualDisbursementDate, txnExternalId, loan.getTotalOverpaidAsMoney());
disbursementTransaction.updateLoan(loan); disbursementTransaction.updateLoan(loan);
loan.addLoanTransaction(disbursementTransaction); loan.addLoanTransaction(disbursementTransaction);
} }
@@ -752,7 +752,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail, LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail,
actualDisbursementDate, txnExternalId); actualDisbursementDate, txnExternalId, loan.getTotalOverpaidAsMoney());
disbursementTransaction.updateLoan(loan); disbursementTransaction.updateLoan(loan);
loan.addLoanTransaction(disbursementTransaction); loan.addLoanTransaction(disbursementTransaction);
businessEventNotifierService businessEventNotifierService
@@ -23,12 +23,21 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions; 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.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@@ -39,11 +48,22 @@ class DefaultLoanLifecycleStateMachineTest {
private DefaultLoanLifecycleStateMachine underTest; private DefaultLoanLifecycleStateMachine underTest;
private MockedStatic<MoneyHelper> moneyHelperStatic;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);
underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService); underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService);
} }
@AfterEach
public void deregister() {
ThreadLocalContextUtil.reset();
moneyHelperStatic.close();
}
@Test @Test
public void testTransitionShouldWorkProperlyForLoanCreation() { public void testTransitionShouldWorkProperlyForLoanCreation() {
// given // given
@@ -113,14 +133,33 @@ class DefaultLoanLifecycleStateMachineTest {
@Test @Test
public void testTransitionShouldWorkProperlyForLoanDisbursementWhenLoanIsOverpaid() { public void testTransitionShouldWorkProperlyForLoanDisbursementWhenLoanIsOverpaid() {
// given // 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 // when
underTest.transition(LoanEvent.LOAN_DISBURSED, loan); underTest.transition(LoanEvent.LOAN_DISBURSED, loan);
// then // then
assertThat(loan.getStatus()).isEqualTo(LoanStatus.ACTIVE); verify(loan, Mockito.times(1)).setLoanStatus(LoanStatus.ACTIVE.getValue());
verify(businessEventNotifierService).notifyPostBusinessEvent(any(LoanStatusChangedBusinessEvent.class)); 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 @Test
public void testTransitionShouldWorkProperlyForLoanApprovalUndo() { public void testTransitionShouldWorkProperlyForLoanApprovalUndo() {
// given // given