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(); updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
applyAccruals();
} }
public void updateLoanSchedule(final Collection<LoanRepaymentScheduleInstallment> installments) { public void updateLoanSchedule(final Collection<LoanRepaymentScheduleInstallment> installments) {
@@ -1279,7 +1278,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} }
updateLoanScheduleDependentDerivedFields(); updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
applyAccruals();
} }
@@ -1293,94 +1291,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return null; 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() { public void updateLoanScheduleDependentDerivedFields() {
if (this.getLoanRepaymentScheduleInstallmentsSize() > 0) { if (this.getLoanRepaymentScheduleInstallmentsSize() > 0) {
this.expectedMaturityDate = determineExpectedMaturityDate(); this.expectedMaturityDate = determineExpectedMaturityDate();
@@ -2061,7 +1971,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return details; return details;
} }
private boolean isDisbursementMissed() { public boolean isDisbursementMissed() {
return getDisbursementDetails().stream() // return getDisbursementDetails().stream() //
.anyMatch(disbursementDetail -> disbursementDetail.actualDisbursementDate() == null .anyMatch(disbursementDetail -> disbursementDetail.actualDisbursementDate() == null
&& DateUtils.isBeforeBusinessDate(disbursementDetail.expectedDisbursementDateAsLocalDate())); && DateUtils.isBeforeBusinessDate(disbursementDetail.expectedDisbursementDateAsLocalDate()));
@@ -2683,12 +2593,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.orElse(null); .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() { public List<LoanTransaction> retrieveListOfTransactionsForReprocessing() {
return getLoanTransactions().stream() return getLoanTransactions().stream()
.filter(transaction -> transaction.isNotReversed() && !transaction.isAccrual() .filter(transaction -> transaction.isNotReversed() && !transaction.isAccrual()
@@ -2715,11 +2619,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return repaymentsOrWaivers; 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) { public List<LoanTransaction> retrieveListOfTransactionsByType(final LoanTransactionType transactionType) {
return this.loanTransactions.stream() return this.loanTransactions.stream()
.filter(transaction -> transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType)) .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); 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) { private void handleLoanOverpayment(LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
@@ -4646,197 +4469,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} }
processPostDisbursementTransactions(); 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() { public void processPostDisbursementTransactions() {
@@ -4996,7 +4628,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return recalculatedOn; return recalculatedOn;
} }
private void updateLoanOutstandingBalances() { public void updateLoanOutstandingBalances() {
Money outstanding = Money.zero(getCurrency()); Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals(); List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) { for (LoanTransaction loanTransaction : loanTransactions) {
@@ -5755,56 +5387,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return interest.doubleValue() / daysInPeriod * days; 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, public ChangedTransactionDetail handleForeClosureTransactions(final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) { final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
LoanEvent event = LoanEvent.LOAN_FORECLOSURE; LoanEvent event = LoanEvent.LOAN_FORECLOSURE;
validateAccountStatus(event); validateAccountStatus(event);
validateForForeclosure(repaymentTransaction.getTransactionDate()); validateForForeclosure(repaymentTransaction.getTransactionDate());
this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue(); this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue();
applyAccruals();
return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO);
} }
@@ -57,14 +57,6 @@ public interface LoanAccountDomainService {
void updateLoanCollateralStatus(Set<LoanCollateralManagement> loanCollateralManagementSet, boolean isReleased); 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 * 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 * not overdue and It has a Delinquency Tag, It is removed
@@ -98,10 +90,7 @@ public interface LoanAccountDomainService {
*/ */
void disableStandingInstructionsLinkedToClosedLoan(Loan loan); void disableStandingInstructionsLinkedToClosedLoan(Loan loan);
void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened);
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText, LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText,
ExternalId externalId, PaymentDetail paymentDetail); ExternalId externalId, PaymentDetail paymentDetail);
void applyFinalIncomeAccrualTransaction(Loan loan);
} }
@@ -19,7 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentries; package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentries;
import org.apache.fineract.infrastructure.jobs.service.JobName; 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.Job;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.job.builder.JobBuilder;
@@ -39,7 +39,7 @@ public class AddPeriodicAccrualEntriesConfig {
@Autowired @Autowired
private PlatformTransactionManager transactionManager; private PlatformTransactionManager transactionManager;
@Autowired @Autowired
private LoanAccrualPlatformService loanAccrualPlatformService; private LoanAccrualsProcessingService loanAccrualsProcessingService;
@Bean @Bean
protected Step addPeriodicAccrualEntriesStep() { protected Step addPeriodicAccrualEntriesStep() {
@@ -55,6 +55,6 @@ public class AddPeriodicAccrualEntriesConfig {
@Bean @Bean
public AddPeriodicAccrualEntriesTasklet addPeriodicAccrualEntriesTasklet() { 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.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; 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.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.core.step.tasklet.Tasklet;
@@ -34,7 +34,7 @@ import org.springframework.batch.repeat.RepeatStatus;
@RequiredArgsConstructor @RequiredArgsConstructor
public class AddPeriodicAccrualEntriesTasklet implements Tasklet { public class AddPeriodicAccrualEntriesTasklet implements Tasklet {
private final LoanAccrualPlatformService loanAccrualPlatformService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { 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 { 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.infrastructure.core.exception.MultiException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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) throws MultiException;
void addPeriodicAccruals(LocalDate tilldate, Loan loan) 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.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; 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 @RequiredArgsConstructor
public class AccrualAccountingWritePlatformServiceImpl implements AccrualAccountingWritePlatformService { public class AccrualAccountingWritePlatformServiceImpl implements AccrualAccountingWritePlatformService {
private final LoanAccrualPlatformService loanAccrualPlatformService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final AccrualAccountingDataValidator accountingDataValidator; private final AccrualAccountingDataValidator accountingDataValidator;
@Override @Override
@@ -46,7 +46,7 @@ public class AccrualAccountingWritePlatformServiceImpl implements AccrualAccount
this.accountingDataValidator.validateLoanPeriodicAccrualData(command.json()); this.accountingDataValidator.validateLoanPeriodicAccrualData(command.json());
LocalDate tillDate = command.localDateValueOfParameterNamed(accrueTillParamName); LocalDate tillDate = command.localDateValueOfParameterNamed(accrueTillParamName);
try { try {
this.loanAccrualPlatformService.addPeriodicAccruals(tillDate); this.loanAccrualsProcessingService.addPeriodicAccruals(tillDate);
} catch (MultiException e) { } catch (MultiException e) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) 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.serialization.AccrualAccountingDataValidator;
import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformService; import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformService;
import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformServiceImpl; 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.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -32,7 +32,7 @@ public class AccountingAccrualConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(AccrualAccountingWritePlatformService.class) @ConditionalOnMissingBean(AccrualAccountingWritePlatformService.class)
public AccrualAccountingWritePlatformService accrualAccountingWritePlatformService( public AccrualAccountingWritePlatformService accrualAccountingWritePlatformService(
LoanAccrualPlatformService loanAccrualPlatformService, AccrualAccountingDataValidator accountingDataValidator) { LoanAccrualsProcessingService loanAccrualsProcessingService, AccrualAccountingDataValidator accountingDataValidator) {
return new AccrualAccountingWritePlatformServiceImpl(loanAccrualPlatformService, 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.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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; import org.springframework.stereotype.Component;
@Component @Component
@@ -32,13 +32,13 @@ import org.springframework.stereotype.Component;
@Slf4j @Slf4j
public class AddPeriodicAccrualEntriesBusinessStep implements LoanCOBBusinessStep { public class AddPeriodicAccrualEntriesBusinessStep implements LoanCOBBusinessStep {
private final LoanAccrualPlatformService loanAccrualPlatformService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override @Override
public Loan execute(Loan loan) { public Loan execute(Loan loan) {
log.debug("start processing period accrual business step for loan with Id [{}]", loan.getId()); log.debug("start processing period accrual business step for loan with Id [{}]", loan.getId());
try { try {
loanAccrualPlatformService.addPeriodicAccruals(DateUtils.getBusinessLocalDate(), loan); loanAccrualsProcessingService.addPeriodicAccruals(DateUtils.getBusinessLocalDate(), loan);
} catch (MultiException e) { } catch (MultiException e) {
throw new BusinessStepException(String.format("Fail to process period accrual for loan id [%s]", loan.getId()), 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.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; 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.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; 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.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; 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.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent; 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.LoanChargePaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCreditBalanceRefundPostBusinessEvent; 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.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; 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.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
@@ -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.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; 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.domain.LoanDelinquencyAction;
import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper; import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; 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.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; 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.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; 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.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
@@ -129,7 +121,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final NoteRepository noteRepository; private final NoteRepository noteRepository;
private final AccountTransferRepository accountTransferRepository; private final AccountTransferRepository accountTransferRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
private final LoanAccrualPlatformService loanAccrualPlatformService;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final StandingInstructionRepository standingInstructionRepository; private final StandingInstructionRepository standingInstructionRepository;
@@ -142,6 +133,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Transactional @Transactional
@Override @Override
@@ -212,6 +204,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment,
scheduleGeneratorDTO, isHolidayValidationDone); scheduleGeneratorDTO, isHolidayValidationDone);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction); saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction);
/*** /***
@@ -236,7 +233,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
setLoanDelinquencyTag(loan, transactionDate); setLoanDelinquencyTag(loan, transactionDate);
@@ -423,7 +421,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newPaymentTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newPaymentTransaction));
return newPaymentTransaction; return newPaymentTransaction;
@@ -572,17 +571,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction); 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 @Override
public void setLoanDelinquencyTag(final Loan loan, final LocalDate transactionDate) { public void setLoanDelinquencyTag(final Loan loan, final LocalDate transactionDate) {
LoanScheduleDelinquencyData loanDelinquencyData = new LoanScheduleDelinquencyData(loan.getId(), transactionDate, null, loan); 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) { private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) { if (transferTransaction != null) {
@@ -748,7 +644,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService businessEventNotifierService
.notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction)); .notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction));
@@ -792,7 +689,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanRefundPostBusinessEvent(newRefundTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanRefundPostBusinessEvent(newRefundTransaction));
@@ -805,6 +703,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@Override @Override
public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final String noteText, final ExternalId externalId, public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final String noteText, final ExternalId externalId,
Map<String, Object> changes) { Map<String, Object> changes) {
if (loan.isChargedOff() && DateUtils.isBefore(foreClosureDate, loan.getChargedOffOnDate())) { if (loan.isChargedOff() && DateUtils.isBefore(foreClosureDate, loan.getChargedOffOnDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: "
+ loan.getId() + loan.getId()
@@ -821,38 +720,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
final ScheduleGeneratorDTO scheduleGeneratorDTO = null; final ScheduleGeneratorDTO scheduleGeneratorDTO = null;
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
&& (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { loanAccrualsProcessingService.processAccrualsForLoanForeClosure(loan, foreClosureDate, newTransactions);
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);
}
}
}
}
Money interestPayable = foreCloseDetail.getInterestCharged(currency); Money interestPayable = foreCloseDetail.getInterestCharged(currency);
Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); Money feePayable = foreCloseDetail.getFeeChargesCharged(currency);
@@ -874,6 +743,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment, final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment,
defaultLoanLifecycleStateMachine, scheduleGeneratorDTO); 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 * 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 * 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 lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; 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.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.scope.context.ChunkContext;
@@ -41,7 +41,7 @@ import org.springframework.stereotype.Component;
public class AddAccrualEntriesTasklet implements Tasklet { public class AddAccrualEntriesTasklet implements Tasklet {
private final LoanReadPlatformService loanReadPlatformService; private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -60,7 +60,7 @@ public class AddAccrualEntriesTasklet implements Tasklet {
List<Throwable> errors = new ArrayList<>(); List<Throwable> errors = new ArrayList<>();
for (Map.Entry<Long, Collection<LoanScheduleAccrualData>> mapEntry : loanDataMap.entrySet()) { for (Map.Entry<Long, Collection<LoanScheduleAccrualData>> mapEntry : loanDataMap.entrySet()) {
try { try {
loanAccrualWritePlatformService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue()); loanAccrualsProcessingService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue());
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e); log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e);
errors.add(e); errors.add(e);
@@ -19,7 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentriesforloanswithincomepostedastransactions; package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentriesforloanswithincomepostedastransactions;
import org.apache.fineract.infrastructure.jobs.service.JobName; 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.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.Job; import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Step;
@@ -42,7 +42,7 @@ public class AddPeriodicAccrualEntriesForLoansConfig {
@Autowired @Autowired
private LoanReadPlatformService loanReadPlatformService; private LoanReadPlatformService loanReadPlatformService;
@Autowired @Autowired
private LoanAccrualWritePlatformService loanAccrualWritePlatformService; private LoanAccrualsProcessingService loanAccrualsProcessingService;
@Bean @Bean
protected Step addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsStep() { protected Step addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsStep() {
@@ -58,6 +58,6 @@ public class AddPeriodicAccrualEntriesForLoansConfig {
@Bean @Bean
public AddPeriodicAccrualEntriesForLoansTasklet addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsTasklet() { 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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; 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.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.scope.context.ChunkContext;
@@ -37,7 +37,7 @@ import org.springframework.batch.repeat.RepeatStatus;
public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet { public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet {
private final LoanReadPlatformService loanReadPlatformService; private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -46,7 +46,7 @@ public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet {
List<Throwable> errors = new ArrayList<>(); List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) { for (Long loanId : loanIds) {
try { try {
loanAccrualWritePlatformService.addIncomeAndAccrualTransactions(loanId); loanAccrualsProcessingService.addIncomeAndAccrualTransactions(loanId);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to add income and accrual transaction for loan {}", loanId, e); log.error("Failed to add income and accrual transaction for loan {}", loanId, e);
errors.add(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.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator; 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.LoanChargeAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
@@ -156,6 +157,7 @@ public class LoanScheduleAssembler {
private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler;
private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element); final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element);
@@ -915,6 +917,8 @@ public class LoanScheduleAssembler {
final LocalDate recalculateFrom = null; final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
} }
private List<LoanTermVariations> adjustExistingVariations(List<LoanTermVariations> variations, List<LoanTermVariations> newVariations, private List<LoanTermVariations> adjustExistingVariations(List<LoanTermVariations> variations, List<LoanTermVariations> newVariations,
@@ -1462,6 +1466,7 @@ public class LoanScheduleAssembler {
if (actualChanges.containsKey(LoanApiConstants.approvedLoanAmountParameterName) if (actualChanges.containsKey(LoanApiConstants.approvedLoanAmountParameterName)
|| actualChanges.containsKey("recalculateLoanSchedule") || actualChanges.containsKey("expectedDisbursementDate")) { || actualChanges.containsKey("recalculateLoanSchedule") || actualChanges.containsKey("expectedDisbursementDate")) {
loan.regenerateRepaymentSchedule(loanUtilService.buildScheduleGeneratorDTO(loan, null)); 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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; 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.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -50,6 +51,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
private final LoanScheduleAssembler loanScheduleAssembler; private final LoanScheduleAssembler loanScheduleAssembler;
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Override @Override
public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) { public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) {
@@ -97,6 +99,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl
final LocalDate recalculateFrom = null; final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan); loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan));
return new CommandProcessingResultBuilder() // 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.domain.LoanRescheduleRequestRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; 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.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
@@ -119,6 +120,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
/** /**
* create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist * create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist
@@ -447,6 +449,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
} else { } else {
loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel()); loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel());
} }
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loan.recalculateAllCharges(); loan.recalculateAllCharges();
ChangedTransactionDetail changedTransactionDetail = loan.processTransactions(); ChangedTransactionDetail changedTransactionDetail = loan.processTransactions();
@@ -476,7 +479,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan, true); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, true);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan));
return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId()).withEntityId(loanRescheduleRequestId) 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 LoanRepository loanRepository;
private final GSIMReadPlatformService gsimReadPlatformService; private final GSIMReadPlatformService gsimReadPlatformService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Transactional @Transactional
@Override @Override
@@ -625,6 +626,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
LocalDate recalculateFrom = null; LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
} }
loan.adjustNetDisbursalAmount(loan.getProposedPrincipal()); loan.adjustNetDisbursalAmount(loan.getProposedPrincipal());
@@ -134,6 +134,7 @@ public class LoanAssembler {
private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler;
private final LoanChargeMapper loanChargeMapper; private final LoanChargeMapper loanChargeMapper;
private final LoanCollateralManagementMapper loanCollateralManagementMapper; private final LoanCollateralManagementMapper loanCollateralManagementMapper;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
public Loan assembleFrom(final Long accountId) { public Loan assembleFrom(final Long accountId) {
final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true);
@@ -274,7 +275,7 @@ public class LoanAssembler {
// TODO: review // TODO: review
loanApplication.recalculateAllCharges(); loanApplication.recalculateAllCharges();
topUpLoanConfiguration(element, loanApplication); topUpLoanConfiguration(element, loanApplication);
loanAccrualsProcessingService.reprocessExistingAccruals(loanApplication);
return loanApplication; return loanApplication;
} }
@@ -827,6 +828,7 @@ public class LoanAssembler {
final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false); final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false);
loan.updateLoanSchedule(loanSchedule); loan.updateLoanSchedule(loanSchedule);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loan.recalculateAllCharges(); loan.recalculateAllCharges();
} }
@@ -165,6 +165,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final NoteRepository noteRepository; private final NoteRepository noteRepository;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate()); 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 if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) { && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
} }
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -532,6 +534,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge, existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge,
externalId); externalId);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()
&& DateUtils.isBefore(loanCharge.getDueLocalDate(), DateUtils.getBusinessLocalDate())) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
this.loanTransactionRepository.saveAndFlush(waiveTransaction); this.loanTransactionRepository.saveAndFlush(waiveTransaction);
this.loanRepositoryWrapper.save(loan); this.loanRepositoryWrapper.save(loan);
@@ -809,7 +818,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) { && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
} }
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
} }
@@ -1140,6 +1150,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan ChangedTransactionDetail changedTransactionDetail = loan
.handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); .handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
if (changedTransactionDetail != null) { if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); 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.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainServiceJpa;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService { public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService {
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccountDomainServiceJpa loanAccountDomainService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@PostConstruct @PostConstruct
public void addListeners() { public void addListeners() {
@@ -47,7 +46,7 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
log.debug("Loan Status change for loan {}", loan.getId()); log.debug("Loan Status change for loan {}", loan.getId());
if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) { if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) {
log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId()); log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId());
loanAccountDomainService.applyFinalIncomeAccrualTransaction(loan); loanAccrualsProcessingService.processAccrualsForLoanClosure(loan);
} }
if (loan.isOpen()) { if (loan.isOpen()) {
loan.handleMaturityDateActivate(); loan.handleMaturityDateActivate();
@@ -260,6 +260,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanDownPaymentHandlerService loanDownPaymentHandlerService; private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
private final AccountTransferRepository accountTransferRepository; private final AccountTransferRepository accountTransferRepository;
private final LoanTransactionAssembler loanTransactionAssembler; private final LoanTransactionAssembler loanTransactionAssembler;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
@Transactional @Transactional
@Override @Override
@@ -474,6 +475,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null);
} }
loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); 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) { if (disbursementTransaction != null) {
loanTransactionRepository.saveAndFlush(disbursementTransaction); loanTransactionRepository.saveAndFlush(disbursementTransaction);
} }
@@ -513,6 +523,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} else { } else {
loanDownPaymentHandlerService.handleDownPayment(scheduleGeneratorDTO, command, disbursementTransaction, loan); 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); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} }
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
// Post Dated Checks // Post Dated Checks
@@ -789,6 +804,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} else { } else {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); 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()) { if (!changes.isEmpty()) {
@@ -834,7 +857,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} }
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan));
} }
@@ -892,6 +916,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds, final Map<String, Object> changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds,
existingReversedTransactionIds); existingReversedTransactionIds);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
if (loan.isTopup() && loan.getClientId() != null) { if (loan.isTopup() && loan.getClientId() != null) {
final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose(); final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose();
@@ -997,7 +1023,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
} }
loan.processIncomeAccrualTransactionOnLoanClosure();
} }
private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate, private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate,
@@ -1117,6 +1142,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO) { ScheduleGeneratorDTO scheduleGeneratorDTO) {
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
} }
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
@@ -1188,7 +1215,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
// disable all active standing orders linked to this loan if status changes to closed // 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, defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds,
scheduleGeneratorDTO, reversalTxnExternalId); scheduleGeneratorDTO, reversalTxnExternalId);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency()); boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency());
if (thereIsNewTransaction) { if (thereIsNewTransaction) {
if (paymentDetail != null) { if (paymentDetail != null) {
@@ -1549,7 +1582,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -1734,6 +1768,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction,
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
this.loanTransactionRepository.saveAndFlush(waiveInterestTransaction); this.loanTransactionRepository.saveAndFlush(waiveInterestTransaction);
/*** /***
@@ -1761,7 +1800,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
@@ -1825,6 +1865,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine, changes, final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L); LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.saveAndFlush(writeOff); this.loanTransactionRepository.saveAndFlush(writeOff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -1841,7 +1887,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff));
@@ -1892,6 +1939,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine, changes, ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L); final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L);
if (possibleClosingTransaction != null) { if (possibleClosingTransaction != null) {
this.loanTransactionRepository.saveAndFlush(possibleClosingTransaction); this.loanTransactionRepository.saveAndFlush(possibleClosingTransaction);
@@ -1913,7 +1966,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
} }
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -2366,6 +2420,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
holidays, workingDays, isSkipRepaymentOnFirstMonth, numberOfDays); holidays, workingDays, isSkipRepaymentOnFirstMonth, numberOfDays);
} }
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueCalendarChangeBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueCalendarChangeBusinessEvent(loan));
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
@@ -2554,6 +2612,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine, existingTransactionIds, ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine, existingTransactionIds,
existingReversedTransactionIds, scheduleGeneratorDTO); existingReversedTransactionIds, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (changedTransactionDetail != null) { if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
@@ -2566,7 +2628,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
if (writeOffTransaction != null) { if (writeOffTransaction != null) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction)); 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) { if (command.entityId() != null && changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
@@ -2716,7 +2784,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
@@ -2761,6 +2830,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(generatorDTO, ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(generatorDTO,
existingTransactionIds, existingReversedTransactionIds); existingTransactionIds, existingReversedTransactionIds);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (changedTransactionDetail != null) { if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
@@ -2772,7 +2846,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan); loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
} }
@@ -2812,6 +2887,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName); BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName);
loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount, loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount,
nextPossibleRepaymentDate, rescheduledRepaymentDate); nextPossibleRepaymentDate, rescheduledRepaymentDate);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
} }
private List<LoanRepaymentScheduleInstallment> retrieveRepaymentScheduleFromModel(LoanScheduleModel model) { private List<LoanRepaymentScheduleInstallment> retrieveRepaymentScheduleFromModel(LoanScheduleModel model) {
@@ -2984,6 +3065,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds, final Map<String, Object> changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds,
existingReversedTransactionIds, loan); existingReversedTransactionIds, loan);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
String noteText; 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.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; 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.domain.StaffRepository;
import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; import org.apache.fineract.organisation.staff.service.StaffReadPlatformService;
import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator; 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.group.service.GroupReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; 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.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; 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.GLIMAccountInfoReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl; 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.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService; import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService;
@@ -170,13 +165,6 @@ public class LoanAccountConfiguration {
return new GLIMAccountInfoWritePlatformServiceImpl(glimAccountRepository); return new GLIMAccountInfoWritePlatformServiceImpl(glimAccountRepository);
} }
@Bean
@ConditionalOnMissingBean(LoanAccrualPlatformService.class)
public LoanAccrualPlatformService loanAccrualPlatformService(LoanReadPlatformService loanReadPlatformService,
LoanAccrualWritePlatformService loanAccrualWritePlatformService) {
return new LoanAccrualPlatformServiceImpl(loanReadPlatformService, loanAccrualWritePlatformService);
}
@Bean @Bean
@ConditionalOnMissingBean(LoanAccrualTransactionBusinessEventService.class) @ConditionalOnMissingBean(LoanAccrualTransactionBusinessEventService.class)
public LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService( public LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService(
@@ -185,22 +173,6 @@ public class LoanAccountConfiguration {
return new LoanAccrualTransactionBusinessEventServiceImpl(businessEventNotifierService); 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 @Bean
@ConditionalOnMissingBean(LoanApplicationWritePlatformService.class) @ConditionalOnMissingBean(LoanApplicationWritePlatformService.class)
public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context, public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context,
@@ -215,13 +187,13 @@ public class LoanAccountConfiguration {
LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService, LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService,
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository,
LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) { LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator, return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator,
loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper, loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper,
loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository, loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository,
accountAssociationsRepository, businessEventNotifierService, loanScheduleAssembler, loanUtilService, accountAssociationsRepository, businessEventNotifierService, loanScheduleAssembler, loanUtilService,
calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository, calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository,
gsimReadPlatformService, defaultLoanLifecycleStateMachine); gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService);
} }
@Bean @Bean
@@ -246,13 +218,14 @@ public class LoanAccountConfiguration {
AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService,
LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService, LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService,
LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper, LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper,
LoanCollateralManagementMapper loanCollateralManagementMapper) { LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository,
fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler, fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler,
loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService,
workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository,
glimRepository, accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler, glimRepository, accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler,
calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper); calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper,
loanAccrualsProcessingService);
} }
@Bean @Bean
@@ -309,7 +282,8 @@ public class LoanAccountConfiguration {
ExternalIdFactory externalIdFactory, AccountTransferDetailRepository accountTransferDetailRepository, ExternalIdFactory externalIdFactory, AccountTransferDetailRepository accountTransferDetailRepository,
LoanChargeAssembler loanChargeAssembler, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService, LoanChargeAssembler loanChargeAssembler, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, NoteRepository noteRepository, PaymentDetailWritePlatformService paymentDetailWritePlatformService, NoteRepository noteRepository,
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService,
LoanAccrualsProcessingService loanAccrualsProcessingService
) { ) {
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository, return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
@@ -318,7 +292,8 @@ public class LoanAccountConfiguration {
loanChargeReadPlatformService, defaultLoanLifecycleStateMachine, accountAssociationsReadPlatformService, fromApiJsonHelper, loanChargeReadPlatformService, defaultLoanLifecycleStateMachine, accountAssociationsReadPlatformService, fromApiJsonHelper,
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory, configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
accountTransferDetailRepository, loanChargeAssembler, replayedTransactionBusinessEventService, accountTransferDetailRepository, loanChargeAssembler, replayedTransactionBusinessEventService,
paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService); paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService,
loanAccrualsProcessingService);
} }
@Bean @Bean
@@ -354,8 +329,8 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanStatusChangePlatformService.class) @ConditionalOnMissingBean(LoanStatusChangePlatformService.class)
public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService, public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService,
LoanAccountDomainServiceJpa loanAccountDomainService) { LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccountDomainService); return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualsProcessingService);
} }
@Bean @Bean
@@ -397,7 +372,7 @@ public class LoanAccountConfiguration {
ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService, ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler, LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler,
LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository, LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository,
LoanTransactionAssembler loanTransactionAssembler) { LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
@@ -410,7 +385,7 @@ public class LoanAccountConfiguration {
repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository,
defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService, defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
loanTransactionAssembler); loanTransactionAssembler, loanAccrualsProcessingService);
} }
@Bean @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.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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.Assert;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -54,7 +54,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
public class AddPeriodicAccrualEntriesBusinessStepTest { public class AddPeriodicAccrualEntriesBusinessStepTest {
@Mock @Mock
private LoanAccrualPlatformService loanAccrualPlatformService; private LoanAccrualsProcessingService loanAccrualsProcessingService;
private AddPeriodicAccrualEntriesBusinessStep underTest; private AddPeriodicAccrualEntriesBusinessStep underTest;
@@ -64,7 +64,7 @@ public class AddPeriodicAccrualEntriesBusinessStepTest {
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil ThreadLocalContextUtil
.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault())))); .setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
underTest = new AddPeriodicAccrualEntriesBusinessStep(loanAccrualPlatformService); underTest = new AddPeriodicAccrualEntriesBusinessStep(loanAccrualsProcessingService);
} }
@AfterEach @AfterEach
@@ -79,7 +79,7 @@ public class AddPeriodicAccrualEntriesBusinessStepTest {
// when // when
final Loan processedLoan = underTest.execute(loanForProcessing); final Loan processedLoan = underTest.execute(loanForProcessing);
// then // then
verify(loanAccrualPlatformService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing)); verify(loanAccrualsProcessingService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing));
assertEquals(processedLoan, loanForProcessing); assertEquals(processedLoan, loanForProcessing);
} }
@@ -89,13 +89,13 @@ public class AddPeriodicAccrualEntriesBusinessStepTest {
final Long loanId = RandomUtils.nextLong(); final Long loanId = RandomUtils.nextLong();
final Loan loanForProcessing = Mockito.mock(Loan.class); final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(loanId); 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)); .addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing));
// when // when
final BusinessStepException businessStepException = Assert.assertThrows(BusinessStepException.class, final BusinessStepException businessStepException = Assert.assertThrows(BusinessStepException.class,
() -> underTest.execute(loanForProcessing)); () -> underTest.execute(loanForProcessing));
// then // 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()); assertEquals(String.format("Fail to process period accrual for loan id [%s]", loanId), businessStepException.getMessage());
} }