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 de2f68cb061..b1e147a5d39 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 @@ 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 36cfe8637da..ef8588c6ce8 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.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 ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { 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/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index c4d49491035..9e909840dea 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.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.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 @@ private String retrieveLoan(final Long loanId, final String loanExternalIdStr, b // 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 00000000000..6e1ce2dabb0 --- /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 00000000000..2d4e06d9d23 --- /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 00000000000..b10f86f7422 --- /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 00000000000..20f9663fcef --- /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 00000000000..6bddb5e95bc --- /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 645ea67feb8..71abae697f4 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 @@ -4670,6 +4670,28 @@ public void uc144() { 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 00000000000..bde69d064b1 --- /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()); + }); + } + } + +}