diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index dab51028d18..88cc7e08628 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -23,6 +23,7 @@ import java.math.MathContext; import java.time.LocalDate; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -228,6 +229,76 @@ public final class LoanApplicationTerms { private final boolean enableAccrualActivityPosting; private final List supportedInterestRefundTypes; + public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData) { + return new LoanApplicationTerms(modelData.currency(), // currency + modelData.numberOfRepayments(), // loanTermFrequency + PeriodFrequencyType.valueOf(modelData.repaymentFrequencyType()), // loanTermPeriodFrequencyType + modelData.numberOfRepayments(), // numberOfRepayments + modelData.repaymentFrequency(), // repaymentEvery + PeriodFrequencyType.valueOf(modelData.repaymentFrequencyType()), // repaymentPeriodFrequencyType + null, // nthDay + null, // weekDayType + null, // amortizationMethod + null, // interestMethod + modelData.nominalInterestRate(), // interestRatePerPeriod + PeriodFrequencyType.valueOf(modelData.repaymentFrequencyType()), // interestRatePeriodFrequencyType + modelData.nominalInterestRate(), // annualNominalInterestRate + null, // interestCalculationPeriodMethod + false, // allowPartialPeriodInterestCalculation + Money.of(modelData.currency().toData(), modelData.disbursementAmount()), // principalMoney + modelData.disbursementDate(), // expectedDisbursementDate + modelData.scheduleGenerationStartDate(), // repaymentsStartingFromDate + null, // calculatedRepaymentsStartingFromDate + null, // graceOnPrincipalPayment + null, // recurringMoratoriumOnPrincipalPeriods + null, // graceOnInterestPayment + null, // graceOnInterestCharged + null, // interestChargedFromDate + Money.zero(modelData.currency().toData()), // inArrearsTolerance + false, // multiDisburseLoan + null, // emiAmount + new ArrayList<>(), // disbursementDatas + null, // maxOutstandingBalance + null, // graceOnArrearsAgeing + modelData.daysInMonth(), // daysInMonthType + modelData.daysInYear(), // daysInYearType + false, // isInterestRecalculationEnabled + null, // rescheduleStrategyMethod + null, // interestRecalculationCompoundingMethod + null, // restCalendarInstance + null, // recalculationFrequencyType + null, // compoundingCalendarInstance + null, // compoundingFrequencyType + null, // principalThresholdForLastInstalment + null, // installmentAmountInMultiplesOf + null, // preClosureInterestCalculationStrategy + null, // loanCalendar + null, // approvedAmount + modelData.loanTermVariations(), // loanTermVariations + null, // calendarHistoryDataWrapper + false, // isInterestChargedFromDateSameAsDisbursalDateEnabled + null, // numberOfDays + false, // isSkipRepaymentOnFirstDayOfMonth + null, // holidayDetailDTO + false, // allowCompoundingOnEod + false, // isEqualAmortization + false, // false (last boolean value) + false, // isInterestToBeRecoveredFirstWhenGreaterThanEMI + null, // fixedPrincipalPercentagePerInstallment + false, // isPrincipalCompoundingDisabledForOverdueLoans + false, // enableDownPayment + null, // disbursedAmountPercentageForDownPayment + false, // isAutoRepaymentForDownPaymentEnabled + null, // repaymentStartDateType + null, // submittedOnDate + null, // loanScheduleType + null, // loanScheduleProcessingType + modelData.fixedLength(), // fixedLength + false, // enableAccrualActivityPosting + List.of() // supportedInterestRefundTypes + ); + } + public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java new file mode 100644 index 00000000000..83ba240f4d1 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java @@ -0,0 +1,40 @@ +/** + * 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.portfolio.loanaccount.loanschedule.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearType; +import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; + +public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerationStartDate, @NotNull ApplicationCurrency currency, + @NotNull BigDecimal disbursementAmount, @NotNull LocalDate disbursementDate, @NotNull int numberOfRepayments, + @NotNull int repaymentFrequency, @NotBlank String repaymentFrequencyType, @NotNull BigDecimal nominalInterestRate, + @NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear, + BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength, + @NotNull List loanTermVariations, HolidayDetailDTO holidayDetailDTO, Set loanCharges) { +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index ded468ca2f6..2d68c6ffa3b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -163,6 +163,13 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding); } + public LoanScheduleModel generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) { + + LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData); + + return generate(mc, loanApplicationTerms, modelData.loanCharges(), modelData.holidayDetailDTO()); + } + private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) { if (loanApplicationTerms.getDisbursementDatas().isEmpty()) { loanApplicationTerms.getDisbursementDatas() diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java new file mode 100644 index 00000000000..f1c4fc68cf6 --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java @@ -0,0 +1,135 @@ +/** + * 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.portfolio.loanaccount.loanschedule.domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearType; +import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; +import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LoanScheduleGeneratorTest { + + private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(null); + private static MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); + private static final ApplicationCurrency applicationCurrency = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$"); + private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency.fromApplicationCurrency(applicationCurrency); + + @BeforeAll + public static void init() { + moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); + moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN)); + } + + @Test + void testGenerateLoanSchedule() { + HolidayDetailDTO holidayDetailDTO = null; + Set loanCharges = Collections.emptySet(); + List loanTermVariationsDataForTest = createLoanTermVariationsDataForTest(); + + LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), // schedule + // generation + // start + // date + applicationCurrency, // currency + BigDecimal.valueOf(192.22), // disbursement amount + LocalDate.of(2024, 1, 15), // disbursement date + 6, // number of repayments + 1, // repayment frequency + "MONTHS", // repayment frequency type + BigDecimal.valueOf(9.99), // nominal interest rate + true, // downpayment enabled + DaysInMonthType.DAYS_30, // daysInMonth + DaysInYearType.DAYS_360, // daysInYear + null, // downpayment percentage (optional) + null, // installmentAmountInMultiplesOf (optional) + null, // fixed_length (optional) + loanTermVariationsDataForTest, // loanTermVariationsDataForTest + holidayDetailDTO, // holidayDetailDTO + loanCharges // loanCharges + ); + + final MathContext mc = MoneyHelper.getMathContext(); + + final List expectedRepaymentPeriods = new ArrayList<>(); + + expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); + expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1))); + expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1))); + expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); + expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); + expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + ScheduledDateGenerator mockScheduledDateGenerator = Mockito.mock(ScheduledDateGenerator.class); + ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(mockScheduledDateGenerator, emiCalculator); + when(mockScheduledDateGenerator.generateRepaymentPeriods(any(), any(), any())).thenReturn(expectedRepaymentPeriods); + LoanScheduleModel loanSchedule = generator.generate(mc, modelData); + + List periods = loanSchedule.getPeriods(); + + assertEquals(7, periods.size(), "Expected 5 periods including the downpayment period."); + } + + public List createLoanTermVariationsDataForTest() { + Long id = 1L; + EnumOptionData termType = new EnumOptionData(1L, LoanTermVariationType.EMI_AMOUNT.getCode(), "Interest Rate Change"); + LocalDate termVariationApplicableFrom = LocalDate.of(2024, 1, 1); + BigDecimal decimalValue = BigDecimal.valueOf(5.0); + LocalDate dateValue = LocalDate.of(2024, 12, 31); + boolean isSpecificToInstallment = true; + + List loanTermVariationsDataList = new ArrayList<>(); + loanTermVariationsDataList.add( + new LoanTermVariationsData(id, termType, termVariationApplicableFrom, decimalValue, dateValue, isSpecificToInstallment)); + + return loanTermVariationsDataList; + } + + private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) { + final Money zeroAmount = Money.zero(monetaryCurrency); + return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount, + zeroAmount, zeroAmount, false); + } +}