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