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"), //
|
NEXT_INSTALLMENT("Next installment"), //
|
||||||
LAST_INSTALLMENT("Last installment"), //
|
LAST_INSTALLMENT("Last installment"), //
|
||||||
|
NEXT_LAST_INSTALLMENT("Next/Last installment"), //
|
||||||
REAMORTIZATION("Reamortization"); //
|
REAMORTIZATION("Reamortization"); //
|
||||||
|
|
||||||
private final String humanReadableName;
|
private final String humanReadableName;
|
||||||
|
|||||||
+39
@@ -1764,6 +1764,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.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;
|
return inAdvanceInstallments;
|
||||||
}
|
}
|
||||||
@@ -1853,6 +1865,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.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);
|
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||||
@@ -2089,6 +2113,21 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
currentInstallments = installments.stream().filter(predicate)
|
currentInstallments = installments.stream().filter(predicate)
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.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();
|
int numberOfInstallments = currentInstallments.size();
|
||||||
paidPortion = Money.zero(currency);
|
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 LAST_INSTALLMENT = "LAST_INSTALLMENT";
|
||||||
public static final String NEXT_INSTALLMENT = "NEXT_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());
|
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(1).getCode());
|
||||||
assertEquals("Last installment",
|
assertEquals("Last installment",
|
||||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(1).getValue());
|
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(1).getValue());
|
||||||
assertEquals("REAMORTIZATION",
|
assertEquals("NEXT_LAST_INSTALLMENT",
|
||||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(2).getCode());
|
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(2).getCode());
|
||||||
assertEquals("Reamortization",
|
assertEquals("Next/Last installment",
|
||||||
loanProductsTemplateResponse.getAdvancedPaymentAllocationFutureInstallmentAllocationRules().get(2).getValue());
|
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)
|
@Deprecated(forRemoval = true)
|
||||||
public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String repaymentTypeCommand, final String date,
|
public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String repaymentTypeCommand, final String date,
|
||||||
final Float amountToBePaid, final Integer loanID) {
|
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));
|
return postLoanTransaction(createLoanTransactionURL(repaymentTypeCommand, loanID), getRepaymentBodyAsJSON(date, amountToBePaid));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final String command, final String date,
|
public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final String command, final String date,
|
||||||
final Double amountToBePaid) {
|
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,
|
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId,
|
||||||
new PostLoansLoanIdTransactionsRequest().transactionAmount(amountToBePaid).transactionDate(date).dateFormat("dd MMMM yyyy")
|
new PostLoansLoanIdTransactionsRequest().transactionAmount(amountToBePaid).transactionDate(date).dateFormat("dd MMMM yyyy")
|
||||||
.locale("en"),
|
.locale("en"),
|
||||||
@@ -2792,6 +2793,22 @@ public class LoanTransactionHelper {
|
|||||||
return Calls.ok(FineractClientHelper.getFineractClient().loans.stateTransitions(loanId, request, "disburse"));
|
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) {
|
public PostLoansLoanIdResponse disburseToSavingsLoan(String loanExternalId, PostLoansLoanIdRequest request) {
|
||||||
return Calls.ok(FineractClientHelper.getFineractClient().loans.stateTransitions1(loanExternalId, request, "disburseToSavings"));
|
return Calls.ok(FineractClientHelper.getFineractClient().loans.stateTransitions1(loanExternalId, request, "disburseToSavings"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user