Skip to content

Commit

Permalink
FINERACT-1981: Update Payable Interest Calculation For LoanSummaryData
Browse files Browse the repository at this point in the history
  • Loading branch information
somasorosdpc committed Nov 25, 2024
1 parent bb18aad commit 6ac3d18
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 182 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<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(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<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;
}

public static LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData) {
return LoanSummaryData.builder().currency(currencyData).build();
}

private static BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection<LoanSchedulePeriodData> 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<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;
}

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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -74,12 +78,15 @@ public <T> ByteBufferSerializable toAvroDTO(BusinessEvent<T> 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(data.getId(), 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 6ac3d18

Please sign in to comment.