FINERACT-2107: Interest Refund - Allocation, Business Event & Full Repayment
This commit is contained in:
+49
-2
@@ -55,6 +55,8 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestRefundPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestRefundPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMerchantIssuedRefundPostBusinessEvent;
|
||||
@@ -89,11 +91,14 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDelegate;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
@@ -133,6 +138,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
|
||||
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -159,6 +165,26 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoanTransaction createInterestRefundLoanTransaction(Loan loan, final LocalDate transactionDate,
|
||||
BigDecimal relatedRefundTransactionAmount) {
|
||||
InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan);
|
||||
if (interestRefundService == null) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal interestRefundAmount = interestRefundService.calculateInterestRefundAmount(loan.getId(), relatedRefundTransactionAmount,
|
||||
transactionDate);
|
||||
|
||||
final ExternalId txnExternalId = externalIdFactory.create();
|
||||
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestRefundPreBusinessEvent(loan));
|
||||
|
||||
LoanTransaction newInterestRefundTransaction;
|
||||
newInterestRefundTransaction = LoanTransaction.interestRefund(loan, interestRefundAmount, transactionDate, txnExternalId);
|
||||
|
||||
return newInterestRefundTransaction;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, Loan loan, final LocalDate transactionDate,
|
||||
@@ -199,6 +225,18 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
|
||||
holidayDetailDto);
|
||||
|
||||
boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream()
|
||||
.map(LoanSupportedInterestRefundTypes::getTransactionType)
|
||||
.anyMatch(transactionType -> transactionType.equals(repaymentTransactionType));
|
||||
LoanTransaction newInterestRefundTransaction = null;
|
||||
|
||||
if (shouldCreateInterestRefundTransaction) {
|
||||
newInterestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount);
|
||||
if (newInterestRefundTransaction != null) {
|
||||
loan.addLoanTransaction(newInterestRefundTransaction);
|
||||
}
|
||||
}
|
||||
|
||||
final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction,
|
||||
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment,
|
||||
scheduleGeneratorDTO, isHolidayValidationDone);
|
||||
@@ -209,6 +247,9 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
|
||||
saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction);
|
||||
if (newInterestRefundTransaction != null) {
|
||||
saveLoanTransactionWithDataIntegrityViolationChecks(newInterestRefundTransaction);
|
||||
}
|
||||
|
||||
/***
|
||||
* TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the
|
||||
@@ -243,7 +284,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
}
|
||||
|
||||
// disable all active standing orders linked to this loan if status
|
||||
// changes to closed
|
||||
disableStandingInstructionsLinkedToClosedLoan(loan);
|
||||
@@ -276,7 +316,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCreateInterestRefundTransaction && newInterestRefundTransaction != null) {
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanTransactionInterestRefundPostBusinessEvent(newInterestRefundTransaction));
|
||||
}
|
||||
return newRepaymentTransaction;
|
||||
}
|
||||
|
||||
@@ -299,6 +342,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isDownPayment()) {
|
||||
repaymentEvent = new LoanTransactionDownPaymentPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isInterestRefund()) {
|
||||
repaymentEvent = new LoanTransactionInterestRefundPreBusinessEvent(loan);
|
||||
}
|
||||
return repaymentEvent;
|
||||
}
|
||||
@@ -322,6 +367,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isDownPayment()) {
|
||||
repaymentEvent = new LoanTransactionDownPaymentPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isInterestRefund()) {
|
||||
repaymentEvent = new LoanTransactionInterestRefundPostBusinessEvent(transaction);
|
||||
}
|
||||
return repaymentEvent;
|
||||
}
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
|
||||
@Service
|
||||
public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundService {
|
||||
|
||||
private final AdvancedPaymentScheduleTransactionProcessor processor;
|
||||
private final EMICalculator emiCalculator;
|
||||
private final LoanAssembler loanAssembler;
|
||||
|
||||
@Override
|
||||
public boolean canHandle(Loan loan) {
|
||||
return loan != null && loan.isInterestBearing() && processor.accept(loan.getTransactionProcessingStrategyCode());
|
||||
}
|
||||
|
||||
private static boolean omitDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal) {
|
||||
if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (lt.getAmount().compareTo(refundFinal.get()) <= 0) {
|
||||
refundFinal.set(refundFinal.get().subtract(lt.getAmount()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static LoanTransaction calculateReducedAmountDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal) {
|
||||
if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) {
|
||||
LoanTransaction result = new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf().getValue(), lt.getDateOf(),
|
||||
lt.getAmount().subtract(refundFinal.get()), lt.getPrincipalPortion(), lt.getInterestPortion(),
|
||||
lt.getFeeChargesPortion(), lt.getPenaltyChargesPortion(),
|
||||
lt.getOverPaymentPortion(lt.getLoan().getCurrency()).getAmount(), lt.isReversed(), lt.getPaymentDetail(),
|
||||
lt.getExternalId());
|
||||
refundFinal.set(BigDecimal.ZERO);
|
||||
return result;
|
||||
}
|
||||
return lt;
|
||||
}
|
||||
|
||||
private BigDecimal totalInterest(final Loan loan, BigDecimal refundAmount, LocalDate relatedRefundTransactionDate) {
|
||||
final AtomicReference<BigDecimal> refundFinal = new AtomicReference<>(refundAmount);
|
||||
List<LoanTransaction> transactionsToReprocess = loan.getLoanTransactions().stream().filter(lt -> !lt.isReversed()) //
|
||||
.filter(lt -> !lt.isAccrual() && !lt.isAccrualActivity()) //
|
||||
.filter(lt -> omitDisbursements(lt, refundFinal)) //
|
||||
.map(lt -> calculateReducedAmountDisbursements(lt, refundFinal)).toList();
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
|
||||
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
|
||||
|
||||
ProgressiveLoanInterestScheduleModel modelAfter = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(),
|
||||
transactionsToReprocess, loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges()).getRight();
|
||||
BigDecimal payableInterest = BigDecimal.ZERO;
|
||||
if (modelAfter != null && loan.getStatus().isActive()) {
|
||||
LoanRepaymentScheduleInstallment actualInstallment = loan.getRelatedRepaymentScheduleInstallment(relatedRefundTransactionDate);
|
||||
if (actualInstallment == null) {
|
||||
actualInstallment = loan.getLastLoanRepaymentScheduleInstallment();
|
||||
}
|
||||
payableInterest = emiCalculator.getPayableDetails(modelAfter, actualInstallment.getDueDate(), relatedRefundTransactionDate)
|
||||
.getPayableInterest().getAmount();
|
||||
}
|
||||
BigDecimal paidInterest = installmentsToReprocess.stream().map(i -> i.getInterestPaid(loan.getCurrency())).filter(Objects::nonNull)
|
||||
.map(Money::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return payableInterest.add(paidInterest);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
|
||||
public BigDecimal calculateInterestRefundAmount(Long loanId, BigDecimal relatedRefundTransactionAmount,
|
||||
LocalDate relatedRefundTransactionDate) {
|
||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||
BigDecimal totalInterestBeforeRefund = totalInterest(loan, BigDecimal.ZERO, relatedRefundTransactionDate);
|
||||
BigDecimal totalInterestAfterRefund = totalInterest(loan, relatedRefundTransactionAmount, relatedRefundTransactionDate);
|
||||
return totalInterestBeforeRefund.subtract(totalInterestAfterRefund);
|
||||
}
|
||||
}
|
||||
@@ -169,4 +169,5 @@
|
||||
<include file="parts/0148_transaction_summary_with_asset_owner_report_performance_improvement.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0149_update_global_configuration_names.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0150_transaction_summary_with_asset_owner_report_interest_waiver_interest_refund_added.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0151_interest_refund_business_events.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanTransactionInterestRefundPostBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanTransactionInterestRefundPreBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+4
-2
@@ -104,7 +104,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent");
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
@@ -189,7 +190,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent");
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
|
||||
Reference in New Issue
Block a user