FINERACT-1981: Update Payable Interest Calculation For LoanSummaryData

This commit is contained in:
Soma Sörös
2024-11-28 13:14:45 +01:00
committed by Adam Saghy
parent 32cc83fdbf
commit f5d9a3a025
12 changed files with 558 additions and 380 deletions
@@ -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 <T> boolean canSerialize(BusinessEvent<T> 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<LoanInstallmentDelinquencyBucketDataV1> installmentsDelinquencyData = installmentLevelDelinquencyEventProducer
@@ -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();
}
}
@@ -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,
@@ -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<LoanTransactionBalance> 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<LoanTransactionBalance> loanTransactionBalances,
final Integer transactionType) {
final Optional<LoanTransactionBalance> 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<LoanTransactionBalance> loanTransactionBalances,
final Integer transactionType) {
final Optional<LoanTransactionBalance> 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<LoanSchedulePeriodData> 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();
}
}
}
@@ -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<LoanSchedulePeriodData> periods, final LocalDate businessDate, final CurrencyData currency) {
// Find the current Period (If exists one) based on the Business date
final Optional<LoanSchedulePeriodData> 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<LoanTransactionBalance> 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();
}
}
@@ -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<LoanSchedulePeriodData> periods, LocalDate businessDate);
BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(Loan loan, Collection<LoanSchedulePeriodData> periods,
LocalDate businessDate, CurrencyData currency);
LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData);
LoanSummaryData withTransactionAmountsSummary(Long loanId, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule,
Collection<LoanTransactionBalance> loanTransactionBalances);
LoanSummaryData withTransactionAmountsSummary(Loan loan, LoanSummaryData defaultSummaryData, LoanScheduleData repaymentSchedule,
Collection<LoanTransactionBalance> loanTransactionBalances);
boolean accept(String loanProcessingStrategyCode);
}
@@ -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<LoanSummaryDataProvider> loanSummaryDataProviders;
public LoanSummaryDataProvider resolveLoanSummaryDataProvider(String loanProcessingStrategyCode) {
return loanSummaryDataProviders.stream().filter(provider -> provider.accept(loanProcessingStrategyCode)).findAny()
.orElseThrow(() -> new IllegalArgumentException("No provider found for :" + loanProcessingStrategyCode));
}
}
@@ -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<LoanTransactionBalance> 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<LoanTransactionBalance> 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<LoanRepaymentScheduleInstallment> 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<LoanSchedulePeriodData> periods, final LocalDate businessDate, final CurrencyData currency) {
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = getRelatedRepaymentScheduleInstallment(loan, businessDate);
if (loan.isInterestBearing() && loanRepaymentScheduleInstallment != null) {
List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream()
.filter(t -> !t.isAccrualActivity()).toList();
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> 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;
}
}