This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit b546a43ce9736cbaeba1348fcdda6ae84f0e4059
Author: Soma Sörös <[email protected]>
AuthorDate: Mon Feb 24 20:28:24 2025 +0100

    FINERACT-2179: Introduce Next/Last in future allocation rule for 
progressive loans
---
 .../domain/FutureInstallmentAllocationRule.java    |   1 +
 ...dvancedPaymentScheduleTransactionProcessor.java |  39 +++++++
 .../integrationtests/BaseLoanIntegrationTest.java  |   1 +
 .../integrationtests/LoanProductTemplateTest.java  |   8 +-
 ...essiveLoanTransactionProcessorNextLastTest.java | 120 +++++++++++++++++++++
 .../common/loans/LoanTransactionHelper.java        |  19 +++-
 6 files changed, 185 insertions(+), 3 deletions(-)

diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/FutureInstallmentAllocationRule.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/FutureInstallmentAllocationRule.java
index 854ac2b73..806c67478 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/FutureInstallmentAllocationRule.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/FutureInstallmentAllocationRule.java
@@ -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;
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index 8d1855b7f..53adf3d5b 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -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);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index f1d3f15fc..414180cc4 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -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";
 
     }
 
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTemplateTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTemplateTest.java
index 269244416..b159809f0 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTemplateTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTemplateTest.java
@@ -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());
     }
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanTransactionProcessorNextLastTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanTransactionProcessorNextLastTest.java
new file mode 100644
index 000000000..73edb99ab
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanTransactionProcessorNextLastTest.java
@@ -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"));
+        });
+    }
+
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 41fb81bc3..58b4236c5 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -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"));
     }

Reply via email to