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 04d6a25ee..c57af23d5 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 reAmortizationReasonOptions = null; private Collection 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 9682a4df3..bed240b3f 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 b0ab25e15..bc4a4bc18 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 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 a594f8705..9b0545397 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 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 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 e8d2e59a5..7fe5ee87a 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 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 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 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 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 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 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 dateArray = (List) dateObj; + return LocalDate.of(dateArray.get(0), dateArray.get(1), dateArray.get(2)); + } + }