FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans
This commit is contained in:
+71
-18
@@ -344,6 +344,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
|
||||
case REAGE -> handleReAge(loanTransaction, ctx);
|
||||
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx);
|
||||
case CAPITALIZED_INCOME -> handleCapitalizedIncome(loanTransaction, ctx);
|
||||
// TODO: Cover rest of the transaction types
|
||||
default -> log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||
}
|
||||
@@ -1199,18 +1200,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
emiCalculator.addDisbursement(model, transactionDate, amortizableAmount);
|
||||
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
model.repaymentPeriods().forEach(rm -> {
|
||||
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
|
||||
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
|
||||
.findFirst().orElse(null);
|
||||
if (installment != null) {
|
||||
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
|
||||
installment.updateInterestCharged(rm.getDueInterest().getAmount());
|
||||
installment.updateObligationsMet(currency, transactionDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
@@ -1222,7 +1212,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(currency);
|
||||
@@ -1240,7 +1229,73 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
||||
|
||||
recalculateRepaymentInstallmentsWithoutEMICalculation(disbursementTransaction, amortizableAmount, candidateRepaymentInstallments,
|
||||
currency, installmentAmountInMultiplesOf);
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
|
||||
if (capitalizedIncomeTransaction.getLoan().isInterestBearing()) {
|
||||
handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx);
|
||||
} else {
|
||||
handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCapitalizedIncomeWithEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
|
||||
ProgressiveLoanInterestScheduleModel model;
|
||||
if (!(transactionCtx instanceof ProgressiveTransactionCtx)
|
||||
|| (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) {
|
||||
throw new IllegalStateException("TransactionCtx has no model");
|
||||
}
|
||||
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
||||
LocalDate transactionDate = capitalizedIncomeTransaction.getTransactionDate();
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
|
||||
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
|
||||
emiCalculator.addCapitalizedIncome(model, transactionDate, amortizableAmount);
|
||||
|
||||
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
|
||||
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void handleCapitalizedIncomeWithoutEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||
i -> i.getDueDate().isAfter(capitalizedIncomeTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
Integer installmentAmountInMultiplesOf = capitalizedIncomeTransaction.getLoan().getLoanProduct()
|
||||
.getInstallmentAmountInMultiplesOf();
|
||||
|
||||
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
|
||||
|
||||
recalculateRepaymentInstallmentsWithoutEMICalculation(capitalizedIncomeTransaction, amortizableAmount,
|
||||
candidateRepaymentInstallments, currency, installmentAmountInMultiplesOf);
|
||||
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void recalculateRepaymentPeriodsWithEMICalculation(Money amortizableAmount, ProgressiveLoanInterestScheduleModel model,
|
||||
List<LoanRepaymentScheduleInstallment> installments, LocalDate transactionDate, MonetaryCurrency currency) {
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
model.repaymentPeriods().forEach(rm -> {
|
||||
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
|
||||
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
|
||||
.findFirst().orElse(null);
|
||||
if (installment != null) {
|
||||
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
|
||||
installment.updateInterestCharged(rm.getDueInterest().getAmount());
|
||||
installment.updateObligationsMet(currency, transactionDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void recalculateRepaymentInstallmentsWithoutEMICalculation(LoanTransaction loanTransaction, Money amortizableAmount,
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments, MonetaryCurrency currency,
|
||||
Integer installmentAmountInMultiplesOf) {
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
|
||||
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
||||
|
||||
@@ -1252,14 +1307,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
i.updatePrincipal(newPrincipal.getAmount());
|
||||
moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal));
|
||||
i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate());
|
||||
i.updateObligationsMet(currency, loanTransaction.getTransactionDate());
|
||||
});
|
||||
// Hence the rounding, we might need to amend the last installment amount
|
||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
|
||||
.addToPrincipal(disbursementTransaction.getTransactionDate(), moneyHolder.getMoneyObject());
|
||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1).addToPrincipal(loanTransaction.getTransactionDate(),
|
||||
moneyHolder.getMoneyObject());
|
||||
}
|
||||
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
|
||||
+7
@@ -20,13 +20,20 @@ package org.apache.fineract.portfolio.loanaccount.repository;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface LoanCapitalizedIncomeBalanceRepository
|
||||
extends JpaRepository<LoanCapitalizedIncomeBalance, Long>, JpaSpecificationExecutor<LoanCapitalizedIncomeBalance> {
|
||||
|
||||
String FIND_BALANCE_REPAYMENT_SCHEDULE_DATA = "SELECT new org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData(lcib.loanTransaction.id, lcib.loan.id, lcib.loanTransaction.dateOf, lcib.loanTransaction.reversed, lcib.amount, lcib.unrecognizedAmount, lcib.loanTransaction.feeChargesPortion) FROM LoanCapitalizedIncomeBalance lcib ";
|
||||
|
||||
List<LoanCapitalizedIncomeBalance> findAllByLoanId(Long loanId);
|
||||
|
||||
LoanCapitalizedIncomeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId);
|
||||
|
||||
@Query(FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + " WHERE lcib.loan.id = :loanId")
|
||||
List<LoanTransactionRepaymentPeriodData> findRepaymentPeriodDataByLoanId(Long loanId);
|
||||
}
|
||||
|
||||
+13
-2
@@ -30,6 +30,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
@@ -54,6 +55,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
private final NoteWritePlatformService noteWritePlatformService;
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
|
||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -74,9 +76,10 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
final Money capitalizedIncomeAmount = calculateCapitalizedIncomeAmount(loan, transactionAmount);
|
||||
final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail,
|
||||
transactionDate, txnExternalId);
|
||||
// TODO: Transaction reverse-replay + loan repayment schedule update (follow-up story)
|
||||
// Update loan with capitalized income
|
||||
loan.addLoanTransaction(capitalizedIncomeTransaction);
|
||||
// Recalculate loan transactions
|
||||
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction);
|
||||
// Create capitalized income balances
|
||||
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
|
||||
// Save and flush (PK is set)
|
||||
@@ -88,7 +91,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
// Create a note if provided
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
if (noteText != null && !noteText.isEmpty()) {
|
||||
noteWritePlatformService.createLoanNote(loanId, noteText);
|
||||
noteWritePlatformService.createLoanTransactionNote(capitalizedIncomeTransaction.getId(), noteText);
|
||||
}
|
||||
// Post journal entries
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
@@ -99,6 +102,14 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
.build();
|
||||
}
|
||||
|
||||
private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction capitalizedIncomeTransaction) {
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
|
||||
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||
} else {
|
||||
reprocessLoanTransactionsService.processLatestTransaction(capitalizedIncomeTransaction, loan);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetBalance(final Long loanId) {
|
||||
capitalizedIncomeBalanceRepository
|
||||
|
||||
+4
-2
@@ -30,6 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -45,10 +46,11 @@ public class ProgressiveLoanAccountConfiguration {
|
||||
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
|
||||
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
|
||||
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
|
||||
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository) {
|
||||
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
|
||||
ReprocessLoanTransactionsService reprocessLoanTransactionsService) {
|
||||
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
|
||||
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
|
||||
capitalizedIncomeBalanceRepository);
|
||||
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+6
@@ -65,6 +65,12 @@ public interface EMICalculator {
|
||||
*/
|
||||
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
|
||||
|
||||
/**
|
||||
* Applies the capitalized income transaction on the interest model. This method recalculates the EMI amounts from
|
||||
* the action date.
|
||||
*/
|
||||
void addCapitalizedIncome(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDueDate, Money transactionAmount);
|
||||
|
||||
/**
|
||||
* Applies the interest rate change on the interest model. This method recalculates the EMI amounts from the action
|
||||
* date.
|
||||
|
||||
+29
-6
@@ -119,7 +119,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||
scheduleModel
|
||||
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
|
||||
scheduleModel.zero())
|
||||
scheduleModel.zero(), scheduleModel.zero())
|
||||
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
|
||||
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
|
||||
operation));
|
||||
@@ -140,6 +140,24 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
return changedRepaymentPeriod.getDueDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add capitalized income to Interest Period
|
||||
*/
|
||||
@Override
|
||||
public void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate transactionDueDate,
|
||||
final Money transactionAmount) {
|
||||
addCapitalizedIncome(scheduleModel, EmiChangeOperation.capitalizedIncome(transactionDueDate, transactionAmount));
|
||||
}
|
||||
|
||||
private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(),
|
||||
scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> {
|
||||
calculateEMIValueAndRateFactors(
|
||||
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
|
||||
operation);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
|
||||
final BigDecimal newInterestRate) {
|
||||
@@ -151,7 +169,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate());
|
||||
scheduleModel
|
||||
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
|
||||
scheduleModel.zero())
|
||||
scheduleModel.zero(), scheduleModel.zero())
|
||||
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
|
||||
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
|
||||
operation));
|
||||
@@ -160,8 +178,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
@Override
|
||||
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
|
||||
Money balanceCorrectionAmount) {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount)
|
||||
.ifPresent(repaymentPeriod -> {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount,
|
||||
scheduleModel.zero()).ifPresent(repaymentPeriod -> {
|
||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||
@@ -237,7 +255,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
|
||||
private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount,
|
||||
Money creditedInterestAmount) {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount)
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount, scheduleModel.zero())
|
||||
.ifPresent(repaymentPeriod -> {
|
||||
addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount);
|
||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||
@@ -442,8 +460,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100
|
||||
.plus(scheduleModel.getTotalCreditedPrincipal(), mc); //
|
||||
Money totalCapitalizedIncome = scheduleModel.repaymentPeriods().stream()
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCapitalizedIncomePrincipal))
|
||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100
|
||||
|
||||
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
|
||||
Money diff = totalDisbursedAmount.plus(totalCapitalizedIncome, mc).plus(totalDueInterest, mc).minus(totalEMI, mc);
|
||||
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
||||
.reduce((first, second) -> second);
|
||||
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
|
||||
@@ -809,7 +830,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
return;
|
||||
}
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts();
|
||||
|
||||
addDisbursement(scheduleModelCopy, operation.withZeroAmount());
|
||||
addCapitalizedIncome(scheduleModelCopy, operation.withZeroAmount());
|
||||
|
||||
final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
|
||||
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
|
||||
|
||||
+7
-2
@@ -31,7 +31,8 @@ public class EmiChangeOperation {
|
||||
|
||||
public enum Action { //
|
||||
DISBURSEMENT, //
|
||||
INTEREST_RATE_CHANGE //
|
||||
INTEREST_RATE_CHANGE, //
|
||||
CAPITALIZED_INCOME //
|
||||
}
|
||||
|
||||
private final Action action;
|
||||
@@ -48,8 +49,12 @@ public class EmiChangeOperation {
|
||||
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate);
|
||||
}
|
||||
|
||||
public static EmiChangeOperation capitalizedIncome(final LocalDate transactionDueDate, final Money transactionAmount) {
|
||||
return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null);
|
||||
}
|
||||
|
||||
public EmiChangeOperation withZeroAmount() {
|
||||
if (action == Action.DISBURSEMENT) {
|
||||
if (action == Action.DISBURSEMENT || action == Action.CAPITALIZED_INCOME) {
|
||||
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null);
|
||||
}
|
||||
return null;
|
||||
|
||||
+15
-7
@@ -62,6 +62,8 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
private Money balanceCorrectionAmount;
|
||||
private Money outstandingLoanBalance;
|
||||
|
||||
private Money capitalizedIncomePrincipal;
|
||||
|
||||
@JsonExclude
|
||||
private final MathContext mc;
|
||||
private final boolean isPaused;
|
||||
@@ -70,38 +72,38 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
||||
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
||||
interestPeriod.getOutstandingLoanBalance(), mc, interestPeriod.isPaused());
|
||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), mc, interestPeriod.isPaused());
|
||||
}
|
||||
|
||||
public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) {
|
||||
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, mc, false);
|
||||
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, null, mc, false);
|
||||
}
|
||||
|
||||
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
|
||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
||||
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), interestPeriod.isPaused());
|
||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), interestPeriod.getMc(), interestPeriod.isPaused());
|
||||
}
|
||||
|
||||
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||
LocalDate dueDate) {
|
||||
final Money zero = repaymentPeriod.getEmi().zero();
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
|
||||
zero.getMc(), false);
|
||||
}
|
||||
|
||||
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate,
|
||||
boolean isPaused) {
|
||||
final Money zero = repaymentPeriod.getEmi().zero();
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
|
||||
zero.getMc(), isPaused);
|
||||
}
|
||||
|
||||
public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||
LocalDate dueDate) {
|
||||
final Money zero = repaymentPeriod.getEmi().zero();
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
|
||||
zero.getMc(), true);
|
||||
}
|
||||
|
||||
@@ -126,6 +128,10 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
this.creditedInterest = MathUtil.plus(this.creditedInterest, creditedInterest, mc);
|
||||
}
|
||||
|
||||
public void addCapitalizedIncomePrincipalAmount(final Money capitalizedIncomePrincipal) {
|
||||
this.capitalizedIncomePrincipal = MathUtil.plus(this.capitalizedIncomePrincipal, capitalizedIncomePrincipal, mc);
|
||||
}
|
||||
|
||||
public BigDecimal getCalculatedDueInterest() {
|
||||
if (isPaused) {
|
||||
return creditedInterest.getAmount();
|
||||
@@ -156,6 +162,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod();
|
||||
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance()//
|
||||
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
|
||||
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc)//
|
||||
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
|
||||
.minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)//
|
||||
.plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc), mc);//
|
||||
@@ -165,6 +172,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1);
|
||||
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance() //
|
||||
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
||||
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
|
||||
.plus(previousInterestPeriod.getDisbursementAmount(), mc)); //
|
||||
}
|
||||
}
|
||||
@@ -173,7 +181,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
* Include principal like amounts (all disbursement amount + credited principal)
|
||||
*/
|
||||
public Money getCreditedAmounts() {
|
||||
return getDisbursementAmount().plus(getCreditedPrincipal(), mc);
|
||||
return getDisbursementAmount().plus(getCreditedPrincipal(), mc).plus(getCapitalizedIncomePrincipal(), mc);
|
||||
}
|
||||
|
||||
public boolean isFirstInterestPeriod() {
|
||||
|
||||
+8
-5
@@ -175,9 +175,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
|
||||
public Optional<RepaymentPeriod> changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate,
|
||||
final Money disbursedAmount, final Money correctionAmount) {
|
||||
final Money disbursedAmount, final Money correctionAmount, final Money capitalizedIncomePrincipal) {
|
||||
return findRepaymentPeriodForBalanceChange(balanceChangeDate).stream()//
|
||||
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))//
|
||||
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount,
|
||||
capitalizedIncomePrincipal))//
|
||||
.findFirst();//
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
|
||||
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
|
||||
final Money correctionAmount) {
|
||||
final Money correctionAmount, final Money capitalizedIncomePrincipal) {
|
||||
return repaymentPeriod -> {
|
||||
final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod)
|
||||
&& balanceChangeDate.isEqual(repaymentPeriod.getDueDate());
|
||||
@@ -222,9 +223,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
isChangeOnMaturityDate);
|
||||
if (interestPeriodOptional.isPresent()) {
|
||||
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
|
||||
interestPeriodOptional.get().addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
|
||||
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
|
||||
} else {
|
||||
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount);
|
||||
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount, capitalizedIncomePrincipal);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -246,7 +248,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
|
||||
void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate, final Money disbursedAmount,
|
||||
final Money correctionAmount) {
|
||||
final Money correctionAmount, Money capitalizedIncomePrincipal) {
|
||||
final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, balanceChangeDate);
|
||||
final LocalDate originalDueDate = previousInterestPeriod.getDueDate();
|
||||
final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, balanceChangeDate);
|
||||
@@ -254,6 +256,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
|
||||
previousInterestPeriod.setDueDate(newDueDate);
|
||||
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
|
||||
previousInterestPeriod.addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
|
||||
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
|
||||
|
||||
final InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(repaymentPeriod, newDueDate, originalDueDate, isPaused);
|
||||
|
||||
+16
-1
@@ -225,6 +225,17 @@ public final class RepaymentPeriod {
|
||||
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); //
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum of capitalized income principals
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getCapitalizedIncomePrincipal() {
|
||||
return MathUtil.negativeToZero(interestPeriods.stream() //
|
||||
.map(InterestPeriod::getCapitalizedIncomePrincipal) //
|
||||
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); //
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back due principal + credited principal or paid principal
|
||||
*
|
||||
@@ -278,6 +289,7 @@ public final class RepaymentPeriod {
|
||||
InterestPeriod lastInterestPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1);
|
||||
Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() //
|
||||
.plus(lastInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
||||
.plus(lastInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
|
||||
.plus(lastInterestPeriod.getDisbursementAmount(), mc) //
|
||||
.minus(getDuePrincipal(), mc)//
|
||||
.plus(getPaidPrincipal(), mc);//
|
||||
@@ -305,7 +317,10 @@ public final class RepaymentPeriod {
|
||||
Money totalDisbursedAmount = getInterestPeriods().stream() //
|
||||
.map(InterestPeriod::getDisbursementAmount) //
|
||||
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
|
||||
return initialBalance.add(totalDisbursedAmount, mc);
|
||||
Money totalCapitalizedIncomeAmount = getInterestPeriods().stream() //
|
||||
.map(InterestPeriod::getCapitalizedIncomePrincipal) //
|
||||
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
|
||||
return initialBalance.add(totalDisbursedAmount, mc).add(totalCapitalizedIncomeAmount, mc);
|
||||
}
|
||||
|
||||
private Money getZero(MathContext mc) {
|
||||
|
||||
Reference in New Issue
Block a user