diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index 6e5376a79..c8480a431 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -124,4 +124,6 @@ public interface LoanApiConstants { public static final String topupAmount = "topupAmount"; public static final String datatables = "datatables"; + + public static final String isEqualAmortizationParam = "isEqualAmortization"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 5109c3e71..ad21674fb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -213,6 +213,7 @@ public class LoanAccountData { private final Integer maximumGap; private List datatables = null; + private final Boolean isEqualAmortization; /** * Used to produce a {@link LoanAccountData} with only collateral options @@ -331,7 +332,7 @@ public class LoanAccountData { final Long closureLoanId = null; final String closureLoanAccountNo = null; final BigDecimal topupAmount = null; - + final boolean isEqualAmortization = false; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal, @@ -350,7 +351,7 @@ public class LoanAccountData { maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount); + maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization); } @@ -471,7 +472,7 @@ public class LoanAccountData { final Long closureLoanId = null; final String closureLoanAccountNo = null; final BigDecimal topupAmount = null; - + final boolean isEqualAmortization = false; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal, @@ -490,7 +491,7 @@ public class LoanAccountData { maxOutstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount); + maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization); } @@ -520,7 +521,7 @@ public class LoanAccountData { acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } /** @@ -642,7 +643,7 @@ public class LoanAccountData { final Long closureLoanId = null; final String closureLoanAccountNo = null; final BigDecimal topupAmount = null; - + final boolean isEqualAmortization = false; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, currencyData, proposedPrincipal, principal, principal, @@ -661,7 +662,7 @@ public class LoanAccountData { maxOutstandingBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount); + maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization); } @@ -691,7 +692,7 @@ public class LoanAccountData { acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } @@ -821,7 +822,6 @@ public class LoanAccountData { final Long closureLoanId = null; final String closureLoanAccountNo = null; final BigDecimal topupAmount = null; - return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, product.getId(), product.getName(), product.getDescription(), product.isLinkedToFloatingInterestRates(), product.getFundId(), product.getFundName(), loanPurposeId, loanPurposeName, loanOfficerId, loanOfficerName, @@ -846,7 +846,7 @@ public class LoanAccountData { originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments(), subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, - closureLoanAccountNo, topupAmount); + closureLoanAccountNo, topupAmount, product.isEqualAmortization()); } public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { @@ -906,7 +906,7 @@ public class LoanAccountData { product.toLoanInterestRecalculationData(), acc.originalSchedule, acc.createStandingInstructionAtDisbursement, paidInAdvance, acc.interestRatesPeriods, product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments(), acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, product.isEqualAmortization()); } @@ -938,7 +938,7 @@ public class LoanAccountData { final LoanInterestRecalculationData interestRecalculationData, final Boolean createStandingInstructionAtDisbursement, final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap, final EnumOptionData subStatus, final boolean canUseForTopup, final boolean isTopup, final Long closureLoanId, final String closureLoanAccountNo, - final BigDecimal topupAmount) { + final BigDecimal topupAmount, final boolean isEqualAmortization) { final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; @@ -994,7 +994,7 @@ public class LoanAccountData { outstandingLoanBalance, emiAmountVariations, memberVariations, product, inArrears, graceOnArrearsAgeing, overdueCharges, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, - maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount); + maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization); } /* @@ -1047,7 +1047,7 @@ public class LoanAccountData { acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, clientActiveLoanOptions, acc.isTopup, - acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, @@ -1089,7 +1089,7 @@ public class LoanAccountData { acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map memberLoanCycle) { @@ -1154,7 +1154,7 @@ public class LoanAccountData { acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } @@ -1188,7 +1188,7 @@ public class LoanAccountData { acc.isInterestRecalculationEnabled, interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) { @@ -1216,7 +1216,7 @@ public class LoanAccountData { acc.isNPA, acc.daysInMonthType, acc.daysInYearType, acc.isInterestRecalculationEnabled, acc.interestRecalculationData, acc.originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) { @@ -1245,7 +1245,7 @@ public class LoanAccountData { acc.isInterestRecalculationEnabled, acc.interestRecalculationData, originalSchedule, acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, - acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount); + acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization); } private LoanAccountData(final Long id, // @@ -1295,7 +1295,7 @@ public class LoanAccountData { final Collection interestRatesPeriods, final Boolean isVariableInstallmentsAllowed, final Integer minimumGap, final Integer maximumGap, final EnumOptionData subStatus, final Boolean canUseForTopup, final Collection clientActiveLoanOptions, final boolean isTopup, - final Long closureLoanId, final String closureLoanAccountNo, final BigDecimal topupAmount) { + final Long closureLoanId, final String closureLoanAccountNo, final BigDecimal topupAmount, final boolean isEqualAmortization) { this.id = id; this.accountNo = accountNo; @@ -1477,6 +1477,7 @@ public class LoanAccountData { this.closureLoanId = closureLoanId; this.closureLoanAccountNo = closureLoanAccountNo; this.topupAmount = topupAmount; + this.isEqualAmortization = isEqualAmortization; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 480b08108..193810d50 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -115,6 +115,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignme import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; @@ -2616,10 +2617,24 @@ public class Loan extends AbstractPersistableCustom { final RoundingMode roundingMode = MoneyHelper.getRoundingMode(); final MathContext mc = new MathContext(8, roundingMode); - final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO); - - final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(interestMethod); + LoanScheduleGenerator loanScheduleGenerator = null; + if (loanApplicationTerms.isEqualAmortization()) { + if (loanApplicationTerms.getInterestMethod().isDecliningBalnce()) { + final LoanScheduleGenerator decliningLoanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create( + InterestMethod.DECLINING_BALANCE); + Set loanCharges = charges(); + LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, scheduleGeneratorDTO.getHolidayDetailDTO()); + + loanApplicationTerms.updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged())); + + + } + loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(InterestMethod.FLAT); + } else { + loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getInterestMethod()); + } + final LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, loanApplicationTerms, charges(), scheduleGeneratorDTO.getHolidayDetailDTO()); return loanSchedule; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index 19a203a5c..fb5c12ab0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -67,7 +67,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final LoanScheduleParams loanScheduleRecalculationDTO = null; return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, loanScheduleRecalculationDTO); } - + private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final HolidayDetailDTO holidayDetailDTO, final LoanScheduleParams loanScheduleParams) { @@ -114,10 +114,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // Determine the total interest owed over the full loan for FLAT // interest method . - if (!scheduleParams.isPartialUpdate()) { + if (!scheduleParams.isPartialUpdate() && !loanApplicationTerms.isEqualAmortization()) { Money totalInterestChargedForFullLoanTerm = loanApplicationTerms.calculateTotalInterestCharged( - this.paymentPeriodsInOneYearCalculator, mc); + this.paymentPeriodsInOneYearCalculator, mc); + loanApplicationTerms.updateTotalInterestDue(totalInterestChargedForFullLoanTerm); + } boolean isFirstRepayment = true; @@ -177,6 +179,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener isFirstRepayment = false; } + while (!scheduleParams.getOutstandingBalance().isZero() || !scheduleParams.getDisburseDetailMap().isEmpty()) { LocalDate previousRepaymentDate = scheduleParams.getActualRepaymentDate(); scheduleParams.setActualRepaymentDate(this.scheduledDateGenerator.generateNextRepaymentDate( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index d5f08cb33..24b166ea5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -210,7 +210,7 @@ public final class LoanApplicationTerms { private Money totalInterestAccounted; private int periodsCompleted = 0; private int extraPeriods = 0; - + private boolean isEqualAmortization; @@ -234,7 +234,8 @@ public final class LoanApplicationTerms { final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy, final Calendar loanCalendar, BigDecimal approvedAmount, List loanTermVariations, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfdays, - boolean isSkipRepaymentOnFirstDayofMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod) { + boolean isSkipRepaymentOnFirstDayofMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, + final boolean isEqualAmortization) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -249,7 +250,7 @@ public final class LoanApplicationTerms { compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfdays, isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO, - allowCompoundingOnEod); + allowCompoundingOnEod, isEqualAmortization); } @@ -317,6 +318,7 @@ public final class LoanApplicationTerms { final DaysInYearType daysInYearType = loanProductRelatedDetail.fetchDaysInYearType(); final boolean isInterestRecalculationEnabled = loanProductRelatedDetail.isInterestRecalculationEnabled(); final boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = false; + final boolean isEqualAmortization = loanProductRelatedDetail.isEqualAmortization(); return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, @@ -328,7 +330,7 @@ public final class LoanApplicationTerms { recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfdays, - isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO, allowCompoundingOnEod); + isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -375,7 +377,7 @@ public final class LoanApplicationTerms { } final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; final boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = false; - + final boolean isEqualAmortization = loanProductRelatedDetail.isEqualAmortization(); return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, null, null, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, @@ -387,7 +389,7 @@ public final class LoanApplicationTerms { compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfdays, isSkipRepaymentOnFirstDayofMonth, holidayDetailDTO, - allowCompoundingOnEod); + allowCompoundingOnEod, isEqualAmortization); } @@ -413,7 +415,7 @@ public final class LoanApplicationTerms { applicationTerms.approvedPrincipal.getAmount(), loanTermVariations, applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled, applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO, - applicationTerms.allowCompoundingOnEod); + applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -437,7 +439,7 @@ public final class LoanApplicationTerms { BigDecimal approvedAmount, List loanTermVariations, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfdays, final boolean isSkipRepaymentOnFirstDayofMonth, final HolidayDetailDTO holidayDetailDTO, - final boolean allowCompoundingOnEod) { + final boolean allowCompoundingOnEod, final boolean isEqualAmortization) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -509,6 +511,7 @@ public final class LoanApplicationTerms { this.totalPrincipalAccountedForInterestCalcualtion = principal.zero(); this.totalInterestAccounted = principal.zero(); this.totalPrincipalAccounted = principal.zero(); + this.isEqualAmortization = isEqualAmortization; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -667,27 +670,36 @@ public final class LoanApplicationTerms { Money interestForInstallment = this.principal.zero(); Money interestBroughtForwardDueToGrace = cumulatingInterestPaymentDueToGrace.copy(); + InterestMethod interestMethod = this.interestMethod; - switch (this.interestMethod) { + if (this.isEqualAmortization() && this.totalInterestDue != null) { + interestMethod = InterestMethod.FLAT; + } + switch (interestMethod) { case FLAT: - - switch (this.amortizationMethod) { - case EQUAL_INSTALLMENTS: - // average out outstanding interest over remaining - // instalments where interest is applicable - interestForInstallment = calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(calculator, periodNumber, - mc); - break; - case EQUAL_PRINCIPAL: - // interest follows time-value of money and is brought - // forward to next applicable interest payment period - final PrincipalInterest result = calculateTotalFlatInterestForPeriod(calculator, periodNumber, mc, - interestBroughtForwardDueToGrace); - interestForInstallment = result.interest(); - interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace(); - break; - case INVALID: - break; + if (this.isEqualAmortization() && this.totalInterestDue != null && this.interestMethod.isDecliningBalnce()) { + interestForInstallment = flatInterestPerInstallment(mc, this.totalInterestDue); + } else { + switch (this.amortizationMethod) { + case EQUAL_INSTALLMENTS: + // average out outstanding interest over remaining + // instalments where interest is applicable + interestForInstallment = calculateTotalFlatInterestForInstallmentAveragingOutGracePeriods(calculator, + periodNumber, mc); + break; + case EQUAL_PRINCIPAL: + // interest follows time-value of money and is + // brought + // forward to next applicable interest payment + // period + final PrincipalInterest result = calculateTotalFlatInterestForPeriod(calculator, periodNumber, mc, + interestBroughtForwardDueToGrace); + interestForInstallment = result.interest(); + interestBroughtForwardDueToGrace = result.interestPaymentDueToGrace(); + break; + case INVALID: + break; + } } break; case DECLINING_BALANCE: @@ -868,13 +880,18 @@ public final class LoanApplicationTerms { private Money calculateTotalInterestPerInstallmentWithoutGrace(final PaymentPeriodsInOneYearCalculator calculator, final MathContext mc) { final Money totalInterestForLoanTerm = calculateTotalFlatInterestDueWithoutGrace(calculator, mc); - Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.excludePeriodsForCalculation), mc.getRoundingMode()); + return flatInterestPerInstallment(mc, totalInterestForLoanTerm); + } + + private Money flatInterestPerInstallment(final MathContext mc, final Money totalInterestForLoanTerm) { + Money interestPerInstallment = totalInterestForLoanTerm.dividedBy(Long.valueOf(this.actualNumberOfRepayments) + - defaultToZeroIfNull(this.excludePeriodsForCalculation), mc.getRoundingMode()); if (this.excludePeriodsForCalculation < this.periodsCompleted) { Money interestLeft = this.totalInterestDue.minus(this.totalInterestAccounted); interestPerInstallment = interestLeft.dividedBy(Long.valueOf(this.actualNumberOfRepayments) - defaultToZeroIfNull(this.periodsCompleted), mc.getRoundingMode()); } - + return interestPerInstallment; } @@ -1320,7 +1337,7 @@ public final class LoanApplicationTerms { this.interestCalculationPeriodMethod, this.allowPartialPeriodInterestCalcualtion, this.repaymentEvery, this.repaymentPeriodFrequencyType, this.numberOfRepayments, this.principalGrace, this.recurringMoratoriumOnPrincipalPeriods, this.interestPaymentGrace, this.interestChargingGrace, this.amortizationMethod, this.inArrearsTolerance.getAmount(), this.graceOnArrearsAgeing, - this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled); + this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled, this.isEqualAmortization); } public Integer getLoanTermFrequency() { @@ -1746,5 +1763,13 @@ public final class LoanApplicationTerms { public void setSeedDate(LocalDate seedDate) { this.seedDate = seedDate; } - + + public boolean isEqualAmortization() { + return isEqualAmortization; + } + + public void setEqualAmortization(boolean isEqualAmortization) { + this.isEqualAmortization = isEqualAmortization; + } + } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java index 22f8bac03..29c092eb3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java @@ -120,4 +120,11 @@ public final class LoanScheduleModel { public BigDecimal getTotalPenaltyChargesCharged() { return this.totalPenaltyChargesCharged; } + + + public BigDecimal getTotalInterestCharged() { + return this.totalInterestCharged; + } + + } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 095da8aea..c3f48a850 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -89,6 +89,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; import org.apache.fineract.portfolio.loanaccount.exception.MinDaysBetweenDisbursalAndFirstRepaymentViolationException; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; @@ -201,6 +202,11 @@ public class LoanScheduleAssembler { final Integer amortizationType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element); final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType); + + boolean isEqualAmortization = false; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, element); + } // interest terms final Integer interestType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element); @@ -432,6 +438,8 @@ public class LoanScheduleAssembler { HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays); + + return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, @@ -442,7 +450,7 @@ public class LoanScheduleAssembler { recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanProduct.preCloseInterestCalculationStrategy(), calendar, BigDecimal.ZERO, loanTermVariations, isInterestChargedFromDateSameAsDisbursalDateEnabled,numberOfDays, isSkipMeetingOnFirstDay, detailDTO, - allowCompoundingOnEod); + allowCompoundingOnEod, isEqualAmortization); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, @@ -609,13 +617,26 @@ public class LoanScheduleAssembler { List disbursementDetails) { final Set loanCharges = this.loanChargeAssembler.fromParsedJson(element, disbursementDetails); - - final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + final RoundingMode roundingMode = MoneyHelper.getRoundingMode(); final MathContext mc = new MathContext(8, roundingMode); - HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays); + + LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + if (loanApplicationTerms.isEqualAmortization()) { + if (loanApplicationTerms.getInterestMethod().isDecliningBalnce()) { + final LoanScheduleGenerator decliningLoanScheduleGenerator = this.loanScheduleFactory + .create(InterestMethod.DECLINING_BALANCE); + LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO); + + loanApplicationTerms.updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged())); + + } + loanScheduleGenerator = this.loanScheduleFactory.create(InterestMethod.FLAT); + } else { + loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + } return loanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java index 57e0c74aa..d33ea1df1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java @@ -69,7 +69,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { LoanProductConstants.graceOnArrearsAgeingParameterName, LoanApiConstants.createStandingInstructionAtDisbursementParameterName, LoanApiConstants.isFloatingInterestRateParameterName, LoanApiConstants.interestRateDifferentialParameterName, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, - LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables)); + LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam)); private final FromJsonHelper fromApiJsonHelper; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index 198f52405..466ec016c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -37,7 +37,6 @@ import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterExc import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; -import org.apache.fineract.portfolio.client.api.ClientApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; @@ -45,6 +44,7 @@ import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.joda.time.LocalDate; import org.springframework.beans.factory.annotation.Autowired; @@ -93,7 +93,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { LoanApiConstants.linkAccountIdParameterName, LoanApiConstants.disbursementDataParameterName, LoanApiConstants.emiAmountParameterName, LoanApiConstants.maxOutstandingBalanceParameterName, LoanProductConstants.graceOnArrearsAgeingParameterName, LoanApiConstants.createStandingInstructionAtDisbursementParameterName, - LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables)); + LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam)); private final FromJsonHelper fromApiJsonHelper; private final CalculateLoanScheduleQueryFromApiJsonHelper apiJsonHelper; @@ -119,7 +119,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { final String loanTypeParameterName = "loanType"; final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element); baseDataValidator.reset().parameter(loanTypeParameterName).value(loanTypeStr).notNull(); - + if (!StringUtils.isBlank(loanTypeStr)) { final AccountType loanType = AccountType.fromName(loanTypeStr); baseDataValidator.reset().parameter(loanTypeParameterName).value(loanType.getValue()).inMinMaxRange(1, 3); @@ -160,6 +160,15 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } + + boolean isEqualAmortization = false; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, element); + baseDataValidator.reset().parameter(LoanApiConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull() + .validateForBooleanValue(); + if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { throw new EqualAmortizationUnsupportedFeatureException( + "interest.recalculation", "interest recalculation"); } + } final Long productId = this.fromApiJsonHelper.extractLongNamed("productId", element); baseDataValidator.reset().parameter("productId").value(productId).notNull().integerGreaterThanZero(); @@ -235,6 +244,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { .inMinMaxRange(0, 1); if (loanProduct.isLinkedToFloatingInterestRate()) { + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", + "floating interest rate"); } if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { baseDataValidator .reset() @@ -461,6 +472,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { unsupportedParameterList.add(LoanApiConstants.emiAmountParameterName); throw new UnsupportedParameterException(unsupportedParameterList); } + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); } final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAnount).ignoreIfNull().positiveAmount(); @@ -531,6 +543,15 @@ public final class LoanApplicationCommandFromApiJsonHelper { final String accountNo = this.fromApiJsonHelper.extractStringNamed(accountNoParameterName, element); baseDataValidator.reset().parameter(accountNoParameterName).value(accountNo).notBlank().notExceedingLengthOf(20); } + + boolean isEqualAmortization = existingLoanApplication.getLoanProductRelatedDetail().isEqualAmortization(); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element); + baseDataValidator.reset().parameter(LoanProductConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull() + .validateForBooleanValue(); + if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { throw new EqualAmortizationUnsupportedFeatureException( + "interest.recalculation", "interest recalculation"); } + } final String externalIdParameterName = "externalId"; if (this.fromApiJsonHelper.parameterExists(externalIdParameterName, element)) { @@ -632,6 +653,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { } if (loanProduct.isLinkedToFloatingInterestRate()) { + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", + "floating interest rate"); } if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { baseDataValidator .reset() @@ -907,6 +930,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { unsupportedParameterList.add(LoanApiConstants.emiAmountParameterName); throw new UnsupportedParameterException(unsupportedParameterList); } + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); } final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAnount).ignoreIfNull().positiveAmount(); @@ -1076,11 +1100,18 @@ public final class LoanApplicationCommandFromApiJsonHelper { final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.disbursementDataParameterName, element) && expectedDisbursement != null && totalPrincipal != null) { + BigDecimal tatalDisbursement = BigDecimal.ZERO; final JsonArray variationArray = this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName, element); List expectedDisbursementDates = new ArrayList<>(); if (variationArray != null && variationArray.size() > 0) { + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) { + boolean isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, + element); + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("tranche.disbursal", + "tranche disbursal"); } + } int i = 0; do { final JsonObject jsonObject = variationArray.get(i).getAsJsonObject(); 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 ab6c31044..8c2994256 100755 --- 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 @@ -563,7 +563,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, " + " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, " + " l.term_frequency as termFrequency, l.term_period_frequency_enum as termPeriodFrequencyType, " - + " l.amortization_method_enum as amortizationType, l.interest_method_enum as interestType, l.interest_calculated_in_period_enum as interestCalculationPeriodType," + + " l.amortization_method_enum as amortizationType, l.interest_method_enum as interestType, l.is_equal_amortization as isEqualAmortization, l.interest_calculated_in_period_enum as interestCalculationPeriodType," + " l.allow_partial_period_interest_calcualtion as allowPartialPeriodInterestCalcualtion," + " l.loan_status_id as lifeCycleStatusId, l.loan_transaction_strategy_id as transactionStrategyId, " + " lps.name as transactionStrategyName, " @@ -790,7 +790,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final int amortizationTypeInt = JdbcSupport.getInteger(rs, "amortizationType"); final int interestTypeInt = JdbcSupport.getInteger(rs, "interestType"); final int interestCalculationPeriodTypeInt = JdbcSupport.getInteger(rs, "interestCalculationPeriodType"); - + final boolean isEqualAmortization = rs.getBoolean("isEqualAmortization"); final EnumOptionData amortizationType = LoanEnumerations.amortizationType(amortizationTypeInt); final EnumOptionData interestType = LoanEnumerations.interestType(interestTypeInt); final EnumOptionData interestCalculationPeriodType = LoanEnumerations @@ -965,7 +965,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { loanProductCounter, multiDisburseLoan, canDefineInstallmentAmount, fixedEmiAmount, outstandingLoanBalance, inArrears, graceOnArrearsAgeing, isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap, - maximumGap, loanSubStatus, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount); + maximumGap, loanSubStatus, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index 5022d013c..317f7f435 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -132,5 +132,7 @@ public interface LoanProductConstants { public static final String canUseForTopup = "canUseForTopup"; + public static final String isEqualAmortizationParam = "isEqualAmortization"; + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index 80f1c3e20..5a56c5a18 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -91,7 +91,7 @@ public class LoanProductsApiResource { "interestCalculationPeriodTypeOptions", "transactionProcessingStrategyOptions", "chargeOptions", "accountingOptions", "accountingRuleOptions", "accountingMappingOptions", "floatingRateOptions", "isLinkedToFloatingInterestRates", "floatingRatesId", "interestRateDifferential", "minDifferentialLendingRate", "defaultDifferentialLendingRate", - "maxDifferentialLendingRate", "isFloatingInterestRateCalculationAllowed", LoanProductConstants.canUseForTopup)); + "maxDifferentialLendingRate", "isFloatingInterestRateCalculationAllowed", LoanProductConstants.canUseForTopup, LoanProductConstants.isEqualAmortizationParam)); private final Set PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>(Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions")); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index d95fcce0c..31729b321 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -183,6 +183,7 @@ public class LoanProductData { private final Boolean accountMovesOutOfNPAOnlyOnArrearsCompletion; private LoanProductConfigurableAttributes allowAttributeOverrides; private final boolean syncExpectedWithDisbursementDate; + private final boolean isEqualAmortization; /** * Used when returning lookup information about loan product for dropdowns. @@ -258,7 +259,7 @@ public class LoanProductData { final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null; final boolean syncExpectedWithDisbursementDate = false; final boolean canUseForTopup = false; - + final boolean isEqualAmortization = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, @@ -273,7 +274,7 @@ public class LoanProductData { loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, - syncExpectedWithDisbursementDate, canUseForTopup); + syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization); } @@ -350,6 +351,7 @@ public class LoanProductData { final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null; final boolean syncExpectedWithDisbursementDate = false; final boolean canUseForTopup = false; + final boolean isEqualAmortization = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -365,7 +367,7 @@ public class LoanProductData { loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, - syncExpectedWithDisbursementDate, canUseForTopup); + syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization); } @@ -449,6 +451,7 @@ public class LoanProductData { final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null; final boolean syncExpectedWithDisbursementDate = false; final boolean canUseForTopup = false; + final boolean isEqualAmortization = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -464,7 +467,7 @@ public class LoanProductData { installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, - syncExpectedWithDisbursementDate, canUseForTopup); + syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization); } @@ -542,6 +545,7 @@ public class LoanProductData { final LoanProductConfigurableAttributes loanProductConfigurableAttributes = null; final boolean syncExpectedWithDisbursementDate = false; final boolean canUseForTopup = false; + final boolean isEqualAmortization = false; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -557,7 +561,7 @@ public class LoanProductData { installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, - syncExpectedWithDisbursementDate, canUseForTopup); + syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization); } @@ -571,7 +575,7 @@ public class LoanProductData { productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings; return productData; } - + public LoanProductData(final Long id, final String name, final String shortName, final String description, final CurrencyData currency, final BigDecimal principal, final BigDecimal minPrincipal, final BigDecimal maxPrincipal, final BigDecimal tolerance, final Integer numberOfRepayments, final Integer minNumberOfRepayments, final Integer maxNumberOfRepayments, @@ -596,7 +600,7 @@ public class LoanProductData { BigDecimal minDifferentialLendingRate, BigDecimal defaultDifferentialLendingRate, BigDecimal maxDifferentialLendingRate, boolean isFloatingInterestRateCalculationAllowed, final boolean isVariableInstallmentsAllowed, final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments, - final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup) { + final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization) { this.id = id; this.name = name; this.shortName = shortName; @@ -700,6 +704,7 @@ public class LoanProductData { this.preClosureInterestCalculationStrategyOptions = null; this.syncExpectedWithDisbursementDate = syncExpectedWithDisbursementDate; this.canUseForTopup = canUseForTopup; + this.isEqualAmortization = isEqualAmortization; } @@ -836,6 +841,7 @@ public class LoanProductData { this.preClosureInterestCalculationStrategyOptions = preCloseInterestCalculationStrategyOptions; this.syncExpectedWithDisbursementDate = productData.syncExpectedWithDisbursementDate; this.canUseForTopup = productData.canUseForTopup; + this.isEqualAmortization = productData.isEqualAmortization; } private Collection nullIfEmpty(final Collection charges) { @@ -1196,4 +1202,9 @@ public class LoanProductData { public BigDecimal getInterestRateDifferential() { return this.interestRateDifferential; } + + public boolean isEqualAmortization() { + return isEqualAmortization; + } + } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index 05173e455..443dd4f28 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -182,6 +182,9 @@ public class LoanProduct extends AbstractPersistableCustom { @Column(name = "can_use_for_topup", nullable = false) private boolean canUseForTopup = false; + + @Column(name = "is_equal_amortization", nullable = false) + private boolean isEqualAmortization = false; public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy, final List productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate) { @@ -331,6 +334,9 @@ public class LoanProduct extends AbstractPersistableCustom { final boolean canUseForTopup = command.parameterExists(LoanProductConstants.canUseForTopup) ? command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.canUseForTopup) : false; + + final boolean isEqualAmortization = command.parameterExists(LoanProductConstants.isEqualAmortizationParam) ? command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isEqualAmortizationParam) : false; return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, @@ -345,7 +351,7 @@ public class LoanProduct extends AbstractPersistableCustom { installmentAmountInMultiplesOf, loanConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRate, interestRateDifferential, minDifferentialLendingRate, maxDifferentialLendingRate, defaultDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGapBetweenInstallments, - maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup); + maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization); } @@ -575,7 +581,7 @@ public class LoanProduct extends AbstractPersistableCustom { BigDecimal minDifferentialLendingRate, BigDecimal maxDifferentialLendingRate, BigDecimal defaultDifferentialLendingRate, Boolean isFloatingInterestRateCalculationAllowed, final Boolean isVariableInstallmentsAllowed, final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments, - final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup) { + final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization) { this.fund = fund; this.transactionProcessingStrategy = transactionProcessingStrategy; this.name = name.trim(); @@ -607,7 +613,7 @@ public class LoanProduct extends AbstractPersistableCustom { interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, considerPartialPeriodInterest, repayEvery, repaymentFrequencyType, defaultNumberOfInstallments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, - daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled); + daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled, isEqualAmortization); this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings(); @@ -652,6 +658,7 @@ public class LoanProduct extends AbstractPersistableCustom { this.syncExpectedWithDisbursementDate = syncExpectedWithDisbursementDate; this.canUseForTopup = canUseForTopup; + this.isEqualAmortization = isEqualAmortization; } public MonetaryCurrency getCurrency() { @@ -1365,4 +1372,12 @@ public class LoanProduct extends AbstractPersistableCustom { return this.canUseForTopup; } + public boolean isEqualAmortization() { + return isEqualAmortization; + } + + public void setEqualAmortization(boolean isEqualAmortization) { + this.isEqualAmortization = isEqualAmortization; + } + } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index 901967ee8..25ce67d52 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -127,6 +127,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "interest_recalculation_enabled") private boolean isInterestRecalculationEnabled; + + @Column(name = "is_equal_amortization", nullable = false) + private boolean isEqualAmortization = false; public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, @@ -135,13 +138,13 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer numberOfRepayments, final Integer graceOnPrincipalPayment, final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing, - final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled) { + final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization) { return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, repaymentEvery, repaymentPeriodFrequencyType, numberOfRepayments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, - isInterestRecalculationEnabled); + isInterestRecalculationEnabled, isEqualAmortization); } protected LoanProductRelatedDetail() { @@ -155,7 +158,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final Integer repayEvery, final PeriodFrequencyType repaymentFrequencyType, final Integer defaultNumberOfRepayments, final Integer graceOnPrincipalPayment, final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final AmortizationMethod amortizationMethod, final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing, - final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled) { + final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization) { this.currency = currency; this.principal = defaultPrincipal; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; @@ -181,6 +184,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.daysInMonthType = daysInMonthType; this.daysInYearType = daysInYearType; this.isInterestRecalculationEnabled = isInterestRecalculationEnabled; + this.isEqualAmortization = isEqualAmortization; } private Integer defaultToNullIfZero(final Integer value) { @@ -493,6 +497,12 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche actualChanges.put(LoanProductConstants.isInterestRecalculationEnabledParameterName, newValue); this.isInterestRecalculationEnabled = newValue; } + + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.isEqualAmortizationParam, this.isEqualAmortization)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isEqualAmortizationParam); + actualChanges.put(LoanProductConstants.isEqualAmortizationParam, newValue); + this.isEqualAmortization = newValue; + } validateRepaymentPeriodWithGraceSettings(); @@ -648,5 +658,13 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche public boolean isAllowPartialPeriodInterestCalcualtion() { return this.allowPartialPeriodInterestCalcualtion; } + + public boolean isEqualAmortization() { + return isEqualAmortization; + } + + public void setEqualAmortization(boolean isEqualAmortization) { + this.isEqualAmortization = isEqualAmortization; + } } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/exception/EqualAmortizationUnsupportedFeatureException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/exception/EqualAmortizationUnsupportedFeatureException.java new file mode 100644 index 000000000..8df7ec737 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/exception/EqualAmortizationUnsupportedFeatureException.java @@ -0,0 +1,30 @@ +/** + * 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.loanproduct.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +@SuppressWarnings("serial") +public class EqualAmortizationUnsupportedFeatureException extends AbstractPlatformDomainRuleException { + + public EqualAmortizationUnsupportedFeatureException(String code, String property) { + super("error.msg.equal.amortization.does.not.support." + code, "Equal Amortization does not support " + property, property); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 8eb132c46..04bfa7f06 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -45,9 +45,9 @@ import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod; import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProductConfigurableAttributes; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; +import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -108,7 +108,7 @@ public final class LoanProductDataValidator { LoanProductConstants.recalculationRestFrequencyWeekdayParamName, LoanProductConstants.recalculationRestFrequencyNthDayParamName, LoanProductConstants.recalculationRestFrequencyOnDayParamName, LoanProductConstants.isCompoundingToBePostedAsTransactionParamName, LoanProductConstants.allowCompoundingOnEodParamName, - LoanProductConstants.canUseForTopup)); + LoanProductConstants.canUseForTopup, LoanProductConstants.isEqualAmortizationParam)); private static final String[] supportedloanConfigurableAttributes = {LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyIdParamName, @@ -148,6 +148,13 @@ public final class LoanProductDataValidator { final Long fundId = this.fromApiJsonHelper.extractLongNamed("fundId", element); baseDataValidator.reset().parameter("fundId").value(fundId).ignoreIfNull().integerGreaterThanZero(); } + + boolean isEqualAmortization = false; + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element); + baseDataValidator.reset().parameter(LoanProductConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull() + .validateForBooleanValue(); + } if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.minimumDaysBetweenDisbursalAndFirstRepayment, element)) { final Long minimumDaysBetweenDisbursalAndFirstRepayment = this.fromApiJsonHelper.extractLongNamed( @@ -314,6 +321,8 @@ public final class LoanProductDataValidator { if (isInterestRecalculationEnabled != null) { if (isInterestRecalculationEnabled.booleanValue()) { + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation", + "interest recalculation"); } validateInterestRecalculationParams(element, baseDataValidator, null); } } @@ -321,7 +330,9 @@ public final class LoanProductDataValidator { // interest rates if (this.fromApiJsonHelper.parameterExists("isLinkedToFloatingInterestRates", element) && this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element) == true) { - if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", + "floating interest rate"); } + if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { baseDataValidator .reset() .parameter("interestRatePerPeriod") @@ -641,6 +652,13 @@ public final class LoanProductDataValidator { private void validateVariableInstallmentSettings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.allowVariableInstallmentsParamName, element) && this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.allowVariableInstallmentsParamName, element)) { + + boolean isEqualAmortization = false; + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element); + } + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("variable.installment", + "variable installment"); } Long minimumGapBetweenInstallments = null; if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.minimumGapBetweenInstallments, element)) { @@ -721,6 +739,13 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductConstants.multiDisburseLoanParameterName).value(multiDisburseLoan) .ignoreIfNull().validateForBooleanValue(); } + + boolean isEqualAmortization = false; + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element); + } + if (isEqualAmortization && multiDisburseLoan) { throw new EqualAmortizationUnsupportedFeatureException("tranche.disbursal", + "tranche disbursal"); } if (multiDisburseLoan) { if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.outstandingLoanBalanceParameterName, element)) { @@ -1114,6 +1139,13 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductConstants.accountMovesOutOfNPAOnlyOnArrearsCompletionParamName) .value(npaChangeConfig).notNull().isOneOfTheseValues(true, false); } + + boolean isEqualAmortization = loanProduct.isEqualAmortization(); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.isEqualAmortizationParam, element); + baseDataValidator.reset().parameter(LoanProductConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull() + .validateForBooleanValue(); + } // Interest recalculation settings Boolean isInterestRecalculationEnabled = loanProduct.isInterestRecalculationEnabled(); @@ -1126,6 +1158,8 @@ public final class LoanProductDataValidator { if (isInterestRecalculationEnabled != null) { if (isInterestRecalculationEnabled) { + if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation", + "interest recalculation"); } validateInterestRecalculationParams(element, baseDataValidator, loanProduct); } } @@ -1136,6 +1170,9 @@ public final class LoanProductDataValidator { isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed("isLinkedToFloatingInterestRates", element); } if (isLinkedToFloatingInterestRates) { + if(isEqualAmortization){ + throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); + } if (this.fromApiJsonHelper.parameterExists("interestRatePerPeriod", element)) { baseDataValidator .reset() diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index 078d74f89..00808fa84 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -227,7 +227,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, " + "lvi.maximum_gap as maximumGap, " - + "lp.can_use_for_topup as canUseForTopup " + + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization " + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id " @@ -308,7 +308,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final int amortizationTypeId = JdbcSupport.getInteger(rs, "amortizationMethod"); final EnumOptionData amortizationType = LoanEnumerations.amortizationType(amortizationTypeId); - + final boolean isEqualAmortization = rs.getBoolean("isEqualAmortization"); + final Integer interestRateFrequencyTypeId = JdbcSupport.getInteger(rs, "interestRatePerPeriodFreq"); final EnumOptionData interestRateFrequencyType = LoanEnumerations.interestRateFrequencyType(interestRateFrequencyTypeId); @@ -464,7 +465,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo installmentAmountInMultiplesOf, allowAttributeOverrides, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap, - maximumGap, syncExpectedWithDisbursementDate, canUseForTopup); + maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization); } } diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V337__equal_amortization.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V337__equal_amortization.sql new file mode 100644 index 000000000..a32ccf44e --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V337__equal_amortization.sql @@ -0,0 +1,21 @@ +-- +-- 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. +-- + +ALTER TABLE `m_product_loan` ADD COLUMN `is_equal_amortization` TINYINT(1) NOT NULL DEFAULT '0'; +ALTER TABLE `m_loan` ADD COLUMN `is_equal_amortization` TINYINT(1) NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java index c42957838..c202ae99f 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/LoanProductRelatedDetailTestHelper.java @@ -153,10 +153,10 @@ public class LoanProductRelatedDetailTestHelper { final Integer daysInYearType = DaysInYearType.ACTUAL.getValue(); final boolean isInterestRecalculationEnabled = false; final boolean considerPartialPeriodInterest = false; - + final boolean isEqualAmortization = false; return new LoanProductRelatedDetail(currency, defaultPrincipal, defaultNominalInterestRatePerPeriod, interestPeriodFrequencyType, defaultAnnualNominalInterestRate, interestMethod, interestCalculationPeriodMethod, considerPartialPeriodInterest, repayEvery, repaymentFrequencyType, defaultNumberOfRepayments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, - amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled); + amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, isEqualAmortization); } } \ No newline at end of file