From f5d9a3a025628eb8b3654a4676fc212a90629fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Wed, 20 Nov 2024 17:18:46 +0100 Subject: [PATCH] FINERACT-1981: Update Payable Interest Calculation For LoanSummaryData --- .../loanaccount/data/LoanSummaryData.java | 174 ----------------- .../loan/LoanBusinessEventSerializer.java | 15 +- .../InternalLoanInformationApiResource.java | 21 --- .../loanaccount/api/LoansApiResource.java | 13 +- .../CommonLoanSummaryDataProvider.java | 173 +++++++++++++++++ .../CumulativeLoanSummaryDataProvider.java | 90 +++++++++ .../service/LoanSummaryDataProvider.java | 47 +++++ .../service/LoanSummaryProviderDelegate.java | 35 ++++ .../ProgressiveLoanSummaryDataProvider.java | 110 +++++++++++ ...ntAllocationLoanRepaymentScheduleTest.java | 22 +++ .../LoanPrepayAmountTest.java | 61 ++++++ .../LoanRefundTransactionTest.java | 177 ------------------ 12 files changed, 558 insertions(+), 380 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java index de2f68cb0..b1e147a5d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java @@ -20,19 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.data; import java.math.BigDecimal; import java.time.LocalDate; -import java.util.Collection; -import java.util.Optional; import lombok.Builder; import lombok.Data; import lombok.experimental.Accessors; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; /** * Immutable data object representing loan summary information. @@ -105,169 +96,4 @@ public class LoanSummaryData { private BigDecimal totalUnpaidPayableDueInterest; private BigDecimal totalUnpaidPayableNotDueInterest; - public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryData defaultSummaryData, - final LoanScheduleData repaymentSchedule, final Collection loanTransactionBalances) { - final LocalDate businessDate = DateUtils.getBusinessLocalDate(); - - BigDecimal totalMerchantRefund = BigDecimal.ZERO; - BigDecimal totalMerchantRefundReversed = BigDecimal.ZERO; - BigDecimal totalPayoutRefund = BigDecimal.ZERO; - BigDecimal totalPayoutRefundReversed = BigDecimal.ZERO; - BigDecimal totalGoodwillCredit = BigDecimal.ZERO; - BigDecimal totalGoodwillCreditReversed = BigDecimal.ZERO; - BigDecimal totalChargeAdjustment = BigDecimal.ZERO; - BigDecimal totalChargeAdjustmentReversed = BigDecimal.ZERO; - BigDecimal totalChargeback = BigDecimal.ZERO; - BigDecimal totalCreditBalanceRefund = BigDecimal.ZERO; - BigDecimal totalCreditBalanceRefundReversed = BigDecimal.ZERO; - BigDecimal totalRepaymentTransaction = BigDecimal.ZERO; - BigDecimal totalRepaymentTransactionReversed = BigDecimal.ZERO; - BigDecimal totalInterestPaymentWaiver = BigDecimal.ZERO; - BigDecimal totalInterestRefund = BigDecimal.ZERO; - BigDecimal totalUnpaidPayableDueInterest = BigDecimal.ZERO; - BigDecimal totalUnpaidPayableNotDueInterest = BigDecimal.ZERO; - - totalChargeAdjustment = fetchLoanTransactionBalanceByType(loanTransactionBalances, - LoanTransactionType.CHARGE_ADJUSTMENT.getValue()); - totalChargeAdjustmentReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, - LoanTransactionType.CHARGE_ADJUSTMENT.getValue()); - - totalChargeback = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.CHARGEBACK.getValue()); - - totalCreditBalanceRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, - LoanTransactionType.CREDIT_BALANCE_REFUND.getValue()); - totalCreditBalanceRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, - LoanTransactionType.CREDIT_BALANCE_REFUND.getValue()); - - totalGoodwillCredit = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.GOODWILL_CREDIT.getValue()); - totalGoodwillCreditReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, - LoanTransactionType.GOODWILL_CREDIT.getValue()); - - totalInterestRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.INTEREST_REFUND.getValue()); - - totalInterestPaymentWaiver = fetchLoanTransactionBalanceByType(loanTransactionBalances, - LoanTransactionType.INTEREST_PAYMENT_WAIVER.getValue()); - - totalMerchantRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, - LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue()); - totalMerchantRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, - LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue()); - - totalPayoutRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.PAYOUT_REFUND.getValue()); - totalPayoutRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, - LoanTransactionType.PAYOUT_REFUND.getValue()); - - totalRepaymentTransaction = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.REPAYMENT.getValue()) - .add(fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.DOWN_PAYMENT.getValue())); - totalRepaymentTransactionReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, - LoanTransactionType.REPAYMENT.getValue()); - - if (repaymentSchedule != null) { - // Outstanding Interest on Past due installments - totalUnpaidPayableDueInterest = computeTotalUnpaidPayableDueInterestAmount(repaymentSchedule.getPeriods(), businessDate); - - // Accumulated daily interest of the current Installment period - totalUnpaidPayableNotDueInterest = computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(repaymentSchedule.getPeriods(), - businessDate, defaultSummaryData.currency); - } - - return LoanSummaryData.builder().currency(defaultSummaryData.currency).principalDisbursed(defaultSummaryData.principalDisbursed) - .principalAdjustments(defaultSummaryData.principalAdjustments).principalPaid(defaultSummaryData.principalPaid) - .principalWrittenOff(defaultSummaryData.principalWrittenOff).principalOutstanding(defaultSummaryData.principalOutstanding) - .principalOverdue(defaultSummaryData.principalOverdue).interestCharged(defaultSummaryData.interestCharged) - .interestPaid(defaultSummaryData.interestPaid).interestWaived(defaultSummaryData.interestWaived) - .interestWrittenOff(defaultSummaryData.interestWrittenOff).interestOutstanding(defaultSummaryData.interestOutstanding) - .interestOverdue(defaultSummaryData.interestOverdue).feeChargesCharged(defaultSummaryData.feeChargesCharged) - .feeAdjustments(defaultSummaryData.feeAdjustments) - .feeChargesDueAtDisbursementCharged(defaultSummaryData.feeChargesDueAtDisbursementCharged) - .feeChargesPaid(defaultSummaryData.feeChargesPaid).feeChargesWaived(defaultSummaryData.feeChargesWaived) - .feeChargesWrittenOff(defaultSummaryData.feeChargesWrittenOff) - .feeChargesOutstanding(defaultSummaryData.feeChargesOutstanding).feeChargesOverdue(defaultSummaryData.feeChargesOverdue) - .penaltyChargesCharged(defaultSummaryData.penaltyChargesCharged).penaltyAdjustments(defaultSummaryData.penaltyAdjustments) - .penaltyChargesPaid(defaultSummaryData.penaltyChargesPaid).penaltyChargesWaived(defaultSummaryData.penaltyChargesWaived) - .penaltyChargesWrittenOff(defaultSummaryData.penaltyChargesWrittenOff) - .penaltyChargesOutstanding(defaultSummaryData.penaltyChargesOutstanding) - .penaltyChargesOverdue(defaultSummaryData.penaltyChargesOverdue) - .totalExpectedRepayment(defaultSummaryData.totalExpectedRepayment).totalRepayment(defaultSummaryData.totalRepayment) - .totalExpectedCostOfLoan(defaultSummaryData.totalExpectedCostOfLoan).totalCostOfLoan(defaultSummaryData.totalCostOfLoan) - .totalWaived(defaultSummaryData.totalWaived).totalWrittenOff(defaultSummaryData.totalWrittenOff) - .totalOutstanding(defaultSummaryData.totalOutstanding).totalOverdue(defaultSummaryData.totalOverdue) - .overdueSinceDate(defaultSummaryData.overdueSinceDate).writeoffReasonId(defaultSummaryData.writeoffReasonId) - .writeoffReason(defaultSummaryData.writeoffReason).totalRecovered(defaultSummaryData.totalRecovered) - .chargeOffReasonId(defaultSummaryData.chargeOffReasonId).chargeOffReason(defaultSummaryData.chargeOffReason) - .totalMerchantRefund(totalMerchantRefund).totalMerchantRefundReversed(totalMerchantRefundReversed) - .totalPayoutRefund(totalPayoutRefund).totalPayoutRefundReversed(totalPayoutRefundReversed) - .totalGoodwillCredit(totalGoodwillCredit).totalGoodwillCreditReversed(totalGoodwillCreditReversed) - .totalChargeAdjustment(totalChargeAdjustment).totalChargeAdjustmentReversed(totalChargeAdjustmentReversed) - .totalChargeback(totalChargeback).totalCreditBalanceRefund(totalCreditBalanceRefund) - .totalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed).totalRepaymentTransaction(totalRepaymentTransaction) - .totalRepaymentTransactionReversed(totalRepaymentTransactionReversed).totalInterestPaymentWaiver(totalInterestPaymentWaiver) - .totalUnpaidPayableDueInterest(totalUnpaidPayableDueInterest) - .totalUnpaidPayableNotDueInterest(totalUnpaidPayableNotDueInterest).totalInterestRefund(totalInterestRefund).build(); - } - - private static BigDecimal fetchLoanTransactionBalanceByType(final Collection loanTransactionBalances, - final Integer transactionType) { - final Optional optLoanTransactionBalance = loanTransactionBalances.stream() - .filter(balance -> balance.getTransactionType().equals(transactionType) && !balance.isReversed()).findFirst(); - return optLoanTransactionBalance.isPresent() ? optLoanTransactionBalance.get().getAmount() : BigDecimal.ZERO; - } - - private static BigDecimal fetchLoanTransactionBalanceReversedByType(final Collection loanTransactionBalances, - final Integer transactionType) { - final Optional optLoanTransactionBalance = loanTransactionBalances.stream() - .filter(balance -> balance.getTransactionType().equals(transactionType) && balance.isReversed() - && balance.isManuallyAdjustedOrReversed()) - .findFirst(); - return optLoanTransactionBalance.isPresent() ? optLoanTransactionBalance.get().getAmount() : BigDecimal.ZERO; - } - - public static LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData) { - return LoanSummaryData.builder().currency(currencyData).build(); - } - - private static BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection periods, - final LocalDate businessDate) { - return periods.stream().filter(period -> !period.getDownPaymentPeriod() && businessDate.compareTo(period.getDueDate()) >= 0) - .map(period -> period.getInterestOutstanding()).reduce(BigDecimal.ZERO, BigDecimal::add); - } - - private static BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Collection periods, - final LocalDate businessDate, final CurrencyData currency) { - // Find the current Period (If exists one) based on the Business date - final Optional optCurrentPeriod = periods.stream() - .filter(period -> !period.getDownPaymentPeriod() && period.isActualPeriodForNotDuePayableCalculation(businessDate)) - .findFirst(); - - if (optCurrentPeriod.isPresent()) { - final LoanSchedulePeriodData currentPeriod = optCurrentPeriod.get(); - final long remainingDays = currentPeriod.getDaysInPeriod() - - DateUtils.getDifferenceInDays(currentPeriod.getFromDate(), businessDate); - - return computeAccruedInterestTillDay(currentPeriod, remainingDays, currency); - } - // Default value equal to Zero - return BigDecimal.ZERO; - } - - public static BigDecimal computeAccruedInterestTillDay(final LoanSchedulePeriodData period, final long untilDay, - final CurrencyData currency) { - Integer remainingDays = period.getDaysInPeriod(); - BigDecimal totalAccruedInterest = BigDecimal.ZERO; - while (remainingDays > untilDay) { - final BigDecimal accruedInterest = period.getInterestDue().subtract(totalAccruedInterest) - .divide(BigDecimal.valueOf(remainingDays), MoneyHelper.getMathContext()); - totalAccruedInterest = totalAccruedInterest.add(accruedInterest); - remainingDays--; - } - - totalAccruedInterest = totalAccruedInterest.subtract(period.getInterestPaid()).subtract(period.getInterestWaived()); - if (MathUtil.isLessThanZero(totalAccruedInterest)) { - // Set Zero If the Interest Paid + Waived is greather than Interest Accrued - totalAccruedInterest = BigDecimal.ZERO; - } - - return Money.of(currency, totalAccruedInterest).getAmount(); - } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java index 36cfe8637..ef8588c6c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java @@ -35,11 +35,13 @@ import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; -import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryDataProvider; +import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryProviderDelegate; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @Component @@ -52,6 +54,8 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer { private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final LoanInstallmentLevelDelinquencyEventProducer installmentLevelDelinquencyEventProducer; private final LoanSummaryBalancesRepository loanSummaryBalancesRepository; + @Lazy + private final LoanSummaryProviderDelegate loanSummaryProviderDelegate; @Override public boolean canSerialize(BusinessEvent event) { @@ -74,12 +78,15 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer { CollectionData delinquentData = delinquencyReadPlatformService.calculateLoanCollectionData(loanId); data.setDelinquent(delinquentData); + LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate + .resolveLoanSummaryDataProvider(data.getTransactionProcessingStrategyCode()); + if (data.getSummary() != null) { - data.setSummary(LoanSummaryData.withTransactionAmountsSummary(data.getSummary(), data.getRepaymentSchedule(), - loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanId, + data.setSummary(loanSummaryDataProvider.withTransactionAmountsSummary(event.get(), data.getSummary(), + data.getRepaymentSchedule(), loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanId, LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES))); } else { - data.setSummary(LoanSummaryData.withOnlyCurrencyData(data.getCurrency())); + data.setSummary(loanSummaryDataProvider.withOnlyCurrencyData(data.getCurrency())); } List installmentsDelinquencyData = installmentLevelDelinquencyEventProducer diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java index 84785072d..ee37e4972 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java @@ -27,7 +27,6 @@ import com.google.gson.Gson; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -43,7 +42,6 @@ import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.boot.FineractProfiles; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; -import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; @@ -158,23 +156,4 @@ public class InternalLoanInformationApiResource implements InitializingBean { final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); return advancedPaymentDataMapper.mapLoanPaymentAllocationRule(loan.getPaymentAllocationRules()); } - - @POST - @Path("{loanId}/apply-interest-refund") - @Consumes({ MediaType.APPLICATION_JSON }) - @Produces({ MediaType.APPLICATION_JSON }) - @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") - public Long applyInterestRefundToLoan(@Context final UriInfo uriInfo, @PathParam("loanId") Long loanId, - final String apiRequestBodyAsJson) { - log.warn("------------------------------------------------------------"); - log.warn(" "); - log.warn(" Apply Loan Transaction to Interest Refund loanId {} ", loanId); - log.warn(" "); - log.warn("------------------------------------------------------------"); - LoanRefundRequestData loanRefundRequestData = gson.fromJson(apiRequestBodyAsJson, LoanRefundRequestData.class); - final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); - final LoanTransaction loanTransaction = loanAccountDomainService.applyInterestRefund(loan, loanRefundRequestData); - return loanTransaction.getId(); - } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index c4d494910..9e909840d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -127,7 +127,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData; -import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; @@ -150,6 +149,8 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermV import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryDataProvider; +import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryProviderDelegate; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; @@ -289,6 +290,7 @@ public class LoansApiResource { private final LoanSummaryBalancesRepository loanSummaryBalancesRepository; private final ClientReadPlatformService clientReadPlatformService; private final LoanTermVariationsRepository loanTermVariationsRepository; + private final LoanSummaryProviderDelegate loanSummaryProviderDelegate; /* * This template API is used for loan approval, ideally this should be invoked on loan that are pending for @@ -1115,9 +1117,12 @@ public class LoansApiResource { // updating summary with transaction amounts summary if (loanBasicDetails.getSummary() != null) { - loanBasicDetails.setSummary(LoanSummaryData.withTransactionAmountsSummary(loanBasicDetails.getSummary(), repaymentSchedule, - loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanBasicDetails.getId(), - LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES))); + LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate + .resolveLoanSummaryDataProvider(loanBasicDetails.getTransactionProcessingStrategyCode()); + loanBasicDetails.setSummary( + loanSummaryDataProvider.withTransactionAmountsSummary(loanBasicDetails.getId(), loanBasicDetails.getSummary(), + repaymentSchedule, loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType( + loanBasicDetails.getId(), LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES))); } final LoanAccountData loanAccount = loanBasicDetails.associationsAndTemplate(repaymentSchedule, loanRepayments, charges, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java new file mode 100644 index 000000000..6e1ce2dab --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java @@ -0,0 +1,173 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import java.util.Optional; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalance; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataProvider { + + @Override + public LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule, + Collection loanTransactionBalances) { + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + + BigDecimal totalMerchantRefund = BigDecimal.ZERO; + BigDecimal totalMerchantRefundReversed = BigDecimal.ZERO; + BigDecimal totalPayoutRefund = BigDecimal.ZERO; + BigDecimal totalPayoutRefundReversed = BigDecimal.ZERO; + BigDecimal totalGoodwillCredit = BigDecimal.ZERO; + BigDecimal totalGoodwillCreditReversed = BigDecimal.ZERO; + BigDecimal totalChargeAdjustment = BigDecimal.ZERO; + BigDecimal totalChargeAdjustmentReversed = BigDecimal.ZERO; + BigDecimal totalChargeback = BigDecimal.ZERO; + BigDecimal totalCreditBalanceRefund = BigDecimal.ZERO; + BigDecimal totalCreditBalanceRefundReversed = BigDecimal.ZERO; + BigDecimal totalRepaymentTransaction = BigDecimal.ZERO; + BigDecimal totalRepaymentTransactionReversed = BigDecimal.ZERO; + BigDecimal totalInterestPaymentWaiver = BigDecimal.ZERO; + BigDecimal totalInterestRefund = BigDecimal.ZERO; + BigDecimal totalUnpaidPayableDueInterest = BigDecimal.ZERO; + BigDecimal totalUnpaidPayableNotDueInterest = BigDecimal.ZERO; + + totalChargeAdjustment = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.CHARGE_ADJUSTMENT.getValue()); + totalChargeAdjustmentReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.CHARGE_ADJUSTMENT.getValue()); + + totalChargeback = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.CHARGEBACK.getValue()); + + totalCreditBalanceRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.CREDIT_BALANCE_REFUND.getValue()); + totalCreditBalanceRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.CREDIT_BALANCE_REFUND.getValue()); + + totalGoodwillCredit = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.GOODWILL_CREDIT.getValue()); + totalGoodwillCreditReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.GOODWILL_CREDIT.getValue()); + + totalInterestRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.INTEREST_REFUND.getValue()); + + totalInterestPaymentWaiver = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.INTEREST_PAYMENT_WAIVER.getValue()); + + totalMerchantRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue()); + totalMerchantRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue()); + + totalPayoutRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.PAYOUT_REFUND.getValue()); + totalPayoutRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.PAYOUT_REFUND.getValue()); + + totalRepaymentTransaction = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.REPAYMENT.getValue()) + .add(fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.DOWN_PAYMENT.getValue())); + totalRepaymentTransactionReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.REPAYMENT.getValue()); + + if (repaymentSchedule != null) { + // Outstanding Interest on Past due installments + totalUnpaidPayableDueInterest = computeTotalUnpaidPayableDueInterestAmount(repaymentSchedule.getPeriods(), businessDate); + + // Accumulated daily interest of the current Installment period + totalUnpaidPayableNotDueInterest = computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(loan, + repaymentSchedule.getPeriods(), businessDate, defaultSummaryData.getCurrency()); + } + + return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency()) + .principalDisbursed(defaultSummaryData.getPrincipalDisbursed()) + .principalAdjustments(defaultSummaryData.getPrincipalAdjustments()).principalPaid(defaultSummaryData.getPrincipalPaid()) + .principalWrittenOff(defaultSummaryData.getPrincipalWrittenOff()) + .principalOutstanding(defaultSummaryData.getPrincipalOutstanding()) + .principalOverdue(defaultSummaryData.getPrincipalOverdue()).interestCharged(defaultSummaryData.getInterestCharged()) + .interestPaid(defaultSummaryData.getInterestPaid()).interestWaived(defaultSummaryData.getInterestWaived()) + .interestWrittenOff(defaultSummaryData.getInterestWrittenOff()) + .interestOutstanding(defaultSummaryData.getInterestOutstanding()).interestOverdue(defaultSummaryData.getInterestOverdue()) + .feeChargesCharged(defaultSummaryData.getFeeChargesCharged()).feeAdjustments(defaultSummaryData.getFeeAdjustments()) + .feeChargesDueAtDisbursementCharged(defaultSummaryData.getFeeChargesDueAtDisbursementCharged()) + .feeChargesPaid(defaultSummaryData.getFeeChargesPaid()).feeChargesWaived(defaultSummaryData.getFeeChargesWaived()) + .feeChargesWrittenOff(defaultSummaryData.getFeeChargesWrittenOff()) + .feeChargesOutstanding(defaultSummaryData.getFeeChargesOutstanding()) + .feeChargesOverdue(defaultSummaryData.getFeeChargesOverdue()) + .penaltyChargesCharged(defaultSummaryData.getPenaltyChargesCharged()) + .penaltyAdjustments(defaultSummaryData.getPenaltyAdjustments()) + .penaltyChargesPaid(defaultSummaryData.getPenaltyChargesPaid()) + .penaltyChargesWaived(defaultSummaryData.getPenaltyChargesWaived()) + .penaltyChargesWrittenOff(defaultSummaryData.getPenaltyChargesWrittenOff()) + .penaltyChargesOutstanding(defaultSummaryData.getPenaltyChargesOutstanding()) + .penaltyChargesOverdue(defaultSummaryData.getPenaltyChargesOverdue()) + .totalExpectedRepayment(defaultSummaryData.getTotalExpectedRepayment()) + .totalRepayment(defaultSummaryData.getTotalRepayment()) + .totalExpectedCostOfLoan(defaultSummaryData.getTotalExpectedCostOfLoan()) + .totalCostOfLoan(defaultSummaryData.getTotalCostOfLoan()).totalWaived(defaultSummaryData.getTotalWaived()) + .totalWrittenOff(defaultSummaryData.getTotalWrittenOff()).totalOutstanding(defaultSummaryData.getTotalOutstanding()) + .totalOverdue(defaultSummaryData.getTotalOverdue()).overdueSinceDate(defaultSummaryData.getOverdueSinceDate()) + .writeoffReasonId(defaultSummaryData.getWriteoffReasonId()).writeoffReason(defaultSummaryData.getWriteoffReason()) + .totalRecovered(defaultSummaryData.getTotalRecovered()).chargeOffReasonId(defaultSummaryData.getChargeOffReasonId()) + .chargeOffReason(defaultSummaryData.getChargeOffReason()).totalMerchantRefund(totalMerchantRefund) + .totalMerchantRefundReversed(totalMerchantRefundReversed).totalPayoutRefund(totalPayoutRefund) + .totalPayoutRefundReversed(totalPayoutRefundReversed).totalGoodwillCredit(totalGoodwillCredit) + .totalGoodwillCreditReversed(totalGoodwillCreditReversed).totalChargeAdjustment(totalChargeAdjustment) + .totalChargeAdjustmentReversed(totalChargeAdjustmentReversed).totalChargeback(totalChargeback) + .totalCreditBalanceRefund(totalCreditBalanceRefund).totalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed) + .totalRepaymentTransaction(totalRepaymentTransaction).totalRepaymentTransactionReversed(totalRepaymentTransactionReversed) + .totalInterestPaymentWaiver(totalInterestPaymentWaiver).totalUnpaidPayableDueInterest(totalUnpaidPayableDueInterest) + .totalUnpaidPayableNotDueInterest(totalUnpaidPayableNotDueInterest).totalInterestRefund(totalInterestRefund).build(); + } + + private static BigDecimal fetchLoanTransactionBalanceByType(final Collection loanTransactionBalances, + final Integer transactionType) { + final Optional optLoanTransactionBalance = loanTransactionBalances.stream() + .filter(balance -> balance.getTransactionType().equals(transactionType) && !balance.isReversed()).findFirst(); + return optLoanTransactionBalance.isPresent() ? optLoanTransactionBalance.get().getAmount() : BigDecimal.ZERO; + } + + private static BigDecimal fetchLoanTransactionBalanceReversedByType(final Collection loanTransactionBalances, + final Integer transactionType) { + final Optional optLoanTransactionBalance = loanTransactionBalances.stream() + .filter(balance -> balance.getTransactionType().equals(transactionType) && balance.isReversed() + && balance.isManuallyAdjustedOrReversed()) + .findFirst(); + return optLoanTransactionBalance.isPresent() ? optLoanTransactionBalance.get().getAmount() : BigDecimal.ZERO; + } + + @Override + public BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection periods, final LocalDate businessDate) { + return periods.stream().filter(period -> !period.getDownPaymentPeriod() && businessDate.compareTo(period.getDueDate()) >= 0) + .map(LoanSchedulePeriodData::getInterestOutstanding).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + @Override + public LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData) { + { + return LoanSummaryData.builder().currency(currencyData).build(); + } + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java new file mode 100644 index 000000000..2d4e06d9d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java @@ -0,0 +1,90 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import java.util.Optional; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalance; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; +import org.springframework.stereotype.Component; + +@Component +public class CumulativeLoanSummaryDataProvider extends CommonLoanSummaryDataProvider { + + @Override + public boolean accept(String loanProcessingStrategyCode) { + return !loanProcessingStrategyCode.equalsIgnoreCase("advanced-payment-allocation-strategy"); + } + + @Override + public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan, + final Collection periods, final LocalDate businessDate, final CurrencyData currency) { + // Find the current Period (If exists one) based on the Business date + final Optional optCurrentPeriod = periods.stream() + .filter(period -> !period.getDownPaymentPeriod() && period.isActualPeriodForNotDuePayableCalculation(businessDate)) + .findFirst(); + + if (optCurrentPeriod.isPresent()) { + final LoanSchedulePeriodData currentPeriod = optCurrentPeriod.get(); + final long remainingDays = currentPeriod.getDaysInPeriod() + - DateUtils.getDifferenceInDays(currentPeriod.getFromDate(), businessDate); + + return computeAccruedInterestTillDay(currentPeriod, remainingDays, currency); + } + // Default value equal to Zero + return BigDecimal.ZERO; + } + + @Override + public LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, + LoanScheduleData repaymentSchedule, Collection loanTransactionBalances) { + Loan loan = null; + return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances); + } + + private static BigDecimal computeAccruedInterestTillDay(final LoanSchedulePeriodData period, final long untilDay, + final CurrencyData currency) { + Integer remainingDays = period.getDaysInPeriod(); + BigDecimal totalAccruedInterest = BigDecimal.ZERO; + while (remainingDays > untilDay) { + final BigDecimal accruedInterest = period.getInterestDue().subtract(totalAccruedInterest) + .divide(BigDecimal.valueOf(remainingDays), MoneyHelper.getMathContext()); + totalAccruedInterest = totalAccruedInterest.add(accruedInterest); + remainingDays--; + } + + totalAccruedInterest = totalAccruedInterest.subtract(period.getInterestPaid()).subtract(period.getInterestWaived()); + if (MathUtil.isLessThanZero(totalAccruedInterest)) { + // Set Zero If the Interest Paid + Waived is greather than Interest Accrued + totalAccruedInterest = BigDecimal.ZERO; + } + + return Money.of(currency, totalAccruedInterest).getAmount(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java new file mode 100644 index 000000000..b10f86f74 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java @@ -0,0 +1,47 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalance; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; + +public interface LoanSummaryDataProvider { + + BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection periods, LocalDate businessDate); + + BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(Loan loan, Collection periods, + LocalDate businessDate, CurrencyData currency); + + LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData); + + LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule, + Collection loanTransactionBalances); + + LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule, + Collection loanTransactionBalances); + + boolean accept(String loanProcessingStrategyCode); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java new file mode 100644 index 000000000..20f9663fc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java @@ -0,0 +1,35 @@ +/** + * 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.util.List; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public class LoanSummaryProviderDelegate { + + private final List loanSummaryDataProviders; + + public LoanSummaryDataProvider resolveLoanSummaryDataProvider(String loanProcessingStrategyCode) { + return loanSummaryDataProviders.stream().filter(provider -> provider.accept(loanProcessingStrategyCode)).findAny() + .orElseThrow(() -> new IllegalArgumentException("No provider found for :" + loanProcessingStrategyCode)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java new file mode 100644 index 000000000..6bddb5e95 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -0,0 +1,110 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalance; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataProvider { + + private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; + private final EMICalculator emiCalculator; + private final LoanRepositoryWrapper loanRepository; + + @Override + public boolean accept(String loanProcessingStrategyCode) { + return loanProcessingStrategyCode.equalsIgnoreCase("advanced-payment-allocation-strategy"); + } + + @Override + public LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, + LoanScheduleData repaymentSchedule, Collection loanTransactionBalances) { + final Loan loan = loanRepository.findOneWithNotFoundDetection(loanId, true); + return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances); + } + + @Override + public LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule, + Collection loanTransactionBalances) { + return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances); + } + + private LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(Loan loan, LocalDate businessDate) { + return loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isDownPayment() && !i.isAdditional() + && !businessDate.isBefore(i.getFromDate()) && businessDate.isBefore(i.getDueDate())).findFirst().orElseGet(() -> { + List list = loan.getRepaymentScheduleInstallments().stream() + .filter(i -> !i.isDownPayment() && !i.isAdditional()).toList(); + return !list.isEmpty() ? list.get(list.size() - 1) : null; + }); + } + + @Override + public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan, + final Collection periods, final LocalDate businessDate, final CurrencyData currency) { + + LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = getRelatedRepaymentScheduleInstallment(loan, businessDate); + if (loan.isInterestBearing() && loanRepaymentScheduleInstallment != null) { + List transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream() + .filter(t -> !t.isAccrualActivity()).toList(); + Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor + .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, + loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight(); + if (!changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getCurrentTransactionToOldId().isEmpty() + || !changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getNewTransactionMappings().isEmpty()) { + throw new RuntimeException("Transactions should not be reverse replayed!"); + } + if (model != null) { + PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(model, loanRepaymentScheduleInstallment.getDueDate(), + businessDate); + if (dueAmounts != null) { + BigDecimal interestPaid = loanRepaymentScheduleInstallment.getInterestPaid(); + BigDecimal dueInterest = dueAmounts.getDueInterest().getAmount(); + if (interestPaid == null) { + return dueInterest; + } + return dueInterest.subtract(interestPaid); + } + } + } + + return BigDecimal.ZERO; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 07bf0a4c7..bfe96eb77 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -4674,6 +4674,28 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan assertEquals(new BigDecimal("0.05"), loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); }); + runAt("31 January 2024", () -> { + // Generate the Accruals + final PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec, + responseSpec); + periodicAccrualAccountingHelper.runPeriodicAccrualAccounting("31 January 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + assertEquals(new BigDecimal("0.09"), loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + }); + + runAt("1 February 2024", () -> { + // Generate the Accruals + final PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec, + responseSpec); + periodicAccrualAccountingHelper.runPeriodicAccrualAccounting("1 February 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + assertEquals(new BigDecimal("0.12"), loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + }); + // Not Due and Due Interest runAt("20 February 2024", () -> { final PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java new file mode 100644 index 000000000..bde69d064 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java @@ -0,0 +1,61 @@ +/** + * 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.integrationtests; + +import java.math.BigDecimal; +import java.util.HashMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; + +@Slf4j +public class LoanPrepayAmountTest extends BaseLoanIntegrationTest { + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + Long loanId; + + @Test + public void testLoanPrepayAmountProgressive() { + runAt("1 January 2024", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, + loanProductsResponse.getResourceId(), "01 January 2024", 1000.0, 9.99, 6, null)); + loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 January 2024")); + disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 January 2024"); + }); + runAt("7 january 2024", () -> { + disburseLoan(loanId, BigDecimal.valueOf(350.0), "04 January 2024"); + disburseLoan(loanId, BigDecimal.valueOf(400.0), "05 January 2024"); + }); + for (int i = 7; i <= 31; i++) { + runAt(i + " January 2024", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + HashMap prepayAmount = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, loanId.intValue()); + Assertions.assertEquals((float) prepayAmount.get("interestPortion"), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().floatValue()); + }); + } + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java index cc9b93e6a..b40068e3b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java @@ -18,11 +18,6 @@ */ package org.apache.fineract.integrationtests; -import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.InterestRateFrequencyType.YEARS; -import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.RepaymentFrequencyType.DAYS; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.google.gson.Gson; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; @@ -30,22 +25,16 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; import java.util.ArrayList; -import java.util.HashMap; -import java.util.concurrent.atomic.AtomicLong; import org.apache.fineract.client.models.GetLoansLoanIdResponse; -import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; -import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; -import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.AccountHelper; -import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -78,158 +67,6 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest { loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(requestSpec, responseSpec); } - // UC1: (Internal case) Generate a totalInterestRefund using Advanced payment allocation with Interest Refund - // options and Apply - // Interest Refund transaction - // 1. Create a Loan product with Adv. Pment. Alloc. and with Accrual and Interest Refund - // 2. Submit, Approve and Disburse a Loan account - // 3. Apply the INTEREST_REFUND transaction to validate the Journal Entries generated - @Test - public void uc1() { - final String operationDate = "1 January 2024"; - AtomicLong createdLoanId = new AtomicLong(); - runAt(operationDate, () -> { - Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); - PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() - .interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS) - .interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_360) - .numberOfRepayments(4)// - .maxInterestRatePerPeriod((double) 110)// - .repaymentEvery(1)// - .repaymentFrequencyType(1L)// - .allowPartialPeriodInterestCalcualtion(false)// - .multiDisburseLoan(false)// - .disallowExpectedDisbursements(null)// - .allowApprovedDisbursedAmountsOverApplied(null)// - .overAppliedCalculationType(null)// - .overAppliedNumber(null)// - .installmentAmountInMultiplesOf(null)// - ;// - PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); - PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, 4) - .interestRatePerPeriod(BigDecimal.valueOf(108.0)); - - applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS) - .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); - - PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); - createdLoanId.set(loanResponse.getLoanId()); - - loanTransactionHelper.approveLoan(loanResponse.getLoanId(), - new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) - .approvedOnDate("1 January 2024").locale("en")); - - loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), - new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2024").dateFormat(DATETIME_PATTERN) - .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); - - // After Disbursement we are expecting zero interest refund - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); - assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - }); - - runAt("10 February 2024", () -> { - // After Interest refund transaction we are expecting non zero interest refund - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); - LOG.info("value {}", loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - - final BigDecimal inteterestRefund = BigDecimal.valueOf(11); - final Long loanTransactionId = loanTransactionHelper.applyInterestRefundLoanTransaction(requestSpec, responseSpec, - createdLoanId.get(), buildJsonBody(BigDecimal.valueOf(5.0), BigDecimal.valueOf(3.0), BigDecimal.valueOf(2.0), - BigDecimal.valueOf(1.0), null)); - - loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); - assertEquals(inteterestRefund, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - - verifyTRJournalEntries(loanTransactionId, journalEntry(5.0, loansReceivableAccount, "CREDIT"), - journalEntry(3.0, interestReceivableAccount, "CREDIT"), journalEntry(2.0, feeReceivableAccount, "CREDIT"), - journalEntry(1.0, penaltyReceivableAccount, "CREDIT"), journalEntry(11.0, overpaymentAccount, "CREDIT"), - journalEntry(22.0, interestIncomeAccount, "DEBIT")); - }); - } - - // UC2: (Internal case) Generate a totalInterestRefund using Advanced payment allocation with Interest Refund - // options and Apply - // Interest Refund transaction when the Loan Account is ChargeBack - // 1. Create a Loan product with Adv. Pment. Alloc. and with Accrual and Interest Refund - // 2. Submit, Approve and Disburse a Loan account - // 3. Apply the MERCHANT_ISSUED_REFUND transaction to validate totalInterestRefund different than Zero - // 4- Apply the INTEREST_REFUND transaction to validate the Journal Entries generated - @Test - public void uc2() { - final String operationDate = "1 January 2024"; - AtomicLong createdLoanId = new AtomicLong(); - runAt(operationDate, () -> { - Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); - PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() - .interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS) - .interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_360) - .numberOfRepayments(4)// - .maxInterestRatePerPeriod((double) 110)// - .repaymentEvery(1)// - .repaymentFrequencyType(1L)// - .allowPartialPeriodInterestCalcualtion(false)// - .multiDisburseLoan(false)// - .disallowExpectedDisbursements(null)// - .allowApprovedDisbursedAmountsOverApplied(null)// - .overAppliedCalculationType(null)// - .overAppliedNumber(null)// - .installmentAmountInMultiplesOf(null)// - ;// - PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); - PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, 4) - .interestRatePerPeriod(BigDecimal.valueOf(108.0)); - - applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS) - .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); - - PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); - createdLoanId.set(loanResponse.getLoanId()); - - loanTransactionHelper.approveLoan(loanResponse.getLoanId(), - new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) - .approvedOnDate("1 January 2024").locale("en")); - - loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), - new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2024").dateFormat(DATETIME_PATTERN) - .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); - - // After Disbursement we are expecting zero interest refund - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); - assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - }); - - runAt("1 February 2024", () -> { - // After Interest refund transaction we are expecting non zero interest refund - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); - LOG.info("value {}", loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - - Long repayment1TransactionId = addRepaymentForLoan(createdLoanId.get(), 250.0, "1 February 2024"); - chargeOffLoan(createdLoanId.get(), "1 February 2024"); - }); - - runAt("10 February 2024", () -> { - // After Interest refund transaction we are expecting non zero interest refund - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); - LOG.info("value {}", loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - LOG.info("chargedOffOnDate {}", loanDetails.getTimeline().getChargedOffOnDate()); - assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - - final BigDecimal inteterestRefund = BigDecimal.valueOf(11); - final Long loanTransactionId = loanTransactionHelper.applyInterestRefundLoanTransaction(requestSpec, responseSpec, - createdLoanId.get(), buildJsonBody(BigDecimal.valueOf(5.0), BigDecimal.valueOf(3.0), BigDecimal.valueOf(2.0), - BigDecimal.valueOf(1.0), null)); - - loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); - assertEquals(inteterestRefund, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros()); - - verifyTRJournalEntries(loanTransactionId, journalEntry(11.0, interestIncomeChargeOffAccount, "CREDIT"), - journalEntry(11.0, overpaymentAccount, "CREDIT"), journalEntry(22.0, interestIncomeAccount, "DEBIT")); - }); - } - @Test public void testMerchantIssuedRefundCreatesAndReversesInterestRefund() { runAt("01 July 2024", () -> { @@ -292,20 +129,6 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest { }); } - private String buildJsonBody(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, - final BigDecimal penaltyCharges, final BigDecimal overpayment) { - final HashMap map = new HashMap<>(); - map.put("principal", principal); - map.put("interest", interest); - map.put("feeCharges", feeCharges); - map.put("penaltyCharges", penaltyCharges); - if (overpayment != null) { - map.put("overpayment", overpayment); - } - - return new Gson().toJson(map); - } - private Long createLoanForMerchantIssuedRefundWithInterestRefund(Long clientId) { return createLoanForRefundWithInterestRefund(clientId, "MERCHANT_ISSUED_REFUND"); }