FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans - E2E tests
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.infrastructure.core.persistence;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.FlushModeType;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class FlushModeHandler {
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
public void withFlushMode(FlushModeType flushMode, Runnable runnable) {
|
||||
FlushModeType original = entityManager.getFlushMode();
|
||||
try {
|
||||
entityManager.setFlushMode(flushMode);
|
||||
runnable.run();
|
||||
} finally {
|
||||
entityManager.setFlushMode(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1222
-33
File diff suppressed because it is too large
Load Diff
+25
-20
@@ -18,10 +18,12 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import jakarta.persistence.FlushModeType;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
@@ -40,6 +42,7 @@ import org.springframework.stereotype.Service;
|
||||
public class LoanBalanceService {
|
||||
|
||||
private final CapitalizedIncomeBalanceService capitalizedIncomeBalanceService;
|
||||
private final FlushModeHandler flushModeHandler;
|
||||
|
||||
public Money calculateTotalOverpayment(final Loan loan) {
|
||||
Money totalPaidInRepayments = loan.getTotalPaidInRepayments();
|
||||
@@ -85,28 +88,30 @@ public class LoanBalanceService {
|
||||
}
|
||||
|
||||
public void updateLoanSummaryDerivedFields(final Loan loan) {
|
||||
if (loan.isNotDisbursed()) {
|
||||
if (loan.getSummary() != null) {
|
||||
loan.getSummary().zeroFields();
|
||||
}
|
||||
loan.setTotalOverpaid(null);
|
||||
} else {
|
||||
final Money overpaidBy = calculateTotalOverpayment(loan);
|
||||
loan.setTotalOverpaid(null);
|
||||
if (!overpaidBy.isLessThanZero()) {
|
||||
loan.setTotalOverpaid(overpaidBy.getAmountDefaultedToNullIfZero());
|
||||
}
|
||||
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
|
||||
if (loan.isNotDisbursed()) {
|
||||
if (loan.getSummary() != null) {
|
||||
loan.getSummary().zeroFields();
|
||||
}
|
||||
loan.setTotalOverpaid(null);
|
||||
} else {
|
||||
final Money overpaidBy = calculateTotalOverpayment(loan);
|
||||
loan.setTotalOverpaid(null);
|
||||
if (!overpaidBy.isLessThanZero()) {
|
||||
loan.setTotalOverpaid(overpaidBy.getAmountDefaultedToNullIfZero());
|
||||
}
|
||||
|
||||
final Money recoveredAmount = loan.calculateTotalRecoveredPayments();
|
||||
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
|
||||
final Money recoveredAmount = loan.calculateTotalRecoveredPayments();
|
||||
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
|
||||
|
||||
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
||||
final Money capitalizedIncome = capitalizedIncomeBalanceService.calculateCapitalizedIncome(loan);
|
||||
final Money capitalizedIncomeAdjustment = capitalizedIncomeBalanceService.calculateCapitalizedIncomeAdjustment(loan);
|
||||
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(),
|
||||
capitalizedIncome, capitalizedIncomeAdjustment);
|
||||
loan.updateLoanOutstandingBalances();
|
||||
}
|
||||
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
||||
final Money capitalizedIncome = capitalizedIncomeBalanceService.calculateCapitalizedIncome(loan);
|
||||
final Money capitalizedIncomeAdjustment = capitalizedIncomeBalanceService.calculateCapitalizedIncomeAdjustment(loan);
|
||||
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(),
|
||||
loan.getLoanCharges(), capitalizedIncome, capitalizedIncomeAdjustment);
|
||||
loan.updateLoanOutstandingBalances();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateLoanToLastDisbursalState(final Loan loan, final LoanDisbursementDetails disbursementDetail) {
|
||||
|
||||
+1
-3
@@ -81,8 +81,6 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
loan.addLoanTransaction(capitalizedIncomeTransaction);
|
||||
// Recalculate loan transactions
|
||||
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction);
|
||||
// Create capitalized income balances
|
||||
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
|
||||
// Save and flush (PK is set)
|
||||
loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction);
|
||||
// Create capitalized income balances
|
||||
@@ -132,6 +130,6 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
capitalizedIncomeBalance.setDate(capitalizedIncomeTransaction.getTransactionDate());
|
||||
capitalizedIncomeBalance.setAmount(capitalizedIncomeTransaction.getAmount());
|
||||
capitalizedIncomeBalance.setUnrecognizedAmount(capitalizedIncomeTransaction.getAmount());
|
||||
capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance);
|
||||
capitalizedIncomeBalanceRepository.saveAndFlush(capitalizedIncomeBalance);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-39
@@ -18,14 +18,13 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.FlushModeType;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
@@ -47,8 +46,7 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
|
||||
private final ProgressiveLoanModelRepository loanModelRepository;
|
||||
private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService;
|
||||
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
|
||||
@PersistenceContext
|
||||
private final EntityManager entityManager;
|
||||
private final FlushModeHandler flushModeHandler;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -57,9 +55,7 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
|
||||
return null;
|
||||
}
|
||||
String jsonModel = progressiveLoanInterestScheduleModelParserService.toJson(model);
|
||||
try {
|
||||
// Note: FlushMode set because otherwise it could cause a concurrent writing exception
|
||||
entityManager.setFlushMode(FlushModeType.COMMIT);
|
||||
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
|
||||
ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> {
|
||||
ProgressiveLoanModel plm = new ProgressiveLoanModel();
|
||||
plm.setLoan(loan);
|
||||
@@ -69,15 +65,17 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
|
||||
progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime());
|
||||
progressiveLoanModel.setJsonModel(jsonModel);
|
||||
loanModelRepository.save(progressiveLoanModel);
|
||||
} finally {
|
||||
entityManager.setFlushMode(FlushModeType.AUTO);
|
||||
}
|
||||
});
|
||||
return jsonModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId) {
|
||||
return loanModelRepository.findOneByLoanId(loanId);
|
||||
final Optional[] progressiveLoanModel = new Optional[1];
|
||||
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
|
||||
progressiveLoanModel[0] = loanModelRepository.findOneByLoanId(loanId);
|
||||
});
|
||||
return progressiveLoanModel[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,40 +88,27 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
|
||||
|
||||
@Override
|
||||
public boolean hasValidModelForDate(Long loanId, LocalDate targetDate) {
|
||||
try {
|
||||
// Note: FlushMode set because an unwanted flush caused reverse replay new transactions
|
||||
entityManager.setFlushMode(FlushModeType.COMMIT);
|
||||
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loanId);
|
||||
LocalDate modelUpdatedTilDate = progressiveLoanModel.map(ProgressiveLoanModel::getBusinessDate).orElse(null);
|
||||
return progressiveLoanModel.isPresent() && !targetDate.isBefore(modelUpdatedTilDate);
|
||||
} finally {
|
||||
entityManager.setFlushMode(FlushModeType.AUTO);
|
||||
}
|
||||
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loanId);
|
||||
LocalDate modelUpdatedTilDate = progressiveLoanModel.map(ProgressiveLoanModel::getBusinessDate).orElse(null);
|
||||
return progressiveLoanModel.isPresent() && !targetDate.isBefore(modelUpdatedTilDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, LocalDate businessDate) {
|
||||
try {
|
||||
// Note: FlushMode set because an unwanted flush caused reverse replay new transactions
|
||||
entityManager.setFlushMode(FlushModeType.COMMIT);
|
||||
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loan.getId());
|
||||
Optional<ProgressiveLoanInterestScheduleModel> savedModel;
|
||||
if (progressiveLoanModel.isPresent() && !progressiveLoanModel.get().getBusinessDate().isAfter(businessDate)) {
|
||||
savedModel = extractModel(progressiveLoanModel);
|
||||
if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) {
|
||||
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
|
||||
new ChangedTransactionDetail(), savedModel.get());
|
||||
ctx.setChargedOff(loan.isChargedOff());
|
||||
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(businessDate, ctx);
|
||||
}
|
||||
} else {
|
||||
savedModel = Optional.empty();
|
||||
Optional<ProgressiveLoanModel> progressiveLoanModel = findOneByLoanId(loan.getId());
|
||||
Optional<ProgressiveLoanInterestScheduleModel> savedModel;
|
||||
if (progressiveLoanModel.isPresent() && !progressiveLoanModel.get().getBusinessDate().isAfter(businessDate)) {
|
||||
savedModel = extractModel(progressiveLoanModel);
|
||||
if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) {
|
||||
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
|
||||
Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get());
|
||||
ctx.setChargedOff(loan.isChargedOff());
|
||||
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(businessDate, ctx);
|
||||
}
|
||||
return savedModel;
|
||||
} finally {
|
||||
entityManager.setFlushMode(FlushModeType.AUTO);
|
||||
} else {
|
||||
savedModel = Optional.empty();
|
||||
}
|
||||
return savedModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -255,8 +255,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
|
||||
private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount,
|
||||
Money creditedInterestAmount) {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount, scheduleModel.zero())
|
||||
.ifPresent(repaymentPeriod -> {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount,
|
||||
scheduleModel.zero()).ifPresent(repaymentPeriod -> {
|
||||
addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount);
|
||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
|
||||
+3
-2
@@ -76,14 +76,15 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
}
|
||||
|
||||
public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) {
|
||||
return new InterestPeriod(repaymentPeriod, null, 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.getCapitalizedIncomePrincipal(), interestPeriod.getMc(), interestPeriod.isPaused());
|
||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), interestPeriod.getMc(),
|
||||
interestPeriod.isPaused());
|
||||
}
|
||||
|
||||
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||
|
||||
Reference in New Issue
Block a user