From 80f1727b821a2fcb247e70ed44cd21c57ef881c1 Mon Sep 17 00:00:00 2001 From: Ruchi Dhamankar Date: Fri, 5 Jul 2024 22:51:56 +0530 Subject: [PATCH] FINERACT-2090: Accruals rework --- .../portfolio/loanaccount/domain/Loan.java | 416 +---- .../domain/LoanAccountDomainService.java | 11 - .../AddPeriodicAccrualEntriesConfig.java | 6 +- .../AddPeriodicAccrualEntriesTasklet.java | 6 +- .../LoanAccrualWritePlatformService.java | 33 - ...ava => LoanAccrualsProcessingService.java} | 16 +- ...ualAccountingWritePlatformServiceImpl.java | 6 +- .../AccountingAccrualConfiguration.java | 6 +- ...AddPeriodicAccrualEntriesBusinessStep.java | 6 +- .../domain/LoanAccountDomainServiceJpa.java | 291 +--- .../AddAccrualEntriesTasklet.java | 6 +- ...dPeriodicAccrualEntriesForLoansConfig.java | 6 +- ...PeriodicAccrualEntriesForLoansTasklet.java | 6 +- .../service/LoanScheduleAssembler.java | 5 + .../LoanScheduleWritePlatformServiceImpl.java | 3 + ...heduleRequestWritePlatformServiceImpl.java | 5 +- .../LoanAccrualPlatformServiceImpl.java | 81 - .../LoanAccrualWritePlatformServiceImpl.java | 589 ------- .../LoanAccrualsProcessingServiceImpl.java | 1480 +++++++++++++++++ ...WritePlatformServiceJpaRepositoryImpl.java | 2 + .../loanaccount/service/LoanAssembler.java | 4 +- .../LoanChargeWritePlatformServiceImpl.java | 17 +- .../LoanStatusChangePlatformServiceImpl.java | 5 +- ...WritePlatformServiceJpaRepositoryImpl.java | 109 +- .../starter/LoanAccountConfiguration.java | 53 +- ...eriodicAccrualEntriesBusinessStepTest.java | 12 +- 26 files changed, 1695 insertions(+), 1485 deletions(-) delete mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java rename fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/{LoanAccrualPlatformService.java => LoanAccrualsProcessingService.java} (64%) delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java delete mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index cfb680585e8..9d1be1bc130 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1258,7 +1258,6 @@ public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) { updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); - applyAccruals(); } public void updateLoanSchedule(final Collection installments) { @@ -1277,7 +1276,6 @@ public void updateLoanSchedule(final Collection accruals = retrieveListOfAccrualTransactions(); - if (!accruals.isEmpty()) { - if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - applyPeriodicAccruals(accruals); - } else if (isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { - updateAccrualsForNonPeriodicAccruals(accruals); - } - } - } - - private void applyPeriodicAccruals(final Collection accruals) { - List 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 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 chargePaidBies = loanTransaction.getLoanChargesPaid(); - for (final LoanChargePaidBy chargePaidBy : chargePaidBies) { - LoanCharge loanCharge = chargePaidBy.getLoanCharge(); - Money chargeAmount = loanCharge.getAmount(getCurrency()); - if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(getCurrency()))) { - loanTransaction.reverse(); - handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate()); - } - } - } - } - } - public void updateLoanScheduleDependentDerivedFields() { if (this.getLoanRepaymentScheduleInstallmentsSize() > 0) { this.expectedMaturityDate = determineExpectedMaturityDate(); @@ -2059,7 +1969,7 @@ private LoanDisbursementDetails fetchLastDisburseDetail() { return details; } - private boolean isDisbursementMissed() { + public boolean isDisbursementMissed() { return getDisbursementDetails().stream() // .anyMatch(disbursementDetail -> disbursementDetail.actualDisbursementDate() == null && DateUtils.isBeforeBusinessDate(disbursementDetail.expectedDisbursementDateAsLocalDate())); @@ -2681,12 +2591,6 @@ public LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(Lo .orElse(null); } - private List retrieveListOfIncomePostingTransactions() { - return getLoanTransactions().stream() // - .filter(transaction -> transaction.isNotReversed() && transaction.isIncomePosting()) // - .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); - } - public List retrieveListOfTransactionsForReprocessing() { return getLoanTransactions().stream() .filter(transaction -> transaction.isNotReversed() && !transaction.isAccrual() @@ -2713,11 +2617,6 @@ private List retrieveListOfTransactionsExcludeAccruals() { return repaymentsOrWaivers; } - private List retrieveListOfAccrualTransactions() { - return this.loanTransactions.stream().filter(transaction -> transaction.isNotReversed() && transaction.isAccrual()) - .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); - } - public List retrieveListOfTransactionsByType(final LoanTransactionType transactionType) { return this.loanTransactions.stream() .filter(transaction -> transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType)) @@ -2760,82 +2659,6 @@ private void handleLoanRepaymentInFull(final LocalDate transactionDate, final Lo } 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 cumulativeIncomeFromInstallments = new HashMap<>(); - determineCumulativeIncomeFromInstallments(cumulativeIncomeFromInstallments); - HashMap 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 updatedAccrualTransactions = retrieveListOfAccrualTransactions(); - LocalDate lastAccruedDate = this.getDisbursementDate(); - if (!updatedAccrualTransactions.isEmpty()) { - lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate(); - } - HashMap 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 cumulativeIncomeFromInstallments) { - BigDecimal interest = BigDecimal.ZERO; - BigDecimal fee = BigDecimal.ZERO; - BigDecimal penalty = BigDecimal.ZERO; - List 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 transactions, HashMap 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) { @@ -4644,197 +4467,6 @@ public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleG } processPostDisbursementTransactions(); - processIncomeTransactions(); - } - - private void updateLoanChargesPaidBy(LoanTransaction accrual, Map feeDetails, - LoanRepaymentScheduleInstallment installment) { - @SuppressWarnings("unchecked") - List loanCharges = (List) feeDetails.get("loanCharges"); - @SuppressWarnings("unchecked") - List loanInstallmentCharges = (List) 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 compoundingDetails = extractInterestRecalculationAdditionalDetails(); - List incomeTransactions = retrieveListOfIncomePostingTransactions(); - List 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 installments = getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment - .getLastNonDownPaymentInstallment(installments); - reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); - reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); - } - } - - private void reverseTransactionsOnOrAfter(List 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 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 feeDetails) { - BigDecimal fee = BigDecimal.ZERO; - BigDecimal penalties = BigDecimal.ZERO; - - List installments = new ArrayList<>(); - List repaymentSchedule = getRepaymentScheduleInstallments(); - for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : repaymentSchedule) { - if (DateUtils.isAfter(loanRepaymentScheduleInstallment.getDueDate(), fromDate) - && !DateUtils.isAfter(loanRepaymentScheduleInstallment.getDueDate(), toDate)) { - installments.add(loanRepaymentScheduleInstallment.getInstallmentNumber()); - } - } - - List loanCharges = new ArrayList<>(); - List 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 transactions, LocalDate effectiveDate) { - for (LoanTransaction loanTransaction : transactions) { - if (DateUtils.isEqual(effectiveDate, loanTransaction.getTransactionDate())) { - return loanTransaction; - } - } - return null; - } - - private void reverseTransactionsPostEffectiveDate(List transactions, LocalDate effectiveDate) { - for (LoanTransaction loanTransaction : transactions) { - if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) { - loanTransaction.reverse(); - } - } - } - - private List extractInterestRecalculationAdditionalDetails() { - List retDetails = new ArrayList<>(); - List 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() { @@ -4993,7 +4625,7 @@ public LocalDate fetchInterestRecalculateFromDate() { return recalculatedOn; } - private void updateLoanOutstandingBalances() { + public void updateLoanOutstandingBalances() { Money outstanding = Money.zero(getCurrency()); List loanTransactions = retrieveListOfTransactionsExcludeAccruals(); for (LoanTransaction loanTransaction : loanTransactions) { @@ -5751,56 +5383,12 @@ private double calculateInterestForDays(int daysInPeriod, BigDecimal interest, i return interest.doubleValue() / daysInPeriod * days; } - public Money[] getReceivableIncome(final LocalDate tillDate) { - MonetaryCurrency currency = getCurrency(); - Money receivableInterest = Money.zero(currency); - Money receivableFee = Money.zero(currency); - Money receivablePenalty = Money.zero(currency); - Money[] receivables = new Money[3]; - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement() - && !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { - if (transaction.isAccrual()) { - receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); - receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); - receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); - } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment()) { - receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency)); - receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); - receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); - } - } - if (receivableInterest.isLessThanZero()) { - receivableInterest = receivableInterest.zero(); - } - if (receivableFee.isLessThanZero()) { - receivableFee = receivableFee.zero(); - } - if (receivablePenalty.isLessThanZero()) { - receivablePenalty = receivablePenalty.zero(); - } - } - receivables[0] = receivableInterest; - receivables[1] = receivableFee; - receivables[2] = receivablePenalty; - return receivables; - } - - public void reverseAccrualsAfter(final LocalDate tillDate) { - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isAccrual() && DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { - transaction.reverse(); - } - } - } - public ChangedTransactionDetail handleForeClosureTransactions(final LoanTransaction repaymentTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) { LoanEvent event = LoanEvent.LOAN_FORECLOSURE; validateAccountStatus(event); validateForForeclosure(repaymentTransaction.getTransactionDate()); this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue(); - applyAccruals(); return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java index c15be59a6d2..51d13848b55 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -57,14 +57,6 @@ LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultB void updateLoanCollateralStatus(Set loanCollateralManagementSet, boolean isReleased); - /** - * This method is to recalculate and accrue the income till the last accrued date. this method is used when the - * schedule changes due to interest recalculation - * - * @param loan - */ - void recalculateAccruals(Loan loan); - /** * This method is to set a Delinquency Tag If the loan is overdue, If the loan after the repayment transaction is * not overdue and It has a Delinquency Tag, It is removed @@ -98,10 +90,7 @@ LoanTransaction foreCloseLoan(Loan loan, LocalDate foreClourseDate, String noteT */ void disableStandingInstructionsLinkedToClosedLoan(Loan loan); - void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened); - LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText, ExternalId externalId, PaymentDetail paymentDetail); - void applyFinalIncomeAccrualTransaction(Loan loan); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesConfig.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesConfig.java index ca5d200f794..ca952a1d6ae 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesConfig.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesConfig.java @@ -19,7 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentries; import org.apache.fineract.infrastructure.jobs.service.JobName; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.job.builder.JobBuilder; @@ -39,7 +39,7 @@ public class AddPeriodicAccrualEntriesConfig { @Autowired private PlatformTransactionManager transactionManager; @Autowired - private LoanAccrualPlatformService loanAccrualPlatformService; + private LoanAccrualsProcessingService loanAccrualsProcessingService; @Bean protected Step addPeriodicAccrualEntriesStep() { @@ -55,6 +55,6 @@ public Job addPeriodicAccrualEntriesJob() { @Bean public AddPeriodicAccrualEntriesTasklet addPeriodicAccrualEntriesTasklet() { - return new AddPeriodicAccrualEntriesTasklet(loanAccrualPlatformService); + return new AddPeriodicAccrualEntriesTasklet(loanAccrualsProcessingService); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesTasklet.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesTasklet.java index 3a7ebbe69e8..405b513522a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesTasklet.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentries/AddPeriodicAccrualEntriesTasklet.java @@ -24,7 +24,7 @@ import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; @@ -34,7 +34,7 @@ @RequiredArgsConstructor public class AddPeriodicAccrualEntriesTasklet implements Tasklet { - private final LoanAccrualPlatformService loanAccrualPlatformService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { @@ -47,6 +47,6 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon } private void addPeriodicAccruals(final LocalDate tilldate) throws MultiException { - loanAccrualPlatformService.addPeriodicAccruals(tilldate); + loanAccrualsProcessingService.addPeriodicAccruals(tilldate); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java deleted file mode 100644 index 817d1a7ab31..00000000000 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformService.java +++ /dev/null @@ -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 loanScheduleAccrualDatas) throws Exception; - - void addPeriodicAccruals(LocalDate tilldate, Long loanId, Collection loanScheduleAccrualDatas) - throws Exception; - - void addIncomeAndAccrualTransactions(Long loanId) throws Exception; -} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java similarity index 64% rename from fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformService.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java index 46111fbb604..cf32cbf7635 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java @@ -23,13 +23,25 @@ import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; -public interface LoanAccrualPlatformService { +public interface LoanAccrualsProcessingService { void addPeriodicAccruals(LocalDate tilldate) throws MultiException; void addPeriodicAccruals(LocalDate tilldate, Loan loan) throws MultiException; - void addPeriodicAccruals(LocalDate tilldate, Collection loanScheduleAccrualDatas) throws MultiException; + void addAccrualAccounting(Long loanId, Collection 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 newAccrualTransactions); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/service/AccrualAccountingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/service/AccrualAccountingWritePlatformServiceImpl.java index 6cd1e50a5d5..2a3efcb8acb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/service/AccrualAccountingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/service/AccrualAccountingWritePlatformServiceImpl.java @@ -33,12 +33,12 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; @RequiredArgsConstructor public class AccrualAccountingWritePlatformServiceImpl implements AccrualAccountingWritePlatformService { - private final LoanAccrualPlatformService loanAccrualPlatformService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final AccrualAccountingDataValidator accountingDataValidator; @Override @@ -46,7 +46,7 @@ public CommandProcessingResult executeLoansPeriodicAccrual(JsonCommand command) this.accountingDataValidator.validateLoanPeriodicAccrualData(command.json()); LocalDate tillDate = command.localDateValueOfParameterNamed(accrueTillParamName); try { - this.loanAccrualPlatformService.addPeriodicAccruals(tillDate); + this.loanAccrualsProcessingService.addPeriodicAccruals(tillDate); } catch (MultiException e) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/starter/AccountingAccrualConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/starter/AccountingAccrualConfiguration.java index 1e704cd217f..4a5094aada6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/starter/AccountingAccrualConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/accrual/starter/AccountingAccrualConfiguration.java @@ -21,7 +21,7 @@ import org.apache.fineract.accounting.accrual.serialization.AccrualAccountingDataValidator; import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformService; import org.apache.fineract.accounting.accrual.service.AccrualAccountingWritePlatformServiceImpl; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,7 +32,7 @@ public class AccountingAccrualConfiguration { @Bean @ConditionalOnMissingBean(AccrualAccountingWritePlatformService.class) public AccrualAccountingWritePlatformService accrualAccountingWritePlatformService( - LoanAccrualPlatformService loanAccrualPlatformService, AccrualAccountingDataValidator accountingDataValidator) { - return new AccrualAccountingWritePlatformServiceImpl(loanAccrualPlatformService, accountingDataValidator); + LoanAccrualsProcessingService loanAccrualsProcessingService, AccrualAccountingDataValidator accountingDataValidator) { + return new AccrualAccountingWritePlatformServiceImpl(loanAccrualsProcessingService, accountingDataValidator); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java index 5b7906f8eb0..58eb63398a9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java @@ -24,7 +24,7 @@ import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.springframework.stereotype.Component; @Component @@ -32,13 +32,13 @@ @Slf4j public class AddPeriodicAccrualEntriesBusinessStep implements LoanCOBBusinessStep { - private final LoanAccrualPlatformService loanAccrualPlatformService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Override public Loan execute(Loan loan) { log.debug("start processing period accrual business step for loan with Id [{}]", loan.getId()); try { - loanAccrualPlatformService.addPeriodicAccruals(DateUtils.getBusinessLocalDate(), loan); + loanAccrualsProcessingService.addPeriodicAccruals(DateUtils.getBusinessLocalDate(), loan); } catch (MultiException e) { throw new BusinessStepException(String.format("Fail to process period accrual for loan id [%s]", loan.getId()), e); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index f1219a47ddf..5a0f488cca3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -36,14 +35,11 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; -import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargePaymentPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCreditBalanceRefundPostBusinessEvent; @@ -71,8 +67,6 @@ import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -85,7 +79,6 @@ import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction; import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; @@ -94,11 +87,10 @@ import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; -import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; @@ -129,7 +121,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final NoteRepository noteRepository; private final AccountTransferRepository accountTransferRepository; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; - private final LoanAccrualPlatformService loanAccrualPlatformService; private final BusinessEventNotifierService businessEventNotifierService; private final LoanUtilService loanUtilService; private final StandingInstructionRepository standingInstructionRepository; @@ -142,6 +133,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final DelinquencyReadPlatformService delinquencyReadPlatformService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Transactional @Override @@ -212,6 +204,11 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, scheduleGeneratorDTO, isHolidayValidationDone); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction); /*** @@ -236,7 +233,8 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); setLoanDelinquencyTag(loan, transactionDate); @@ -423,7 +421,8 @@ public LoanTransaction makeChargePayment(final Loan loan, final Long chargeId, f postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newPaymentTransaction)); return newPaymentTransaction; @@ -572,17 +571,6 @@ public void reverseTransfer(final LoanTransaction 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 public void setLoanDelinquencyTag(final Loan loan, final LocalDate transactionDate) { LoanScheduleDelinquencyData loanDelinquencyData = new LoanScheduleDelinquencyData(loan.getId(), transactionDate, null, loan); @@ -615,98 +603,6 @@ public void setLoanDelinquencyTag(Loan loan, LocalDate transactionDate, List loanScheduleAccrualList = new ArrayList<>(); - List 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 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 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 loanCharges, final LoanRepaymentScheduleInstallment installment, - boolean isFirstNormalInstallment) { - - if (!DateUtils.isBefore(accruedTill, installment.getDueDate()) || (DateUtils.isAfter(accruedTill, installment.getFromDate()) - && !DateUtils.isAfter(accruedTill, installment.getDueDate()))) { - BigDecimal dueDateFeeIncome = BigDecimal.ZERO; - BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO; - LocalDate chargesTillDate = installment.getDueDate(); - if (!DateUtils.isAfter(accruedTill, installment.getDueDate())) { - chargesTillDate = accruedTill; - } - - for (final LoanCharge loanCharge : loanCharges) { - boolean isDue = isFirstNormalInstallment - ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(installment.getFromDate(), chargesTillDate) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), chargesTillDate); - if (isDue) { - if (loanCharge.isFeeCharge()) { - dueDateFeeIncome = dueDateFeeIncome.add(loanCharge.amount()); - } else if (loanCharge.isPenaltyCharge()) { - dueDatePenaltyIncome = dueDatePenaltyIncome.add(loanCharge.amount()); - } - } - } - LoanScheduleAccrualData accrualData = new LoanScheduleAccrualData(loanId, officeId, installment.getInstallmentNumber(), - accrualStartDate, repaymentFrequency, repayEvery, installment.getDueDate(), installment.getFromDate(), - installment.getId(), loanProductId, installment.getInterestCharged(currency).getAmount(), - installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(), - installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(), - installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom, - installment.getInterestWaived(currency).getAmount(), installment.getCreditedFee(currency).getAmount(), - installment.getCreditedPenalty(currency).getAmount()); - loanScheduleAccrualDatas.add(accrualData); - - } - } - private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); if (transferTransaction != null) { @@ -748,7 +644,8 @@ public LoanTransaction creditBalanceRefund(final Loan loan, final LocalDate tran } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); - recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService .notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction)); @@ -792,7 +689,8 @@ public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessing postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanRefundPostBusinessEvent(newRefundTransaction)); @@ -805,6 +703,7 @@ public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessing @Override public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final String noteText, final ExternalId externalId, Map changes) { + if (loan.isChargedOff() && DateUtils.isBefore(foreClosureDate, loan.getChargedOffOnDate())) { throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " + loan.getId() @@ -821,38 +720,8 @@ public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); final ScheduleGeneratorDTO scheduleGeneratorDTO = null; final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); - if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() - && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { - loan.reverseAccrualsAfter(foreClosureDate); - Money[] accruedReceivables = loan.getReceivableIncome(foreClosureDate); - Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus(accruedReceivables[0]); - Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus(accruedReceivables[1]); - Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(accruedReceivables[2]); - Money total = interestPortion.plus(feePortion).plus(penaltyPortion); - if (total.isGreaterThanZero()) { - ExternalId accrualExternalId = externalIdFactory.create(); - LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), foreClosureDate, - total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), - accrualExternalId); - LocalDate fromDate = loan.getDisbursementDate(); - if (loan.getAccruedTill() != null) { - fromDate = loan.getAccruedTill(); - } - newTransactions.add(accrualTransaction); - loan.addLoanTransaction(accrualTransaction); - Set accrualCharges = accrualTransaction.getLoanChargesPaid(); - for (LoanCharge loanCharge : loan.getActiveCharges()) { - boolean isDue = DateUtils.isEqual(fromDate, loan.getDisbursementDate()) - ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(fromDate, foreClosureDate) - : loanCharge.isDueForCollectionFromAndUpToAndIncluding(fromDate, foreClosureDate); - if (loanCharge.isActive() && !loanCharge.isPaid() && (isDue || loanCharge.isInstalmentFee())) { - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge, - loanCharge.getAmountOutstanding(currency).getAmount(), null); - accrualCharges.add(loanChargePaidBy); - } - } - } - } + + loanAccrualsProcessingService.processAccrualsForLoanForeClosure(loan, foreClosureDate, newTransactions); Money interestPayable = foreCloseDetail.getInterestCharged(currency); Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); @@ -874,6 +743,11 @@ public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment, defaultLoanLifecycleStateMachine, scheduleGeneratorDTO); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + /*** * TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the * time being, not a major issue for now as this loop is entered only in edge cases (when a payment is made @@ -927,123 +801,4 @@ public void disableStandingInstructionsLinkedToClosedLoan(Loan loan) { } } - @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 accrualCharges = accrualTransaction.getLoanChargesPaid(); - - Map 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()); - }; - } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java index 12be78e3912..208f33532ad 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java @@ -27,7 +27,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -41,7 +41,7 @@ public class AddAccrualEntriesTasklet implements Tasklet { private final LoanReadPlatformService loanReadPlatformService; - private final LoanAccrualWritePlatformService loanAccrualWritePlatformService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { @@ -60,7 +60,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon List errors = new ArrayList<>(); for (Map.Entry> mapEntry : loanDataMap.entrySet()) { try { - loanAccrualWritePlatformService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue()); + loanAccrualsProcessingService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue()); } catch (Exception e) { log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e); errors.add(e); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansConfig.java index 581aa56afd3..27fb15cdf99 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansConfig.java @@ -19,7 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentriesforloanswithincomepostedastransactions; import org.apache.fineract.infrastructure.jobs.service.JobName; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; @@ -42,7 +42,7 @@ public class AddPeriodicAccrualEntriesForLoansConfig { @Autowired private LoanReadPlatformService loanReadPlatformService; @Autowired - private LoanAccrualWritePlatformService loanAccrualWritePlatformService; + private LoanAccrualsProcessingService loanAccrualsProcessingService; @Bean protected Step addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsStep() { @@ -58,6 +58,6 @@ public Job addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsJob() @Bean public AddPeriodicAccrualEntriesForLoansTasklet addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsTasklet() { - return new AddPeriodicAccrualEntriesForLoansTasklet(loanReadPlatformService, loanAccrualWritePlatformService); + return new AddPeriodicAccrualEntriesForLoansTasklet(loanReadPlatformService, loanAccrualsProcessingService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansTasklet.java index 65024113158..d7f18f226f1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addperiodicaccrualentriesforloanswithincomepostedastransactions/AddPeriodicAccrualEntriesForLoansTasklet.java @@ -25,7 +25,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -37,7 +37,7 @@ public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet { private final LoanReadPlatformService loanReadPlatformService; - private final LoanAccrualWritePlatformService loanAccrualWritePlatformService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { @@ -46,7 +46,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon List errors = new ArrayList<>(); for (Long loanId : loanIds) { try { - loanAccrualWritePlatformService.addIncomeAndAccrualTransactions(loanId); + loanAccrualsProcessingService.addIncomeAndAccrualTransactions(loanId); } catch (Exception e) { log.error("Failed to add income and accrual transaction for loan {}", loanId, e); errors.add(e); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 4b7173df580..c4a219a8c1e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -113,6 +113,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; @@ -156,6 +157,7 @@ public class LoanScheduleAssembler { private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element); @@ -914,6 +916,8 @@ public void assempleVariableScheduleFrom(final Loan loan, final String json) { final LocalDate recalculateFrom = null; ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + } private List adjustExistingVariations(List variations, List newVariations, @@ -1461,6 +1465,7 @@ public Pair> assembleLoanApproval(AppUser currentUser, if (actualChanges.containsKey(LoanApiConstants.approvedLoanAmountParameterName) || actualChanges.containsKey("recalculateLoanSchedule") || actualChanges.containsKey("expectedDisbursementDate")) { loan.regenerateRepaymentSchedule(loanUtilService.buildScheduleGeneratorDTO(loan, null)); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java index 2282c42ae58..9b9b7e4ee53 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java @@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.springframework.stereotype.Service; @@ -50,6 +51,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl private final LoanScheduleAssembler loanScheduleAssembler; private final LoanUtilService loanUtilService; private final BusinessEventNotifierService businessEventNotifierService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Override public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) { @@ -97,6 +99,7 @@ public CommandProcessingResult deleteLoanScheduleVariations(final Long loanId) { final LocalDate recalculateFrom = null; ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan); businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan)); return new CommandProcessingResultBuilder() // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index 05a98e07eb1..266514dac77 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -80,6 +80,7 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; @@ -119,6 +120,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final BusinessEventNotifierService businessEventNotifierService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; /** * create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist @@ -447,6 +449,7 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) { } else { loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel()); } + loanAccrualsProcessingService.reprocessExistingAccruals(loan); loan.recalculateAllCharges(); ChangedTransactionDetail changedTransactionDetail = loan.processTransactions(); @@ -476,7 +479,7 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) { postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - this.loanAccountDomainService.recalculateAccruals(loan, true); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, true); businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan)); return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId()).withEntityId(loanRescheduleRequestId) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java deleted file mode 100644 index 85a6a8dcfc9..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java +++ /dev/null @@ -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 loanScheduleAccrualDataList = this.loanReadPlatformService - .retrievePeriodicAccrualData(tillDate); - addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); - } - - @Override - public void addPeriodicAccruals(final LocalDate tillDate, Loan loan) throws JobExecutionException { - Collection loanScheduleAccrualDataList = this.loanReadPlatformService.retrievePeriodicAccrualData(tillDate, - loan); - addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); - } - - @Override - public void addPeriodicAccruals(final LocalDate tillDate, Collection loanScheduleAccrualDataList) - throws JobExecutionException { - Map> loanDataMap = new HashMap<>(); - for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) { - if (loanDataMap.containsKey(accrualData.getLoanId())) { - loanDataMap.get(accrualData.getLoanId()).add(accrualData); - } else { - Collection accrualDataList = new ArrayList<>(); - accrualDataList.add(accrualData); - loanDataMap.put(accrualData.getLoanId(), accrualDataList); - } - } - - List errors = new ArrayList<>(); - for (Map.Entry> 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); - } - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java deleted file mode 100644 index 4172f5c81b9..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java +++ /dev/null @@ -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) { - Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); - Collection loanWaiverScheduleData = new ArrayList<>(1); - Collection 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) { - boolean firstTime = true; - LocalDate accruedTill = null; - Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); - Collection loanWaiverScheduleData = new ArrayList<>(1); - Collection 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 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 entry : applicableCharges.entrySet()) { - LoanChargeData chargeData = entry.getKey(); - this.jdbcTemplate.update(chargesPaidSql, loanTransaction.getId(), chargeData.getId(), entry.getValue(), - scheduleAccrualData.getInstallmentNumber()); - } - - Map 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 accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap); - this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); - } - - private Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, - final Map transactionMap) { - - final Map 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> newLoanTransactions = new ArrayList<>(); - newLoanTransactions.add(transactionMap); - - accountingBridgeData.put("newLoanTransactions", newLoanTransactions); - return accountingBridgeData; - } - - public Map toMapData(final Long id, final BigDecimal amount, final BigDecimal interestPortion, - final BigDecimal feePortion, final BigDecimal penaltyPortion, final LoanScheduleAccrualData loanScheduleAccrualData, - final LocalDate accruedTill) { - final Map 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 applicableCharges = loanScheduleAccrualData.getApplicableCharges(); - if (applicableCharges != null && !applicableCharges.isEmpty()) { - final List> loanChargesPaidData = new ArrayList<>(); - for (Map.Entry entry : applicableCharges.entrySet()) { - LoanChargeData chargeData = entry.getKey(); - final Map 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 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 chargesData, LoanScheduleAccrualData accrualData, - LocalDate startDate, LocalDate endDate) { - final Map 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 chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate, - LocalDate endDate) { - final Map 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 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 loanWaiverTransactions, - final Collection 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 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 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 existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); - final List 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 existingTransactionIds, - final List existingReversedTransactionIds) { - final MonetaryCurrency currency = loan.getCurrency(); - boolean isAccountTransfer = false; - final Map accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, - existingReversedTransactionIds, isAccountTransfer); - journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); - } -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java new file mode 100644 index 00000000000..fd60e6c53cc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -0,0 +1,1480 @@ +/** + * 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.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.exception.MultiException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +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.common.domain.PeriodFrequencyType; +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.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; +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.LoanTransactionComparator; +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.domain.InterestRecalculationCompoundingMethod; +import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.dao.DataAccessException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessingService { + + 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 LoanChargeReadPlatformService loanChargeReadPlatformService; + private final ExternalIdFactory externalIdFactory; + private final BusinessEventNotifierService businessEventNotifierService; + private final ConfigurationDomainService configurationDomainService; + private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; + private final LoanReadPlatformService loanReadPlatformService; + private final LoanRepositoryWrapper loanRepositoryWrapper; + private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final LoanTransactionRepository loanTransactionRepository; + private final PlatformSecurityContext context; + private final LoanRepository loanRepository; + private final OfficeRepository officeRepository; + private final LoanChargeRepository loanChargeRepository; + + /** + * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan + */ + @Override + @Transactional + public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionException { + Collection loanScheduleAccrualDataList = this.loanReadPlatformService + .retrievePeriodicAccrualData(tillDate); + addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); + } + + /** + * method adds accrual for Loan COB business step + */ + @Override + @Transactional + public void addPeriodicAccruals(final LocalDate tillDate, Loan loan) throws JobExecutionException { + Collection loanScheduleAccrualDataList = this.loanReadPlatformService.retrievePeriodicAccrualData(tillDate, + loan); + addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); + } + + private void addPeriodicAccruals(final LocalDate tillDate, Collection loanScheduleAccrualDataList) + throws JobExecutionException { + Map> loanDataMap = new HashMap<>(); + for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) { + if (loanDataMap.containsKey(accrualData.getLoanId())) { + loanDataMap.get(accrualData.getLoanId()).add(accrualData); + } else { + Collection accrualDataList = new ArrayList<>(); + accrualDataList.add(accrualData); + loanDataMap.put(accrualData.getLoanId(), accrualDataList); + } + } + + List errors = new ArrayList<>(); + for (Map.Entry> mapEntry : loanDataMap.entrySet()) { + try { + 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); + } + } + + /** + * method adds accrual for batch job "Add Accrual Transactions" + */ + + @Override + @Transactional + public void addAccrualAccounting(final Long loanId, final Collection loanScheduleAccrualData) { + Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); + Collection loanWaiverScheduleData = new ArrayList<>(1); + Collection 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()); + calculateFinalAccrualsForScheduleAndAddAccrualAccounting(accrualData); + } + } + + private void addPeriodicAccruals(final LocalDate tillDate, Long loanId, Collection loanScheduleAccrualData) { + boolean firstTime = true; + LocalDate accruedTill = null; + Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); + Collection loanWaiverScheduleData = new ArrayList<>(1); + Collection 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); + calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(tillDate, accrualData); + } + } else { + updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); + updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate); + calculateFinalAccrualsForScheduleAndAddAccrualAccounting(accrualData); + accruedTill = accrualData.getDueDateAsLocaldate(); + } + } + } + + @Transactional + @Override + 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 existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + processIncomePostingAndAccruals(loan); + this.loanRepositoryWrapper.saveAndFlush(loan); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); + } + } + + /** + * method updates accrual derived fields on installments and reverse the unprocessed transactions for loan + * reschedule + */ + @Override + public void reprocessExistingAccruals(Loan loan) { + Collection accruals = retrieveListOfAccrualTransactions(loan); + if (!accruals.isEmpty()) { + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + reprocessPeriodicAccruals(loan, accruals); + } else if (loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { + reprocessNonPeriodicAccruals(loan, accruals); + } + } + + } + + /** + * method calculates accruals for loan with interest recalculation on loan schedule when interest is recalculated + */ + @Override + @Transactional + public void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled) { + LocalDate accruedTill = loan.getAccruedTill(); + if (!loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() || !isInterestRecalculationEnabled || accruedTill == null + || loan.isNpa() || !loan.getStatus().isActive() || loan.isChargedOff()) { + return; + } + + Collection loanScheduleAccrualList = new ArrayList<>(); + accruedTill = createLoanScheduleAccrualDataList(loan, accruedTill, loanScheduleAccrualList); + + if (!loanScheduleAccrualList.isEmpty()) { + try { + addPeriodicAccruals(accruedTill, loanScheduleAccrualList); + } catch (MultiException e) { + String globalisationMessageCode = "error.msg.accrual.exception"; + throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e); + } + } + + } + + /** + * method calculates accruals for loan with interest recalculation and compounding to be posted as income + */ + @Override + public void processIncomePostingAndAccruals(Loan loan) { + if (loan.getLoanInterestRecalculationDetails() != null + && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction()) { + LocalDate lastCompoundingDate = loan.getDisbursementDate(); + List compoundingDetails = extractInterestRecalculationAdditionalDetails(loan); + List incomeTransactions = retrieveListOfIncomePostingTransactions(loan); + List accrualTransactions = retrieveListOfAccrualTransactions(loan); + for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) { + if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) { + break; + } + LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate()); + LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate()); + addUpdateIncomeAndAccrualTransaction(loan, compoundingDetail, lastCompoundingDate, incomeTransaction, accrualTransaction); + lastCompoundingDate = compoundingDetail.getEffectiveDate(); + } + List installments = loan.getRepaymentScheduleInstallments(); + LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment + .getLastNonDownPaymentInstallment(installments); + reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); + reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); + } + } + + /** + * method calculates accruals for loan on loan closure + */ + + @Override + public void processAccrualsForLoanClosure(Loan loan) { + // check and process accruals for loan WITHOUT interest recalculation details and compounding posted as income + processAccrualTransactionsOnLoanClosure(loan); + + // check and process accruals for loan WITH interest recalculation details and compounding posted as income + processIncomeAndAccrualTransactionOnLoanClosure(loan); + + } + + /** + * method calculates accruals for loan on loan fore closure + */ + + @Override + public void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, + Collection newAccrualTransactions) { + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() + && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { + final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); + MonetaryCurrency currency = loan.getCurrency(); + reverseTransactionsPostEffectiveDate(retrieveListOfAccrualTransactions(loan), foreClosureDate); + + HashMap incomeDetails = new HashMap<>(); + + determineReceivableIncomeForeClosure(loan, foreClosureDate, incomeDetails); + + Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus((Money) incomeDetails.get(Loan.INTEREST)); + Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus((Money) incomeDetails.get(Loan.FEE)); + Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus((Money) incomeDetails.get(Loan.PENALTIES)); + Money total = interestPortion.plus(feePortion).plus(penaltyPortion); + + if (total.isGreaterThanZero()) { + createAccrualTransactionAndUpdateChargesPaidBy(loan, foreClosureDate, newAccrualTransactions, currency, interestPortion, + feePortion, penaltyPortion, total); + } + } + + } + + private void calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(final LocalDate tillDate, + final LoanScheduleAccrualData accrualData) { + + BigDecimal amount = BigDecimal.ZERO; + BigDecimal feePortion = accrualData.getDueDateFeeIncome(); + BigDecimal penaltyPortion = accrualData.getDueDatePenaltyIncome(); + BigDecimal interestPortion = getInterestAccruedTillDate(tillDate, accrualData); + + BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome(); + BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome(); + BigDecimal totalCreditedPenalty = accrualData.getCreditedPenalty(); + BigDecimal totalAccFee = accrualData.getAccruedFeeIncome(); + BigDecimal totalCreditedFee = accrualData.getCreditedFee(); + + // interest + 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; + } + + // fee + 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; + } + } + + // penalty + 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); + } + } + + private BigDecimal getInterestAccruedTillDate(LocalDate tillDate, LoanScheduleAccrualData accrualData) { + BigDecimal interestPortion; + 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; + + if (daysToBeAccrued >= totalNumberOfDays) { + interestPortion = accrualData.getAccruableIncome(); + } else { + interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued); + } + interestPortion = interestPortion.setScale(accrualData.getCurrencyData().getDecimalPlaces(), MoneyHelper.getRoundingMode()); + return interestPortion; + } + + private void calculateFinalAccrualsForScheduleAndAddAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) { + + BigDecimal amount = BigDecimal.ZERO; + BigDecimal interestPortion = null; + BigDecimal totalAccInterest = null; + + // interest + 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; + } + } + + // fee + 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; + } + } + + // penalty + 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()); + MonetaryCurrency currency = loan.getCurrency(); + + // create accrual Transaction + LoanTransaction loanTransaction = accrueTransaction(loan, office, accruedTill, amount, interestPortion, feePortion, penaltyPortion, + externalIdFactory.create()); + + // update charges paid by + Map applicableCharges = scheduleAccrualData.getApplicableCharges(); + + for (Map.Entry entry : applicableCharges.entrySet()) { + LoanChargeData chargeData = entry.getKey(); + // + LoanCharge loanCharge = loanChargeRepository.getReferenceById(chargeData.getId()); + loanTransaction.getLoanChargesPaid() + .add(new LoanChargePaidBy(loanTransaction, loanCharge, entry.getValue(), scheduleAccrualData.getInstallmentNumber())); + + } + + loanTransactionRepository.saveAndFlush(loanTransaction); + loan.addLoanTransaction(loanTransaction); + + Map transactionMap = toMapData(loanTransaction.getId(), amount, interestPortion, feePortion, penaltyPortion, + scheduleAccrualData, accruedTill); + + // update repayment schedule portions + + LoanRepaymentScheduleInstallment loanScheduleInstallment = loan.getRepaymentScheduleInstallment(scheduleAccrualData.getDueDate()); + loanScheduleInstallment.updateAccrualPortion(Money.of(currency, totalAccInterest), Money.of(currency, totalAccFee), + Money.of(currency, totalAccPenalty)); + + // update loan accrued till date + loan.setAccruedTill(accruedTill); + loan.setLastModifiedBy(user.getId()); + loan.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); + + loanRepository.saveAndFlush(loan); + + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction)); + + final Map accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + } + + private Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, + final Map transactionMap) { + + final Map 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> newLoanTransactions = new ArrayList<>(); + newLoanTransactions.add(transactionMap); + + accountingBridgeData.put("newLoanTransactions", newLoanTransactions); + return accountingBridgeData; + } + + public Map toMapData(final Long id, final BigDecimal amount, final BigDecimal interestPortion, + final BigDecimal feePortion, final BigDecimal penaltyPortion, final LoanScheduleAccrualData loanScheduleAccrualData, + final LocalDate accruedTill) { + final Map 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 applicableCharges = loanScheduleAccrualData.getApplicableCharges(); + if (applicableCharges != null && !applicableCharges.isEmpty()) { + final List> loanChargesPaidData = new ArrayList<>(); + for (Map.Entry entry : applicableCharges.entrySet()) { + LoanChargeData chargeData = entry.getKey(); + final Map 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 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 chargesData, LoanScheduleAccrualData accrualData, + LocalDate startDate, LocalDate endDate) { + final Map applicableCharges = new HashMap<>(); + BigDecimal submittedDateFeeIncome = BigDecimal.ZERO; + BigDecimal submittedDatePenaltyIncome = BigDecimal.ZERO; + LocalDate scheduleEndDate = accrualData.getDueDateAsLocaldate(); + for (LoanChargeData loanCharge : chargesData) { + BigDecimal chargeAmount = BigDecimal.ZERO; + if (isChargeSubmittedDateAndDueDateInRange(accrualData, startDate, endDate, scheduleEndDate, loanCharge)) { + chargeAmount = loanCharge.getAmount(); + chargeAmount = calculateDueDateCharges(applicableCharges, loanCharge, chargeAmount); + } + 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 boolean isChargeSubmittedDateAndDueDateInRange(LoanScheduleAccrualData accrualData, LocalDate startDate, LocalDate endDate, + LocalDate scheduleEndDate, LoanChargeData loanCharge) { + return ((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()); + } + + private void updateChargeForDueDate(Collection chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate, + LocalDate endDate) { + final Map 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())) { + chargeAmount = calculateInstallmentFeeCharges(accrualData, applicableCharges, loanCharge, chargeAmount); + } + } else if (isChargeDueDateInRange(accrualData, startDate, endDate, loanCharge)) { + chargeAmount = loanCharge.getAmount(); + chargeAmount = calculateDueDateCharges(applicableCharges, loanCharge, chargeAmount); + } + + 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 boolean isChargeDueDateInRange(LoanScheduleAccrualData accrualData, LocalDate startDate, LocalDate endDate, + LoanChargeData loanCharge) { + return ((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(loanCharge.getDueDate(), startDate)) + || DateUtils.isAfter(loanCharge.getDueDate(), startDate)) && !DateUtils.isAfter(loanCharge.getDueDate(), endDate); + } + + private BigDecimal calculateDueDateCharges(Map applicableCharges, LoanChargeData loanCharge, + BigDecimal chargeAmount) { + BigDecimal dueDateChargeAmount = chargeAmount; + if (loanCharge.getAmountUnrecognized() != null) { + dueDateChargeAmount = dueDateChargeAmount.subtract(loanCharge.getAmountUnrecognized()); + } + boolean canAddCharge = dueDateChargeAmount.compareTo(BigDecimal.ZERO) > 0; + if (canAddCharge && (loanCharge.getAmountAccrued() == null || chargeAmount.compareTo(loanCharge.getAmountAccrued()) != 0)) { + BigDecimal amountForAccrual = dueDateChargeAmount; + if (loanCharge.getAmountAccrued() != null) { + amountForAccrual = dueDateChargeAmount.subtract(loanCharge.getAmountAccrued()); + } + applicableCharges.put(loanCharge, amountForAccrual); + } + return dueDateChargeAmount; + } + + private BigDecimal calculateInstallmentFeeCharges(LoanScheduleAccrualData accrualData, + Map applicableCharges, LoanChargeData loanCharge, BigDecimal chargeAmount) { + BigDecimal installmentFeeChargeAmount = chargeAmount; + Collection 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()); + } + installmentFeeChargeAmount = accruableForInstallment; + boolean canAddCharge = installmentFeeChargeAmount.compareTo(BigDecimal.ZERO) > 0; + if (canAddCharge && (installmentChargeData.getAmountAccrued() == null + || installmentFeeChargeAmount.compareTo(installmentChargeData.getAmountAccrued()) != 0)) { + BigDecimal amountForAccrual = installmentFeeChargeAmount; + if (installmentChargeData.getAmountAccrued() != null) { + amountForAccrual = installmentFeeChargeAmount.subtract(installmentChargeData.getAmountAccrued()); + } + applicableCharges.put(loanCharge, amountForAccrual); + BigDecimal amountAccrued = installmentFeeChargeAmount; + if (loanCharge.getAmountAccrued() != null) { + amountAccrued = amountAccrued.add(loanCharge.getAmountAccrued()); + } + loanCharge.updateAmountAccrued(amountAccrued); + } + break; + } + } + return installmentFeeChargeAmount; + } + + private void updateInterestIncome(final LoanScheduleAccrualData accrualData, + final Collection loanWaiverTransactions, + final Collection loanSchedulePeriodDataList, final LocalDate tillDate) { + + BigDecimal interestIncome = BigDecimal.ZERO; + if (accrualData.getInterestIncome() != null) { + interestIncome = accrualData.getInterestIncome(); + } + if (accrualData.getWaivedInterestIncome() != null) { + Collection loanTransactionDatas = new ArrayList<>(); + + getLoanWaiverTransactionsInRange(accrualData, loanWaiverTransactions, tillDate, loanTransactionDatas); + + BigDecimal recognized = getWaivedInterestIncome(accrualData, loanSchedulePeriodDataList, loanTransactionDatas); + + BigDecimal interestWaived = accrualData.getWaivedInterestIncome(); + if (interestWaived.compareTo(recognized) > 0) { + interestIncome = interestIncome.subtract(interestWaived.subtract(recognized)); + } + } + + accrualData.updateAccruableIncome(interestIncome); + } + + private BigDecimal getWaivedInterestIncome(LoanScheduleAccrualData accrualData, + Collection loanSchedulePeriodDataList, Collection loanTransactionDatas) { + BigDecimal recognized = BigDecimal.ZERO; + BigDecimal unrecognized = BigDecimal.ZERO; + BigDecimal remainingAmt = BigDecimal.ZERO; + + Iterator 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; + } + } + + } + } + return recognized; + } + + private void getLoanWaiverTransactionsInRange(LoanScheduleAccrualData accrualData, + Collection loanWaiverTransactions, LocalDate tillDate, + Collection loanTransactionDatas) { + 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); + } + } + } + + private void postJournalEntries(final Loan loan, final List existingTransactionIds, + final List existingReversedTransactionIds) { + final MonetaryCurrency currency = loan.getCurrency(); + boolean isAccountTransfer = false; + final Map accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, + existingReversedTransactionIds, isAccountTransfer); + journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + } + + private void reprocessPeriodicAccruals(Loan loan, final Collection accruals) { + if (!loan.isChargedOff()) { + List installments = loan.getRepaymentScheduleInstallments(); + boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge() + .equalsIgnoreCase("submitted-date"); + for (LoanRepaymentScheduleInstallment installment : installments) { + checkAndUpdateAccrualsForInstallment(loan, accruals, installments, isBasedOnSubmittedOnDate, installment); + } + // reverse accruals after last installment + LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); + reverseTransactionsPostEffectiveDate(accruals, lastInstallment.getDueDate()); + } + } + + private void checkAndUpdateAccrualsForInstallment(Loan loan, Collection accruals, + List installments, boolean isBasedOnSubmittedOnDate, + LoanRepaymentScheduleInstallment installment) { + Money interest = Money.zero(loan.getCurrency()); + Money fee = Money.zero(loan.getCurrency()); + Money penalty = Money.zero(loan.getCurrency()); + for (LoanTransaction loanTransaction : accruals) { + LocalDate transactionDateForRange = getDateForRangeCalculation(loanTransaction, isBasedOnSubmittedOnDate); + boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments); + if (isInPeriod) { + interest = interest.plus(loanTransaction.getInterestPortion(loan.getCurrency())); + fee = fee.plus(loanTransaction.getFeeChargesPortion(loan.getCurrency())); + penalty = penalty.plus(loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); + if (hasIncomeAmountChangedForInstallment(loan, installment, interest, fee, penalty, loanTransaction)) { + interest = interest.minus(loanTransaction.getInterestPortion(loan.getCurrency())); + fee = fee.minus(loanTransaction.getFeeChargesPortion(loan.getCurrency())); + penalty = penalty.minus(loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); + loanTransaction.reverse(); + } + + } + } + installment.updateAccrualPortion(interest, fee, penalty); + } + + private boolean hasIncomeAmountChangedForInstallment(Loan loan, LoanRepaymentScheduleInstallment installment, Money interest, Money fee, + Money penalty, LoanTransaction loanTransaction) { + // if installment income amount is changed or if loan is interest bearing and interest income not accrued + return installment.getFeeChargesCharged(loan.getCurrency()).isLessThan(fee) + || installment.getInterestCharged(loan.getCurrency()).isLessThan(interest) + || installment.getPenaltyChargesCharged(loan.getCurrency()).isLessThan(penalty) + || (loan.isInterestBearing() && DateUtils.isEqual(loan.getAccruedTill(), loanTransaction.getTransactionDate()) + && !DateUtils.isEqual(loan.getAccruedTill(), installment.getDueDate())); + } + + private LocalDate getDateForRangeCalculation(LoanTransaction loanTransaction, boolean isChargeAccrualBasedOnSubmittedOnDate) { + // check config for charge accrual date and return date + return isChargeAccrualBasedOnSubmittedOnDate && !loanTransaction.getLoanChargesPaid().isEmpty() + ? loanTransaction.getLoanChargesPaid().stream().findFirst().get().getLoanCharge().getEffectiveDueDate() + : loanTransaction.getTransactionDate(); + } + + private void reprocessNonPeriodicAccruals(Loan loan, final Collection accruals) { + final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged()); + ExternalId externalId = ExternalId.empty(); + boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled(); + + for (LoanTransaction loanTransaction : accruals) { + if (loanTransaction.getInterestPortion(loan.getCurrency()).isGreaterThanZero()) { + if (loanTransaction.getInterestPortion(loan.getCurrency()).isNotEqualTo(interestApplied)) { + loanTransaction.reverse(); + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } + final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(loan.getOffice(), loan, + interestApplied, loan.getDisbursementDate(), externalId); + loan.addLoanTransaction(interestAppliedTransaction); + } + } else { + Set chargePaidBies = loanTransaction.getLoanChargesPaid(); + for (final LoanChargePaidBy chargePaidBy : chargePaidBies) { + LoanCharge loanCharge = chargePaidBy.getLoanCharge(); + Money chargeAmount = loanCharge.getAmount(loan.getCurrency()); + if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(loan.getCurrency()))) { + loanTransaction.reverse(); + loan.handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate()); + } + } + } + } + } + + private LocalDate createLoanScheduleAccrualDataList(Loan loan, LocalDate accruedTill, + Collection loanScheduleAccrualList) { + boolean isOrganisationDateEnabled = configurationDomainService.isOrganisationstartDateEnabled(); + LocalDate organisationStartDate = DateUtils.getBusinessLocalDate(); + if (isOrganisationDateEnabled) { + organisationStartDate = configurationDomainService.retrieveOrganisationStartDate(); + } + List 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 = applicationCurrencyRepository.findOneWithNotFoundDetection(currency); + CurrencyData currencyData = applicationCurrency.toData(); + Set 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); + } + } + return accruedTill; + } + + private void generateLoanScheduleAccrualData(final LocalDate accruedTill, + final Collection 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 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 createAccrualTransactionAndUpdateChargesPaidBy(Loan loan, LocalDate foreClosureDate, + Collection newAccrualTransactions, MonetaryCurrency currency, Money interestPortion, Money feePortion, + Money penaltyPortion, Money total) { + 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(); + } + newAccrualTransactions.add(accrualTransaction); + loan.addLoanTransaction(accrualTransaction); + Set 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); + } + } + } + + private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate tillDate, Map incomeDetails) { + MonetaryCurrency currency = loan.getCurrency(); + Money receivableInterest = Money.zero(currency); + Money receivableFee = Money.zero(currency); + Money receivablePenalty = Money.zero(currency); + for (final LoanTransaction transaction : loan.getLoanTransactions()) { + 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(); + } + } + + incomeDetails.put(Loan.INTEREST, receivableInterest); + incomeDetails.put(Loan.FEE, receivableFee); + incomeDetails.put(Loan.PENALTIES, receivablePenalty); + } + + private List retrieveListOfAccrualTransactions(Loan loan) { + return loan.getLoanTransactions().stream().filter(transaction -> transaction.isNotReversed() && transaction.isAccrual()) + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); + } + + private List extractInterestRecalculationAdditionalDetails(Loan loan) { + List retDetails = new ArrayList<>(); + List repaymentSchedule = loan.getRepaymentScheduleInstallments(); + if (null != repaymentSchedule) { + for (LoanRepaymentScheduleInstallment installment : repaymentSchedule) { + if (null != installment.getLoanCompoundingDetails()) { + retDetails.addAll(installment.getLoanCompoundingDetails()); + } + } + } + retDetails.sort(Comparator.comparing(LoanInterestRecalcualtionAdditionalDetails::getEffectiveDate)); + return retDetails; + } + + private List retrieveListOfIncomePostingTransactions(Loan loan) { + return loan.getLoanTransactions().stream() // + .filter(transaction -> transaction.isNotReversed() && transaction.isIncomePosting()) // + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); + } + + private LoanTransaction getTransactionForDate(List transactions, LocalDate effectiveDate) { + for (LoanTransaction loanTransaction : transactions) { + if (DateUtils.isEqual(effectiveDate, loanTransaction.getTransactionDate())) { + return loanTransaction; + } + } + return null; + } + + private void addUpdateIncomeAndAccrualTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, + LocalDate lastCompoundingDate, LoanTransaction existingIncomeTransaction, LoanTransaction existingAccrualTransaction) { + BigDecimal interest = BigDecimal.ZERO; + BigDecimal fee = BigDecimal.ZERO; + BigDecimal penalties = BigDecimal.ZERO; + HashMap feeDetails = new HashMap<>(); + + if (loan.getLoanInterestRecalculationDetails().getInterestRecalculationCompoundingMethod() + .equals(InterestRecalculationCompoundingMethod.INTEREST)) { + interest = compoundingDetail.getAmount(); + } else if (loan.getLoanInterestRecalculationDetails().getInterestRecalculationCompoundingMethod() + .equals(InterestRecalculationCompoundingMethod.FEE)) { + determineFeeDetails(loan, lastCompoundingDate, compoundingDetail.getEffectiveDate(), feeDetails); + fee = (BigDecimal) feeDetails.get(Loan.FEE); + penalties = (BigDecimal) feeDetails.get(Loan.PENALTIES); + } else if (loan.getLoanInterestRecalculationDetails().getInterestRecalculationCompoundingMethod() + .equals(InterestRecalculationCompoundingMethod.INTEREST_AND_FEE)) { + determineFeeDetails(loan, lastCompoundingDate, compoundingDetail.getEffectiveDate(), feeDetails); + fee = (BigDecimal) feeDetails.get(Loan.FEE); + penalties = (BigDecimal) feeDetails.get(Loan.PENALTIES); + interest = compoundingDetail.getAmount().subtract(fee).subtract(penalties); + } + + ExternalId externalId = ExternalId.empty(); + if (configurationDomainService.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + + createUpdateIncomePostingTransaction(loan, compoundingDetail, existingIncomeTransaction, interest, fee, penalties, externalId); + createUpdateAccrualTransaction(loan, compoundingDetail, existingAccrualTransaction, interest, fee, penalties, feeDetails, + externalId); + loan.updateLoanOutstandingBalances(); + } + + private void createUpdateAccrualTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, + LoanTransaction existingAccrualTransaction, BigDecimal interest, BigDecimal fee, BigDecimal penalties, + HashMap feeDetails, ExternalId externalId) { + if (configurationDomainService.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + if (existingAccrualTransaction == null) { + LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), + compoundingDetail.getAmount(), interest, fee, penalties, externalId); + updateLoanChargesPaidBy(loan, accrual, feeDetails, null); + loan.addLoanTransaction(accrual); + } else if (existingAccrualTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { + existingAccrualTransaction.reverse(); + LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), + compoundingDetail.getAmount(), interest, fee, penalties, externalId); + updateLoanChargesPaidBy(loan, accrual, feeDetails, null); + loan.addLoanTransaction(accrual); + } + } + } + + private void createUpdateIncomePostingTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, + LoanTransaction existingIncomeTransaction, BigDecimal interest, BigDecimal fee, BigDecimal penalties, ExternalId externalId) { + if (existingIncomeTransaction == null) { + LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), + compoundingDetail.getAmount(), interest, fee, penalties, externalId); + loan.addLoanTransaction(transaction); + } else if (existingIncomeTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { + existingIncomeTransaction.reverse(); + LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), + compoundingDetail.getAmount(), interest, fee, penalties, externalId); + loan.addLoanTransaction(transaction); + } + } + + private void determineFeeDetails(Loan loan, LocalDate fromDate, LocalDate toDate, Map feeDetails) { + BigDecimal fee = BigDecimal.ZERO; + BigDecimal penalties = BigDecimal.ZERO; + + List installments = new ArrayList<>(); + List repaymentSchedule = loan.getRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : repaymentSchedule) { + if (DateUtils.isAfter(loanRepaymentScheduleInstallment.getDueDate(), fromDate) + && !DateUtils.isAfter(loanRepaymentScheduleInstallment.getDueDate(), toDate)) { + installments.add(loanRepaymentScheduleInstallment.getInstallmentNumber()); + } + } + + List loanCharges = new ArrayList<>(); + List loanInstallmentCharges = new ArrayList<>(); + for (LoanCharge loanCharge : loan.getActiveCharges()) { + boolean isDue = DateUtils.isEqual(fromDate, loan.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(Loan.FEE, fee); + feeDetails.put(Loan.PENALTIES, penalties); + feeDetails.put("loanCharges", loanCharges); + feeDetails.put("loanInstallmentCharges", loanInstallmentCharges); + } + + private void updateLoanChargesPaidBy(Loan loan, LoanTransaction accrual, Map feeDetails, + LoanRepaymentScheduleInstallment installment) { + @SuppressWarnings("unchecked") + List loanCharges = (List) feeDetails.get("loanCharges"); + @SuppressWarnings("unchecked") + List loanInstallmentCharges = (List) 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(loan.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(loan.getCurrency()).getAmount(), installmentNumber); + accrual.getLoanChargesPaid().add(loanChargePaidBy); + } + } + } + + private void reverseTransactionsPostEffectiveDate(Collection transactions, LocalDate effectiveDate) { + for (LoanTransaction loanTransaction : transactions) { + if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) { + loanTransaction.reverse(); + } + } + } + + private void processAccrualTransactionsOnLoanClosure(Loan loan) { + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() + // to avoid collision with processIncomeAccrualTransactionOnLoanClosure() + && !(loan.getLoanInterestRecalculationDetails() != null + && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction()) + && !loan.isNpa() && !loan.isChargedOff()) { + HashMap incomeDetails = new HashMap<>(); + MonetaryCurrency currency = loan.getCurrency(); + Money interestPortion = Money.zero(currency); + Money feePortion = Money.zero(currency); + Money penaltyPortion = Money.zero(currency); + + determineReceivableIncomeDetailsForLoanClosure(loan, incomeDetails); + + interestPortion = interestPortion.plus((Money) incomeDetails.get(Loan.INTEREST)); + feePortion = feePortion.plus((Money) incomeDetails.get(Loan.FEE)); + penaltyPortion = penaltyPortion.plus((Money) incomeDetails.get(Loan.PENALTIES)); + + Money total = interestPortion.plus(feePortion).plus(penaltyPortion); + + if (total.isGreaterThanZero()) { + LocalDate accrualTransactionDate = getFinalAccrualTransactionDate(loan); + LoanTransaction accrualTransaction = createAccrualTransaction(loan, interestPortion, feePortion, penaltyPortion, total, + accrualTransactionDate); + updateLoanChargesAndInstallmentChargesPaidBy(loan, accrualTransaction); + // TODO check if this is required + // saveLoanTransactionWithDataIntegrityViolationChecks(accrualTransaction); + loan.addLoanTransaction(accrualTransaction); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)); + + updateLoanInstallmentAccruedPortion(loan); + } + } + } + + private void updateLoanInstallmentAccruedPortion(Loan loan) { + MonetaryCurrency currency = loan.getCurrency(); + 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 void updateLoanChargesAndInstallmentChargesPaidBy(Loan loan, LoanTransaction accrualTransaction) { + MonetaryCurrency currency = loan.getCurrency(); + Set accrualCharges = accrualTransaction.getLoanChargesPaid(); + + Map 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())))); + } + } + } + } + } + + private LoanTransaction createAccrualTransaction(Loan loan, Money interestPortion, Money feePortion, Money penaltyPortion, Money total, + LocalDate accrualTransactionDate) { + ExternalId externalId = externalIdFactory.create(); + LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), accrualTransactionDate, + total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), externalId); + return accrualTransaction; + } + + private void determineReceivableIncomeDetailsForLoanClosure(Loan loan, Map incomeDetails) { + + 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); + } + } + + incomeDetails.put(Loan.INTEREST, interestPortion); + incomeDetails.put(Loan.FEE, feePortion); + incomeDetails.put(Loan.PENALTIES, penaltyPortion); + + } + + private void processIncomeAndAccrualTransactionOnLoanClosure(Loan loan) { + if (loan.getLoanInterestRecalculationDetails() != null + && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction() + && loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) { + + LocalDate closedDate = loan.getClosedOnDate(); + reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate); + reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate); + + HashMap cumulativeIncomeFromInstallments = new HashMap<>(); + determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments); + HashMap cumulativeIncomeFromIncomePosting = new HashMap<>(); + determineCumulativeIncomeDetails(loan, retrieveListOfIncomePostingTransactions(loan), cumulativeIncomeFromIncomePosting); + + BigDecimal interestToPost = cumulativeIncomeFromInstallments.get(Loan.INTEREST) + .subtract(cumulativeIncomeFromIncomePosting.get(Loan.INTEREST)); + BigDecimal feeToPost = cumulativeIncomeFromInstallments.get(Loan.FEE).subtract(cumulativeIncomeFromIncomePosting.get(Loan.FEE)); + BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get(Loan.PENALTY) + .subtract(cumulativeIncomeFromIncomePosting.get(Loan.PENALTY)); + BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost); + + createIncomePostingAndAccrualTransactionOnLoanClosure(loan, closedDate, interestToPost, feeToPost, penaltyToPost, amountToPost); + } + loan.updateLoanOutstandingBalances(); + } + + private void createIncomePostingAndAccrualTransactionOnLoanClosure(Loan loan, LocalDate closedDate, BigDecimal interestToPost, + BigDecimal feeToPost, BigDecimal penaltyToPost, BigDecimal amountToPost) { + ExternalId externalId = ExternalId.empty(); + boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled(); + + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } + LoanTransaction finalIncomeTransaction = LoanTransaction.incomePosting(loan, loan.getOffice(), closedDate, amountToPost, + interestToPost, feeToPost, penaltyToPost, externalId); + loan.addLoanTransaction(finalIncomeTransaction); + + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + List updatedAccrualTransactions = retrieveListOfAccrualTransactions(loan); + LocalDate lastAccruedDate = loan.getDisbursementDate(); + if (!updatedAccrualTransactions.isEmpty()) { + lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate(); + } + HashMap feeDetails = new HashMap<>(); + determineFeeDetails(loan, lastAccruedDate, closedDate, feeDetails); + if (isExternalIdAutoGenerationEnabled) { + externalId = ExternalId.generate(); + } + LoanTransaction finalAccrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), closedDate, amountToPost, + interestToPost, feeToPost, penaltyToPost, externalId); + updateLoanChargesPaidBy(loan, finalAccrual, feeDetails, null); + loan.addLoanTransaction(finalAccrual); + } + } + + private void reverseTransactionsOnOrAfter(List transactions, LocalDate date) { + for (LoanTransaction loanTransaction : transactions) { + if (!DateUtils.isBefore(loanTransaction.getTransactionDate(), date)) { + loanTransaction.reverse(); + } + } + } + + private void determineCumulativeIncomeFromInstallments(Loan loan, HashMap cumulativeIncomeFromInstallments) { + BigDecimal interest = BigDecimal.ZERO; + BigDecimal fee = BigDecimal.ZERO; + BigDecimal penalty = BigDecimal.ZERO; + List installments = loan.getRepaymentScheduleInstallments(); + for (LoanRepaymentScheduleInstallment installment : installments) { + interest = interest.add(installment.getInterestCharged(loan.getCurrency()).getAmount()); + fee = fee.add(installment.getFeeChargesCharged(loan.getCurrency()).getAmount()); + penalty = penalty.add(installment.getPenaltyChargesCharged(loan.getCurrency()).getAmount()); + } + cumulativeIncomeFromInstallments.put(Loan.INTEREST, interest); + cumulativeIncomeFromInstallments.put(Loan.FEE, fee); + cumulativeIncomeFromInstallments.put(Loan.PENALTY, penalty); + } + + private void determineCumulativeIncomeDetails(Loan loan, Collection transactions, + HashMap incomeDetailsMap) { + BigDecimal interest = BigDecimal.ZERO; + BigDecimal fee = BigDecimal.ZERO; + BigDecimal penalty = BigDecimal.ZERO; + for (LoanTransaction transaction : transactions) { + interest = interest.add(transaction.getInterestPortion(loan.getCurrency()).getAmount()); + fee = fee.add(transaction.getFeeChargesPortion(loan.getCurrency()).getAmount()); + penalty = penalty.add(transaction.getPenaltyChargesPortion(loan.getCurrency()).getAmount()); + } + incomeDetailsMap.put(Loan.INTEREST, interest); + incomeDetailsMap.put(Loan.FEE, fee); + incomeDetailsMap.put(Loan.PENALTY, penalty); + } + + 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()); + }; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 783873b2086..1101a259423 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -123,6 +123,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final LoanRepository loanRepository; private final GSIMReadPlatformService gsimReadPlatformService; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Transactional @Override @@ -625,6 +626,7 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final LocalDate recalculateFrom = null; ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); } loan.adjustNetDisbursalAmount(loan.getProposedPrincipal()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index ebfce389e59..ac65d2c3340 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -134,6 +134,7 @@ public class LoanAssembler { private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; private final LoanChargeMapper loanChargeMapper; private final LoanCollateralManagementMapper loanCollateralManagementMapper; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; public Loan assembleFrom(final Long accountId) { final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); @@ -274,7 +275,7 @@ public Loan assembleFrom(final JsonCommand command) { // TODO: review loanApplication.recalculateAllCharges(); topUpLoanConfiguration(element, loanApplication); - + loanAccrualsProcessingService.reprocessExistingAccruals(loanApplication); return loanApplication; } @@ -827,6 +828,7 @@ public Map updateFrom(JsonCommand command, Loan loan) { final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false); loan.updateLoanSchedule(loanSchedule); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); loan.recalculateAllCharges(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 346f1fcfbaa..67bc2085e75 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -165,6 +165,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; private final NoteRepository noteRepository; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate()); @@ -290,7 +291,8 @@ public CommandProcessingResult addLoanCharge(final Long loanId, final JsonComman if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) { - this.loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); } this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); @@ -532,6 +534,13 @@ public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loa existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge, externalId); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled() + && DateUtils.isBefore(loanCharge.getDueLocalDate(), DateUtils.getBusinessLocalDate())) { + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + + } + this.loanTransactionRepository.saveAndFlush(waiveTransaction); this.loanRepositoryWrapper.save(loan); @@ -809,7 +818,8 @@ public void applyOverdueChargesForLoan(final Long loanId, Collection mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java index bbfd56df19c..e9f077e3584 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java @@ -25,14 +25,13 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainServiceJpa; @Slf4j @RequiredArgsConstructor public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService { private final BusinessEventNotifierService businessEventNotifierService; - private final LoanAccountDomainServiceJpa loanAccountDomainService; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @PostConstruct public void addListeners() { @@ -47,7 +46,7 @@ public void onBusinessEvent(LoanStatusChangedBusinessEvent event) { log.debug("Loan Status change for loan {}", loan.getId()); if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) { log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId()); - loanAccountDomainService.applyFinalIncomeAccrualTransaction(loan); + loanAccrualsProcessingService.processAccrualsForLoanClosure(loan); } if (loan.isOpen()) { loan.handleMaturityDateActivate(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 2eaa893db50..4232bcd0ea2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -258,6 +258,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanDownPaymentHandlerService loanDownPaymentHandlerService; private final AccountTransferRepository accountTransferRepository; private final LoanTransactionAssembler loanTransactionAssembler; + private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Transactional @Override @@ -472,6 +473,15 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); } loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); + + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + + LocalDate firstInstallmentDueDate = loan.fetchRepaymentScheduleInstallment(1).getDueDate(); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled() + && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || loan.isDisbursementMissed())) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + if (disbursementTransaction != null) { loanTransactionRepository.saveAndFlush(disbursementTransaction); } @@ -511,6 +521,10 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } else { loanDownPaymentHandlerService.handleDownPayment(scheduleGeneratorDTO, command, disbursementTransaction, loan); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } } } } @@ -557,7 +571,8 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } updateRecurringCalendarDatesForInterestRecalculation(loan); - this.loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); // Post Dated Checks @@ -787,6 +802,14 @@ public Map bulkLoanDisbursal(final JsonCommand command, final Co } else { changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); } + + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + + LocalDate firstInstallmentDueDate = loan.fetchRepaymentScheduleInstallment(1).getDueDate(); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled() + && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || loan.isDisbursementMissed())) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } } if (!changes.isEmpty()) { @@ -832,7 +855,8 @@ public Map bulkLoanDisbursal(final JsonCommand command, final Co this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } updateRecurringCalendarDatesForInterestRecalculation(loan); - loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); } @@ -890,6 +914,8 @@ public CommandProcessingResult undoLoanDisbursal(final Long loanId, final JsonCo final Map changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds, existingReversedTransactionIds); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (!changes.isEmpty()) { if (loan.isTopup() && loan.getClientId() != null) { final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose(); @@ -995,7 +1021,6 @@ private void handleLoanRepaymentInFull(final Loan loan, final LocalDate transact } loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); } - loan.processIncomeAccrualTransactionOnLoanClosure(); } private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate, @@ -1115,6 +1140,8 @@ private ChangedTransactionDetail reprocessChangedLoanTransactions(Loan loan, ScheduleGeneratorDTO scheduleGeneratorDTO) { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); } final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( @@ -1167,7 +1194,8 @@ public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand comma postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate()); // disable all active standing orders linked to this loan if status changes to closed @@ -1473,6 +1501,11 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalTxnExternalId); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency()); if (thereIsNewTransaction) { if (paymentDetail != null) { @@ -1528,7 +1561,8 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - this.loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); @@ -1713,6 +1747,11 @@ public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final Json final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + this.loanTransactionRepository.saveAndFlush(waiveInterestTransaction); /*** @@ -1740,7 +1779,8 @@ public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final Json postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); @@ -1804,6 +1844,12 @@ public CommandProcessingResult writeOff(final Long loanId, final JsonCommand com final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine, changes, existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); + + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L); this.loanTransactionRepository.saveAndFlush(writeOff); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -1820,7 +1866,8 @@ public CommandProcessingResult writeOff(final Long loanId, final JsonCommand com postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff)); @@ -1871,6 +1918,12 @@ public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand co ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine, changes, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); + + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L); if (possibleClosingTransaction != null) { this.loanTransactionRepository.saveAndFlush(possibleClosingTransaction); @@ -1892,7 +1945,8 @@ public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand co postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); } loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); @@ -2345,6 +2399,10 @@ public void applyMeetingDateChanges(final Calendar calendar, final Collection mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); @@ -2545,7 +2607,8 @@ public CommandProcessingResult undoWriteOff(Long loanId) { postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - this.loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); if (writeOffTransaction != null) { businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction)); @@ -2681,6 +2744,11 @@ private CommandProcessingResult processLoanDisbursementDetail(Loan loan, Long lo } } + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + if (command.entityId() != null && changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); @@ -2695,7 +2763,8 @@ private CommandProcessingResult processLoanDisbursementDetail(Loan loan, Long lo } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - this.loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); return new CommandProcessingResultBuilder() // .withOfficeId(loan.getOfficeId()) // @@ -2740,6 +2809,11 @@ public void recalculateInterest(final long loanId) { ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(generatorDTO, existingTransactionIds, existingReversedTransactionIds); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); @@ -2751,7 +2825,8 @@ public void recalculateInterest(final long loanId) { loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); - loanAccountDomainService.recalculateAccruals(loan); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan)); } @@ -2791,6 +2866,12 @@ private void regenerateScheduleOnDisbursement(final JsonCommand command, final L BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName); loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount, nextPossibleRepaymentDate, rescheduledRepaymentDate); + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + } private List retrieveRepaymentScheduleFromModel(LoanScheduleModel model) { @@ -2963,6 +3044,12 @@ public CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand co final Map changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds, existingReversedTransactionIds, loan); + + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + if (!changes.isEmpty()) { loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); String noteText; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index c89b911da4a..71828af657f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -36,7 +36,6 @@ import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; -import org.apache.fineract.organisation.office.domain.OfficeRepository; import org.apache.fineract.organisation.staff.domain.StaffRepository; import org.apache.fineract.organisation.staff.service.StaffReadPlatformService; import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator; @@ -67,7 +66,6 @@ import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; -import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainServiceJpa; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; @@ -96,12 +94,9 @@ import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformServiceImpl; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService; @@ -170,13 +165,6 @@ public GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService(G return new GLIMAccountInfoWritePlatformServiceImpl(glimAccountRepository); } - @Bean - @ConditionalOnMissingBean(LoanAccrualPlatformService.class) - public LoanAccrualPlatformService loanAccrualPlatformService(LoanReadPlatformService loanReadPlatformService, - LoanAccrualWritePlatformService loanAccrualWritePlatformService) { - return new LoanAccrualPlatformServiceImpl(loanReadPlatformService, loanAccrualWritePlatformService); - } - @Bean @ConditionalOnMissingBean(LoanAccrualTransactionBusinessEventService.class) public LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService( @@ -185,22 +173,6 @@ public LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusiness return new LoanAccrualTransactionBusinessEventServiceImpl(businessEventNotifierService); } - @Bean - @ConditionalOnMissingBean(LoanAccrualWritePlatformService.class) - public LoanAccrualWritePlatformService loanAccrualWritePlatformService(LoanReadPlatformService loanReadPlatformService, - LoanChargeReadPlatformService loanChargeReadPlatformService, JdbcTemplate jdbcTemplate, - DatabaseSpecificSQLGenerator sqlGenerator, JournalEntryWritePlatformService journalEntryWritePlatformService, - PlatformSecurityContext context, LoanRepositoryWrapper loanRepositoryWrapper, LoanRepository loanRepository, - OfficeRepository officeRepository, BusinessEventNotifierService businessEventNotifierService, - LoanTransactionRepository loanTransactionRepository, - LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, - ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory) { - return new LoanAccrualWritePlatformServiceImpl(loanReadPlatformService, loanChargeReadPlatformService, jdbcTemplate, sqlGenerator, - journalEntryWritePlatformService, context, loanRepositoryWrapper, loanRepository, officeRepository, - businessEventNotifierService, loanTransactionRepository, loanAccrualTransactionBusinessEventService, - configurationDomainService, externalIdFactory); - } - @Bean @ConditionalOnMissingBean(LoanApplicationWritePlatformService.class) public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context, @@ -215,13 +187,13 @@ public LoanApplicationWritePlatformService loanApplicationWritePlatformService(P LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService, - LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) { + LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService) { return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator, loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository, accountAssociationsRepository, businessEventNotifierService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository, - gsimReadPlatformService, defaultLoanLifecycleStateMachine); + gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService); } @Bean @@ -246,13 +218,14 @@ public LoanAssembler loanAssembler(FromJsonHelper fromApiJsonHelper, LoanReposit AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService, LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper, - LoanCollateralManagementMapper loanCollateralManagementMapper) { + LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) { return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, glimRepository, accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler, - calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper); + calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper, + loanAccrualsProcessingService); } @Bean @@ -309,7 +282,8 @@ public LoanChargeWritePlatformService loanChargeWritePlatformService(LoanChargeA ExternalIdFactory externalIdFactory, AccountTransferDetailRepository accountTransferDetailRepository, LoanChargeAssembler loanChargeAssembler, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService, PaymentDetailWritePlatformService paymentDetailWritePlatformService, NoteRepository noteRepository, - LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService + LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, + LoanAccrualsProcessingService loanAccrualsProcessingService ) { return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository, @@ -318,7 +292,8 @@ public LoanChargeWritePlatformService loanChargeWritePlatformService(LoanChargeA loanChargeReadPlatformService, defaultLoanLifecycleStateMachine, accountAssociationsReadPlatformService, fromApiJsonHelper, configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory, accountTransferDetailRepository, loanChargeAssembler, replayedTransactionBusinessEventService, - paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService); + paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService, + loanAccrualsProcessingService); } @Bean @@ -354,8 +329,8 @@ public LoanReadPlatformServiceImpl loanReadPlatformService(JdbcTemplate jdbcTemp @Bean @ConditionalOnMissingBean(LoanStatusChangePlatformService.class) public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService, - LoanAccountDomainServiceJpa loanAccountDomainService) { - return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccountDomainService); + LoanAccrualsProcessingService loanAccrualsProcessingService) { + return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualsProcessingService); } @Bean @@ -397,7 +372,7 @@ public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService, LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler, LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository, - LoanTransactionAssembler loanTransactionAssembler) { + LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService) { return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository, @@ -410,7 +385,7 @@ public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository, - loanTransactionAssembler); + loanTransactionAssembler, loanAccrualsProcessingService); } @Bean diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStepTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStepTest.java index 84d0e956ac1..49fa70b4541 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStepTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStepTest.java @@ -40,7 +40,7 @@ import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -54,7 +54,7 @@ public class AddPeriodicAccrualEntriesBusinessStepTest { @Mock - private LoanAccrualPlatformService loanAccrualPlatformService; + private LoanAccrualsProcessingService loanAccrualsProcessingService; private AddPeriodicAccrualEntriesBusinessStep underTest; @@ -64,7 +64,7 @@ public void setUp() { ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil .setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault())))); - underTest = new AddPeriodicAccrualEntriesBusinessStep(loanAccrualPlatformService); + underTest = new AddPeriodicAccrualEntriesBusinessStep(loanAccrualsProcessingService); } @AfterEach @@ -79,7 +79,7 @@ public void givenLoanWithAccrual() throws MultiException { // when final Loan processedLoan = underTest.execute(loanForProcessing); // then - verify(loanAccrualPlatformService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing)); + verify(loanAccrualsProcessingService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing)); assertEquals(processedLoan, loanForProcessing); } @@ -89,13 +89,13 @@ public void givenLoanWithAccrualThrowException() throws MultiException { final Long loanId = RandomUtils.nextLong(); final Loan loanForProcessing = Mockito.mock(Loan.class); when(loanForProcessing.getId()).thenReturn(loanId); - doThrow(new MultiException(Collections.singletonList(new RuntimeException()))).when(loanAccrualPlatformService) + doThrow(new MultiException(Collections.singletonList(new RuntimeException()))).when(loanAccrualsProcessingService) .addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing)); // when final BusinessStepException businessStepException = Assert.assertThrows(BusinessStepException.class, () -> underTest.execute(loanForProcessing)); // then - verify(loanAccrualPlatformService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing)); + verify(loanAccrualsProcessingService, times(1)).addPeriodicAccruals(any(LocalDate.class), eq(loanForProcessing)); assertEquals(String.format("Fail to process period accrual for loan id [%s]", loanId), businessStepException.getMessage()); }