Skip to content

Commit

Permalink
FINERACT-2179: Introduce Next/Last in future allocation rule for prog…
Browse files Browse the repository at this point in the history
…ressive loans
  • Loading branch information
somasorosdpc committed Feb 25, 2025
1 parent 8be31bd commit d630a2d
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1745,6 +1745,18 @@ private static List<LoanRepaymentScheduleInstallment> getFutureInstallmentsForRe
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;
}
Expand Down Expand Up @@ -1834,6 +1846,18 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr
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);
Expand Down Expand Up @@ -2070,6 +2094,21 @@ private Money processPeriodsVertically(LoanTransaction loanTransaction, Transact
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,7 @@ public static class FuturePaymentAllocationRule {

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";

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,13 @@ public void testLoanProductTemplateForAdvancedPaymentAllocation() {
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());
}
}
Original file line number Diff line number Diff line change
@@ -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"));
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -871,12 +871,13 @@ public HashMap reverseRepayment(final Integer loanId, final Integer transactionI
@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"),
Expand Down Expand Up @@ -2792,6 +2793,22 @@ public PostLoansLoanIdResponse disburseLoan(Long loanId, PostLoansLoanIdRequest
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"));
}
Expand Down

0 comments on commit d630a2d

Please sign in to comment.