Skip to content

Commit

Permalink
FINERACT-1981: separate library to generate loan schedule
Browse files Browse the repository at this point in the history
  • Loading branch information
kulminsky committed Oct 4, 2024
1 parent d7c5947 commit 9d44349
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -228,6 +229,76 @@ public final class LoanApplicationTerms {
private final boolean enableAccrualActivityPosting;
private final List<LoanSupportedInterestRefundTypes> 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LoanTermVariationsData> loanTermVariations, HolidayDetailDTO holidayDetailDTO, Set<LoanCharge> loanCharges) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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<LoanCharge> loanCharges = Collections.emptySet();
List<LoanTermVariationsData> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelPeriod> periods = loanSchedule.getPeriods();

assertEquals(7, periods.size(), "Expected 5 periods including the downpayment period.");
}

public List<LoanTermVariationsData> 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<LoanTermVariationsData> 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);
}
}

0 comments on commit 9d44349

Please sign in to comment.