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


The following commit(s) were added to refs/heads/develop by this push:
     new 73d8659390 FINERACT-2354: add reaging interest template data
73d8659390 is described below

commit 73d86593903f905dbe38e1acf6284af7e5cb9616
Author: Attila Budai <[email protected]>
AuthorDate: Wed Nov 26 10:06:41 2025 +0100

    FINERACT-2354: add reaging interest template data
---
 .../loanaccount/data/LoanTransactionData.java      |   5 +
 .../loanaccount/mapper/LoanTransactionMapper.java  |   4 +
 .../api/LoanTransactionsApiResource.java           |   3 +-
 .../service/LoanReadPlatformServiceImpl.java       |  54 +++-
 .../loan/reaging/LoanReAgingIntegrationTest.java   | 275 +++++++++++++++++++++
 5 files changed, 334 insertions(+), 7 deletions(-)

diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
index 04d6a25ee5..c57af23d50 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
@@ -124,6 +124,11 @@ public class LoanTransactionData implements Serializable {
     private Collection<CodeValueData> reAmortizationReasonOptions = null;
     private Collection<StringEnumOptionData> 
reAmortizationInterestHandlingOptions = null;
 
+    private Integer numberOfFutureInstallments;
+    private Integer numberOfPastInstallments;
+    private LocalDate nextInstallmentDueDate;
+    private LocalDate calculatedStartDate;
+
     public static LoanTransactionData importInstance(BigDecimal 
repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId,
             Integer rowIndex, String locale, String dateFormat) {
         return 
LoanTransactionData.builder().transactionAmount(repaymentAmount).transactionDate(lastRepaymentDate)
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
index 9682a4df3f..bed240b3f6 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
@@ -40,6 +40,10 @@ public interface LoanTransactionMapper {
     @Mapping(target = "reAmortizationInterestHandlingOptions", ignore = true)
     @Mapping(target = "classificationOptions", ignore = true)
     @Mapping(target = "paymentTypeOptions", ignore = true)
+    @Mapping(target = "numberOfFutureInstallments", ignore = true)
+    @Mapping(target = "numberOfPastInstallments", ignore = true)
+    @Mapping(target = "nextInstallmentDueDate", ignore = true)
+    @Mapping(target = "calculatedStartDate", ignore = true)
     @Mapping(target = "overpaymentPortion", ignore = true)
     @Mapping(target = "transfer", ignore = true)
     @Mapping(target = "fixedEmiAmount", ignore = true)
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index b0ab25e158..bc4a4bc188 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -98,7 +98,8 @@ public class LoanTransactionsApiResource {
     public static final String CAPITALIZED_INCOME = "capitalizedIncome";
     public static final String INTEREST_REFUND_COMMAND_VALUE = 
"interest-refund";
     private final Set<String> responseDataParameters = new 
HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", 
"externalId",
-            LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, 
LoanApiConstants.REVERSED_ON_DATE_PARAMNAME, "classification"));
+            LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, 
LoanApiConstants.REVERSED_ON_DATE_PARAMNAME, "classification",
+            "numberOfFutureInstallments", "numberOfPastInstallments", 
"nextInstallmentDueDate", "calculatedStartDate"));
 
     private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN";
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index a594f8705e..9b05453977 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -82,6 +82,7 @@ import org.apache.fineract.portfolio.client.data.ClientData;
 import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
 import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
 import 
org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
+import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
 import org.apache.fineract.portfolio.common.service.CommonEnumerations;
 import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
 import 
org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
@@ -1728,16 +1729,57 @@ public class LoanReadPlatformServiceImpl implements 
LoanReadPlatformService, Loa
 
     @Override
     public LoanTransactionData retrieveLoanReAgeTemplate(final Long loanId) {
-        final LoanAccountData loan = this.retrieveOne(loanId);
+        final Loan loan = 
this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
         final LoanTransactionEnumData transactionType = 
LoanEnumerations.transactionType(LoanTransactionType.REAGE);
         final BigDecimal totalOutstanding = loan.getSummary() != null ? 
loan.getSummary().getTotalOutstanding() : null;
         final List<CodeValueData> reAgeReasonOptions = new ArrayList<>(
                 
codeValueReadPlatformService.retrieveCodeValuesByCode(LoanApiConstants.REAGE_REASONS));
-        return 
LoanTransactionData.builder().type(transactionType).currency(loan.getCurrency()).date(DateUtils.getBusinessLocalDate())
-                
.amount(totalOutstanding).netDisbursalAmount(loan.getNetDisbursalAmount()).loanId(loanId)
-                
.externalLoanId(loan.getExternalId()).periodFrequencyOptions(CommonEnumerations.BASIC_PERIOD_FREQUENCY_TYPES)
-                .reAgeReasonOptions(reAgeReasonOptions)
-                
.reAgeInterestHandlingOptions(LoanReAgeInterestHandlingType.getValuesAsEnumOptionDataList()).build();
+
+        final LocalDate businessDate = DateUtils.getBusinessLocalDate();
+        final List<LoanRepaymentScheduleInstallment> installments = 
loan.getRepaymentScheduleInstallments();
+
+        int futureInstallmentCount = 0;
+        int pastInstallmentCount = 0;
+        LocalDate nextInstallmentDueDate = null;
+
+        for (LoanRepaymentScheduleInstallment installment : installments) {
+            LocalDate dueDate = installment.getDueDate();
+            if (DateUtils.isAfter(dueDate, businessDate)) {
+                futureInstallmentCount++;
+                if (nextInstallmentDueDate == null || 
DateUtils.isBefore(dueDate, nextInstallmentDueDate)) {
+                    nextInstallmentDueDate = dueDate;
+                }
+            } else {
+                pastInstallmentCount++;
+            }
+        }
+
+        final PeriodFrequencyType frequencyType = 
loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType();
+        final LocalDate calculatedStartDate = 
calculateReAgeStartDate(businessDate, frequencyType);
+
+        final CurrencyData currencyData = new 
CurrencyData(loan.getCurrencyCode(), null, 
loan.getCurrency().getDigitsAfterDecimal(),
+                loan.getCurrency().getInMultiplesOf(), null, null);
+
+        return 
LoanTransactionData.builder().type(transactionType).currency(currencyData).date(businessDate).amount(totalOutstanding)
+                
.netDisbursalAmount(loan.getNetDisbursalAmount()).loanId(loanId).externalLoanId(loan.getExternalId())
+                
.periodFrequencyOptions(CommonEnumerations.BASIC_PERIOD_FREQUENCY_TYPES).reAgeReasonOptions(reAgeReasonOptions)
+                
.reAgeInterestHandlingOptions(LoanReAgeInterestHandlingType.getValuesAsEnumOptionDataList())
+                
.numberOfFutureInstallments(futureInstallmentCount).numberOfPastInstallments(pastInstallmentCount)
+                
.nextInstallmentDueDate(nextInstallmentDueDate).calculatedStartDate(calculatedStartDate).build();
+    }
+
+    private LocalDate calculateReAgeStartDate(LocalDate businessDate, 
PeriodFrequencyType frequencyType) {
+        if (frequencyType == null) {
+            return null;
+        }
+        // Per PS-2795: calculated start date = current date + 1 unit of 
original frequency type
+        return switch (frequencyType) {
+            case DAYS -> businessDate.plusDays(1);
+            case WEEKS -> businessDate.plusWeeks(1);
+            case MONTHS -> businessDate.plusMonths(1);
+            case YEARS -> businessDate.plusYears(1);
+            case WHOLE_TERM, INVALID -> null;
+        };
     }
 
     @Override
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
index e8d2e59a57..7fe5ee87ae 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
@@ -19,11 +19,15 @@
 package org.apache.fineract.integrationtests.loan.reaging;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicLong;
 import org.apache.fineract.client.models.PostChargesResponse;
@@ -39,6 +43,7 @@ import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.util.CallFailedRuntimeException;
 import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
 import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
 import 
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
 import 
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
@@ -689,4 +694,274 @@ public class LoanReAgingIntegrationTest extends 
BaseLoanIntegrationTest {
         });
     }
 
+    @Test
+    public void testReAgeTemplate_WithMixOfPastAndFutureInstallments() {
+        AtomicLong createdLoanId = new AtomicLong();
+
+        runAt("01 January 2023", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 1;
+
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+                    
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
+                    
.enableDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))
+                    
.enableAutoRepaymentForDownPayment(true).repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue());
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+                    
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
+                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+            disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 
2023");
+            createdLoanId.set(loanId);
+        });
+
+        runAt("15 February 2023", () -> {
+            long loanId = createdLoanId.get();
+
+            HashMap<String, Object> templateData = getReAgeTemplate(loanId);
+
+            assertNotNull(templateData, "ReAge template should not be null");
+
+            Integer numberOfPastInstallments = (Integer) 
templateData.get("numberOfPastInstallments");
+            Integer numberOfFutureInstallments = (Integer) 
templateData.get("numberOfFutureInstallments");
+            LocalDate nextInstallmentDueDate = 
parseDate(templateData.get("nextInstallmentDueDate"));
+            LocalDate calculatedStartDate = 
parseDate(templateData.get("calculatedStartDate"));
+
+            assertEquals(2, numberOfPastInstallments, "Should have 2 past 
installments (Jan 1 downpayment and Feb 1)");
+            assertEquals(2, numberOfFutureInstallments, "Should have 2 future 
installments (Mar 1 and Apr 1)");
+            assertNotNull(nextInstallmentDueDate, "nextInstallmentDueDate 
should not be null");
+            assertEquals(LocalDate.of(2023, 3, 1), nextInstallmentDueDate, 
"Next installment due date should be March 1");
+            assertNotNull(calculatedStartDate, "calculatedStartDate should not 
be null");
+            assertEquals(LocalDate.of(2023, 3, 15), calculatedStartDate, 
"Calculated start date should be business date + 1 month");
+        });
+    }
+
+    @Test
+    public void testReAgeTemplate_NewlyDisbursedLoan_AllFutureInstallments() {
+        runAt("01 January 2023", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 1;
+
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+                    
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
+                    
.enableDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))
+                    
.enableAutoRepaymentForDownPayment(true).repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue());
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+                    
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
+                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+            disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 
2023");
+
+            HashMap<String, Object> templateData = getReAgeTemplate(loanId);
+
+            assertNotNull(templateData, "ReAge template should not be null");
+
+            Integer numberOfPastInstallments = (Integer) 
templateData.get("numberOfPastInstallments");
+            Integer numberOfFutureInstallments = (Integer) 
templateData.get("numberOfFutureInstallments");
+            LocalDate nextInstallmentDueDate = 
parseDate(templateData.get("nextInstallmentDueDate"));
+            LocalDate calculatedStartDate = 
parseDate(templateData.get("calculatedStartDate"));
+
+            assertEquals(1, numberOfPastInstallments, "Should have 1 past 
installment (downpayment on same day counts as past)");
+            assertEquals(3, numberOfFutureInstallments, "Should have 3 future 
installments");
+            assertNotNull(nextInstallmentDueDate, "nextInstallmentDueDate 
should not be null");
+            assertEquals(LocalDate.of(2023, 2, 1), nextInstallmentDueDate, 
"Next installment due date should be February 1");
+            assertNotNull(calculatedStartDate, "calculatedStartDate should not 
be null");
+            assertEquals(LocalDate.of(2023, 2, 1), calculatedStartDate, 
"Calculated start date should be business date + 1 month");
+        });
+    }
+
+    @Test
+    public void testReAgeTemplate_InstallmentDueOnBusinessDate_CountsAsPast() {
+        AtomicLong createdLoanId = new AtomicLong();
+
+        runAt("01 January 2023", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 1;
+
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+                    
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
+                    
.enableDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))
+                    
.enableAutoRepaymentForDownPayment(true).repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue());
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+                    
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
+                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+            disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 
2023");
+            createdLoanId.set(loanId);
+        });
+
+        runAt("01 March 2023", () -> {
+            long loanId = createdLoanId.get();
+
+            HashMap<String, Object> templateData = getReAgeTemplate(loanId);
+
+            assertNotNull(templateData, "ReAge template should not be null");
+
+            Integer numberOfPastInstallments = (Integer) 
templateData.get("numberOfPastInstallments");
+            Integer numberOfFutureInstallments = (Integer) 
templateData.get("numberOfFutureInstallments");
+            LocalDate nextInstallmentDueDate = 
parseDate(templateData.get("nextInstallmentDueDate"));
+            LocalDate calculatedStartDate = 
parseDate(templateData.get("calculatedStartDate"));
+
+            assertEquals(3, numberOfPastInstallments,
+                    "Should have 3 past installments (downpayment Jan 1, Feb 
1, and Mar 1 - due on business date counts as past)");
+            assertEquals(1, numberOfFutureInstallments, "Should have 1 future 
installment (Apr 1)");
+            assertNotNull(nextInstallmentDueDate, "nextInstallmentDueDate 
should not be null");
+            assertEquals(LocalDate.of(2023, 4, 1), nextInstallmentDueDate, 
"Next installment due date should be April 1");
+            assertNotNull(calculatedStartDate, "calculatedStartDate should not 
be null");
+            assertEquals(LocalDate.of(2023, 4, 1), calculatedStartDate, 
"Calculated start date should be business date + 1 month");
+        });
+    }
+
+    @Test
+    public void testReAgeTemplate_AllInstallmentsPast_NoFutureInstallments() {
+        AtomicLong createdLoanId = new AtomicLong();
+
+        runAt("01 January 2023", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 1;
+
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+                    
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
+                    
.enableDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))
+                    
.enableAutoRepaymentForDownPayment(true).repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue());
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+                    
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
+                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+            disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 
2023");
+            createdLoanId.set(loanId);
+        });
+
+        runAt("15 April 2023", () -> {
+            long loanId = createdLoanId.get();
+
+            HashMap<String, Object> templateData = getReAgeTemplate(loanId);
+
+            assertNotNull(templateData, "ReAge template should not be null");
+
+            Integer numberOfPastInstallments = (Integer) 
templateData.get("numberOfPastInstallments");
+            Integer numberOfFutureInstallments = (Integer) 
templateData.get("numberOfFutureInstallments");
+            LocalDate nextInstallmentDueDate = 
parseDate(templateData.get("nextInstallmentDueDate"));
+            LocalDate calculatedStartDate = 
parseDate(templateData.get("calculatedStartDate"));
+
+            assertEquals(4, numberOfPastInstallments, "Should have 4 past 
installments (all installments are past due)");
+            assertEquals(0, numberOfFutureInstallments, "Should have 0 future 
installments");
+            assertNull(nextInstallmentDueDate, "nextInstallmentDueDate should 
be null when no future installments");
+            assertNotNull(calculatedStartDate, "calculatedStartDate should 
still be computed");
+            assertEquals(LocalDate.of(2023, 5, 15), calculatedStartDate, 
"Calculated start date should be business date + 1 month");
+        });
+    }
+
+    @Test
+    public void 
testReAgeTemplate_BiMonthlyLoan_CalculatedStartDateUsesOneMonthNotRepayEvery() {
+        runAt("01 January 2023", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 2;
+            int repaymentEvery = 2;
+
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+                    
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
+                    
.enableDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25))
+                    
.enableAutoRepaymentForDownPayment(true).repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue());
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+                    
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments * 
repaymentEvery)
+                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+            disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 
2023");
+
+            HashMap<String, Object> templateData = getReAgeTemplate(loanId);
+
+            assertNotNull(templateData, "ReAge template should not be null");
+
+            LocalDate calculatedStartDate = 
parseDate(templateData.get("calculatedStartDate"));
+
+            assertNotNull(calculatedStartDate, "calculatedStartDate should not 
be null");
+            assertEquals(LocalDate.of(2023, 2, 1), calculatedStartDate,
+                    "Calculated start date should be business date + 1 month 
(not + 2 months based on repayEvery)");
+        });
+    }
+
+    private HashMap<String, Object> getReAgeTemplate(Long loanId) {
+        final String GET_REAGE_TEMPLATE_URL = 
"/fineract-provider/api/v1/loans/" + loanId + 
"/transactions/template?command=reAge&"
+                + Utils.TENANT_IDENTIFIER;
+        return Utils.performServerGet(requestSpec, responseSpec, 
GET_REAGE_TEMPLATE_URL, "");
+    }
+
+    @SuppressWarnings("unchecked")
+    private LocalDate parseDate(Object dateObj) {
+        if (dateObj == null) {
+            return null;
+        }
+        List<Integer> dateArray = (List<Integer>) dateObj;
+        return LocalDate.of(dateArray.get(0), dateArray.get(1), 
dateArray.get(2));
+    }
+
 }

Reply via email to