FINERACT-1981: Update Payable Interest Calculation For LoanSummaryData
This commit is contained in:
-174
@@ -20,19 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.data;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Optional;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
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.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.
|
* Immutable data object representing loan summary information.
|
||||||
@@ -105,169 +96,4 @@ public class LoanSummaryData {
|
|||||||
private BigDecimal totalUnpaidPayableDueInterest;
|
private BigDecimal totalUnpaidPayableDueInterest;
|
||||||
private BigDecimal totalUnpaidPayableNotDueInterest;
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -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.CollectionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
|
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.LoanSummaryBalancesRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
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;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -52,6 +54,8 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer {
|
|||||||
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
|
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
|
||||||
private final LoanInstallmentLevelDelinquencyEventProducer installmentLevelDelinquencyEventProducer;
|
private final LoanInstallmentLevelDelinquencyEventProducer installmentLevelDelinquencyEventProducer;
|
||||||
private final LoanSummaryBalancesRepository loanSummaryBalancesRepository;
|
private final LoanSummaryBalancesRepository loanSummaryBalancesRepository;
|
||||||
|
@Lazy
|
||||||
|
private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> boolean canSerialize(BusinessEvent<T> event) {
|
public <T> boolean canSerialize(BusinessEvent<T> event) {
|
||||||
@@ -74,12 +78,15 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer {
|
|||||||
CollectionData delinquentData = delinquencyReadPlatformService.calculateLoanCollectionData(loanId);
|
CollectionData delinquentData = delinquencyReadPlatformService.calculateLoanCollectionData(loanId);
|
||||||
data.setDelinquent(delinquentData);
|
data.setDelinquent(delinquentData);
|
||||||
|
|
||||||
|
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
|
||||||
|
.resolveLoanSummaryDataProvider(data.getTransactionProcessingStrategyCode());
|
||||||
|
|
||||||
if (data.getSummary() != null) {
|
if (data.getSummary() != null) {
|
||||||
data.setSummary(LoanSummaryData.withTransactionAmountsSummary(data.getSummary(), data.getRepaymentSchedule(),
|
data.setSummary(loanSummaryDataProvider.withTransactionAmountsSummary(event.get(), data.getSummary(),
|
||||||
loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanId,
|
data.getRepaymentSchedule(), loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanId,
|
||||||
LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES)));
|
LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES)));
|
||||||
} else {
|
} else {
|
||||||
data.setSummary(LoanSummaryData.withOnlyCurrencyData(data.getCurrency()));
|
data.setSummary(loanSummaryDataProvider.withOnlyCurrencyData(data.getCurrency()));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LoanInstallmentDelinquencyBucketDataV1> installmentsDelinquencyData = installmentLevelDelinquencyEventProducer
|
List<LoanInstallmentDelinquencyBucketDataV1> installmentsDelinquencyData = installmentLevelDelinquencyEventProducer
|
||||||
|
|||||||
-21
@@ -27,7 +27,6 @@ import com.google.gson.Gson;
|
|||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
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.boot.FineractProfiles;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
|
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.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||||
@@ -158,23 +156,4 @@ public class InternalLoanInformationApiResource implements InitializingBean {
|
|||||||
final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
|
final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
|
||||||
return advancedPaymentDataMapper.mapLoanPaymentAllocationRule(loan.getPaymentAllocationRules());
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -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.LoanApprovalData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData;
|
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.LoanTermVariationsData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
|
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.GLIMAccountInfoReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
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.LoanProductConstants;
|
||||||
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
|
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
|
||||||
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
|
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
|
||||||
@@ -289,6 +290,7 @@ public class LoansApiResource {
|
|||||||
private final LoanSummaryBalancesRepository loanSummaryBalancesRepository;
|
private final LoanSummaryBalancesRepository loanSummaryBalancesRepository;
|
||||||
private final ClientReadPlatformService clientReadPlatformService;
|
private final ClientReadPlatformService clientReadPlatformService;
|
||||||
private final LoanTermVariationsRepository loanTermVariationsRepository;
|
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
|
* 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
|
// updating summary with transaction amounts summary
|
||||||
if (loanBasicDetails.getSummary() != null) {
|
if (loanBasicDetails.getSummary() != null) {
|
||||||
loanBasicDetails.setSummary(LoanSummaryData.withTransactionAmountsSummary(loanBasicDetails.getSummary(), repaymentSchedule,
|
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
|
||||||
loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanBasicDetails.getId(),
|
.resolveLoanSummaryDataProvider(loanBasicDetails.getTransactionProcessingStrategyCode());
|
||||||
LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES)));
|
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,
|
final LoanAccountData loanAccount = loanBasicDetails.associationsAndTemplate(repaymentSchedule, loanRepayments, charges,
|
||||||
|
|||||||
+173
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+90
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -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);
|
||||||
|
}
|
||||||
+35
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -4674,6 +4674,28 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
|||||||
assertEquals(new BigDecimal("0.05"), loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros());
|
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
|
// Not Due and Due Interest
|
||||||
runAt("20 February 2024", () -> {
|
runAt("20 February 2024", () -> {
|
||||||
final PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec,
|
final PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec,
|
||||||
|
|||||||
+61
@@ -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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
-177
@@ -18,11 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.integrationtests;
|
package org.apache.fineract.integrationtests;
|
||||||
|
|
||||||
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.InterestRateFrequencyType.YEARS;
|
|
||||||
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.RepaymentFrequencyType.DAYS;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import io.restassured.builder.RequestSpecBuilder;
|
import io.restassured.builder.RequestSpecBuilder;
|
||||||
import io.restassured.builder.ResponseSpecBuilder;
|
import io.restassured.builder.ResponseSpecBuilder;
|
||||||
import io.restassured.http.ContentType;
|
import io.restassured.http.ContentType;
|
||||||
@@ -30,22 +25,16 @@ import io.restassured.specification.RequestSpecification;
|
|||||||
import io.restassured.specification.ResponseSpecification;
|
import io.restassured.specification.ResponseSpecification;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
|
||||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansRequest;
|
|
||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
|
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
|
||||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
|
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
|
||||||
import org.apache.fineract.integrationtests.common.Utils;
|
import org.apache.fineract.integrationtests.common.Utils;
|
||||||
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
|
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
@@ -78,158 +67,6 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
|
|||||||
loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(requestSpec, responseSpec);
|
loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(requestSpec, responseSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UC1: (Internal case) Generate a totalInterestRefund using Advanced payment allocation with Interest Refund
|
|
||||||
// options and Apply
|
|
||||||
// Interest Refund transaction
|
|
||||||
// 1. Create a Loan product with Adv. Pment. Alloc. and with Accrual and Interest Refund
|
|
||||||
// 2. Submit, Approve and Disburse a Loan account
|
|
||||||
// 3. Apply the INTEREST_REFUND transaction to validate the Journal Entries generated
|
|
||||||
@Test
|
|
||||||
public void uc1() {
|
|
||||||
final String operationDate = "1 January 2024";
|
|
||||||
AtomicLong createdLoanId = new AtomicLong();
|
|
||||||
runAt(operationDate, () -> {
|
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
|
||||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
|
||||||
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
|
||||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_360)
|
|
||||||
.numberOfRepayments(4)//
|
|
||||||
.maxInterestRatePerPeriod((double) 110)//
|
|
||||||
.repaymentEvery(1)//
|
|
||||||
.repaymentFrequencyType(1L)//
|
|
||||||
.allowPartialPeriodInterestCalcualtion(false)//
|
|
||||||
.multiDisburseLoan(false)//
|
|
||||||
.disallowExpectedDisbursements(null)//
|
|
||||||
.allowApprovedDisbursedAmountsOverApplied(null)//
|
|
||||||
.overAppliedCalculationType(null)//
|
|
||||||
.overAppliedNumber(null)//
|
|
||||||
.installmentAmountInMultiplesOf(null)//
|
|
||||||
;//
|
|
||||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
|
||||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, 4)
|
|
||||||
.interestRatePerPeriod(BigDecimal.valueOf(108.0));
|
|
||||||
|
|
||||||
applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS)
|
|
||||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
|
|
||||||
|
|
||||||
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
|
||||||
createdLoanId.set(loanResponse.getLoanId());
|
|
||||||
|
|
||||||
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
|
|
||||||
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN)
|
|
||||||
.approvedOnDate("1 January 2024").locale("en"));
|
|
||||||
|
|
||||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
|
||||||
new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2024").dateFormat(DATETIME_PATTERN)
|
|
||||||
.transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
|
|
||||||
|
|
||||||
// After Disbursement we are expecting zero interest refund
|
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
|
||||||
assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
});
|
|
||||||
|
|
||||||
runAt("10 February 2024", () -> {
|
|
||||||
// After Interest refund transaction we are expecting non zero interest refund
|
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
|
|
||||||
LOG.info("value {}", loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
|
|
||||||
final BigDecimal inteterestRefund = BigDecimal.valueOf(11);
|
|
||||||
final Long loanTransactionId = loanTransactionHelper.applyInterestRefundLoanTransaction(requestSpec, responseSpec,
|
|
||||||
createdLoanId.get(), buildJsonBody(BigDecimal.valueOf(5.0), BigDecimal.valueOf(3.0), BigDecimal.valueOf(2.0),
|
|
||||||
BigDecimal.valueOf(1.0), null));
|
|
||||||
|
|
||||||
loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
|
|
||||||
assertEquals(inteterestRefund, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
|
|
||||||
verifyTRJournalEntries(loanTransactionId, journalEntry(5.0, loansReceivableAccount, "CREDIT"),
|
|
||||||
journalEntry(3.0, interestReceivableAccount, "CREDIT"), journalEntry(2.0, feeReceivableAccount, "CREDIT"),
|
|
||||||
journalEntry(1.0, penaltyReceivableAccount, "CREDIT"), journalEntry(11.0, overpaymentAccount, "CREDIT"),
|
|
||||||
journalEntry(22.0, interestIncomeAccount, "DEBIT"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// UC2: (Internal case) Generate a totalInterestRefund using Advanced payment allocation with Interest Refund
|
|
||||||
// options and Apply
|
|
||||||
// Interest Refund transaction when the Loan Account is ChargeBack
|
|
||||||
// 1. Create a Loan product with Adv. Pment. Alloc. and with Accrual and Interest Refund
|
|
||||||
// 2. Submit, Approve and Disburse a Loan account
|
|
||||||
// 3. Apply the MERCHANT_ISSUED_REFUND transaction to validate totalInterestRefund different than Zero
|
|
||||||
// 4- Apply the INTEREST_REFUND transaction to validate the Journal Entries generated
|
|
||||||
@Test
|
|
||||||
public void uc2() {
|
|
||||||
final String operationDate = "1 January 2024";
|
|
||||||
AtomicLong createdLoanId = new AtomicLong();
|
|
||||||
runAt(operationDate, () -> {
|
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
|
||||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
|
||||||
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
|
||||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_360)
|
|
||||||
.numberOfRepayments(4)//
|
|
||||||
.maxInterestRatePerPeriod((double) 110)//
|
|
||||||
.repaymentEvery(1)//
|
|
||||||
.repaymentFrequencyType(1L)//
|
|
||||||
.allowPartialPeriodInterestCalcualtion(false)//
|
|
||||||
.multiDisburseLoan(false)//
|
|
||||||
.disallowExpectedDisbursements(null)//
|
|
||||||
.allowApprovedDisbursedAmountsOverApplied(null)//
|
|
||||||
.overAppliedCalculationType(null)//
|
|
||||||
.overAppliedNumber(null)//
|
|
||||||
.installmentAmountInMultiplesOf(null)//
|
|
||||||
;//
|
|
||||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
|
||||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, 4)
|
|
||||||
.interestRatePerPeriod(BigDecimal.valueOf(108.0));
|
|
||||||
|
|
||||||
applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS)
|
|
||||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
|
|
||||||
|
|
||||||
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
|
||||||
createdLoanId.set(loanResponse.getLoanId());
|
|
||||||
|
|
||||||
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
|
|
||||||
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN)
|
|
||||||
.approvedOnDate("1 January 2024").locale("en"));
|
|
||||||
|
|
||||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
|
||||||
new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2024").dateFormat(DATETIME_PATTERN)
|
|
||||||
.transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
|
|
||||||
|
|
||||||
// After Disbursement we are expecting zero interest refund
|
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
|
||||||
assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
});
|
|
||||||
|
|
||||||
runAt("1 February 2024", () -> {
|
|
||||||
// After Interest refund transaction we are expecting non zero interest refund
|
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
|
|
||||||
LOG.info("value {}", loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
|
|
||||||
Long repayment1TransactionId = addRepaymentForLoan(createdLoanId.get(), 250.0, "1 February 2024");
|
|
||||||
chargeOffLoan(createdLoanId.get(), "1 February 2024");
|
|
||||||
});
|
|
||||||
|
|
||||||
runAt("10 February 2024", () -> {
|
|
||||||
// After Interest refund transaction we are expecting non zero interest refund
|
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
|
|
||||||
LOG.info("value {}", loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
LOG.info("chargedOffOnDate {}", loanDetails.getTimeline().getChargedOffOnDate());
|
|
||||||
assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
|
|
||||||
final BigDecimal inteterestRefund = BigDecimal.valueOf(11);
|
|
||||||
final Long loanTransactionId = loanTransactionHelper.applyInterestRefundLoanTransaction(requestSpec, responseSpec,
|
|
||||||
createdLoanId.get(), buildJsonBody(BigDecimal.valueOf(5.0), BigDecimal.valueOf(3.0), BigDecimal.valueOf(2.0),
|
|
||||||
BigDecimal.valueOf(1.0), null));
|
|
||||||
|
|
||||||
loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
|
|
||||||
assertEquals(inteterestRefund, loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
|
|
||||||
|
|
||||||
verifyTRJournalEntries(loanTransactionId, journalEntry(11.0, interestIncomeChargeOffAccount, "CREDIT"),
|
|
||||||
journalEntry(11.0, overpaymentAccount, "CREDIT"), journalEntry(22.0, interestIncomeAccount, "DEBIT"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMerchantIssuedRefundCreatesAndReversesInterestRefund() {
|
public void testMerchantIssuedRefundCreatesAndReversesInterestRefund() {
|
||||||
runAt("01 July 2024", () -> {
|
runAt("01 July 2024", () -> {
|
||||||
@@ -292,20 +129,6 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildJsonBody(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
|
|
||||||
final BigDecimal penaltyCharges, final BigDecimal overpayment) {
|
|
||||||
final HashMap<String, BigDecimal> map = new HashMap<>();
|
|
||||||
map.put("principal", principal);
|
|
||||||
map.put("interest", interest);
|
|
||||||
map.put("feeCharges", feeCharges);
|
|
||||||
map.put("penaltyCharges", penaltyCharges);
|
|
||||||
if (overpayment != null) {
|
|
||||||
map.put("overpayment", overpayment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Gson().toJson(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long createLoanForMerchantIssuedRefundWithInterestRefund(Long clientId) {
|
private Long createLoanForMerchantIssuedRefundWithInterestRefund(Long clientId) {
|
||||||
return createLoanForRefundWithInterestRefund(clientId, "MERCHANT_ISSUED_REFUND");
|
return createLoanForRefundWithInterestRefund(clientId, "MERCHANT_ISSUED_REFUND");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user