FINERACT-2179: Introduce Next/Last in future allocation rule for progressive loans
This commit is contained in:
+1
@@ -31,6 +31,7 @@ public enum FutureInstallmentAllocationRule {
|
||||
|
||||
NEXT_INSTALLMENT("Next installment"), //
|
||||
LAST_INSTALLMENT("Last installment"), //
|
||||
NEXT_LAST_INSTALLMENT("Next/Last installment"), //
|
||||
REAMORTIZATION("Reamortization"); //
|
||||
|
||||
private final String humanReadableName;
|
||||
|
||||
+39
@@ -1764,6 +1764,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
// try to resolve as current installment ( not due )
|
||||
inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).filter(f -> loanTransaction.isAfter(f.getFromDate())
|
||||
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
|
||||
.toList();
|
||||
// if there is no current installment, resolve similar to LAST_INSTALLMENT
|
||||
if (inAdvanceInstallments.isEmpty()) {
|
||||
inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
}
|
||||
}
|
||||
return inAdvanceInstallments;
|
||||
}
|
||||
@@ -1853,6 +1865,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
// try to resolve as current installment ( not due )
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).filter(f -> loanTransaction.isAfter(f.getFromDate())
|
||||
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
|
||||
.toList();
|
||||
// if there is no current installment, resolve similar to LAST_INSTALLMENT
|
||||
if (inAdvanceInstallments.isEmpty()) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
}
|
||||
}
|
||||
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||
@@ -2089,6 +2113,21 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
// get current installment where from date < transaction date < to date OR transaction date
|
||||
// is on first installment's first day ( from day )
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.filter(f -> loanTransaction.isAfter(f.getFromDate())
|
||||
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
|
||||
.toList();
|
||||
// if there is no current in advance installment resolve similar to LAST_INSTALLMENT
|
||||
if (currentInstallments.isEmpty()) {
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
int numberOfInstallments = currentInstallments.size();
|
||||
paidPortion = Money.zero(currency);
|
||||
|
||||
+1
@@ -1427,6 +1427,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
|
||||
public static final String LAST_INSTALLMENT = "LAST_INSTALLMENT";
|
||||
public static final String NEXT_INSTALLMENT = "NEXT_INSTALLMENT";
|
||||
public static final String NEXT_LAST_INSTALLMENT = "NEXT_LAST_INSTALLMENT";
|
||||
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -93,9 +93,13 @@ public class LoanProductTemplateTest {
|
||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(1).getCode());
|
||||
assertEquals("Last installment",
|
||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(1).getValue());
|
||||
assertEquals("REAMORTIZATION",
|
||||
assertEquals("NEXT_LAST_INSTALLMENT",
|
||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(2).getCode());
|
||||
assertEquals("Reamortization",
|
||||
assertEquals("Next/Last installment",
|
||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(2).getValue());
|
||||
assertEquals("REAMORTIZATION",
|
||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(3).getCode());
|
||||
assertEquals("Reamortization",
|
||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(3).getValue());
|
||||
}
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ProgressiveLoanTransactionProcessorNextLastTest extends BaseLoanIntegrationTest {
|
||||
|
||||
private final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
|
||||
@Test
|
||||
public void testPartialEarlyRepaymentWithNextLast() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2024", () -> {
|
||||
Long progressiveLoanInterestRecalculationNextLastId = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true).loanScheduleProcessingType("HORIZONTAL")
|
||||
.paymentAllocation(
|
||||
List.of(createPaymentAllocation("DEFAULT", FuturePaymentAllocationRule.NEXT_LAST_INSTALLMENT))))
|
||||
.getResourceId();
|
||||
Long loanId = applyAndApproveProgressiveLoan(clientId, progressiveLoanInterestRecalculationNextLastId, "1 January 2024", 100.0,
|
||||
65.7, 6, null);
|
||||
loanIdRef.set(loanId);
|
||||
|
||||
loanTransactionHelper.disburseLoan(loanId, "1 January 2024", 100.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"),
|
||||
installment(14.52, 5.48, 20.0, false, "01 February 2024"), //
|
||||
installment(15.32, 4.68, 20.0, false, "01 March 2024"), //
|
||||
installment(16.16, 3.84, 20.0, false, "01 April 2024"), //
|
||||
installment(17.04, 2.96, 20.0, false, "01 May 2024"), //
|
||||
installment(17.98, 2.02, 20.0, false, "01 June 2024"), //
|
||||
installment(18.98, 1.04, 20.02, false, "01 July 2024"));
|
||||
|
||||
// should pay to first installment - edge case coming from implementation
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "1 January 2024", 5.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"), //
|
||||
installment(14.8, 5.2, 15.0, false, "01 February 2024"), //
|
||||
installment(15.34, 4.66, 20.0, false, "01 March 2024"), //
|
||||
installment(16.18, 3.82, 20.0, false, "01 April 2024"), //
|
||||
installment(17.06, 2.94, 20.0, false, "01 May 2024"), //
|
||||
installment(18.0, 2.0, 20.0, false, "01 June 2024"), //
|
||||
installment(18.62, 1.02, 19.64, false, "01 July 2024"));
|
||||
});
|
||||
runAt("31 January 2024", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
|
||||
// test the repayment before the due date. Should go to 1st installment.
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "31 January 2024", 4.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"), //
|
||||
installment(14.81, 5.19, 11.0, false, "01 February 2024"), //
|
||||
installment(15.34, 4.66, 20.0, false, "01 March 2024"), //
|
||||
installment(16.18, 3.82, 20.0, false, "01 April 2024"), //
|
||||
installment(17.06, 2.94, 20.0, false, "01 May 2024"), //
|
||||
installment(18.0, 2.0, 20.0, false, "01 June 2024"), //
|
||||
installment(18.61, 1.02, 19.63, false, "01 July 2024"));
|
||||
|
||||
// test the repayment before the due date. Should go to 1st installment, and rest to last installment.
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "31 January 2024", 20.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"),
|
||||
installment(14.97, 5.03, 0.0, true, "01 February 2024"), installment(15.7, 4.3, 20.0, false, "01 March 2024"),
|
||||
installment(16.7, 3.3, 20.0, false, "01 April 2024"), installment(17.61, 2.39, 20.0, false, "01 May 2024"),
|
||||
installment(18.58, 1.42, 20.0, false, "01 June 2024"), installment(16.44, 0.41, 7.85, false, "01 July 2024"));
|
||||
});
|
||||
runAt("1 March 2024", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
// test repayment on due date. should repay 2nd installment normally and rest should go to last installment.
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "1 March 2024", 26.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"),
|
||||
installment(14.97, 5.03, 0.0, true, "01 February 2024"), installment(15.7, 4.3, 0.0, true, "01 March 2024"),
|
||||
installment(17.03, 2.97, 20.0, false, "01 April 2024"), installment(17.96, 2.04, 20.0, false, "01 May 2024"),
|
||||
installment(18.94, 1.06, 20.0, false, "01 June 2024"), installment(15.4, 0.02, 0.42, false, "01 July 2024"));
|
||||
});
|
||||
runAt("2 March 2024", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
// verify multiple partial repayment for "current" installment
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "2 March 2024", 7.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"),
|
||||
installment(14.97, 5.03, 0.0, true, "01 February 2024"), installment(15.7, 4.3, 0.0, true, "01 March 2024"),
|
||||
installment(17.4, 2.6, 13.0, false, "01 April 2024"), installment(17.98, 2.02, 20.0, false, "01 May 2024"),
|
||||
installment(18.95, 1.04, 19.99, false, "01 June 2024"), installment(15.0, 0.0, 0.0, true, "01 July 2024"));
|
||||
// verify multiple partial repayment for "current" installment
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "2 March 2024", 7.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"),
|
||||
installment(14.97, 5.03, 0.0, true, "01 February 2024"), installment(15.7, 4.3, 0.0, true, "01 March 2024"),
|
||||
installment(17.77, 2.23, 6.0, false, "01 April 2024"), installment(18.0, 2.0, 20.0, false, "01 May 2024"),
|
||||
installment(18.56, 1.02, 19.58, false, "01 June 2024"), installment(15.0, 0.0, 0.0, true, "01 July 2024"));
|
||||
// verify next then last installment logic.
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "2 March 2024", 22.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"),
|
||||
installment(14.97, 5.03, 0.0, true, "01 February 2024"), installment(15.7, 4.3, 0.0, true, "01 March 2024"),
|
||||
installment(19.9, 0.1, 0.0, true, "01 April 2024"), installment(18.02, 1.98, 20.0, false, "01 May 2024"),
|
||||
installment(16.41, 0.02, 0.43, false, "01 June 2024"), installment(15.0, 0.0, 0.0, true, "01 July 2024"));
|
||||
// verify last installment logic.
|
||||
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "2 March 2024", 22.0);
|
||||
verifyRepaymentSchedule(loanId, installment(100.0, null, "01 January 2024"),
|
||||
installment(14.97, 5.03, 0.0, true, "01 February 2024"), installment(15.7, 4.3, 0.0, true, "01 March 2024"),
|
||||
installment(19.9, 0.1, 0.0, true, "01 April 2024"), installment(14.43, 0.0, 0.0, true, "01 May 2024"),
|
||||
installment(20.0, 0.0, 0.0, true, "01 June 2024"), installment(15.0, 0.0, 0.0, true, "01 July 2024"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+18
-1
@@ -871,12 +871,13 @@ public class LoanTransactionHelper {
|
||||
@Deprecated(forRemoval = true)
|
||||
public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String repaymentTypeCommand, final String date,
|
||||
final Float amountToBePaid, final Integer loanID) {
|
||||
log.info("Repayment with amount {} in {} for Loan {}", amountToBePaid, date, loanID);
|
||||
log.info("{} with amount {} in {} for Loan {}", repaymentTypeCommand, amountToBePaid, date, loanID);
|
||||
return postLoanTransaction(createLoanTransactionURL(repaymentTypeCommand, loanID), getRepaymentBodyAsJSON(date, amountToBePaid));
|
||||
}
|
||||
|
||||
public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final String command, final String date,
|
||||
final Double amountToBePaid) {
|
||||
log.info("Make loan transaction. Command - {} with amount {} in {} for Loan {}", command, amountToBePaid, date, loanId);
|
||||
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().transactionAmount(amountToBePaid).transactionDate(date).dateFormat("dd MMMM yyyy")
|
||||
.locale("en"),
|
||||
@@ -2792,6 +2793,22 @@ public class LoanTransactionHelper {
|
||||
return Calls.ok(FineractClientHelper.getFineractClient().loans.stateTransitions(loanId, request, "disburse"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disburse loan on provided date and amount.
|
||||
*
|
||||
* @param loanId
|
||||
* loan Id
|
||||
* @param date
|
||||
* formatted to "d MMMM yyyy"
|
||||
* @param amount
|
||||
* amount to disburse
|
||||
* @return Post Loans Loan Id Response
|
||||
*/
|
||||
public PostLoansLoanIdResponse disburseLoan(Long loanId, String date, Double amount) {
|
||||
return disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATE_FORMAT)
|
||||
.transactionAmount(BigDecimal.valueOf(amount)).locale("en"));
|
||||
}
|
||||
|
||||
public PostLoansLoanIdResponse disburseToSavingsLoan(String loanExternalId, PostLoansLoanIdRequest request) {
|
||||
return Calls.ok(FineractClientHelper.getFineractClient().loans.stateTransitions1(loanExternalId, request, "disburseToSavings"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user