FINERACT-2211: Second disbursement error fix
This commit is contained in:
+53
@@ -1246,6 +1246,22 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
||||
emiCalculator.addDisbursement(model, transactionDate, amortizableAmount);
|
||||
|
||||
boolean needsNPlusOneInstallment = installments.stream()
|
||||
.filter(i -> i.getDueDate().isAfter(transactionDate) || i.getDueDate().isEqual(transactionDate))
|
||||
.filter(i -> !i.isDownPayment() && !i.isAdditional()).findAny().isEmpty();
|
||||
|
||||
if (needsNPlusOneInstallment) {
|
||||
// CREATE N+1 installment like the non-EMI version does
|
||||
LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(disbursementTransaction.getLoan(),
|
||||
installments.size() + 1, disbursementTransaction.getTransactionDate(), disbursementTransaction.getTransactionDate(),
|
||||
Money.zero(currency).getAmount(), Money.zero(currency).getAmount(), Money.zero(currency).getAmount(),
|
||||
Money.zero(currency).getAmount(), true, null);
|
||||
newInstallment.updatePrincipal(amortizableAmount.getAmount());
|
||||
newInstallment.markAsAdditional();
|
||||
disbursementTransaction.getLoan().addLoanRepaymentScheduleInstallment(newInstallment);
|
||||
installments.add(newInstallment);
|
||||
}
|
||||
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
@@ -1259,6 +1275,23 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
if (candidateRepaymentInstallments.isEmpty()) {
|
||||
LoanRepaymentScheduleInstallment newInstallment;
|
||||
if (installments.stream().filter(LoanRepaymentScheduleInstallment::isAdditional).findAny().isEmpty()) {
|
||||
newInstallment = new LoanRepaymentScheduleInstallment(disbursementTransaction.getLoan(), installments.size() + 1,
|
||||
disbursementTransaction.getTransactionDate(), disbursementTransaction.getTransactionDate(),
|
||||
Money.zero(currency).getAmount(), Money.zero(currency).getAmount(), Money.zero(currency).getAmount(),
|
||||
Money.zero(currency).getAmount(), false, null);
|
||||
newInstallment.markAsAdditional();
|
||||
disbursementTransaction.getLoan().addLoanRepaymentScheduleInstallment(newInstallment);
|
||||
installments.add(newInstallment);
|
||||
} else {
|
||||
newInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isAdditional).findFirst().orElseThrow();
|
||||
newInstallment.updateDueDate(disbursementTransaction.getTransactionDate());
|
||||
}
|
||||
candidateRepaymentInstallments = Collections.singletonList(newInstallment);
|
||||
}
|
||||
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProductRelatedDetail()
|
||||
.getInstallmentAmountInMultiplesOf();
|
||||
@@ -1325,7 +1358,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
private void recalculateRepaymentPeriodsWithEMICalculation(Money amortizableAmount, ProgressiveLoanInterestScheduleModel model,
|
||||
List<LoanRepaymentScheduleInstallment> installments, LocalDate transactionDate, MonetaryCurrency currency) {
|
||||
boolean isPostMaturityDisbursement = installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional())
|
||||
.allMatch(i -> i.getDueDate().isBefore(transactionDate));
|
||||
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
if (isPostMaturityDisbursement) {
|
||||
LoanRepaymentScheduleInstallment additionalInstallment = installments.stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isAdditional).findFirst().orElse(null);
|
||||
if (additionalInstallment != null && additionalInstallment.getPrincipal(currency).isZero()) {
|
||||
additionalInstallment.updatePrincipal(amortizableAmount.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
model.repaymentPeriods().forEach(rm -> {
|
||||
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
|
||||
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
|
||||
@@ -1344,6 +1388,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Integer installmentAmountInMultiplesOf) {
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
|
||||
// Handle the case where no future installments exist (e.g., second disbursement after loan closure)
|
||||
if (noCandidateRepaymentInstallments == 0) {
|
||||
log.debug("No candidate repayment installments found for disbursement on {}. Creating new installments.",
|
||||
loanTransaction.getTransactionDate());
|
||||
return;
|
||||
}
|
||||
|
||||
// Original logic for when candidate installments exist
|
||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
|
||||
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
||||
|
||||
|
||||
+66
@@ -25,6 +25,7 @@ import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PE
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.refEq;
|
||||
@@ -515,6 +516,71 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
assertEquals(transactionAmountMoney.toString(), paidPortion.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisbursementAfterMaturityDateWithEMICalculator() {
|
||||
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
|
||||
LocalDate maturityDate = LocalDate.of(2023, 6, 1);
|
||||
LocalDate postMaturityDisbursementDate = LocalDate.of(2023, 7, 15); // After maturity date
|
||||
|
||||
MonetaryCurrency currency = MONETARY_CURRENCY;
|
||||
BigDecimal postMaturityDisbursementAmount = BigDecimal.valueOf(500.0);
|
||||
Money disbursementMoney = Money.of(currency, postMaturityDisbursementAmount);
|
||||
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = mock(LoanProductRelatedDetail.class);
|
||||
org.apache.fineract.portfolio.loanproduct.domain.LoanProduct loanProduct = mock(
|
||||
org.apache.fineract.portfolio.loanproduct.domain.LoanProduct.class);
|
||||
when(loanProduct.getInstallmentAmountInMultiplesOf()).thenReturn(null);
|
||||
when(loanProductRelatedDetail.isEnableDownPayment()).thenReturn(false);
|
||||
|
||||
Loan loan = mock(Loan.class);
|
||||
when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanProductRelatedDetail);
|
||||
when(loan.getLoanProduct()).thenReturn(loanProduct);
|
||||
when(loan.isInterestBearing()).thenReturn(true);
|
||||
|
||||
LoanRepaymentScheduleInstallment installment1 = spy(
|
||||
new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(200.0),
|
||||
BigDecimal.valueOf(10.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, null, BigDecimal.ZERO));
|
||||
|
||||
LoanRepaymentScheduleInstallment installment2 = spy(new LoanRepaymentScheduleInstallment(loan, 2, disbursementDate.plusMonths(1),
|
||||
disbursementDate.plusMonths(2), BigDecimal.valueOf(200.0), BigDecimal.valueOf(10.0), BigDecimal.valueOf(0.0),
|
||||
BigDecimal.valueOf(0.0), false, null, BigDecimal.ZERO));
|
||||
|
||||
LoanRepaymentScheduleInstallment installment3 = spy(
|
||||
new LoanRepaymentScheduleInstallment(loan, 3, disbursementDate.plusMonths(2), maturityDate, BigDecimal.valueOf(600.0),
|
||||
BigDecimal.valueOf(10.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, null, BigDecimal.ZERO));
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>(Arrays.asList(installment1, installment2, installment3));
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> spyInstallments = spy(installments);
|
||||
|
||||
LoanTransaction disbursementTransaction = mock(LoanTransaction.class);
|
||||
when(disbursementTransaction.getTypeOf()).thenReturn(LoanTransactionType.DISBURSEMENT);
|
||||
when(disbursementTransaction.getTransactionDate()).thenReturn(postMaturityDisbursementDate);
|
||||
when(disbursementTransaction.getAmount(currency)).thenReturn(disbursementMoney);
|
||||
when(disbursementTransaction.getLoan()).thenReturn(loan);
|
||||
|
||||
ArgumentCaptor<LoanRepaymentScheduleInstallment> installmentCaptor = ArgumentCaptor
|
||||
.forClass(LoanRepaymentScheduleInstallment.class);
|
||||
Mockito.doNothing().when(loan).addLoanRepaymentScheduleInstallment(installmentCaptor.capture());
|
||||
|
||||
ProgressiveLoanInterestScheduleModel model = mock(ProgressiveLoanInterestScheduleModel.class);
|
||||
|
||||
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, spyInstallments, Set.of(), new MoneyHolder(Money.zero(currency)),
|
||||
mock(ChangedTransactionDetail.class), model, Money.zero(currency));
|
||||
|
||||
underTest.processLatestTransaction(disbursementTransaction, ctx);
|
||||
|
||||
Mockito.verify(emiCalculator).addDisbursement(eq(model), eq(postMaturityDisbursementDate), eq(disbursementMoney));
|
||||
Mockito.verify(loan).addLoanRepaymentScheduleInstallment(any(LoanRepaymentScheduleInstallment.class));
|
||||
|
||||
LoanRepaymentScheduleInstallment newInstallment = installmentCaptor.getValue();
|
||||
assertNotNull(newInstallment);
|
||||
assertTrue(newInstallment.isAdditional());
|
||||
assertEquals(postMaturityDisbursementDate, newInstallment.getDueDate());
|
||||
|
||||
assertEquals(0, newInstallment.getPrincipal(currency).getAmount().compareTo(postMaturityDisbursementAmount));
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment createMockInstallment(LocalDate localDate, boolean isAdditional) {
|
||||
LoanRepaymentScheduleInstallment installment = mock(LoanRepaymentScheduleInstallment.class);
|
||||
lenient().when(installment.isAdditional()).thenReturn(isAdditional);
|
||||
|
||||
Reference in New Issue
Block a user