FINERACT-2090: Accruals rework

This commit is contained in:
Ruchi Dhamankar
2024-07-09 14:55:04 +02:00
committed by Adam Saghy
parent 3f4bbf3e14
commit 1b75c868ba
26 changed files with 1695 additions and 1485 deletions
@@ -1260,7 +1260,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
applyAccruals();
}
public void updateLoanSchedule(final Collection<LoanRepaymentScheduleInstallment> installments) {
@@ -1279,7 +1278,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
applyAccruals();
}
@@ -1293,94 +1291,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return null;
}
/**
* method updates accrual derived fields on installments and reverse the unprocessed transactions
*/
private void applyAccruals() {
Collection<LoanTransaction> accruals = retrieveListOfAccrualTransactions();
if (!accruals.isEmpty()) {
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
applyPeriodicAccruals(accruals);
} else if (isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
updateAccrualsForNonPeriodicAccruals(accruals);
}
}
}
private void applyPeriodicAccruals(final Collection<LoanTransaction> accruals) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
boolean isBasedOnSubmittedOnDate = TemporaryConfigurationServiceContainer.getAccrualDateConfigForCharge()
.equalsIgnoreCase("submitted-date");
for (LoanRepaymentScheduleInstallment installment : installments) {
Money interest = Money.zero(getCurrency());
Money fee = Money.zero(getCurrency());
Money penalty = Money.zero(getCurrency());
for (LoanTransaction loanTransaction : accruals) {
LocalDate transactionDateForRange = getDateForRangeCalculation(loanTransaction, isBasedOnSubmittedOnDate);
boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments);
if (isInPeriod) {
interest = interest.plus(loanTransaction.getInterestPortion(getCurrency()));
fee = fee.plus(loanTransaction.getFeeChargesPortion(getCurrency()));
penalty = penalty.plus(loanTransaction.getPenaltyChargesPortion(getCurrency()));
if (installment.getFeeChargesCharged(getCurrency()).isLessThan(fee)
|| installment.getInterestCharged(getCurrency()).isLessThan(interest)
|| installment.getPenaltyChargesCharged(getCurrency()).isLessThan(penalty)
|| (isInterestBearing() && DateUtils.isEqual(getAccruedTill(), loanTransaction.getTransactionDate())
&& !DateUtils.isEqual(getAccruedTill(), installment.getDueDate()))) {
interest = interest.minus(loanTransaction.getInterestPortion(getCurrency()));
fee = fee.minus(loanTransaction.getFeeChargesPortion(getCurrency()));
penalty = penalty.minus(loanTransaction.getPenaltyChargesPortion(getCurrency()));
loanTransaction.reverse();
}
}
}
installment.updateAccrualPortion(interest, fee, penalty);
}
LoanRepaymentScheduleInstallment lastInstallment = getLastLoanRepaymentScheduleInstallment();
for (LoanTransaction loanTransaction : accruals) {
if (!loanTransaction.isReversed() && DateUtils.isAfter(loanTransaction.getTransactionDate(), lastInstallment.getDueDate())) {
loanTransaction.reverse();
}
}
}
private LocalDate getDateForRangeCalculation(LoanTransaction loanTransaction, boolean isChargeAccrualBasedOnSubmittedOnDate) {
return isChargeAccrualBasedOnSubmittedOnDate && !loanTransaction.getLoanChargesPaid().isEmpty()
? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getEffectiveDueDate()
: loanTransaction.getTransactionDate();
}
private void updateAccrualsForNonPeriodicAccruals(final Collection<LoanTransaction> accruals) {
final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged());
ExternalId externalId = ExternalId.empty();
boolean isExternalIdAutoGenerationEnabled = TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled();
for (LoanTransaction loanTransaction : accruals) {
if (loanTransaction.getInterestPortion(getCurrency()).isGreaterThanZero()) {
if (loanTransaction.getInterestPortion(getCurrency()).isNotEqualTo(interestApplied)) {
loanTransaction.reverse();
if (isExternalIdAutoGenerationEnabled) {
externalId = ExternalId.generate();
}
final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied,
getDisbursementDate(), externalId);
addLoanTransaction(interestAppliedTransaction);
}
} else {
Set<LoanChargePaidBy> chargePaidBies = loanTransaction.getLoanChargesPaid();
for (final LoanChargePaidBy chargePaidBy : chargePaidBies) {
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
Money chargeAmount = loanCharge.getAmount(getCurrency());
if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(getCurrency()))) {
loanTransaction.reverse();
handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate());
}
}
}
}
}
public void updateLoanScheduleDependentDerivedFields() {
if (this.getLoanRepaymentScheduleInstallmentsSize() > 0) {
this.expectedMaturityDate = determineExpectedMaturityDate();
@@ -2061,7 +1971,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return details;
}
private boolean isDisbursementMissed() {
public boolean isDisbursementMissed() {
return getDisbursementDetails().stream() //
.anyMatch(disbursementDetail -> disbursementDetail.actualDisbursementDate() == null
&& DateUtils.isBeforeBusinessDate(disbursementDetail.expectedDisbursementDateAsLocalDate()));
@@ -2683,12 +2593,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.orElse(null);
}
private List<LoanTransaction> retrieveListOfIncomePostingTransactions() {
return getLoanTransactions().stream() //
.filter(transaction -> transaction.isNotReversed() && transaction.isIncomePosting()) //
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
}
public List<LoanTransaction> retrieveListOfTransactionsForReprocessing() {
return getLoanTransactions().stream()
.filter(transaction -> transaction.isNotReversed() && !transaction.isAccrual()
@@ -2715,11 +2619,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return repaymentsOrWaivers;
}
private List<LoanTransaction> retrieveListOfAccrualTransactions() {
return this.loanTransactions.stream().filter(transaction -> transaction.isNotReversed() && transaction.isAccrual())
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
}
public List<LoanTransaction> retrieveListOfTransactionsByType(final LoanTransactionType transactionType) {
return this.loanTransactions.stream()
.filter(transaction -> transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType))
@@ -2762,82 +2661,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
}
processIncomeAccrualTransactionOnLoanClosure();
}
public void processIncomeAccrualTransactionOnLoanClosure() {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& this.getStatus().isClosedObligationsMet() && !isNpa() && !isChargedOff()) {
ExternalId externalId = ExternalId.empty();
boolean isExternalIdAutoGenerationEnabled = TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled();
LocalDate closedDate = this.getClosedOnDate();
reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(), closedDate);
reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(), closedDate);
HashMap<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>();
determineCumulativeIncomeFromInstallments(cumulativeIncomeFromInstallments);
HashMap<String, BigDecimal> cumulativeIncomeFromIncomePosting = new HashMap<>();
determineCumulativeIncomeDetails(retrieveListOfIncomePostingTransactions(), cumulativeIncomeFromIncomePosting);
BigDecimal interestToPost = cumulativeIncomeFromInstallments.get(INTEREST)
.subtract(cumulativeIncomeFromIncomePosting.get(INTEREST));
BigDecimal feeToPost = cumulativeIncomeFromInstallments.get(FEE).subtract(cumulativeIncomeFromIncomePosting.get(FEE));
BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get(PENALTY)
.subtract(cumulativeIncomeFromIncomePosting.get(PENALTY));
BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost);
if (isExternalIdAutoGenerationEnabled) {
externalId = ExternalId.generate();
}
LoanTransaction finalIncomeTransaction = LoanTransaction.incomePosting(this, this.getOffice(), closedDate, amountToPost,
interestToPost, feeToPost, penaltyToPost, externalId);
addLoanTransaction(finalIncomeTransaction);
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
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();
}
LoanTransaction finalAccrual = LoanTransaction.accrueTransaction(this, this.getOffice(), closedDate, amountToPost,
interestToPost, feeToPost, penaltyToPost, externalId);
updateLoanChargesPaidBy(finalAccrual, feeDetails, null);
addLoanTransaction(finalAccrual);
}
}
updateLoanOutstandingBalances();
}
private void determineCumulativeIncomeFromInstallments(HashMap<String, BigDecimal> cumulativeIncomeFromInstallments) {
BigDecimal interest = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalty = BigDecimal.ZERO;
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment installment : installments) {
interest = interest.add(installment.getInterestCharged(getCurrency()).getAmount());
fee = fee.add(installment.getFeeChargesCharged(getCurrency()).getAmount());
penalty = penalty.add(installment.getPenaltyChargesCharged(getCurrency()).getAmount());
}
cumulativeIncomeFromInstallments.put(INTEREST, interest);
cumulativeIncomeFromInstallments.put(FEE, fee);
cumulativeIncomeFromInstallments.put(PENALTY, penalty);
}
private void determineCumulativeIncomeDetails(Collection<LoanTransaction> transactions, HashMap<String, BigDecimal> incomeDetailsMap) {
BigDecimal interest = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalty = BigDecimal.ZERO;
for (LoanTransaction transaction : transactions) {
interest = interest.add(transaction.getInterestPortion(getCurrency()).getAmount());
fee = fee.add(transaction.getFeeChargesPortion(getCurrency()).getAmount());
penalty = penalty.add(transaction.getPenaltyChargesPortion(getCurrency()).getAmount());
}
incomeDetailsMap.put(INTEREST, interest);
incomeDetailsMap.put(FEE, fee);
incomeDetailsMap.put(PENALTY, penalty);
}
private void handleLoanOverpayment(LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
@@ -4646,197 +4469,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
processPostDisbursementTransactions();
processIncomeTransactions();
}
private void updateLoanChargesPaidBy(LoanTransaction accrual, Map<String, Object> feeDetails,
LoanRepaymentScheduleInstallment installment) {
@SuppressWarnings("unchecked")
List<LoanCharge> loanCharges = (List<LoanCharge>) feeDetails.get("loanCharges");
@SuppressWarnings("unchecked")
List<LoanInstallmentCharge> loanInstallmentCharges = (List<LoanInstallmentCharge>) feeDetails.get("loanInstallmentCharges");
if (loanCharges != null) {
for (LoanCharge loanCharge : loanCharges) {
Integer installmentNumber = null == installment ? null : installment.getInstallmentNumber();
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrual, loanCharge,
loanCharge.getAmount(getCurrency()).getAmount(), installmentNumber);
accrual.getLoanChargesPaid().add(loanChargePaidBy);
}
}
if (loanInstallmentCharges != null) {
for (LoanInstallmentCharge loanInstallmentCharge : loanInstallmentCharges) {
Integer installmentNumber = null == loanInstallmentCharge.getInstallment() ? null
: loanInstallmentCharge.getInstallment().getInstallmentNumber();
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrual, loanInstallmentCharge.getLoanCharge(),
loanInstallmentCharge.getAmount(getCurrency()).getAmount(), installmentNumber);
accrual.getLoanChargesPaid().add(loanChargePaidBy);
}
}
}
public void processIncomeTransactions() {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()) {
LocalDate lastCompoundingDate = this.getDisbursementDate();
List<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = extractInterestRecalculationAdditionalDetails();
List<LoanTransaction> incomeTransactions = retrieveListOfIncomePostingTransactions();
List<LoanTransaction> accrualTransactions = retrieveListOfAccrualTransactions();
for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) {
if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) {
break;
}
LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate());
LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate());
addUpdateIncomeAndAccrualTransaction(compoundingDetail, lastCompoundingDate, incomeTransaction, accrualTransaction);
lastCompoundingDate = compoundingDetail.getEffectiveDate();
}
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment
.getLastNonDownPaymentInstallment(installments);
reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate());
reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate());
}
}
private void reverseTransactionsOnOrAfter(List<LoanTransaction> transactions, LocalDate date) {
for (LoanTransaction loanTransaction : transactions) {
if (!DateUtils.isBefore(loanTransaction.getTransactionDate(), date)) {
loanTransaction.reverse();
}
}
}
private void addUpdateIncomeAndAccrualTransaction(LoanInterestRecalcualtionAdditionalDetails compoundingDetail,
LocalDate lastCompoundingDate, LoanTransaction existingIncomeTransaction, LoanTransaction existingAccrualTransaction) {
BigDecimal interest = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalties = BigDecimal.ZERO;
HashMap<String, Object> feeDetails = new HashMap<>();
if (this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod()
.equals(InterestRecalculationCompoundingMethod.INTEREST)) {
interest = compoundingDetail.getAmount();
} else if (this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod()
.equals(InterestRecalculationCompoundingMethod.FEE)) {
determineFeeDetails(lastCompoundingDate, compoundingDetail.getEffectiveDate(), feeDetails);
fee = (BigDecimal) feeDetails.get(FEE);
penalties = (BigDecimal) feeDetails.get(PENALTIES);
} else if (this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod()
.equals(InterestRecalculationCompoundingMethod.INTEREST_AND_FEE)) {
determineFeeDetails(lastCompoundingDate, compoundingDetail.getEffectiveDate(), feeDetails);
fee = (BigDecimal) feeDetails.get(FEE);
penalties = (BigDecimal) feeDetails.get(PENALTIES);
interest = compoundingDetail.getAmount().subtract(fee).subtract(penalties);
}
ExternalId externalId = ExternalId.empty();
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate();
}
if (existingIncomeTransaction == null) {
LoanTransaction transaction = LoanTransaction.incomePosting(this, this.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
addLoanTransaction(transaction);
} else if (existingIncomeTransaction.getAmount(getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) {
existingIncomeTransaction.reverse();
LoanTransaction transaction = LoanTransaction.incomePosting(this, this.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
addLoanTransaction(transaction);
}
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate();
}
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
if (existingAccrualTransaction == null) {
LoanTransaction accrual = LoanTransaction.accrueTransaction(this, this.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
updateLoanChargesPaidBy(accrual, feeDetails, null);
addLoanTransaction(accrual);
} else if (existingAccrualTransaction.getAmount(getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) {
existingAccrualTransaction.reverse();
LoanTransaction accrual = LoanTransaction.accrueTransaction(this, this.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, externalId);
updateLoanChargesPaidBy(accrual, feeDetails, null);
addLoanTransaction(accrual);
}
}
updateLoanOutstandingBalances();
}
private void determineFeeDetails(LocalDate fromDate, LocalDate toDate, Map<String, Object> feeDetails) {
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalties = BigDecimal.ZERO;
List<Integer> installments = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> repaymentSchedule = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : repaymentSchedule) {
if (DateUtils.isAfter(loanRepaymentScheduleInstallment.getDueDate(), fromDate)
&& !DateUtils.isAfter(loanRepaymentScheduleInstallment.getDueDate(), toDate)) {
installments.add(loanRepaymentScheduleInstallment.getInstallmentNumber());
}
}
List<LoanCharge> loanCharges = new ArrayList<>();
List<LoanInstallmentCharge> loanInstallmentCharges = new ArrayList<>();
for (LoanCharge loanCharge : this.getActiveCharges()) {
boolean isDue = DateUtils.isEqual(fromDate, this.getDisbursementDate())
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(fromDate, toDate)
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(fromDate, toDate);
if (isDue) {
if (loanCharge.isPenaltyCharge() && !loanCharge.isInstalmentFee()) {
penalties = penalties.add(loanCharge.amount());
loanCharges.add(loanCharge);
} else if (!loanCharge.isInstalmentFee()) {
fee = fee.add(loanCharge.amount());
loanCharges.add(loanCharge);
}
} else if (loanCharge.isInstalmentFee()) {
for (LoanInstallmentCharge installmentCharge : loanCharge.installmentCharges()) {
if (installments.contains(installmentCharge.getRepaymentInstallment().getInstallmentNumber())) {
fee = fee.add(installmentCharge.getAmount());
loanInstallmentCharges.add(installmentCharge);
}
}
}
}
feeDetails.put(FEE, fee);
feeDetails.put(PENALTIES, penalties);
feeDetails.put("loanCharges", loanCharges);
feeDetails.put("loanInstallmentCharges", loanInstallmentCharges);
}
private LoanTransaction getTransactionForDate(List<LoanTransaction> transactions, LocalDate effectiveDate) {
for (LoanTransaction loanTransaction : transactions) {
if (DateUtils.isEqual(effectiveDate, loanTransaction.getTransactionDate())) {
return loanTransaction;
}
}
return null;
}
private void reverseTransactionsPostEffectiveDate(List<LoanTransaction> transactions, LocalDate effectiveDate) {
for (LoanTransaction loanTransaction : transactions) {
if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) {
loanTransaction.reverse();
}
}
}
private List<LoanInterestRecalcualtionAdditionalDetails> extractInterestRecalculationAdditionalDetails() {
List<LoanInterestRecalcualtionAdditionalDetails> retDetails = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> repaymentSchedule = getRepaymentScheduleInstallments();
if (null != repaymentSchedule) {
for (LoanRepaymentScheduleInstallment installment : repaymentSchedule) {
if (null != installment.getLoanCompoundingDetails()) {
retDetails.addAll(installment.getLoanCompoundingDetails());
}
}
}
retDetails.sort(Comparator.comparing(LoanInterestRecalcualtionAdditionalDetails::getEffectiveDate));
return retDetails;
}
public void processPostDisbursementTransactions() {
@@ -4996,7 +4628,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return recalculatedOn;
}
private void updateLoanOutstandingBalances() {
public void updateLoanOutstandingBalances() {
Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) {
@@ -5755,56 +5387,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return interest.doubleValue() / daysInPeriod * days;
}
public Money[] getReceivableIncome(final LocalDate tillDate) {
MonetaryCurrency currency = getCurrency();
Money receivableInterest = Money.zero(currency);
Money receivableFee = Money.zero(currency);
Money receivablePenalty = Money.zero(currency);
Money[] receivables = new Money[3];
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement()
&& !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) {
if (transaction.isAccrual()) {
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency));
receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency));
receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency));
} else if (transaction.isRepaymentLikeType() || transaction.isChargePayment()) {
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency));
receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency));
receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency));
}
}
if (receivableInterest.isLessThanZero()) {
receivableInterest = receivableInterest.zero();
}
if (receivableFee.isLessThanZero()) {
receivableFee = receivableFee.zero();
}
if (receivablePenalty.isLessThanZero()) {
receivablePenalty = receivablePenalty.zero();
}
}
receivables[0] = receivableInterest;
receivables[1] = receivableFee;
receivables[2] = receivablePenalty;
return receivables;
}
public void reverseAccrualsAfter(final LocalDate tillDate) {
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isAccrual() && DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) {
transaction.reverse();
}
}
}
public ChangedTransactionDetail handleForeClosureTransactions(final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
LoanEvent event = LoanEvent.LOAN_FORECLOSURE;
validateAccountStatus(event);
validateForForeclosure(repaymentTransaction.getTransactionDate());
this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue();
applyAccruals();
return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO);
}
@@ -57,14 +57,6 @@ public interface LoanAccountDomainService {
void updateLoanCollateralStatus(Set<LoanCollateralManagement> loanCollateralManagementSet, boolean isReleased);
/**
* This method is to recalculate and accrue the income till the last accrued date. this method is used when the
* schedule changes due to interest recalculation
*
* @param loan
*/
void recalculateAccruals(Loan loan);
/**
* This method is to set a Delinquency Tag If the loan is overdue, If the loan after the repayment transaction is
* not overdue and It has a Delinquency Tag, It is removed
@@ -98,10 +90,7 @@ public interface LoanAccountDomainService {
*/
void disableStandingInstructionsLinkedToClosedLoan(Loan loan);
void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened);
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText,
ExternalId externalId, PaymentDetail paymentDetail);
void applyFinalIncomeAccrualTransaction(Loan loan);
}
@@ -19,7 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentries;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
@@ -39,7 +39,7 @@ public class AddPeriodicAccrualEntriesConfig {
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private LoanAccrualPlatformService loanAccrualPlatformService;
private LoanAccrualsProcessingService loanAccrualsProcessingService;
@Bean
protected Step addPeriodicAccrualEntriesStep() {
@@ -55,6 +55,6 @@ public class AddPeriodicAccrualEntriesConfig {
@Bean
public AddPeriodicAccrualEntriesTasklet addPeriodicAccrualEntriesTasklet() {
return new AddPeriodicAccrualEntriesTasklet(loanAccrualPlatformService);
return new AddPeriodicAccrualEntriesTasklet(loanAccrualsProcessingService);
}
}
@@ -24,7 +24,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
@@ -34,7 +34,7 @@ import org.springframework.batch.repeat.RepeatStatus;
@RequiredArgsConstructor
public class AddPeriodicAccrualEntriesTasklet implements Tasklet {
private final LoanAccrualPlatformService loanAccrualPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -47,6 +47,6 @@ public class AddPeriodicAccrualEntriesTasklet implements Tasklet {
}
private void addPeriodicAccruals(final LocalDate tilldate) throws MultiException {
loanAccrualPlatformService.addPeriodicAccruals(tilldate);
loanAccrualsProcessingService.addPeriodicAccruals(tilldate);
}
}
@@ -1,33 +0,0 @@
/**
* 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.time.LocalDate;
import java.util.Collection;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
public interface LoanAccrualWritePlatformService {
void addAccrualAccounting(Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws Exception;
void addPeriodicAccruals(LocalDate tilldate, Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas)
throws Exception;
void addIncomeAndAccrualTransactions(Long loanId) throws Exception;
}
@@ -23,13 +23,25 @@ import java.util.Collection;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public interface LoanAccrualPlatformService {
public interface LoanAccrualsProcessingService {
void addPeriodicAccruals(LocalDate tilldate) throws MultiException;
void addPeriodicAccruals(LocalDate tilldate, Loan loan) throws MultiException;
void addPeriodicAccruals(LocalDate tilldate, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws MultiException;
void addAccrualAccounting(Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws Exception;
void addIncomeAndAccrualTransactions(Long loanId) throws Exception;
void reprocessExistingAccruals(Loan loan);
void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled);
void processIncomePostingAndAccruals(Loan loan);
void processAccrualsForLoanClosure(Loan loan);
void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, Collection<LoanTransaction> newAccrualTransactions);
}
@@ -33,12 +33,12 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
@RequiredArgsConstructor
public class AccrualAccountingWritePlatformServiceImpl implements AccrualAccountingWritePlatformService {
private final LoanAccrualPlatformService loanAccrualPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final AccrualAccountingDataValidator accountingDataValidator;
@Override
@@ -46,7 +46,7 @@ public class AccrualAccountingWritePlatformServiceImpl implements AccrualAccount
this.accountingDataValidator.validateLoanPeriodicAccrualData(command.json());
LocalDate tillDate = command.localDateValueOfParameterNamed(accrueTillParamName);
try {
this.loanAccrualPlatformService.addPeriodicAccruals(tillDate);
this.loanAccrualsProcessingService.addPeriodicAccruals(tillDate);
} catch (MultiException e) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
@@ -21,7 +21,7 @@ package org.apache.fineract.accounting.accrual.starter;
import org.apache.fineract.accounting.accrual.serialization.AccrualAccountingDataValidator;
import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformService;
import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -32,7 +32,7 @@ public class AccountingAccrualConfiguration {
@Bean
@ConditionalOnMissingBean(AccrualAccountingWritePlatformService.class)
public AccrualAccountingWritePlatformService accrualAccountingWritePlatformService(
LoanAccrualPlatformService loanAccrualPlatformService, AccrualAccountingDataValidator accountingDataValidator) {
return new AccrualAccountingWritePlatformServiceImpl(loanAccrualPlatformService, accountingDataValidator);
LoanAccrualsProcessingService loanAccrualsProcessingService, AccrualAccountingDataValidator accountingDataValidator) {
return new AccrualAccountingWritePlatformServiceImpl(loanAccrualsProcessingService, accountingDataValidator);
}
}
@@ -24,7 +24,7 @@ import org.apache.fineract.cob.exceptions.BusinessStepException;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.springframework.stereotype.Component;
@Component
@@ -32,13 +32,13 @@ import org.springframework.stereotype.Component;
@Slf4j
public class AddPeriodicAccrualEntriesBusinessStep implements LoanCOBBusinessStep {
private final LoanAccrualPlatformService loanAccrualPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override
public Loan execute(Loan loan) {
log.debug("start processing period accrual business step for loan with Id [{}]", loan.getId());
try {
loanAccrualPlatformService.addPeriodicAccruals(DateUtils.getBusinessLocalDate(), loan);
loanAccrualsProcessingService.addPeriodicAccruals(DateUtils.getBusinessLocalDate(), loan);
} catch (MultiException e) {
throw new BusinessStepException(String.format("Fail to process period accrual for loan id [%s]", loan.getId()), e);
}
@@ -25,7 +25,6 @@ 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;
@@ -36,14 +35,11 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCreditBalanceRefundPostBusinessEvent;
@@ -71,8 +67,6 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -85,7 +79,6 @@ import org.apache.fineract.portfolio.account.domain.StandingInstructionRepositor
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
@@ -94,11 +87,10 @@ import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyAction
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
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;
@@ -129,7 +121,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final NoteRepository noteRepository;
private final AccountTransferRepository accountTransferRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
private final LoanAccrualPlatformService loanAccrualPlatformService;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanUtilService loanUtilService;
private final StandingInstructionRepository standingInstructionRepository;
@@ -142,6 +133,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Transactional
@Override
@@ -212,6 +204,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment,
scheduleGeneratorDTO, isHolidayValidationDone);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction);
/***
@@ -236,7 +233,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
setLoanDelinquencyTag(loan, transactionDate);
@@ -423,7 +421,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newPaymentTransaction));
return newPaymentTransaction;
@@ -572,17 +571,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction);
}
/*
* (non-Javadoc)
*
* @see org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService
* #recalculateAccruals(org.apache.fineract.portfolio.loanaccount.domain. Loan)
*/
@Override
public void recalculateAccruals(Loan loan) {
recalculateAccruals(loan, loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
}
@Override
public void setLoanDelinquencyTag(final Loan loan, final LocalDate transactionDate) {
LoanScheduleDelinquencyData loanDelinquencyData = new LoanScheduleDelinquencyData(loan.getId(), transactionDate, null, loan);
@@ -615,98 +603,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
}
@Override
public void recalculateAccruals(Loan loan, boolean isInterestCalculationHappened) {
LocalDate accruedTill = loan.getAccruedTill();
if (!loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() || !isInterestCalculationHappened || accruedTill == null || loan.isNpa()
|| !loan.getStatus().isActive() || loan.isChargedOff()) {
return;
}
boolean isOrganisationDateEnabled = this.configurationDomainService.isOrganisationstartDateEnabled();
LocalDate organisationStartDate = DateUtils.getBusinessLocalDate();
if (isOrganisationDateEnabled) {
organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
}
Collection<LoanScheduleAccrualData> loanScheduleAccrualList = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
Long loanId = loan.getId();
Long officeId = loan.getOfficeId();
LocalDate accrualStartDate = null;
PeriodFrequencyType repaymentFrequency = loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType();
Integer repayEvery = loan.repaymentScheduleDetail().getRepayEvery();
LocalDate interestCalculatedFrom = loan.getInterestChargedFromDate();
Long loanProductId = loan.productId();
MonetaryCurrency currency = loan.getCurrency();
ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
CurrencyData currencyData = applicationCurrency.toData();
Set<LoanCharge> loanCharges = loan.getActiveCharges();
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (DateUtils.isAfter(installment.getDueDate(), loan.getMaturityDate())) {
accruedTill = DateUtils.getBusinessLocalDate();
}
if (!isOrganisationDateEnabled || DateUtils.isBefore(organisationStartDate, installment.getDueDate())) {
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
generateLoanScheduleAccrualData(accruedTill, loanScheduleAccrualList, loanId, officeId, accrualStartDate,
repaymentFrequency, repayEvery, interestCalculatedFrom, loanProductId, currency, currencyData, loanCharges,
installment, isFirstNormalInstallment);
}
}
if (!loanScheduleAccrualList.isEmpty()) {
try {
this.loanAccrualPlatformService.addPeriodicAccruals(accruedTill, loanScheduleAccrualList);
} catch (MultiException e) {
String globalisationMessageCode = "error.msg.accrual.exception";
throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e);
}
}
}
private void generateLoanScheduleAccrualData(final LocalDate accruedTill,
final Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas, final Long loanId, Long officeId,
final LocalDate accrualStartDate, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery,
final LocalDate interestCalculatedFrom, final Long loanProductId, final MonetaryCurrency currency,
final CurrencyData currencyData, final Set<LoanCharge> loanCharges, final LoanRepaymentScheduleInstallment installment,
boolean isFirstNormalInstallment) {
if (!DateUtils.isBefore(accruedTill, installment.getDueDate()) || (DateUtils.isAfter(accruedTill, installment.getFromDate())
&& !DateUtils.isAfter(accruedTill, installment.getDueDate()))) {
BigDecimal dueDateFeeIncome = BigDecimal.ZERO;
BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO;
LocalDate chargesTillDate = installment.getDueDate();
if (!DateUtils.isAfter(accruedTill, installment.getDueDate())) {
chargesTillDate = accruedTill;
}
for (final LoanCharge loanCharge : loanCharges) {
boolean isDue = isFirstNormalInstallment
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), chargesTillDate)
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), chargesTillDate);
if (isDue) {
if (loanCharge.isFeeCharge()) {
dueDateFeeIncome = dueDateFeeIncome.add(loanCharge.amount());
} else if (loanCharge.isPenaltyCharge()) {
dueDatePenaltyIncome = dueDatePenaltyIncome.add(loanCharge.amount());
}
}
}
LoanScheduleAccrualData accrualData = new LoanScheduleAccrualData(loanId, officeId, installment.getInstallmentNumber(),
accrualStartDate, repaymentFrequency, repayEvery, installment.getDueDate(), installment.getFromDate(),
installment.getId(), loanProductId, installment.getInterestCharged(currency).getAmount(),
installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(),
installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(),
installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom,
installment.getInterestWaived(currency).getAmount(), installment.getCreditedFee(currency).getAmount(),
installment.getCreditedPenalty(currency).getAmount());
loanScheduleAccrualDatas.add(accrualData);
}
}
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) {
@@ -748,7 +644,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService
.notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction));
@@ -792,7 +689,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanRefundPostBusinessEvent(newRefundTransaction));
@@ -805,6 +703,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@Override
public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final String noteText, final ExternalId externalId,
Map<String, Object> changes) {
if (loan.isChargedOff() && DateUtils.isBefore(foreClosureDate, loan.getChargedOffOnDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: "
+ loan.getId()
@@ -821,38 +720,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
final ScheduleGeneratorDTO scheduleGeneratorDTO = null;
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
&& (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) {
loan.reverseAccrualsAfter(foreClosureDate);
Money[] accruedReceivables = loan.getReceivableIncome(foreClosureDate);
Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus(accruedReceivables[0]);
Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus(accruedReceivables[1]);
Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(accruedReceivables[2]);
Money total = interestPortion.plus(feePortion).plus(penaltyPortion);
if (total.isGreaterThanZero()) {
ExternalId accrualExternalId = externalIdFactory.create();
LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), foreClosureDate,
total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(),
accrualExternalId);
LocalDate fromDate = loan.getDisbursementDate();
if (loan.getAccruedTill() != null) {
fromDate = loan.getAccruedTill();
}
newTransactions.add(accrualTransaction);
loan.addLoanTransaction(accrualTransaction);
Set<LoanChargePaidBy> accrualCharges = accrualTransaction.getLoanChargesPaid();
for (LoanCharge loanCharge : loan.getActiveCharges()) {
boolean isDue = DateUtils.isEqual(fromDate, loan.getDisbursementDate())
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(fromDate, foreClosureDate)
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(fromDate, foreClosureDate);
if (loanCharge.isActive() && !loanCharge.isPaid() && (isDue || loanCharge.isInstalmentFee())) {
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge,
loanCharge.getAmountOutstanding(currency).getAmount(), null);
accrualCharges.add(loanChargePaidBy);
}
}
}
}
loanAccrualsProcessingService.processAccrualsForLoanForeClosure(loan, foreClosureDate, newTransactions);
Money interestPayable = foreCloseDetail.getInterestCharged(currency);
Money feePayable = foreCloseDetail.getFeeChargesCharged(currency);
@@ -874,6 +743,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment,
defaultLoanLifecycleStateMachine, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
/***
* TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the
* time being, not a major issue for now as this loop is entered only in edge cases (when a payment is made
@@ -927,123 +801,4 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
}
@Override
public void applyFinalIncomeAccrualTransaction(Loan loan) {
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
// to avoid collision with processIncomeAccrualTransactionOnLoanClosure()
&& !(loan.getLoanInterestRecalculationDetails() != null
&& loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction())
&& !loan.isNpa() && !loan.isChargedOff()) {
MonetaryCurrency currency = loan.getCurrency();
Money interestPortion = Money.zero(currency);
Money feePortion = Money.zero(currency);
Money penaltyPortion = Money.zero(currency);
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : loan.getRepaymentScheduleInstallments()) {
// TODO: test with interest waiving
interestPortion = interestPortion.add(loanRepaymentScheduleInstallment.getInterestCharged(currency))
.minus(loanRepaymentScheduleInstallment.getInterestAccrued(currency))
.minus(loanRepaymentScheduleInstallment.getInterestWaived(currency));
}
for (LoanCharge loanCharge : loan.getLoanCharges()) {
if (!loanCharge.isActive()) {
continue;
}
BigDecimal accruedAmount = BigDecimal.ZERO;
BigDecimal waivedAmount = BigDecimal.ZERO;
for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) {
if (loanChargePaidBy.getLoanTransaction().isAccrual()) {
accruedAmount = accruedAmount.add(loanChargePaidBy.getLoanTransaction().getAmount());
} else if (loanChargePaidBy.getLoanTransaction().isChargesWaiver()) {
waivedAmount = waivedAmount.add(loanChargePaidBy.getLoanTransaction().getAmount());
}
}
Money needToAccrueAmount = MathUtil.negativeToZero(loanCharge.getAmount(currency).minus(accruedAmount).minus(waivedAmount));
if (loanCharge.isPenaltyCharge()) {
penaltyPortion = penaltyPortion.add(needToAccrueAmount);
} else if (loanCharge.isFeeCharge()) {
feePortion = feePortion.add(needToAccrueAmount);
}
}
Money total = interestPortion.plus(feePortion).plus(penaltyPortion);
if (total.isGreaterThanZero()) {
ExternalId externalId = externalIdFactory.create();
LocalDate accrualTransactionDate = getFinalAccrualTransactionDate(loan);
LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), accrualTransactionDate,
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);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(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)));
});
}
}
}
private LocalDate getFinalAccrualTransactionDate(Loan loan) {
return switch (loan.getStatus()) {
case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
case OVERPAID -> loan.getOverpaidOnDate();
default -> throw new IllegalStateException("Unexpected value: " + loan.getStatus());
};
}
}
@@ -27,7 +27,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
@@ -41,7 +41,7 @@ import org.springframework.stereotype.Component;
public class AddAccrualEntriesTasklet implements Tasklet {
private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -60,7 +60,7 @@ public class AddAccrualEntriesTasklet implements Tasklet {
List<Throwable> errors = new ArrayList<>();
for (Map.Entry<Long, Collection<LoanScheduleAccrualData>> mapEntry : loanDataMap.entrySet()) {
try {
loanAccrualWritePlatformService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue());
loanAccrualsProcessingService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue());
} catch (Exception e) {
log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e);
errors.add(e);
@@ -19,7 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentriesforloanswithincomepostedastransactions;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
@@ -42,7 +42,7 @@ public class AddPeriodicAccrualEntriesForLoansConfig {
@Autowired
private LoanReadPlatformService loanReadPlatformService;
@Autowired
private LoanAccrualWritePlatformService loanAccrualWritePlatformService;
private LoanAccrualsProcessingService loanAccrualsProcessingService;
@Bean
protected Step addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsStep() {
@@ -58,6 +58,6 @@ public class AddPeriodicAccrualEntriesForLoansConfig {
@Bean
public AddPeriodicAccrualEntriesForLoansTasklet addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsTasklet() {
return new AddPeriodicAccrualEntriesForLoansTasklet(loanReadPlatformService, loanAccrualWritePlatformService);
return new AddPeriodicAccrualEntriesForLoansTasklet(loanReadPlatformService, loanAccrualsProcessingService);
}
}
@@ -25,7 +25,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
@@ -37,7 +37,7 @@ import org.springframework.batch.repeat.RepeatStatus;
public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet {
private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -46,7 +46,7 @@ public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet {
List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) {
try {
loanAccrualWritePlatformService.addIncomeAndAccrualTransactions(loanId);
loanAccrualsProcessingService.addIncomeAndAccrualTransactions(loanId);
} catch (Exception e) {
log.error("Failed to add income and accrual transaction for loan {}", loanId, e);
errors.add(e);
@@ -113,6 +113,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
@@ -156,6 +157,7 @@ public class LoanScheduleAssembler {
private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element);
@@ -915,6 +917,8 @@ public class LoanScheduleAssembler {
final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
}
private List<LoanTermVariations> adjustExistingVariations(List<LoanTermVariations> variations, List<LoanTermVariations> newVariations,
@@ -1462,6 +1466,7 @@ public class LoanScheduleAssembler {
if (actualChanges.containsKey(LoanApiConstants.approvedLoanAmountParameterName)
|| actualChanges.containsKey("recalculateLoanSchedule") || actualChanges.containsKey("expectedDisbursementDate")) {
loan.regenerateRepaymentSchedule(loanUtilService.buildScheduleGeneratorDTO(loan, null));
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
}
}
@@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
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.springframework.stereotype.Service;
@@ -50,6 +51,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
private final LoanScheduleAssembler loanScheduleAssembler;
private final LoanUtilService loanUtilService;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override
public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) {
@@ -97,6 +99,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan));
return new CommandProcessingResultBuilder() //
@@ -80,6 +80,7 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanResch
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException;
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;
@@ -119,6 +120,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
/**
* create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist
@@ -447,6 +449,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
} else {
loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel());
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loan.recalculateAllCharges();
ChangedTransactionDetail changedTransactionDetail = loan.processTransactions();
@@ -476,7 +479,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan, true);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, true);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan));
return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId()).withEntityId(loanRescheduleRequestId)
@@ -1,81 +0,0 @@
/**
* 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.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@Slf4j
@RequiredArgsConstructor
public class LoanAccrualPlatformServiceImpl implements LoanAccrualPlatformService {
private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService;
@Override
public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionException {
Collection<LoanScheduleAccrualData> loanScheduleAccrualDataList = this.loanReadPlatformService
.retrievePeriodicAccrualData(tillDate);
addPeriodicAccruals(tillDate, loanScheduleAccrualDataList);
}
@Override
public void addPeriodicAccruals(final LocalDate tillDate, Loan loan) throws JobExecutionException {
Collection<LoanScheduleAccrualData> loanScheduleAccrualDataList = this.loanReadPlatformService.retrievePeriodicAccrualData(tillDate,
loan);
addPeriodicAccruals(tillDate, loanScheduleAccrualDataList);
}
@Override
public void addPeriodicAccruals(final LocalDate tillDate, Collection<LoanScheduleAccrualData> loanScheduleAccrualDataList)
throws JobExecutionException {
Map<Long, Collection<LoanScheduleAccrualData>> loanDataMap = new HashMap<>();
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) {
if (loanDataMap.containsKey(accrualData.getLoanId())) {
loanDataMap.get(accrualData.getLoanId()).add(accrualData);
} else {
Collection<LoanScheduleAccrualData> accrualDataList = new ArrayList<>();
accrualDataList.add(accrualData);
loanDataMap.put(accrualData.getLoanId(), accrualDataList);
}
}
List<Throwable> errors = new ArrayList<>();
for (Map.Entry<Long, Collection<LoanScheduleAccrualData>> mapEntry : loanDataMap.entrySet()) {
try {
this.loanAccrualWritePlatformService.addPeriodicAccruals(tillDate, mapEntry.getKey(), mapEntry.getValue());
} catch (Exception e) {
log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e);
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
}
@@ -1,589 +0,0 @@
/**
* 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 static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepository;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlatformService {
private static final String ACCRUAL_ON_CHARGE_DUE_DATE = "due-date";
private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date";
private final LoanReadPlatformService loanReadPlatformService;
private final LoanChargeReadPlatformService loanChargeReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final PlatformSecurityContext context;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanRepository loanRepository;
private final OfficeRepository officeRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionRepository loanTransactionRepository;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final ConfigurationDomainService configurationDomainService;
private final ExternalIdFactory externalIdFactory;
@Override
@Transactional
public void addAccrualAccounting(final Long loanId, final Collection<LoanScheduleAccrualData> loanScheduleAccrualData) {
Collection<LoanChargeData> chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId);
Collection<LoanSchedulePeriodData> loanWaiverScheduleData = new ArrayList<>(1);
Collection<LoanTransactionData> loanWaiverTransactionData = new ArrayList<>(1);
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) {
if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) {
loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId());
loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId());
}
updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate());
updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, accrualData.getDueDateAsLocaldate());
addAccrualAccounting(accrualData);
}
}
@Override
@Transactional
public void addPeriodicAccruals(final LocalDate tillDate, Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualData) {
boolean firstTime = true;
LocalDate accruedTill = null;
Collection<LoanChargeData> chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId);
Collection<LoanSchedulePeriodData> loanWaiverScheduleData = new ArrayList<>(1);
Collection<LoanTransactionData> loanWaiverTransactionData = new ArrayList<>(1);
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) {
if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) {
loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId());
loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId());
}
if (DateUtils.isAfter(accrualData.getDueDateAsLocaldate(), tillDate)) {
if (accruedTill == null || firstTime) {
accruedTill = accrualData.getAccruedTill();
firstTime = false;
}
if (accruedTill == null || DateUtils.isBefore(accruedTill, tillDate)) {
updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), tillDate);
updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate);
addAccrualTillSpecificDate(tillDate, accrualData);
}
} else {
updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate());
updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate);
addAccrualAccounting(accrualData);
accruedTill = accrualData.getDueDateAsLocaldate();
}
}
}
private void addAccrualTillSpecificDate(final LocalDate tillDate, final LoanScheduleAccrualData accrualData) {
LocalDate interestStartDate = accrualData.getFromDateAsLocaldate();
if (DateUtils.isBefore(accrualData.getFromDateAsLocaldate(), accrualData.getInterestCalculatedFrom())) {
if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), accrualData.getDueDateAsLocaldate())) {
interestStartDate = accrualData.getInterestCalculatedFrom();
} else {
interestStartDate = accrualData.getDueDateAsLocaldate();
}
}
int totalNumberOfDays = Math.toIntExact(ChronoUnit.DAYS.between(interestStartDate, accrualData.getDueDateAsLocaldate()));
LocalDate startDate = accrualData.getFromDateAsLocaldate();
if (DateUtils.isBefore(startDate, accrualData.getInterestCalculatedFrom())) {
if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), tillDate)) {
startDate = accrualData.getInterestCalculatedFrom();
} else {
startDate = tillDate;
}
}
int daysToBeAccrued = Math.toIntExact(ChronoUnit.DAYS.between(startDate, tillDate));
double interestPerDay = accrualData.getAccruableIncome().doubleValue() / totalNumberOfDays;
BigDecimal amount = BigDecimal.ZERO;
BigDecimal interestPortion;
BigDecimal feePortion = accrualData.getDueDateFeeIncome();
BigDecimal penaltyPortion = accrualData.getDueDatePenaltyIncome();
if (daysToBeAccrued >= totalNumberOfDays) {
interestPortion = accrualData.getAccruableIncome();
} else {
interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued);
}
interestPortion = interestPortion.setScale(accrualData.getCurrencyData().getDecimalPlaces(), MoneyHelper.getRoundingMode());
BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome();
BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome();
BigDecimal totalCreditedPenalty = accrualData.getCreditedPenalty();
BigDecimal totalAccFee = accrualData.getAccruedFeeIncome();
BigDecimal totalCreditedFee = accrualData.getCreditedFee();
if (totalAccInterest == null) {
totalAccInterest = BigDecimal.ZERO;
}
interestPortion = interestPortion.subtract(totalAccInterest);
amount = amount.add(interestPortion);
totalAccInterest = totalAccInterest.add(interestPortion);
if (interestPortion.compareTo(BigDecimal.ZERO) == 0) {
interestPortion = null;
}
if (feePortion != null) {
if (totalAccFee == null) {
totalAccFee = BigDecimal.ZERO;
}
if (totalCreditedFee == null) {
totalCreditedFee = BigDecimal.ZERO;
}
feePortion = feePortion.subtract(totalAccFee).subtract(totalCreditedFee);
amount = amount.add(feePortion);
totalAccFee = totalAccFee.add(feePortion);
if (feePortion.compareTo(BigDecimal.ZERO) == 0) {
feePortion = null;
}
}
if (penaltyPortion != null) {
if (totalAccPenalty == null) {
totalAccPenalty = BigDecimal.ZERO;
}
if (totalCreditedPenalty == null) {
totalCreditedPenalty = BigDecimal.ZERO;
}
penaltyPortion = penaltyPortion.subtract(totalAccPenalty).subtract(totalCreditedPenalty);
amount = amount.add(penaltyPortion);
totalAccPenalty = totalAccPenalty.add(penaltyPortion);
if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) {
penaltyPortion = null;
}
}
if (amount.compareTo(BigDecimal.ZERO) > 0) {
addAccrualAccounting(accrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, penaltyPortion,
totalAccPenalty, tillDate);
}
}
@Transactional
public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) {
BigDecimal amount = BigDecimal.ZERO;
BigDecimal interestPortion = null;
BigDecimal totalAccInterest = null;
if (scheduleAccrualData.getAccruableIncome() != null) {
interestPortion = scheduleAccrualData.getAccruableIncome();
totalAccInterest = interestPortion;
if (scheduleAccrualData.getAccruedInterestIncome() != null) {
interestPortion = interestPortion.subtract(scheduleAccrualData.getAccruedInterestIncome());
}
amount = amount.add(interestPortion);
if (interestPortion.compareTo(BigDecimal.ZERO) == 0) {
interestPortion = null;
}
}
BigDecimal feePortion = null;
BigDecimal totalAccFee = null;
if (scheduleAccrualData.getDueDateFeeIncome() != null) {
feePortion = scheduleAccrualData.getDueDateFeeIncome();
totalAccFee = feePortion;
if (scheduleAccrualData.getAccruedFeeIncome() != null) {
feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome());
}
if (scheduleAccrualData.getCreditedFee() != null) {
feePortion = feePortion.subtract(scheduleAccrualData.getCreditedFee());
}
amount = amount.add(feePortion);
if (feePortion.compareTo(BigDecimal.ZERO) == 0) {
feePortion = null;
}
}
BigDecimal penaltyPortion = null;
BigDecimal totalAccPenalty = null;
if (scheduleAccrualData.getDueDatePenaltyIncome() != null) {
penaltyPortion = scheduleAccrualData.getDueDatePenaltyIncome();
totalAccPenalty = penaltyPortion;
if (scheduleAccrualData.getAccruedPenaltyIncome() != null) {
penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome());
}
if (scheduleAccrualData.getCreditedPenalty() != null) {
penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getCreditedPenalty());
}
amount = amount.add(penaltyPortion);
if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) {
penaltyPortion = null;
}
}
if (amount.compareTo(BigDecimal.ZERO) > 0) {
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) {
addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee,
penaltyPortion, totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate());
} else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee,
penaltyPortion, totalAccPenalty, DateUtils.getBusinessLocalDate());
}
}
}
private void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData, BigDecimal amount, BigDecimal interestPortion,
BigDecimal totalAccInterest, BigDecimal feePortion, BigDecimal totalAccFee, BigDecimal penaltyPortion,
BigDecimal totalAccPenalty, final LocalDate accruedTill) throws DataAccessException {
AppUser user = context.authenticatedUser();
Loan loan = loanRepository.getReferenceById(scheduleAccrualData.getLoanId());
Office office = officeRepository.getReferenceById(scheduleAccrualData.getOfficeId());
LoanTransaction loanTransaction = loanTransactionRepository.saveAndFlush(accrueTransaction(loan, office, accruedTill, amount,
interestPortion, feePortion, penaltyPortion, externalIdFactory.create()));
Map<LoanChargeData, BigDecimal> applicableCharges = scheduleAccrualData.getApplicableCharges();
String chargesPaidSql = "INSERT INTO m_loan_charge_paid_by (loan_transaction_id, loan_charge_id, amount,installment_number) VALUES (?,?,?,?)";
for (Map.Entry<LoanChargeData, BigDecimal> entry : applicableCharges.entrySet()) {
LoanChargeData chargeData = entry.getKey();
this.jdbcTemplate.update(chargesPaidSql, loanTransaction.getId(), chargeData.getId(), entry.getValue(),
scheduleAccrualData.getInstallmentNumber());
}
Map<String, Object> transactionMap = toMapData(loanTransaction.getId(), amount, interestPortion, feePortion, penaltyPortion,
scheduleAccrualData, accruedTill);
String repaymentUpdateSql = "UPDATE m_loan_repayment_schedule SET accrual_interest_derived=?, accrual_fee_charges_derived=?, "
+ "accrual_penalty_charges_derived=? WHERE id=?";
this.jdbcTemplate.update(repaymentUpdateSql, totalAccInterest, totalAccFee, totalAccPenalty,
scheduleAccrualData.getRepaymentScheduleId());
String updateLoan = "UPDATE m_loan SET accrued_till=?, last_modified_by=?, last_modified_on_utc=? WHERE id=?";
this.jdbcTemplate.update(updateLoan, accruedTill, user.getId(), DateUtils.getAuditOffsetDateTime(),
scheduleAccrualData.getLoanId());
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction));
final Map<String, Object> accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
private Map<String, Object> deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData,
final Map<String, Object> transactionMap) {
final Map<String, Object> accountingBridgeData = new LinkedHashMap<>();
accountingBridgeData.put("loanId", loanScheduleAccrualData.getLoanId());
accountingBridgeData.put("loanProductId", loanScheduleAccrualData.getLoanProductId());
accountingBridgeData.put("officeId", loanScheduleAccrualData.getOfficeId());
accountingBridgeData.put("currencyCode", loanScheduleAccrualData.getCurrencyData().getCode());
accountingBridgeData.put("cashBasedAccountingEnabled", false);
accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", false);
accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", true);
accountingBridgeData.put("isAccountTransfer", false);
accountingBridgeData.put("isChargeOff", false);
accountingBridgeData.put("isFraud", false);
final List<Map<String, Object>> newLoanTransactions = new ArrayList<>();
newLoanTransactions.add(transactionMap);
accountingBridgeData.put("newLoanTransactions", newLoanTransactions);
return accountingBridgeData;
}
public Map<String, Object> toMapData(final Long id, final BigDecimal amount, final BigDecimal interestPortion,
final BigDecimal feePortion, final BigDecimal penaltyPortion, final LoanScheduleAccrualData loanScheduleAccrualData,
final LocalDate accruedTill) {
final Map<String, Object> thisTransactionData = new LinkedHashMap<>();
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.ACCRUAL);
thisTransactionData.put("id", id);
thisTransactionData.put("officeId", loanScheduleAccrualData.getOfficeId());
thisTransactionData.put("type", transactionType);
thisTransactionData.put("reversed", false);
thisTransactionData.put("date", accruedTill);
thisTransactionData.put("currency", loanScheduleAccrualData.getCurrencyData());
thisTransactionData.put("amount", amount);
thisTransactionData.put("principalPortion", null);
thisTransactionData.put("interestPortion", interestPortion);
thisTransactionData.put("feeChargesPortion", feePortion);
thisTransactionData.put("penaltyChargesPortion", penaltyPortion);
thisTransactionData.put("overPaymentPortion", null);
Map<LoanChargeData, BigDecimal> applicableCharges = loanScheduleAccrualData.getApplicableCharges();
if (applicableCharges != null && !applicableCharges.isEmpty()) {
final List<Map<String, Object>> loanChargesPaidData = new ArrayList<>();
for (Map.Entry<LoanChargeData, BigDecimal> entry : applicableCharges.entrySet()) {
LoanChargeData chargeData = entry.getKey();
final Map<String, Object> loanChargePaidData = new LinkedHashMap<>();
loanChargePaidData.put("chargeId", chargeData.getChargeId());
loanChargePaidData.put("isPenalty", chargeData.isPenalty());
loanChargePaidData.put("loanChargeId", chargeData.getId());
loanChargePaidData.put("amount", entry.getValue());
loanChargesPaidData.add(loanChargePaidData);
}
thisTransactionData.put("loanChargesPaid", loanChargesPaidData);
}
return thisTransactionData;
}
private void updateCharges(final Collection<LoanChargeData> chargesData, final LoanScheduleAccrualData accrualData,
final LocalDate startDate, final LocalDate endDate) {
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) {
updateChargeForDueDate(chargesData, accrualData, startDate, endDate);
} else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
updateChargeForSubmittedOnDate(chargesData, accrualData, startDate, endDate);
}
}
private void updateChargeForSubmittedOnDate(Collection<LoanChargeData> chargesData, LoanScheduleAccrualData accrualData,
LocalDate startDate, LocalDate endDate) {
final Map<LoanChargeData, BigDecimal> applicableCharges = new HashMap<>();
BigDecimal submittedDateFeeIncome = BigDecimal.ZERO;
BigDecimal submittedDatePenaltyIncome = BigDecimal.ZERO;
LocalDate scheduleEndDate = accrualData.getDueDateAsLocaldate();
for (LoanChargeData loanCharge : chargesData) {
BigDecimal chargeAmount = BigDecimal.ZERO;
if (((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(startDate, loanCharge.getSubmittedOnDate())
&& DateUtils.isEqual(startDate, loanCharge.getDueDate())) || DateUtils.isBefore(startDate, loanCharge.getDueDate()))
&& !DateUtils.isBefore(endDate, loanCharge.getSubmittedOnDate())
&& !DateUtils.isBefore(scheduleEndDate, loanCharge.getDueDate())) {
chargeAmount = loanCharge.getAmount();
if (loanCharge.getAmountUnrecognized() != null) {
chargeAmount = chargeAmount.subtract(loanCharge.getAmountUnrecognized());
}
boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) > 0;
if (canAddCharge && (loanCharge.getAmountAccrued() == null || chargeAmount.compareTo(loanCharge.getAmountAccrued()) != 0)) {
BigDecimal amountForAccrual = chargeAmount;
if (loanCharge.getAmountAccrued() != null) {
amountForAccrual = chargeAmount.subtract(loanCharge.getAmountAccrued());
}
applicableCharges.put(loanCharge, amountForAccrual);
}
}
if (loanCharge.isPenalty()) {
submittedDatePenaltyIncome = submittedDatePenaltyIncome.add(chargeAmount);
} else {
submittedDateFeeIncome = submittedDateFeeIncome.add(chargeAmount);
}
}
if (submittedDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) {
submittedDateFeeIncome = null;
}
if (submittedDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) {
submittedDatePenaltyIncome = null;
}
accrualData.updateChargeDetails(applicableCharges, submittedDateFeeIncome, submittedDatePenaltyIncome);
}
private void updateChargeForDueDate(Collection<LoanChargeData> chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate,
LocalDate endDate) {
final Map<LoanChargeData, BigDecimal> applicableCharges = new HashMap<>();
BigDecimal dueDateFeeIncome = BigDecimal.ZERO;
BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO;
for (LoanChargeData loanCharge : chargesData) {
BigDecimal chargeAmount = BigDecimal.ZERO;
if (loanCharge.getDueDate() == null) {
if (loanCharge.isInstallmentFee() && DateUtils.isEqual(endDate, accrualData.getDueDateAsLocaldate())) {
Collection<LoanInstallmentChargeData> installmentData = loanCharge.getInstallmentChargeData();
for (LoanInstallmentChargeData installmentChargeData : installmentData) {
if (installmentChargeData.getInstallmentNumber().equals(accrualData.getInstallmentNumber())) {
BigDecimal accruableForInstallment = installmentChargeData.getAmount();
if (installmentChargeData.getAmountUnrecognized() != null) {
accruableForInstallment = accruableForInstallment.subtract(installmentChargeData.getAmountUnrecognized());
}
chargeAmount = accruableForInstallment;
boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) > 0;
if (canAddCharge && (installmentChargeData.getAmountAccrued() == null
|| chargeAmount.compareTo(installmentChargeData.getAmountAccrued()) != 0)) {
BigDecimal amountForAccrual = chargeAmount;
if (installmentChargeData.getAmountAccrued() != null) {
amountForAccrual = chargeAmount.subtract(installmentChargeData.getAmountAccrued());
}
applicableCharges.put(loanCharge, amountForAccrual);
BigDecimal amountAccrued = chargeAmount;
if (loanCharge.getAmountAccrued() != null) {
amountAccrued = amountAccrued.add(loanCharge.getAmountAccrued());
}
loanCharge.updateAmountAccrued(amountAccrued);
}
break;
}
}
}
} else if (((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(loanCharge.getDueDate(), startDate))
|| DateUtils.isAfter(loanCharge.getDueDate(), startDate)) && !DateUtils.isAfter(loanCharge.getDueDate(), endDate)) {
chargeAmount = loanCharge.getAmount();
if (loanCharge.getAmountUnrecognized() != null) {
chargeAmount = chargeAmount.subtract(loanCharge.getAmountUnrecognized());
}
boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) > 0;
if (canAddCharge && (loanCharge.getAmountAccrued() == null || chargeAmount.compareTo(loanCharge.getAmountAccrued()) != 0)) {
BigDecimal amountForAccrual = chargeAmount;
if (loanCharge.getAmountAccrued() != null) {
amountForAccrual = chargeAmount.subtract(loanCharge.getAmountAccrued());
}
applicableCharges.put(loanCharge, amountForAccrual);
}
}
if (loanCharge.isPenalty()) {
dueDatePenaltyIncome = dueDatePenaltyIncome.add(chargeAmount);
} else {
dueDateFeeIncome = dueDateFeeIncome.add(chargeAmount);
}
}
if (dueDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) {
dueDateFeeIncome = null;
}
if (dueDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) {
dueDatePenaltyIncome = null;
}
accrualData.updateChargeDetails(applicableCharges, dueDateFeeIncome, dueDatePenaltyIncome);
}
private void updateInterestIncome(final LoanScheduleAccrualData accrualData,
final Collection<LoanTransactionData> loanWaiverTransactions,
final Collection<LoanSchedulePeriodData> loanSchedulePeriodDataList, final LocalDate tillDate) {
BigDecimal interestIncome = BigDecimal.ZERO;
if (accrualData.getInterestIncome() != null) {
interestIncome = accrualData.getInterestIncome();
}
if (accrualData.getWaivedInterestIncome() != null) {
BigDecimal recognized = BigDecimal.ZERO;
BigDecimal unrecognized = BigDecimal.ZERO;
BigDecimal remainingAmt = BigDecimal.ZERO;
Collection<LoanTransactionData> loanTransactionDatas = new ArrayList<>();
for (LoanTransactionData loanTransactionData : loanWaiverTransactions) {
LocalDate transactionDate = loanTransactionData.getDate();
if (!DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate())
|| (DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate())
&& !DateUtils.isAfter(transactionDate, accrualData.getDueDateAsLocaldate())
&& !DateUtils.isAfter(transactionDate, tillDate))) {
loanTransactionDatas.add(loanTransactionData);
}
}
Iterator<LoanTransactionData> iterator = loanTransactionDatas.iterator();
for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDataList) {
if (MathUtil.isLessThanOrEqualZero(recognized) && MathUtil.isLessThanOrEqualZero(unrecognized) && iterator.hasNext()) {
LoanTransactionData loanTransactionData = iterator.next();
recognized = recognized.add(loanTransactionData.getInterestPortion());
unrecognized = unrecognized.add(loanTransactionData.getUnrecognizedIncomePortion());
}
if (DateUtils.isBefore(loanSchedulePeriodData.getDueDate(), accrualData.getDueDateAsLocaldate())) {
remainingAmt = remainingAmt.add(loanSchedulePeriodData.getInterestWaived());
if (recognized.compareTo(remainingAmt) > 0) {
recognized = recognized.subtract(remainingAmt);
remainingAmt = BigDecimal.ZERO;
} else {
remainingAmt = remainingAmt.subtract(recognized);
recognized = BigDecimal.ZERO;
if (unrecognized.compareTo(remainingAmt) >= 0) {
unrecognized = unrecognized.subtract(remainingAmt);
remainingAmt = BigDecimal.ZERO;
} else if (iterator.hasNext()) {
remainingAmt = remainingAmt.subtract(unrecognized);
unrecognized = BigDecimal.ZERO;
}
}
}
}
BigDecimal interestWaived = accrualData.getWaivedInterestIncome();
if (interestWaived.compareTo(recognized) > 0) {
interestIncome = interestIncome.subtract(interestWaived.subtract(recognized));
}
}
accrualData.updateAccruableIncome(interestIncome);
}
@Override
@Transactional
public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException {
if (loanId != null) {
Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
loan.processIncomeTransactions();
this.loanRepositoryWrapper.saveAndFlush(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
}
}
private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}
@@ -123,6 +123,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final LoanRepository loanRepository;
private final GSIMReadPlatformService gsimReadPlatformService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Transactional
@Override
@@ -625,6 +626,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
}
loan.adjustNetDisbursalAmount(loan.getProposedPrincipal());
@@ -134,6 +134,7 @@ public class LoanAssembler {
private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler;
private final LoanChargeMapper loanChargeMapper;
private final LoanCollateralManagementMapper loanCollateralManagementMapper;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
public Loan assembleFrom(final Long accountId) {
final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true);
@@ -274,7 +275,7 @@ public class LoanAssembler {
// TODO: review
loanApplication.recalculateAllCharges();
topUpLoanConfiguration(element, loanApplication);
loanAccrualsProcessingService.reprocessExistingAccruals(loanApplication);
return loanApplication;
}
@@ -827,6 +828,7 @@ public class LoanAssembler {
final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false);
loan.updateLoanSchedule(loanSchedule);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loan.recalculateAllCharges();
}
@@ -165,6 +165,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final NoteRepository noteRepository;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
@@ -290,7 +291,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
}
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -532,6 +534,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge,
externalId);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()
&& DateUtils.isBefore(loanCharge.getDueLocalDate(), DateUtils.getBusinessLocalDate())) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
this.loanTransactionRepository.saveAndFlush(waiveTransaction);
this.loanRepositoryWrapper.save(loan);
@@ -809,7 +818,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
}
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
}
@@ -1140,6 +1150,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan
.handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
@@ -25,14 +25,13 @@ 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;
@Slf4j
@RequiredArgsConstructor
public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccountDomainServiceJpa loanAccountDomainService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@PostConstruct
public void addListeners() {
@@ -47,7 +46,7 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
log.debug("Loan Status change for loan {}", loan.getId());
if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) {
log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId());
loanAccountDomainService.applyFinalIncomeAccrualTransaction(loan);
loanAccrualsProcessingService.processAccrualsForLoanClosure(loan);
}
if (loan.isOpen()) {
loan.handleMaturityDateActivate();
@@ -260,6 +260,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
private final AccountTransferRepository accountTransferRepository;
private final LoanTransactionAssembler loanTransactionAssembler;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Transactional
@Override
@@ -474,6 +475,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null);
}
loan.adjustNetDisbursalAmount(amountToDisburse.getAmount());
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
LocalDate firstInstallmentDueDate = loan.fetchRepaymentScheduleInstallment(1).getDueDate();
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()
&& (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || loan.isDisbursementMissed())) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (disbursementTransaction != null) {
loanTransactionRepository.saveAndFlush(disbursementTransaction);
}
@@ -513,6 +523,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} else {
loanDownPaymentHandlerService.handleDownPayment(scheduleGeneratorDTO, command, disbursementTransaction, loan);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
}
}
}
@@ -559,7 +573,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
// Post Dated Checks
@@ -789,6 +804,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} else {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null);
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
LocalDate firstInstallmentDueDate = loan.fetchRepaymentScheduleInstallment(1).getDueDate();
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()
&& (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || loan.isDisbursementMissed())) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
}
if (!changes.isEmpty()) {
@@ -834,7 +857,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateRecurringCalendarDatesForInterestRecalculation(loan);
loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan));
}
@@ -892,6 +916,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (!changes.isEmpty()) {
if (loan.isTopup() && loan.getClientId() != null) {
final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose();
@@ -997,7 +1023,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
}
loan.processIncomeAccrualTransactionOnLoanClosure();
}
private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate,
@@ -1117,6 +1142,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO) {
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
@@ -1188,7 +1215,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
// disable all active standing orders linked to this loan if status changes to closed
@@ -1494,6 +1522,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds,
scheduleGeneratorDTO, reversalTxnExternalId);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency());
if (thereIsNewTransaction) {
if (paymentDetail != null) {
@@ -1549,7 +1582,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -1734,6 +1768,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction,
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
this.loanTransactionRepository.saveAndFlush(waiveInterestTransaction);
/***
@@ -1761,7 +1800,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
@@ -1825,6 +1865,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.saveAndFlush(writeOff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -1841,7 +1887,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff));
@@ -1892,6 +1939,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L);
if (possibleClosingTransaction != null) {
this.loanTransactionRepository.saveAndFlush(possibleClosingTransaction);
@@ -1913,7 +1966,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -2366,6 +2420,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
holidays, workingDays, isSkipRepaymentOnFirstMonth, numberOfDays);
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueCalendarChangeBusinessEvent(loan));
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
@@ -2554,6 +2612,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine, existingTransactionIds,
existingReversedTransactionIds, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
@@ -2566,7 +2628,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
if (writeOffTransaction != null) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
@@ -2702,6 +2765,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (command.entityId() != null && changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
@@ -2716,7 +2784,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
@@ -2761,6 +2830,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(generatorDTO,
existingTransactionIds, existingReversedTransactionIds);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
@@ -2772,7 +2846,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
}
@@ -2812,6 +2887,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName);
loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount,
nextPossibleRepaymentDate, rescheduledRepaymentDate);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
}
private List<LoanRepaymentScheduleInstallment> retrieveRepaymentScheduleFromModel(LoanScheduleModel model) {
@@ -2984,6 +3065,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds,
existingReversedTransactionIds, loan);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (!changes.isEmpty()) {
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
String noteText;
@@ -36,7 +36,6 @@ import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.office.domain.OfficeRepository;
import org.apache.fineract.organisation.staff.domain.StaffRepository;
import org.apache.fineract.organisation.staff.service.StaffReadPlatformService;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator;
@@ -67,7 +66,6 @@ import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.group.service.GroupReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainServiceJpa;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
@@ -96,12 +94,9 @@ import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlat
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService;
@@ -170,13 +165,6 @@ public class LoanAccountConfiguration {
return new GLIMAccountInfoWritePlatformServiceImpl(glimAccountRepository);
}
@Bean
@ConditionalOnMissingBean(LoanAccrualPlatformService.class)
public LoanAccrualPlatformService loanAccrualPlatformService(LoanReadPlatformService loanReadPlatformService,
LoanAccrualWritePlatformService loanAccrualWritePlatformService) {
return new LoanAccrualPlatformServiceImpl(loanReadPlatformService, loanAccrualWritePlatformService);
}
@Bean
@ConditionalOnMissingBean(LoanAccrualTransactionBusinessEventService.class)
public LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService(
@@ -185,22 +173,6 @@ public class LoanAccountConfiguration {
return new LoanAccrualTransactionBusinessEventServiceImpl(businessEventNotifierService);
}
@Bean
@ConditionalOnMissingBean(LoanAccrualWritePlatformService.class)
public LoanAccrualWritePlatformService loanAccrualWritePlatformService(LoanReadPlatformService loanReadPlatformService,
LoanChargeReadPlatformService loanChargeReadPlatformService, JdbcTemplate jdbcTemplate,
DatabaseSpecificSQLGenerator sqlGenerator, JournalEntryWritePlatformService journalEntryWritePlatformService,
PlatformSecurityContext context, LoanRepositoryWrapper loanRepositoryWrapper, LoanRepository loanRepository,
OfficeRepository officeRepository, BusinessEventNotifierService businessEventNotifierService,
LoanTransactionRepository loanTransactionRepository,
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService,
ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory) {
return new LoanAccrualWritePlatformServiceImpl(loanReadPlatformService, loanChargeReadPlatformService, jdbcTemplate, sqlGenerator,
journalEntryWritePlatformService, context, loanRepositoryWrapper, loanRepository, officeRepository,
businessEventNotifierService, loanTransactionRepository, loanAccrualTransactionBusinessEventService,
configurationDomainService, externalIdFactory);
}
@Bean
@ConditionalOnMissingBean(LoanApplicationWritePlatformService.class)
public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context,
@@ -215,13 +187,13 @@ public class LoanAccountConfiguration {
LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService,
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository,
LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) {
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator,
loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper,
loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository,
accountAssociationsRepository, businessEventNotifierService, loanScheduleAssembler, loanUtilService,
calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository,
gsimReadPlatformService, defaultLoanLifecycleStateMachine);
gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService);
}
@Bean
@@ -246,13 +218,14 @@ public class LoanAccountConfiguration {
AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService,
LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService,
LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper,
LoanCollateralManagementMapper loanCollateralManagementMapper) {
LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository,
fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler,
loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService,
workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository,
glimRepository, accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler,
calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper);
calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper,
loanAccrualsProcessingService);
}
@Bean
@@ -309,7 +282,8 @@ public class LoanAccountConfiguration {
ExternalIdFactory externalIdFactory, AccountTransferDetailRepository accountTransferDetailRepository,
LoanChargeAssembler loanChargeAssembler, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, NoteRepository noteRepository,
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService,
LoanAccrualsProcessingService loanAccrualsProcessingService
) {
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
@@ -318,7 +292,8 @@ public class LoanAccountConfiguration {
loanChargeReadPlatformService, defaultLoanLifecycleStateMachine, accountAssociationsReadPlatformService, fromApiJsonHelper,
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
accountTransferDetailRepository, loanChargeAssembler, replayedTransactionBusinessEventService,
paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService);
paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService,
loanAccrualsProcessingService);
}
@Bean
@@ -354,8 +329,8 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanStatusChangePlatformService.class)
public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService,
LoanAccountDomainServiceJpa loanAccountDomainService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccountDomainService);
LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualsProcessingService);
}
@Bean
@@ -397,7 +372,7 @@ public class LoanAccountConfiguration {
ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler,
LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository,
LoanTransactionAssembler loanTransactionAssembler) {
LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
@@ -410,7 +385,7 @@ public class LoanAccountConfiguration {
repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository,
defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
loanTransactionAssembler);
loanTransactionAssembler, loanAccrualsProcessingService);
}
@Bean
@@ -40,7 +40,7 @@ import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -54,7 +54,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
public class AddPeriodicAccrualEntriesBusinessStepTest {
@Mock
private LoanAccrualPlatformService loanAccrualPlatformService;
private LoanAccrualsProcessingService loanAccrualsProcessingService;
private AddPeriodicAccrualEntriesBusinessStep underTest;
@@ -64,7 +64,7 @@ public class AddPeriodicAccrualEntriesBusinessStepTest {
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil
.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
underTest = new AddPeriodicAccrualEntriesBusinessStep(loanAccrualPlatformService);
underTest = new AddPeriodicAccrualEntriesBusinessStep(loanAccrualsProcessingService);
}
@AfterEach
@@ -79,7 +79,7 @@ public class AddPeriodicAccrualEntriesBusinessStepTest {
// when
final Loan processedLoan = underTest.execute(loanForProcessing);
// then
verify(loanAccrualPlatformService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing));
verify(loanAccrualsProcessingService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing));
assertEquals(processedLoan, loanForProcessing);
}
@@ -89,13 +89,13 @@ public class AddPeriodicAccrualEntriesBusinessStepTest {
final Long loanId = RandomUtils.nextLong();
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(loanId);
doThrow(new MultiException(Collections.singletonList(new RuntimeException()))).when(loanAccrualPlatformService)
doThrow(new MultiException(Collections.singletonList(new RuntimeException()))).when(loanAccrualsProcessingService)
.addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing));
// when
final BusinessStepException businessStepException = Assert.assertThrows(BusinessStepException.class,
() -> underTest.execute(loanForProcessing));
// then
verify(loanAccrualPlatformService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing));
verify(loanAccrualsProcessingService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing));
assertEquals(String.format("Fail to process period accrual for loan id [%s]", loanId), businessStepException.getMessage());
}