FINERACT-1981: Fix down-payment handling on overpaid loan (Advanced payment allocation loan)
This commit is contained in:
+8
-3
@@ -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,
|
||||||
|
|||||||
+3
-1
@@ -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;
|
||||||
|
|||||||
+1
-1
@@ -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);
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+41
-2
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user