FINERACT-1840: Fee income is not realised if the loan got fully repaid

This commit is contained in:
Adam Saghy
2023-03-01 14:08:29 +01:00
committed by Arnold Gálovics
parent c1570049ed
commit ee72978a49
8 changed files with 195 additions and 45 deletions
@@ -3506,43 +3506,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
updateLoanOutstandingBalances();
}
public void applyIncomeAccrualTransaction(LocalDate closedDate) {
ExternalId externalId = ExternalId.empty();
boolean isExternalIdAutoGenerationEnabled = TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled();
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()
// to avoid collision with processIncomeAccrualTransactionOnLoanClosure()
// TODO: review after interest calculation implemented
&& !(this.loanInterestRecalculationDetails != null
&& this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction())) {
List<LoanTransaction> updatedAccrualTransactions = retrieveListOfAccrualTransactions();
LocalDate lastAccruedDate = this.getDisbursementDate();
if (!updatedAccrualTransactions.isEmpty()) {
lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate();
}
HashMap<String, Object> feeDetails = new HashMap<>();
determineFeeDetails(lastAccruedDate, closedDate, feeDetails);
if (isExternalIdAutoGenerationEnabled) {
externalId = ExternalId.generate();
}
BigDecimal fee = (BigDecimal) feeDetails.get(FEE);
if (fee == null) {
fee = BigDecimal.ZERO;
}
BigDecimal penalty = (BigDecimal) feeDetails.get(PENALTIES);
if (penalty == null) {
penalty = BigDecimal.ZERO;
}
BigDecimal total = fee.add(penalty);
// TODO: calculate interest?
if (total.compareTo(BigDecimal.ZERO) > 0) {
LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(this, this.getOffice(), closedDate, total, null, fee,
penalty, externalId);
updateLoanChargesPaidBy(accrualTransaction, feeDetails, null);
addLoanTransaction(accrualTransaction);
}
}
}
private void determineCumulativeIncomeFromInstallments(HashMap<String, BigDecimal> cumulativeIncomeFromInstallments) {
BigDecimal interest = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
@@ -5484,7 +5447,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
for (LoanInstallmentCharge loanInstallmentCharge : loanInstallmentCharges) {
Integer installmentNumber = null == loanInstallmentCharge.getInstallment() ? null
: loanInstallmentCharge.getInstallment().getInstallmentNumber();
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrual, loanInstallmentCharge.getLoancharge(),
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrual, loanInstallmentCharge.getLoanCharge(),
loanInstallmentCharge.getAmount(getCurrency()).getAmount(), installmentNumber);
accrual.getLoanChargesPaid().add(loanChargePaidBy);
}
@@ -7060,4 +7023,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public LocalDate getChargedOffOnDate() {
return chargedOffOnDate;
}
public LoanInterestRecalculationDetails getLoanInterestRecalculationDetails() {
return loanInterestRecalculationDetails;
}
}
@@ -98,4 +98,6 @@ public interface LoanAccountDomainService {
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText,
ExternalId externalId, PaymentDetail paymentDetail);
void applyIncomeAccrualTransaction(Loan loan);
}
@@ -25,6 +25,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -61,6 +62,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
@@ -865,4 +867,94 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
}
@Override
public void applyIncomeAccrualTransaction(Loan loan) {
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
// to avoid collision with processIncomeAccrualTransactionOnLoanClosure()
&& !(loan.getLoanInterestRecalculationDetails() != null
&& loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction())) {
MonetaryCurrency currency = loan.getCurrency();
Money interestPortion = Money.zero(currency);
Money feePortion = Money.zero(currency);
Money penaltyPortion = Money.zero(currency);
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) {
interestPortion = interestPortion.add(loanRepaymentScheduleInstallment.getInterestCharged(currency))
.minus(loanRepaymentScheduleInstallment.getInterestAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getInterestWaived(currency));
feePortion = feePortion.add(loanRepaymentScheduleInstallment.getFeeChargesCharged(currency))
.minus(loanRepaymentScheduleInstallment.getFeeAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getFeeChargesWaived(currency));
penaltyPortion = penaltyPortion.add(loanRepaymentScheduleInstallment.getPenaltyChargesCharged(currency))
.minus(loanRepaymentScheduleInstallment.getPenaltyAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getPenaltyChargesWaived(currency));
}
Money total = interestPortion.plus(feePortion).plus(penaltyPortion);
if (total.isGreaterThanZero()) {
ExternalId externalId = externalIdFactory.create();
LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), loan.getClosedOnDate(),
total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), externalId);
Set<LoanChargePaidBy> accrualCharges = accrualTransaction.getLoanChargesPaid();
Map<Long, Money> accrualDetails = loan.getActiveCharges().stream()
.collect(Collectors.toMap(LoanCharge::getId, v -> Money.zero(currency)));
loan.getLoanTransactions(LoanTransaction::isAccrual).forEach(transaction -> {
transaction.getLoanChargesPaid().forEach(loanChargePaid -> {
accrualDetails.computeIfPresent(loanChargePaid.getLoanCharge().getId(),
(mappedKey, mappedValue) -> mappedValue.add(Money.of(currency, loanChargePaid.getAmount())));
});
});
loan.getActiveCharges().forEach(loanCharge -> {
Money amount = loanCharge.getAmount(currency).minus(loanCharge.getAmountWaived(currency));
if (!loanCharge.isInstalmentFee() && loanCharge.isActive()
&& accrualDetails.get(loanCharge.getId()).isLessThan(amount)) {
Money amountToBeAccrued = amount.minus(accrualDetails.get(loanCharge.getId()));
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge,
amountToBeAccrued.getAmount(), null);
accrualCharges.add(loanChargePaidBy);
}
});
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) {
for (LoanInstallmentCharge installmentCharge : loanRepaymentScheduleInstallment.getInstallmentCharges()) {
if (installmentCharge.getLoanCharge().isActive()) {
Money notWaivedAmount = installmentCharge.getAmount(currency)
.minus(installmentCharge.getAmountWaived(currency));
if (notWaivedAmount.isGreaterThanZero()) {
Money amountToBeAccrued = notWaivedAmount
.minus(accrualDetails.get(installmentCharge.getLoanCharge().getId()));
if (amountToBeAccrued.isGreaterThanZero()) {
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction,
installmentCharge.getLoanCharge(), amountToBeAccrued.getAmount(),
installmentCharge.getInstallment().getInstallmentNumber());
accrualCharges.add(loanChargePaidBy);
accrualDetails.computeIfPresent(installmentCharge.getLoanCharge().getId(),
(mappedKey, mappedValue) -> mappedValue.add(amountToBeAccrued));
}
accrualDetails.computeIfPresent(installmentCharge.getLoanCharge().getId(),
(mappedKey, mappedValue) -> MathUtil
.negativeToZero(mappedValue.minus(Money.of(currency, installmentCharge.getAmount()))));
}
}
}
}
saveLoanTransactionWithDataIntegrityViolationChecks(accrualTransaction);
loan.addLoanTransaction(accrualTransaction);
loan.getRepaymentScheduleInstallments().forEach(installment -> {
installment.updateAccrualPortion(
installment.getInterestCharged(currency).minus(installment.getInterestWaived(currency)),
installment.getFeeChargesCharged(currency).minus(installment.getFeeChargesWaived(currency)),
installment.getPenaltyChargesCharged(currency).minus(installment.getPenaltyChargesWaived(currency)));
});
}
}
}
}
@@ -140,7 +140,7 @@ public class LoanCharge extends AbstractPersistableCustom {
private LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
@OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private Set<LoanChargePaidBy> loanChargePaidBySet;
private Set<LoanChargePaidBy> loanChargePaidBySet = new HashSet<>();
protected LoanCharge() {
//
@@ -312,7 +312,7 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements
return amountToDeductOnThisCharge;
}
public LoanCharge getLoancharge() {
public LoanCharge getLoanCharge() {
return this.loancharge;
}
@@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainServiceJpa;
import org.springframework.stereotype.Service;
@Slf4j
@@ -33,22 +34,22 @@ import org.springframework.stereotype.Service;
public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccountDomainServiceJpa loanAccountDomainService;
@PostConstruct
public void addListeners() {
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, new LoanStatusChangedListener());
}
private static class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
private class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
@Override
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
final Loan loan = event.get();
log.debug("Loan Status change for loan {}", loan.getId());
// Apply common actions on Loan account closed
if (loan.isClosed()) {
if (loan.getStatus().isClosedObligationsMet()) {
log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId());
loan.applyIncomeAccrualTransaction(loan.getClosedOnDate());
loanAccountDomainService.applyIncomeAccrualTransaction(loan);
}
if (loan.isOpen()) {
loan.handleMaturityDateActivate();
@@ -133,7 +133,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
BigDecimal.valueOf(30), BigDecimal.valueOf(50), BigDecimal.valueOf(185), new BigDecimal("100.5"), new BigDecimal("200.3")));
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loanChargeReadPlatformService.retrieveLoanCharges(anyLong())).thenAnswer(a -> repaymentScheduleInstallments.get(0)
.getInstallmentCharges().stream().map(c -> c.getLoancharge().toData()).collect(Collectors.toList()));
.getInstallmentCharges().stream().map(c -> c.getLoanCharge().toData()).collect(Collectors.toList()));
moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP);