FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans
This commit is contained in:
+2
-3
@@ -30,7 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public final class DisbursementData implements Comparable<DisbursementData> {
|
||||
public final class DisbursementData implements LoanPrincipalRelatedDataHolder, Comparable<DisbursementData> {
|
||||
|
||||
private final Long id;
|
||||
private final LocalDate expectedDisbursementDate;
|
||||
@@ -102,8 +102,7 @@ public final class DisbursementData implements Comparable<DisbursementData> {
|
||||
|
||||
private boolean occursOnDayFromAndIncludingAndUpTo(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
|
||||
final LocalDate target) {
|
||||
return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive))
|
||||
&& DateUtils.isBefore(target, upToNotInclusive);
|
||||
return DateUtils.isDateInRangeFromInclusiveToExclusive(fromInclusive, upToNotInclusive, target);
|
||||
}
|
||||
|
||||
public BigDecimal getWaivedChargeAmount() {
|
||||
|
||||
+21
@@ -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.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.data;
|
||||
|
||||
public interface LoanPrincipalRelatedDataHolder {}
|
||||
+2
@@ -34,6 +34,8 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
public class LoanSummaryData {
|
||||
|
||||
private final CurrencyData currency;
|
||||
private final BigDecimal totalPrincipal;
|
||||
private final BigDecimal totalPrincipalFromCapitalizedIncome;
|
||||
private final BigDecimal principalDisbursed;
|
||||
private final BigDecimal principalAdjustments;
|
||||
private final BigDecimal principalPaid;
|
||||
|
||||
+17
-4
@@ -38,6 +38,12 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
public class LoanSummary {
|
||||
|
||||
// derived totals fields
|
||||
@Column(name = "total_principal_derived", scale = 6, precision = 19)
|
||||
private BigDecimal totalPrincipal;
|
||||
|
||||
@Column(name = "capitalized_income_derived", scale = 6, precision = 19)
|
||||
private BigDecimal totalPrincipalFromCapitalizedIncome;
|
||||
|
||||
@Column(name = "principal_disbursed_derived", scale = 6, precision = 19)
|
||||
private BigDecimal totalPrincipalDisbursed;
|
||||
|
||||
@@ -166,6 +172,8 @@ public class LoanSummary {
|
||||
this.totalPenaltyChargesWaived = BigDecimal.ZERO;
|
||||
this.totalPenaltyChargesWrittenOff = BigDecimal.ZERO;
|
||||
this.totalPrincipalAdjustments = BigDecimal.ZERO;
|
||||
this.totalPrincipal = BigDecimal.ZERO;
|
||||
this.totalPrincipalFromCapitalizedIncome = BigDecimal.ZERO;
|
||||
this.totalPrincipalDisbursed = BigDecimal.ZERO;
|
||||
this.totalPrincipalOutstanding = BigDecimal.ZERO;
|
||||
this.totalPrincipalRepaid = BigDecimal.ZERO;
|
||||
@@ -176,17 +184,22 @@ public class LoanSummary {
|
||||
}
|
||||
|
||||
public void updateSummary(final MonetaryCurrency currency, final Money principal,
|
||||
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges) {
|
||||
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges,
|
||||
List<LoanTransaction> capitalizedIncomeTransactions) {
|
||||
final BigDecimal principalFromCapitalizedIncome = capitalizedIncomeTransactions.stream().filter(LoanTransaction::isNotReversed)
|
||||
.map(LoanTransaction::getPrincipalPortion).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
this.totalPrincipalDisbursed = principal.getAmount();
|
||||
this.totalPrincipalFromCapitalizedIncome = principalFromCapitalizedIncome;
|
||||
this.totalPrincipal = principal.plus(principalFromCapitalizedIncome).getAmount();
|
||||
this.totalPrincipalAdjustments = calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalFeeAdjustments = calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalPenaltyAdjustments = calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalPrincipalRepaid = calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalPrincipalWrittenOff = calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency).getAmount();
|
||||
|
||||
this.totalPrincipalOutstanding = principal.plus(this.totalPrincipalAdjustments).minus(this.totalPrincipalRepaid)
|
||||
.minus(this.totalPrincipalWrittenOff).getAmount();
|
||||
this.totalPrincipalOutstanding = principal.plus(principalFromCapitalizedIncome).plus(this.totalPrincipalAdjustments)
|
||||
.minus(this.totalPrincipalRepaid).minus(this.totalPrincipalWrittenOff).getAmount();
|
||||
|
||||
final Money totalInterestCharged = calculateTotalInterestCharged(repaymentScheduleInstallments, currency);
|
||||
this.totalInterestCharged = totalInterestCharged.getAmount();
|
||||
@@ -225,7 +238,7 @@ public class LoanSummary {
|
||||
this.totalPenaltyChargesOutstanding = totalPenaltyChargesCharged.minus(this.totalPenaltyChargesRepaid)
|
||||
.minus(this.totalPenaltyChargesWaived).minus(this.totalPenaltyChargesWrittenOff).getAmount();
|
||||
|
||||
final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipalDisbursed).plus(this.totalInterestCharged)
|
||||
final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipal).plus(this.totalInterestCharged)
|
||||
.plus(this.totalFeeChargesCharged).plus(this.totalPenaltyChargesCharged);
|
||||
this.totalExpectedRepayment = totalExpectedRepayment.getAmount();
|
||||
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanPrincipalRelatedDataHolder;
|
||||
|
||||
/**
|
||||
* Immutable data object representing a subset of loan transaction data.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
public class LoanTransactionRepaymentPeriodData implements LoanPrincipalRelatedDataHolder, Serializable {
|
||||
|
||||
private final Long transactionId;
|
||||
private final Long loanId;
|
||||
private final LocalDate date;
|
||||
private final boolean reversed;
|
||||
private final BigDecimal amount;
|
||||
private final BigDecimal unrecognizedAmount;
|
||||
private final BigDecimal feeChargesPortion;
|
||||
|
||||
public LoanTransactionRepaymentPeriodData(Long transactionId, Long loanId, LocalDate date, boolean reversed, BigDecimal amount,
|
||||
BigDecimal unrecognizedAmount, BigDecimal feeChargesPortion) {
|
||||
this.transactionId = transactionId;
|
||||
this.loanId = loanId;
|
||||
this.date = date;
|
||||
this.reversed = reversed;
|
||||
this.amount = amount;
|
||||
this.unrecognizedAmount = unrecognizedAmount;
|
||||
this.feeChargesPortion = feeChargesPortion;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -104,7 +104,10 @@ public final class LoanRefundValidator {
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) {
|
||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
||||
final BigDecimal totalPrincipalFromCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream()
|
||||
.filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalPrincipalFromCapitalizedIncome);
|
||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
|
||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
|
||||
|
||||
+2
-1
@@ -97,7 +97,8 @@ public class LoanBalanceService {
|
||||
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
|
||||
|
||||
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
||||
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges());
|
||||
List<LoanTransaction> capitalizedIncomeTransactions = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome);
|
||||
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(), capitalizedIncomeTransactions);
|
||||
loan.updateLoanOutstandingBalances();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -173,7 +173,10 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
||||
final BigDecimal totalPrincipalFromCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream()
|
||||
.filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalPrincipalFromCapitalizedIncome);
|
||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
|
||||
&& loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
|
||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||
|
||||
+3
-1
@@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
||||
@@ -50,7 +51,8 @@ public interface LoanReadPlatformService {
|
||||
LoanAccountData fetchRepaymentScheduleData(LoanAccountData accountData);
|
||||
|
||||
LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData,
|
||||
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType);
|
||||
Collection<DisbursementData> disbursementData, Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData,
|
||||
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType);
|
||||
|
||||
Collection<LoanTransactionData> retrieveLoanTransactions(Long loanId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user