FINERACT-2080: Extract calculations from loan entity
This commit is contained in:
committed by
Adam Saghy
parent
18a55874ba
commit
47d5330802
+65
-1
@@ -18,6 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.delinquency.service;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EARLIEST_UNPAID_DATE;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.NEXT_UNPAID_DUE_DATE;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
@@ -27,6 +30,7 @@ import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
||||
@@ -51,6 +55,7 @@ import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -142,7 +147,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
||||
// loans
|
||||
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
|
||||
collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()));
|
||||
collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate(nextPaymentDueDateConfig));
|
||||
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
|
||||
|
||||
final LoanTransaction lastPayment = loan.getLastPaymentTransaction();
|
||||
if (lastPayment != null) {
|
||||
@@ -228,4 +233,63 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private LocalDate possibleNextRepaymentDate(final String nextPaymentDueDateConfig, final Loan loan) {
|
||||
if (nextPaymentDueDateConfig == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (nextPaymentDueDateConfig.toLowerCase()) {
|
||||
case EARLIEST_UNPAID_DATE -> getEarliestUnpaidInstallmentDate(loan);
|
||||
case NEXT_UNPAID_DUE_DATE -> getNextUnpaidInstallmentDueDate(loan);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private LocalDate getEarliestUnpaidInstallmentDate(final Loan loan) {
|
||||
LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate();
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
if (installment.isNotFullyPaidOff()) {
|
||||
earliestUnpaidInstallmentDate = installment.getDueDate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LocalDate lastTransactionDate = null;
|
||||
for (final LoanTransaction transaction : loan.getLoanTransactions()) {
|
||||
if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) {
|
||||
lastTransactionDate = transaction.getTransactionDate();
|
||||
}
|
||||
}
|
||||
|
||||
LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate;
|
||||
if (DateUtils.isAfter(lastTransactionDate, earliestUnpaidInstallmentDate)) {
|
||||
possibleNextRepaymentDate = lastTransactionDate;
|
||||
}
|
||||
|
||||
return possibleNextRepaymentDate;
|
||||
}
|
||||
|
||||
private LocalDate getNextUnpaidInstallmentDueDate(final Loan loan) {
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
LocalDate currentBusinessDate = DateUtils.getBusinessLocalDate();
|
||||
LocalDate expectedMaturityDate = loan.determineExpectedMaturityDate();
|
||||
LocalDate nextUnpaidInstallmentDate = expectedMaturityDate;
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
boolean isCurrentDateBeforeInstallmentAndLoanPeriod = DateUtils.isBefore(currentBusinessDate, installment.getDueDate())
|
||||
&& DateUtils.isBefore(currentBusinessDate, expectedMaturityDate);
|
||||
if (installment.isDownPayment()) {
|
||||
isCurrentDateBeforeInstallmentAndLoanPeriod = DateUtils.isEqual(currentBusinessDate, installment.getDueDate())
|
||||
&& DateUtils.isBefore(currentBusinessDate, expectedMaturityDate);
|
||||
}
|
||||
if (isCurrentDateBeforeInstallmentAndLoanPeriod) {
|
||||
if (installment.isNotFullyPaidOff()) {
|
||||
nextUnpaidInstallmentDate = installment.getDueDate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nextUnpaidInstallmentDate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-2
@@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
// TODO: introduce tests for the state machine
|
||||
@@ -36,6 +37,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
|
||||
|
||||
private static final List<LoanStatus> ALLOWED_LOAN_STATUSES = List.of(LoanStatus.values());
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
|
||||
@Override
|
||||
public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) {
|
||||
@@ -45,7 +47,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
|
||||
|
||||
@Override
|
||||
public void transition(final LoanEvent loanEvent, final Loan loan) {
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||
internalTransition(loanEvent, loan);
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
|
||||
return;
|
||||
}
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||
|
||||
final LoanStatusTransition transition = determineTransition(loan, loan.getStatus(), transactionDate);
|
||||
|
||||
|
||||
+15
-374
@@ -67,7 +67,6 @@ import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.apache.fineract.organisation.staff.domain.Staff;
|
||||
import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.collateral.domain.LoanCollateral;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
@@ -76,7 +75,6 @@ import org.apache.fineract.portfolio.group.domain.Group;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
|
||||
@@ -275,6 +273,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@Column(name = "interest_calculated_from_date")
|
||||
private LocalDate interestChargedFromDate;
|
||||
|
||||
@Setter
|
||||
@Column(name = "total_overpaid_derived", scale = 6, precision = 19)
|
||||
private BigDecimal totalOverpaid;
|
||||
|
||||
@@ -347,6 +346,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
private List<LoanTermVariations> loanTermVariations = new ArrayList<>();
|
||||
|
||||
@Setter
|
||||
@Column(name = "total_recovered_derived", scale = 6, precision = 19)
|
||||
private BigDecimal totalRecovered;
|
||||
|
||||
@@ -716,28 +716,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLoanSummaryDerivedFields() {
|
||||
if (isNotDisbursed()) {
|
||||
if (this.summary != null) {
|
||||
this.summary.zeroFields();
|
||||
}
|
||||
this.totalOverpaid = null;
|
||||
} else {
|
||||
final Money overpaidBy = calculateTotalOverpayment();
|
||||
this.totalOverpaid = null;
|
||||
if (!overpaidBy.isLessThanZero()) {
|
||||
this.totalOverpaid = overpaidBy.getAmountDefaultedToNullIfZero();
|
||||
}
|
||||
|
||||
final Money recoveredAmount = calculateTotalRecoveredPayments();
|
||||
this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero();
|
||||
|
||||
final Money principal = this.loanRepaymentScheduleDetail.getPrincipal();
|
||||
this.summary.updateSummary(getCurrency(), principal, getRepaymentScheduleInstallments(), this.charges);
|
||||
updateLoanOutstandingBalances();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInterestRecalculationEnabledForProduct() {
|
||||
return this.loanProduct.isInterestRecalculationEnabled();
|
||||
}
|
||||
@@ -746,33 +724,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.loanProduct.isMultiDisburseLoan();
|
||||
}
|
||||
|
||||
public BigDecimal calculateOverdueAmountPercentageAppliedTo(final LoanCharge loanCharge, final int penaltyWaitPeriod) {
|
||||
LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment();
|
||||
LocalDate graceDate = DateUtils.getBusinessLocalDate().minusDays(penaltyWaitPeriod);
|
||||
Money amount = Money.zero(getCurrency());
|
||||
|
||||
if (DateUtils.isAfter(graceDate, installment.getDueDate())) {
|
||||
amount = calculateOverdueAmountPercentageAppliedTo(installment, loanCharge.getChargeCalculation());
|
||||
if (!amount.isGreaterThanZero()) {
|
||||
loanCharge.setActive(false);
|
||||
}
|
||||
} else {
|
||||
loanCharge.setActive(false);
|
||||
}
|
||||
return amount.getAmount();
|
||||
}
|
||||
|
||||
private Money calculateOverdueAmountPercentageAppliedTo(LoanRepaymentScheduleInstallment installment,
|
||||
ChargeCalculationType calculationType) {
|
||||
return switch (calculationType) {
|
||||
case PERCENT_OF_AMOUNT -> installment.getPrincipalOutstanding(getCurrency());
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST ->
|
||||
installment.getPrincipalOutstanding(getCurrency()).plus(installment.getInterestOutstanding(getCurrency()));
|
||||
case PERCENT_OF_INTEREST -> installment.getInterestOutstanding(getCurrency());
|
||||
default -> Money.zero(getCurrency());
|
||||
};
|
||||
}
|
||||
|
||||
public List<Long> fetchLoanTrancheChargeIds() {
|
||||
return getCharges().stream()//
|
||||
.filter(charge -> charge.isTrancheDisbursementCharge() && charge.isActive()) //
|
||||
@@ -793,7 +744,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private LocalDate determineExpectedMaturityDate() {
|
||||
public LocalDate determineExpectedMaturityDate() {
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments().stream()
|
||||
.filter(i -> !i.isDownPayment() && !i.isAdditional()).toList();
|
||||
final int numberOfInstallments = installments.size();
|
||||
@@ -918,16 +869,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.postDatedChecks = new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<LoanTransaction> retrieveListOfTransactionsForReprocessing() {
|
||||
return getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate()).sorted(LoanTransactionComparator.INSTANCE)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static Predicate<LoanTransaction> loanTransactionForReprocessingPredicate() {
|
||||
return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge()
|
||||
|| transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction());
|
||||
}
|
||||
|
||||
public List<LoanTransaction> retrieveListOfTransactionsExcludeAccruals() {
|
||||
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
|
||||
for (final LoanTransaction transaction : this.loanTransactions) {
|
||||
@@ -945,95 +886,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction) {
|
||||
boolean isChronologicallyLatestRepaymentOrWaiver = true;
|
||||
|
||||
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
|
||||
for (final LoanTransaction previousTransaction : loanTransactions) {
|
||||
if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed()
|
||||
&& (DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())
|
||||
|| (DateUtils.isEqual(currentTransactionDate, previousTransaction.getTransactionDate())
|
||||
&& ((loanTransaction.getId() == null && previousTransaction.getId() == null)
|
||||
|| (loanTransaction.getId() != null && (previousTransaction.getId() == null
|
||||
|| loanTransaction.getId().compareTo(previousTransaction.getId()) < 0)))))) {
|
||||
isChronologicallyLatestRepaymentOrWaiver = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isChronologicallyLatestRepaymentOrWaiver;
|
||||
}
|
||||
|
||||
public boolean isAfterLastRepayment(final LoanTransaction loanTransaction, final List<LoanTransaction> loanTransactions) {
|
||||
return loanTransactions.stream() //
|
||||
.filter(t -> t.isRepaymentLikeType() && t.isNotReversed()) //
|
||||
.noneMatch(t -> DateUtils.isBefore(loanTransaction.getTransactionDate(), t.getTransactionDate()));
|
||||
}
|
||||
|
||||
public boolean isChronologicallyLatestTransaction(final LoanTransaction loanTransaction, final List<LoanTransaction> loanTransactions) {
|
||||
return loanTransactions.stream() //
|
||||
.filter(LoanTransaction::isNotReversed) //
|
||||
.allMatch(t -> DateUtils.isAfter(loanTransaction.getTransactionDate(), t.getTransactionDate()));
|
||||
}
|
||||
|
||||
public LocalDate possibleNextRepaymentDate(final String nextPaymentDueDateConfig) {
|
||||
if (nextPaymentDueDateConfig == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (nextPaymentDueDateConfig.toLowerCase()) {
|
||||
case EARLIEST_UNPAID_DATE -> getEarliestUnpaidInstallmentDate();
|
||||
case NEXT_UNPAID_DUE_DATE -> getNextUnpaidInstallmentDueDate();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private LocalDate getNextUnpaidInstallmentDueDate() {
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
LocalDate currentBusinessDate = DateUtils.getBusinessLocalDate();
|
||||
LocalDate expectedMaturityDate = determineExpectedMaturityDate();
|
||||
LocalDate nextUnpaidInstallmentDate = expectedMaturityDate;
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
boolean isCurrentDateBeforeInstallmentAndLoanPeriod = DateUtils.isBefore(currentBusinessDate, installment.getDueDate())
|
||||
&& DateUtils.isBefore(currentBusinessDate, expectedMaturityDate);
|
||||
if (installment.isDownPayment()) {
|
||||
isCurrentDateBeforeInstallmentAndLoanPeriod = DateUtils.isEqual(currentBusinessDate, installment.getDueDate())
|
||||
&& DateUtils.isBefore(currentBusinessDate, expectedMaturityDate);
|
||||
}
|
||||
if (isCurrentDateBeforeInstallmentAndLoanPeriod) {
|
||||
if (installment.isNotFullyPaidOff()) {
|
||||
nextUnpaidInstallmentDate = installment.getDueDate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nextUnpaidInstallmentDate;
|
||||
}
|
||||
|
||||
private LocalDate getEarliestUnpaidInstallmentDate() {
|
||||
LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate();
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
if (installment.isNotFullyPaidOff()) {
|
||||
earliestUnpaidInstallmentDate = installment.getDueDate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LocalDate lastTransactionDate = null;
|
||||
for (final LoanTransaction transaction : this.loanTransactions) {
|
||||
if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) {
|
||||
lastTransactionDate = transaction.getTransactionDate();
|
||||
}
|
||||
}
|
||||
|
||||
LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate;
|
||||
if (DateUtils.isAfter(lastTransactionDate, earliestUnpaidInstallmentDate)) {
|
||||
possibleNextRepaymentDate = lastTransactionDate;
|
||||
}
|
||||
|
||||
return possibleNextRepaymentDate;
|
||||
}
|
||||
|
||||
public LoanTransaction findWriteOffTransaction() {
|
||||
return this.loanTransactions.stream() //
|
||||
.filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) //
|
||||
@@ -1041,49 +899,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public boolean isOverPaid() {
|
||||
return calculateTotalOverpayment().isGreaterThanZero();
|
||||
}
|
||||
|
||||
public Money calculateTotalOverpayment() {
|
||||
Money totalPaidInRepayments = getTotalPaidInRepayments();
|
||||
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
|
||||
Money cumulativeTotalWaivedOnInstallments = Money.zero(currency);
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
|
||||
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments
|
||||
.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency)))
|
||||
.plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency));
|
||||
|
||||
cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency));
|
||||
}
|
||||
|
||||
for (final LoanTransaction loanTransaction : this.loanTransactions) {
|
||||
if (loanTransaction.isReversed()) {
|
||||
continue;
|
||||
}
|
||||
if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
|
||||
} else if (loanTransaction.isCreditBalanceRefund()) {
|
||||
if (loanTransaction.getPrincipalPortion(currency).isZero()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency));
|
||||
}
|
||||
} else if (loanTransaction.isChargeback()) {
|
||||
if (loanTransaction.getPrincipalPortion(currency).isZero() && getCreditAllocationRules().stream() //
|
||||
.filter(car -> car.getTransactionType().equals(CreditAllocationTransactionType.CHARGEBACK)) //
|
||||
.findAny() //
|
||||
.isEmpty()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if total paid in transactions doesn't match repayment schedule then there's an overpayment.
|
||||
return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
|
||||
}
|
||||
|
||||
public Money calculateTotalRecoveredPayments() {
|
||||
// in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments
|
||||
return getTotalRecoveredPayments();
|
||||
@@ -1392,30 +1207,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return isChargedOff() && getChargeOffReason() != null ? getChargeOffReason().getId() : null;
|
||||
}
|
||||
|
||||
public Money getReceivableInterest(final LocalDate tillDate) {
|
||||
Money receivableInterest = Money.zero(getCurrency());
|
||||
for (final LoanTransaction transaction : this.loanTransactions) {
|
||||
if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement()
|
||||
&& !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) {
|
||||
if (transaction.isAccrual()) {
|
||||
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency()));
|
||||
} else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver() || transaction.isAccrualAdjustment()) {
|
||||
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency()));
|
||||
}
|
||||
}
|
||||
if (receivableInterest.isLessThanZero()) {
|
||||
receivableInterest = receivableInterest.zero();
|
||||
}
|
||||
/*
|
||||
* if (transaction.getTransactionDate().isAfter(tillDate) && transaction.isAccrual()) { final String
|
||||
* errorMessage = "The date on which a loan is interest waived cannot be in after accrual transactions." ;
|
||||
* throw new InvalidLoanStateTransitionException("waive", "cannot.be.after.accrual.date", errorMessage,
|
||||
* tillDate); }
|
||||
*/
|
||||
}
|
||||
return receivableInterest;
|
||||
}
|
||||
|
||||
public boolean isSyncDisbursementWithMeeting() {
|
||||
return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting;
|
||||
}
|
||||
@@ -1480,13 +1271,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return currentTransactionDate;
|
||||
}
|
||||
|
||||
public LoanTransaction getLastTransactionForReprocessing() {
|
||||
return loanTransactions.stream() //
|
||||
.filter(Loan.loanTransactionForReprocessingPredicate()) //
|
||||
.reduce((first, second) -> second) //
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public LoanTransaction getLastPaymentTransaction() {
|
||||
return loanTransactions.stream() //
|
||||
.filter(loanTransaction -> !loanTransaction.isReversed()) //
|
||||
@@ -1507,6 +1291,17 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.charges == null ? new HashSet<>() : this.charges.stream().filter(LoanCharge::isActive).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public LoanCharge fetchLoanChargesById(final Long id) {
|
||||
LoanCharge charge = null;
|
||||
for (LoanCharge loanCharge : this.charges) {
|
||||
if (id.equals(loanCharge.getId())) {
|
||||
charge = loanCharge;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return charge;
|
||||
}
|
||||
|
||||
public List<LoanDisbursementDetails> getAllDisbursementDetails() {
|
||||
return this.disbursementDetails;
|
||||
}
|
||||
@@ -1800,21 +1595,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursementDetail) {
|
||||
for (final LoanCharge charge : getActiveCharges()) {
|
||||
if (charge.isOverdueInstallmentCharge()) {
|
||||
charge.setActive(false);
|
||||
} else if (charge.isTrancheDisbursementCharge() && disbursementDetail.getDisbursementDate()
|
||||
.equals(charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate())) {
|
||||
charge.resetToOriginal(getCurrency());
|
||||
}
|
||||
}
|
||||
this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(disbursementDetail.principal()));
|
||||
disbursementDetail.updateActualDisbursementDate(null);
|
||||
disbursementDetail.reverse();
|
||||
updateLoanSummaryDerivedFields();
|
||||
}
|
||||
|
||||
private int adjustNumberOfRepayments() {
|
||||
int repaymetsForAdjust = 0;
|
||||
for (LoanTermVariations loanTermVariations : this.loanTermVariations) {
|
||||
@@ -1867,145 +1647,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.writeOffReason = writeOffReason;
|
||||
}
|
||||
|
||||
public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) {
|
||||
Money[] receivables = retrieveIncomeOutstandingTillDate(closureDate);
|
||||
Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding());
|
||||
totalPrincipal = totalPrincipal.minus(receivables[3]);
|
||||
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
|
||||
return new LoanRepaymentScheduleInstallment(null, 0, currentDate, currentDate, totalPrincipal.getAmount(),
|
||||
receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, null);
|
||||
}
|
||||
|
||||
private Money[] retrieveIncomeOutstandingTillDate(final LocalDate paymentDate) {
|
||||
Money[] balances = new Money[4];
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
Money interest = Money.zero(currency);
|
||||
Money paidFromFutureInstallments = Money.zero(currency);
|
||||
Money fee = Money.zero(currency);
|
||||
Money penalty = Money.zero(currency);
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
|
||||
if (!DateUtils.isBefore(paymentDate, installment.getDueDate())) {
|
||||
interest = interest.plus(installment.getInterestOutstanding(currency));
|
||||
penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency));
|
||||
fee = fee.plus(installment.getFeeChargesOutstanding(currency));
|
||||
} else if (DateUtils.isAfter(paymentDate, installment.getFromDate())) {
|
||||
Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment,
|
||||
isFirstNormalInstallment);
|
||||
if (balancesForCurrentPeroid[0].isGreaterThan(balancesForCurrentPeroid[5])) {
|
||||
interest = interest.plus(balancesForCurrentPeroid[0]).minus(balancesForCurrentPeroid[5]);
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeroid[5])
|
||||
.minus(balancesForCurrentPeroid[0]);
|
||||
}
|
||||
if (balancesForCurrentPeroid[1].isGreaterThan(balancesForCurrentPeroid[3])) {
|
||||
fee = fee.plus(balancesForCurrentPeroid[1].minus(balancesForCurrentPeroid[3]));
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments
|
||||
.plus(balancesForCurrentPeroid[3].minus(balancesForCurrentPeroid[1]));
|
||||
}
|
||||
if (balancesForCurrentPeroid[2].isGreaterThan(balancesForCurrentPeroid[4])) {
|
||||
penalty = penalty.plus(balancesForCurrentPeroid[2].minus(balancesForCurrentPeroid[4]));
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeroid[4])
|
||||
.minus(balancesForCurrentPeroid[2]);
|
||||
}
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments.plus(installment.getInterestPaid(currency))
|
||||
.plus(installment.getPenaltyChargesPaid(currency)).plus(installment.getFeeChargesPaid(currency));
|
||||
}
|
||||
|
||||
}
|
||||
balances[0] = interest;
|
||||
balances[1] = fee;
|
||||
balances[2] = penalty;
|
||||
balances[3] = paidFromFutureInstallments;
|
||||
return balances;
|
||||
}
|
||||
|
||||
private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate, final MonetaryCurrency currency,
|
||||
final LoanRepaymentScheduleInstallment installment, boolean isFirstNormalInstallment) {
|
||||
Money penaltyForCurrentPeriod = Money.zero(getCurrency());
|
||||
Money penaltyAccoutedForCurrentPeriod = Money.zero(getCurrency());
|
||||
Money feeForCurrentPeriod = Money.zero(getCurrency());
|
||||
Money feeAccountedForCurrentPeriod = Money.zero(getCurrency());
|
||||
int totalPeriodDays = DateUtils.getExactDifferenceInDays(installment.getFromDate(), installment.getDueDate());
|
||||
int tillDays = DateUtils.getExactDifferenceInDays(installment.getFromDate(), paymentDate);
|
||||
Money interestForCurrentPeriod = Money.of(getCurrency(), BigDecimal
|
||||
.valueOf(calculateInterestForDays(totalPeriodDays, installment.getInterestCharged(getCurrency()).getAmount(), tillDays)));
|
||||
Money interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency())
|
||||
.plus(installment.getInterestPaid(getCurrency()));
|
||||
for (LoanCharge loanCharge : this.charges) {
|
||||
if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) {
|
||||
boolean isDue = loanCharge.isDueInPeriod(installment.getFromDate(), paymentDate, isFirstNormalInstallment);
|
||||
if (isDue) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyForCurrentPeriod = penaltyForCurrentPeriod.plus(loanCharge.getAmount(getCurrency()));
|
||||
penaltyAccoutedForCurrentPeriod = penaltyAccoutedForCurrentPeriod
|
||||
.plus(loanCharge.getAmountWaived(getCurrency()).plus(loanCharge.getAmountPaid(getCurrency())));
|
||||
} else {
|
||||
feeForCurrentPeriod = feeForCurrentPeriod.plus(loanCharge.getAmount(currency));
|
||||
feeAccountedForCurrentPeriod = feeAccountedForCurrentPeriod.plus(loanCharge.getAmountWaived(getCurrency()).plus(
|
||||
|
||||
loanCharge.getAmountPaid(getCurrency())));
|
||||
}
|
||||
} else if (loanCharge.isInstalmentFee()) {
|
||||
LoanInstallmentCharge loanInstallmentCharge = loanCharge.getInstallmentLoanCharge(installment.getInstallmentNumber());
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyAccoutedForCurrentPeriod = penaltyAccoutedForCurrentPeriod
|
||||
.plus(loanInstallmentCharge.getAmountPaid(currency));
|
||||
} else {
|
||||
feeAccountedForCurrentPeriod = feeAccountedForCurrentPeriod.plus(loanInstallmentCharge.getAmountPaid(currency));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Money[] balances = new Money[6];
|
||||
balances[0] = interestForCurrentPeriod;
|
||||
balances[1] = feeForCurrentPeriod;
|
||||
balances[2] = penaltyForCurrentPeriod;
|
||||
balances[3] = feeAccountedForCurrentPeriod;
|
||||
balances[4] = penaltyAccoutedForCurrentPeriod;
|
||||
balances[5] = interestAccountedForCurrentPeriod;
|
||||
return balances;
|
||||
}
|
||||
|
||||
public Money[] retrieveIncomeForOverlappingPeriod(final LocalDate paymentDate) {
|
||||
Money[] balances = new Money[3];
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
balances[0] = balances[1] = balances[2] = Money.zero(currency);
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(repaymentScheduleInstallments);
|
||||
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
|
||||
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
|
||||
if (DateUtils.isEqual(paymentDate, installment.getDueDate())) {
|
||||
Money interest = installment.getInterestCharged(currency);
|
||||
Money fee = installment.getFeeChargesCharged(currency);
|
||||
Money penalty = installment.getPenaltyChargesCharged(currency);
|
||||
balances[0] = interest;
|
||||
balances[1] = fee;
|
||||
balances[2] = penalty;
|
||||
break;
|
||||
} else if (DateUtils.isDateInRangeExclusive(paymentDate, installment.getFromDate(), installment.getDueDate())) {
|
||||
balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment, isFirstNormalInstallment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return balances;
|
||||
}
|
||||
|
||||
private double calculateInterestForDays(int daysInPeriod, BigDecimal interest, int days) {
|
||||
if (interest.doubleValue() == 0) {
|
||||
return 0;
|
||||
}
|
||||
return interest.doubleValue() / daysInPeriod * days;
|
||||
}
|
||||
|
||||
public void updateLoanScheduleOnForeclosure(final Collection<LoanRepaymentScheduleInstallment> installments) {
|
||||
this.repaymentScheduleInstallments.clear();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
@@ -2046,7 +1687,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.loanTopupDetails;
|
||||
}
|
||||
|
||||
public Collection<LoanCharge> getLoanCharges() {
|
||||
public Set<LoanCharge> getLoanCharges() {
|
||||
return this.charges;
|
||||
}
|
||||
|
||||
|
||||
+3
-347
@@ -33,18 +33,13 @@ import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
@@ -56,11 +51,11 @@ import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
||||
import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") })
|
||||
@@ -104,7 +99,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@Column(name = "amount_paid_derived", scale = 6, precision = 19)
|
||||
private BigDecimal amountPaid;
|
||||
|
||||
@Setter
|
||||
@Column(name = "amount_waived_derived", scale = 6, precision = 19)
|
||||
private BigDecimal amountWaived;
|
||||
|
||||
@@ -149,111 +143,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
private Set<LoanChargePaidBy> loanChargePaidBySet = new HashSet<>();
|
||||
|
||||
protected LoanCharge() {
|
||||
//
|
||||
}
|
||||
|
||||
public LoanCharge(final Loan loan, final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount,
|
||||
final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate,
|
||||
final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final BigDecimal loanCharge,
|
||||
final ExternalId externalId) {
|
||||
this.loan = loan;
|
||||
this.charge = chargeDefinition;
|
||||
this.submittedOnDate = DateUtils.getBusinessLocalDate();
|
||||
this.penaltyCharge = chargeDefinition.isPenalty();
|
||||
this.minCap = chargeDefinition.getMinCap();
|
||||
this.maxCap = chargeDefinition.getMaxCap();
|
||||
|
||||
this.chargeTime = chargeDefinition.getChargeTimeType();
|
||||
if (chargeTime != null) {
|
||||
this.chargeTime = chargeTime.getValue();
|
||||
}
|
||||
|
||||
if (ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.SPECIFIED_DUE_DATE)
|
||||
|| ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.OVERDUE_INSTALLMENT)) {
|
||||
|
||||
if (dueDate == null) {
|
||||
final String defaultUserMessage = "Loan charge is missing due date.";
|
||||
throw new LoanChargeWithoutMandatoryFieldException("loanCharge", "dueDate", defaultUserMessage, chargeDefinition.getId(),
|
||||
chargeDefinition.getName());
|
||||
}
|
||||
|
||||
this.dueDate = dueDate;
|
||||
} else {
|
||||
this.dueDate = null;
|
||||
}
|
||||
|
||||
this.chargeCalculation = chargeDefinition.getChargeCalculation();
|
||||
if (chargeCalculation != null) {
|
||||
this.chargeCalculation = chargeCalculation.getValue();
|
||||
}
|
||||
|
||||
BigDecimal chargeAmount = chargeDefinition.getAmount();
|
||||
if (amount != null) {
|
||||
chargeAmount = amount;
|
||||
}
|
||||
|
||||
this.chargePaymentMode = chargeDefinition.getChargePaymentMode();
|
||||
if (chargePaymentMode != null) {
|
||||
this.chargePaymentMode = chargePaymentMode.getValue();
|
||||
}
|
||||
|
||||
populateDerivedFields(loanPrincipal, chargeAmount, numberOfRepayments, loanCharge);
|
||||
this.paid = determineIfFullyPaid();
|
||||
this.externalId = externalId;
|
||||
}
|
||||
|
||||
public void populateDerivedFields(final BigDecimal amountPercentageAppliedTo, final BigDecimal chargeAmount, Integer numberOfRepayments,
|
||||
BigDecimal loanCharge) {
|
||||
|
||||
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
|
||||
case INVALID:
|
||||
this.percentage = null;
|
||||
this.amount = null;
|
||||
this.amountPercentageAppliedTo = null;
|
||||
this.amountPaid = null;
|
||||
this.amountOutstanding = BigDecimal.ZERO;
|
||||
this.amountWaived = null;
|
||||
this.amountWrittenOff = null;
|
||||
break;
|
||||
case FLAT:
|
||||
this.percentage = null;
|
||||
this.amountPercentageAppliedTo = null;
|
||||
this.amountPaid = null;
|
||||
if (isInstalmentFee()) {
|
||||
if (numberOfRepayments == null) {
|
||||
numberOfRepayments = this.loan.fetchNumberOfInstallmensAfterExceptions();
|
||||
}
|
||||
this.amount = chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments));
|
||||
} else {
|
||||
this.amount = chargeAmount;
|
||||
}
|
||||
this.amountOutstanding = this.amount;
|
||||
this.amountWaived = null;
|
||||
this.amountWrittenOff = null;
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT:
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
case PERCENT_OF_INTEREST:
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
this.percentage = chargeAmount;
|
||||
this.amountPercentageAppliedTo = amountPercentageAppliedTo;
|
||||
if (loanCharge.compareTo(BigDecimal.ZERO) == 0) {
|
||||
loanCharge = percentageOf(this.amountPercentageAppliedTo);
|
||||
}
|
||||
this.amount = minimumAndMaximumCap(loanCharge);
|
||||
this.amountPaid = null;
|
||||
this.amountOutstanding = calculateOutstanding();
|
||||
this.amountWaived = null;
|
||||
this.amountWrittenOff = null;
|
||||
break;
|
||||
}
|
||||
this.amountOrPercentage = chargeAmount;
|
||||
if (this.loan != null && isInstalmentFee()) {
|
||||
updateInstallmentCharges();
|
||||
}
|
||||
}
|
||||
|
||||
public void markAsFullyPaid() {
|
||||
this.amountPaid = this.amount;
|
||||
this.amountOutstanding = BigDecimal.ZERO;
|
||||
@@ -340,234 +229,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.loan = loan;
|
||||
}
|
||||
|
||||
public void update(final BigDecimal amount, final LocalDate dueDate, final BigDecimal loanPrincipal, Integer numberOfRepayments,
|
||||
BigDecimal loanCharge) {
|
||||
if (dueDate != null) {
|
||||
this.dueDate = dueDate;
|
||||
}
|
||||
|
||||
if (amount != null) {
|
||||
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
|
||||
case INVALID:
|
||||
break;
|
||||
case FLAT:
|
||||
if (isInstalmentFee()) {
|
||||
if (numberOfRepayments == null) {
|
||||
numberOfRepayments = this.loan.fetchNumberOfInstallmensAfterExceptions();
|
||||
}
|
||||
this.amount = amount.multiply(BigDecimal.valueOf(numberOfRepayments));
|
||||
} else {
|
||||
this.amount = amount;
|
||||
}
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT:
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
case PERCENT_OF_INTEREST:
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
this.percentage = amount;
|
||||
this.amountPercentageAppliedTo = loanPrincipal;
|
||||
if (loanCharge.compareTo(BigDecimal.ZERO) == 0) {
|
||||
loanCharge = percentageOf(this.amountPercentageAppliedTo);
|
||||
}
|
||||
this.amount = minimumAndMaximumCap(loanCharge);
|
||||
break;
|
||||
}
|
||||
this.amountOrPercentage = amount;
|
||||
this.amountOutstanding = calculateOutstanding();
|
||||
if (this.loan != null && isInstalmentFee()) {
|
||||
updateInstallmentCharges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void update(final BigDecimal amount, final LocalDate dueDate, final Integer numberOfRepayments) {
|
||||
BigDecimal amountPercentageAppliedTo = BigDecimal.ZERO;
|
||||
if (this.loan != null) {
|
||||
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
|
||||
case PERCENT_OF_AMOUNT:
|
||||
// If charge type is specified due date and loan is multi disburment loan.
|
||||
// Then we need to get as of this loan charge due date how much amount disbursed.
|
||||
if (this.loan.isMultiDisburmentLoan() && this.isSpecifiedDueDate()) {
|
||||
for (final LoanDisbursementDetails loanDisbursementDetails : this.loan.getDisbursementDetails()) {
|
||||
if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), this.getDueDate())) {
|
||||
amountPercentageAppliedTo = amountPercentageAppliedTo.add(loanDisbursementDetails.principal());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
amountPercentageAppliedTo = this.loan.getPrincipal().getAmount();
|
||||
}
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
amountPercentageAppliedTo = this.loan.getPrincipal().getAmount().add(this.loan.getTotalInterest());
|
||||
break;
|
||||
case PERCENT_OF_INTEREST:
|
||||
amountPercentageAppliedTo = this.loan.getTotalInterest();
|
||||
break;
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = this.loanTrancheDisbursementCharge;
|
||||
amountPercentageAppliedTo = loanTrancheDisbursementCharge.getloanDisbursementDetails().principal();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
update(amount, dueDate, amountPercentageAppliedTo, numberOfRepayments, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
public Map<String, Object> update(final JsonCommand command, final BigDecimal amount) {
|
||||
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>(7);
|
||||
|
||||
final String dateFormatAsInput = command.dateFormat();
|
||||
final String localeAsInput = command.locale();
|
||||
|
||||
final String dueDateParamName = "dueDate";
|
||||
if (command.isChangeInLocalDateParameterNamed(dueDateParamName, getDueLocalDate())) {
|
||||
final String valueAsInput = command.stringValueOfParameterNamed(dueDateParamName);
|
||||
actualChanges.put(dueDateParamName, valueAsInput);
|
||||
actualChanges.put("dateFormat", dateFormatAsInput);
|
||||
actualChanges.put("locale", localeAsInput);
|
||||
|
||||
this.dueDate = command.localDateValueOfParameterNamed(dueDateParamName);
|
||||
}
|
||||
|
||||
final String amountParamName = "amount";
|
||||
if (command.isChangeInBigDecimalParameterNamed(amountParamName, this.amount)) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
|
||||
BigDecimal loanCharge = null;
|
||||
actualChanges.put(amountParamName, newValue);
|
||||
actualChanges.put("locale", localeAsInput);
|
||||
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
|
||||
case INVALID:
|
||||
break;
|
||||
case FLAT:
|
||||
if (isInstalmentFee()) {
|
||||
this.amount = newValue.multiply(BigDecimal.valueOf(this.loan.fetchNumberOfInstallmensAfterExceptions()));
|
||||
} else {
|
||||
this.amount = newValue;
|
||||
}
|
||||
this.amountOutstanding = calculateOutstanding();
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT:
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
case PERCENT_OF_INTEREST:
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
this.percentage = newValue;
|
||||
this.amountPercentageAppliedTo = amount;
|
||||
loanCharge = BigDecimal.ZERO;
|
||||
if (isInstalmentFee()) {
|
||||
loanCharge = calculatePerInstallmentChargeAmount(loan, ChargeCalculationType.fromInt(this.chargeCalculation),
|
||||
this.percentage);
|
||||
}
|
||||
if (loanCharge.compareTo(BigDecimal.ZERO) == 0) {
|
||||
loanCharge = percentageOf(this.amountPercentageAppliedTo);
|
||||
}
|
||||
this.amount = minimumAndMaximumCap(loanCharge);
|
||||
this.amountOutstanding = calculateOutstanding();
|
||||
break;
|
||||
}
|
||||
this.amountOrPercentage = newValue;
|
||||
if (isInstalmentFee()) {
|
||||
updateInstallmentCharges();
|
||||
}
|
||||
}
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
public static BigDecimal calculatePerInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType,
|
||||
final BigDecimal percentage) {
|
||||
Money amount = Money.zero(loan.getCurrency());
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
amount = amount.plus(calculateInstallmentChargeAmount(loan, calculationType, percentage, installment));
|
||||
}
|
||||
return amount.getAmount();
|
||||
}
|
||||
|
||||
public BigDecimal calculatePerInstallmentChargeAmount() {
|
||||
Loan loan = this.loan;
|
||||
Money amount = Money.zero(loan.getCurrency());
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
amount = amount.plus(calculateInstallmentChargeAmount(loan, getChargeCalculation(), getPercentage(), installment));
|
||||
}
|
||||
return amount.getAmount();
|
||||
}
|
||||
|
||||
private List<LoanInstallmentCharge> generateInstallmentLoanCharges(final LoanCharge loanCharge) {
|
||||
Loan loan = this.loan;
|
||||
final List<LoanInstallmentCharge> loanChargePerInstallments = new ArrayList<>();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
if (installment.isRecalculatedInterestComponent()) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal amount;
|
||||
if (loanCharge.getChargeCalculation().isFlat()) {
|
||||
amount = loanCharge.amountOrPercentage();
|
||||
} else {
|
||||
amount = calculateInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage(),
|
||||
installment).getAmount();
|
||||
}
|
||||
final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment);
|
||||
installment.getInstallmentCharges().add(loanInstallmentCharge);
|
||||
loanChargePerInstallments.add(loanInstallmentCharge);
|
||||
}
|
||||
}
|
||||
return loanChargePerInstallments;
|
||||
}
|
||||
|
||||
private static Money calculateInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType,
|
||||
final BigDecimal percentage, final LoanRepaymentScheduleInstallment installment) {
|
||||
Money percentOf = switch (calculationType) {
|
||||
case PERCENT_OF_AMOUNT -> installment.getPrincipal(loan.getCurrency());
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST ->
|
||||
installment.getPrincipal(loan.getCurrency()).plus(installment.getInterestCharged(loan.getCurrency()));
|
||||
case PERCENT_OF_INTEREST -> installment.getInterestCharged(loan.getCurrency());
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT, INVALID, FLAT -> Money.zero(loan.getCurrency());
|
||||
|
||||
};
|
||||
return Money.zero(loan.getCurrency()) //
|
||||
.plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage));
|
||||
}
|
||||
|
||||
private void updateInstallmentCharges() {
|
||||
final Collection<LoanInstallmentCharge> remove = new HashSet<>();
|
||||
final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(this);
|
||||
if (this.loanInstallmentCharge.isEmpty()) {
|
||||
this.loanInstallmentCharge.addAll(newChargeInstallments);
|
||||
} else {
|
||||
int index = 0;
|
||||
final List<LoanInstallmentCharge> oldChargeInstallments = new ArrayList<>();
|
||||
if (this.loanInstallmentCharge != null && !this.loanInstallmentCharge.isEmpty()) {
|
||||
oldChargeInstallments.addAll(this.loanInstallmentCharge);
|
||||
}
|
||||
Collections.sort(oldChargeInstallments);
|
||||
final LoanInstallmentCharge[] loanChargePerInstallmentArray = newChargeInstallments
|
||||
.toArray(new LoanInstallmentCharge[newChargeInstallments.size()]);
|
||||
for (final LoanInstallmentCharge chargePerInstallment : oldChargeInstallments) {
|
||||
if (index == loanChargePerInstallmentArray.length) {
|
||||
remove.add(chargePerInstallment);
|
||||
chargePerInstallment.getInstallment().getInstallmentCharges().remove(chargePerInstallment);
|
||||
} else {
|
||||
LoanInstallmentCharge newLoanInstallmentCharge = loanChargePerInstallmentArray[index++];
|
||||
newLoanInstallmentCharge.getInstallment().getInstallmentCharges().remove(newLoanInstallmentCharge);
|
||||
chargePerInstallment.copyFrom(newLoanInstallmentCharge);
|
||||
}
|
||||
}
|
||||
this.loanInstallmentCharge.removeAll(remove);
|
||||
while (index < loanChargePerInstallmentArray.length) {
|
||||
this.loanInstallmentCharge.add(loanChargePerInstallmentArray[index++]);
|
||||
}
|
||||
}
|
||||
Money amount = Money.zero(this.loan.getCurrency());
|
||||
for (LoanInstallmentCharge charge : this.loanInstallmentCharge) {
|
||||
amount = amount.plus(charge.getAmount());
|
||||
}
|
||||
this.amount = amount.getAmount();
|
||||
}
|
||||
|
||||
public boolean isDueAtDisbursement() {
|
||||
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT)
|
||||
|| ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.TRANCHE_DISBURSEMENT);
|
||||
@@ -600,7 +261,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return BigDecimal.ZERO.compareTo(calculateOutstanding()) == 0;
|
||||
}
|
||||
|
||||
private BigDecimal calculateOutstanding() {
|
||||
public BigDecimal calculateOutstanding() {
|
||||
if (this.amount == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -645,7 +306,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
* @returns a minimum cap or maximum cap set on charges if the criteria fits else it returns the percentageOf if the
|
||||
* amount is within min and max cap
|
||||
*/
|
||||
private BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) {
|
||||
public BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) {
|
||||
BigDecimal minMaxCap;
|
||||
if (this.minCap != null) {
|
||||
final int minimumCap = percentageOf.compareTo(this.minCap);
|
||||
@@ -817,11 +478,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return ChargeCalculationType.fromInt(this.chargeCalculation);
|
||||
}
|
||||
|
||||
public void updateAmount(final BigDecimal amount) {
|
||||
this.amount = amount;
|
||||
calculateOutstanding();
|
||||
}
|
||||
|
||||
public LoanInstallmentCharge getUnpaidInstallmentLoanCharge() {
|
||||
LoanInstallmentCharge unpaidChargePerInstallment = null;
|
||||
for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) {
|
||||
|
||||
+1
@@ -925,4 +925,5 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
public void updateTransactionDate(final LocalDate transactionDate) {
|
||||
this.dateOf = transactionDate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
@@ -70,4 +70,32 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
|
||||
|
||||
@Query(FIND_LOAN_ID_BY_ID)
|
||||
Optional<Long> findLoanIdById(@Param("id") Long id);
|
||||
|
||||
@Query("""
|
||||
SELECT CASE WHEN COUNT(lt) > 0 THEN false ELSE true END
|
||||
FROM LoanTransaction lt
|
||||
WHERE lt.loan = :loan
|
||||
AND lt.reversed = false
|
||||
AND lt.dateOf > :transactionDate
|
||||
""")
|
||||
boolean isChronologicallyLatest(@Param("transactionDate") LocalDate transactionDate, @Param("loan") Loan loan);
|
||||
|
||||
@Query("""
|
||||
SELECT MAX(lt.dateOf) FROM LoanTransaction lt
|
||||
WHERE lt.loan = :loan
|
||||
AND lt.reversed = false
|
||||
AND lt.typeOf NOT IN (
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRA,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MARKED_FOR_RESCHEDULING,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.APPROVE_TRANSFER,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER,
|
||||
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION
|
||||
)
|
||||
""")
|
||||
Optional<LocalDate> findLastTransactionDateForReprocessing(@Param("loan") Loan loan);
|
||||
|
||||
}
|
||||
|
||||
+4
-2
@@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
@@ -76,8 +77,9 @@ import org.springframework.util.CollectionUtils;
|
||||
public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
protected final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||
protected final LoanChargeValidator loanChargeValidator = new LoanChargeValidator();
|
||||
protected final ExternalIdFactory externalIdFactory;
|
||||
protected final LoanChargeValidator loanChargeValidator;
|
||||
protected final LoanBalanceService loanBalanceService;
|
||||
|
||||
@Override
|
||||
public boolean accept(String s) {
|
||||
@@ -256,7 +258,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
} else {
|
||||
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
if ((loan.isClosedObligationsMet() || loan.isOverPaid()) && currentInstallment.isObligationsMet()
|
||||
if ((loan.isClosedObligationsMet() || loanBalanceService.isOverPaid(loan)) && currentInstallment.isObligationsMet()
|
||||
&& currentInstallment.isTransactionDateWithinPeriod(currentInstallment.getObligationsMetOnDate())) {
|
||||
loanTransaction.updateTransactionDate(currentInstallment.getObligationsMetOnDate());
|
||||
}
|
||||
|
||||
+5
-2
@@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* Creocore style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
@@ -47,8 +49,9 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL
|
||||
|
||||
public static final String STRATEGY_NAME = "Creocore Unique";
|
||||
|
||||
public CreocoreLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public CreocoreLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -32,6 +32,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* `First due/late charges, interest, principal, after in advance principal, charges, interest` style
|
||||
@@ -48,8 +50,9 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
|
||||
|
||||
public static final String STRATEGY_NAME = "Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest";
|
||||
|
||||
public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -32,6 +32,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* `First due/late penalty, interest, principal, fee, after in advance penalty, interest, principal, fee` style
|
||||
@@ -48,8 +50,9 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
|
||||
|
||||
public static final String STRATEGY_NAME = "Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee";
|
||||
|
||||
public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of Interest first, then
|
||||
@@ -41,8 +43,9 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
|
||||
|
||||
public static final String STRATEGY_NAME = "Early Repayment Strategy";
|
||||
|
||||
public EarlyPaymentLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public EarlyPaymentLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* Old style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
@@ -47,8 +49,9 @@ public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends Abst
|
||||
|
||||
public static final String STRATEGY_NAME = "Penalties, Fees, Interest, Principal order";
|
||||
|
||||
public FineractStyleLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public FineractStyleLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -31,6 +31,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* Heavensfamily style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
@@ -49,8 +51,9 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
|
||||
|
||||
public static final String STRATEGY_NAME = "HeavensFamily Unique";
|
||||
|
||||
public HeavensFamilyLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public HeavensFamilyLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of Interest first, then
|
||||
@@ -42,8 +44,9 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
|
||||
|
||||
public static final String STRATEGY_NAME = "Interest, Principal, Penalties, Fees Order";
|
||||
|
||||
public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of principal first, then
|
||||
@@ -42,8 +44,9 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
|
||||
|
||||
public static final String STRATEGY_NAME = "Principal, Interest, Penalties, Fees Order";
|
||||
|
||||
public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -31,6 +31,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* Adhikar/RBI style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
@@ -51,8 +53,9 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
|
||||
|
||||
public static final String STRATEGY_NAME = "Overdue/Due Fee/Int,Principal";
|
||||
|
||||
public RBILoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
|
||||
super(externalIdFactory);
|
||||
public RBILoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+6
-1
@@ -35,6 +35,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
@@ -61,10 +62,14 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.ScheduleDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
|
||||
private final LoanTransactionService loanTransactionService;
|
||||
|
||||
@Override
|
||||
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
|
||||
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
|
||||
@@ -2796,7 +2801,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
|
||||
|
||||
LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO,
|
||||
loanRepaymentScheduleTransactionProcessor, onDate, calculateTill);
|
||||
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsForReprocessing();
|
||||
final List<LoanTransaction> loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
|
||||
|
||||
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(),
|
||||
loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges());
|
||||
|
||||
+9
-2
@@ -25,10 +25,10 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -56,12 +56,19 @@ import org.springframework.stereotype.Component;
|
||||
* </p>
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CumulativeDecliningBalanceInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator {
|
||||
|
||||
private final ScheduledDateGenerator scheduledDateGenerator;
|
||||
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
|
||||
|
||||
public CumulativeDecliningBalanceInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator,
|
||||
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator,
|
||||
final LoanTransactionService loanTransactionService) {
|
||||
super(loanTransactionService);
|
||||
this.scheduledDateGenerator = scheduledDateGenerator;
|
||||
this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledDateGenerator getScheduledDateGenerator() {
|
||||
return scheduledDateGenerator;
|
||||
|
||||
+9
-2
@@ -24,18 +24,25 @@ import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CumulativeFlatInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator {
|
||||
|
||||
private final ScheduledDateGenerator scheduledDateGenerator;
|
||||
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
|
||||
|
||||
public CumulativeFlatInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator,
|
||||
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator,
|
||||
final LoanTransactionService loanTransactionService) {
|
||||
super(loanTransactionService);
|
||||
this.scheduledDateGenerator = scheduledDateGenerator;
|
||||
this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledDateGenerator getScheduledDateGenerator() {
|
||||
return scheduledDateGenerator;
|
||||
|
||||
+6
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.serialization;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
||||
@@ -31,11 +32,15 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class LoanDownPaymentTransactionValidator {
|
||||
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
|
||||
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
|
||||
final boolean allowTransactionsOnNonWorkingDay) {
|
||||
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
|
||||
@@ -53,7 +58,7 @@ public final class LoanDownPaymentTransactionValidator {
|
||||
}
|
||||
|
||||
public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) {
|
||||
if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) {
|
||||
if (!(loan.isOpen() || loan.isClosedObligationsMet() || loanBalanceService.isOverPaid(loan))) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid",
|
||||
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 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.List;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class LoanBalanceService {
|
||||
|
||||
public Money calculateTotalOverpayment(final Loan loan) {
|
||||
Money totalPaidInRepayments = loan.getTotalPaidInRepayments();
|
||||
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
|
||||
Money cumulativeTotalWaivedOnInstallments = Money.zero(currency);
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
|
||||
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments
|
||||
.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency)))
|
||||
.plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency));
|
||||
|
||||
cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency));
|
||||
}
|
||||
|
||||
for (final LoanTransaction loanTransaction : loan.getLoanTransactions()) {
|
||||
if (loanTransaction.isReversed()) {
|
||||
continue;
|
||||
}
|
||||
if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
|
||||
} else if (loanTransaction.isCreditBalanceRefund()) {
|
||||
if (loanTransaction.getPrincipalPortion(currency).isZero()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency));
|
||||
}
|
||||
} else if (loanTransaction.isChargeback()) {
|
||||
if (loanTransaction.getPrincipalPortion(currency).isZero() && loan.getCreditAllocationRules().stream() //
|
||||
.filter(car -> car.getTransactionType().equals(CreditAllocationTransactionType.CHARGEBACK)) //
|
||||
.findAny() //
|
||||
.isEmpty()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if total paid in transactions doesn't match repayment schedule then there's an overpayment.
|
||||
return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
|
||||
}
|
||||
|
||||
public boolean isOverPaid(final Loan loan) {
|
||||
return calculateTotalOverpayment(loan).isGreaterThanZero();
|
||||
}
|
||||
|
||||
public void updateLoanSummaryDerivedFields(final Loan loan) {
|
||||
if (loan.isNotDisbursed()) {
|
||||
if (loan.getSummary() != null) {
|
||||
loan.getSummary().zeroFields();
|
||||
}
|
||||
loan.setTotalOverpaid(null);
|
||||
} else {
|
||||
final Money overpaidBy = calculateTotalOverpayment(loan);
|
||||
loan.setTotalOverpaid(null);
|
||||
if (!overpaidBy.isLessThanZero()) {
|
||||
loan.setTotalOverpaid(overpaidBy.getAmountDefaultedToNullIfZero());
|
||||
}
|
||||
|
||||
final Money recoveredAmount = loan.calculateTotalRecoveredPayments();
|
||||
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
|
||||
|
||||
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
||||
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges());
|
||||
loan.updateLoanOutstandingBalances();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLoanToLastDisbursalState(final Loan loan, final LoanDisbursementDetails disbursementDetail) {
|
||||
for (final LoanCharge charge : loan.getActiveCharges()) {
|
||||
if (charge.isOverdueInstallmentCharge()) {
|
||||
charge.setActive(false);
|
||||
} else if (charge.isTrancheDisbursementCharge() && disbursementDetail.getDisbursementDate()
|
||||
.equals(charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate())) {
|
||||
charge.resetToOriginal(loan.getCurrency());
|
||||
}
|
||||
}
|
||||
loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getDisbursedAmount().subtract(disbursementDetail.principal()));
|
||||
disbursementDetail.updateActualDisbursementDate(null);
|
||||
disbursementDetail.reverse();
|
||||
updateLoanSummaryDerivedFields(loan);
|
||||
}
|
||||
|
||||
public Money getReceivableInterest(final Loan loan, final LocalDate tillDate) {
|
||||
Money receivableInterest = Money.zero(loan.getCurrency());
|
||||
for (final LoanTransaction transaction : loan.getLoanTransactions()) {
|
||||
if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement()
|
||||
&& !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) {
|
||||
if (transaction.isAccrual()) {
|
||||
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(loan.getCurrency()));
|
||||
} else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver() || transaction.isAccrualAdjustment()) {
|
||||
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(loan.getCurrency()));
|
||||
}
|
||||
}
|
||||
if (receivableInterest.isLessThanZero()) {
|
||||
receivableInterest = receivableInterest.zero();
|
||||
}
|
||||
}
|
||||
return receivableInterest;
|
||||
}
|
||||
|
||||
public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final Loan loan, final LocalDate closureDate) {
|
||||
Money[] receivables = retrieveIncomeOutstandingTillDate(loan, closureDate);
|
||||
Money totalPrincipal = Money.of(loan.getCurrency(), loan.getSummary().getTotalPrincipalOutstanding());
|
||||
totalPrincipal = totalPrincipal.minus(receivables[3]);
|
||||
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
|
||||
return new LoanRepaymentScheduleInstallment(null, 0, currentDate, currentDate, totalPrincipal.getAmount(),
|
||||
receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, null);
|
||||
}
|
||||
|
||||
public Money[] retrieveIncomeForOverlappingPeriod(final Loan loan, final LocalDate paymentDate) {
|
||||
Money[] balances = new Money[3];
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
balances[0] = balances[1] = balances[2] = Money.zero(currency);
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments());
|
||||
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
|
||||
if (DateUtils.isEqual(paymentDate, installment.getDueDate())) {
|
||||
Money interest = installment.getInterestCharged(currency);
|
||||
Money fee = installment.getFeeChargesCharged(currency);
|
||||
Money penalty = installment.getPenaltyChargesCharged(currency);
|
||||
balances[0] = interest;
|
||||
balances[1] = fee;
|
||||
balances[2] = penalty;
|
||||
break;
|
||||
} else if (DateUtils.isDateInRangeExclusive(paymentDate, installment.getFromDate(), installment.getDueDate())) {
|
||||
balances = fetchInterestFeeAndPenaltyTillDate(loan, paymentDate, currency, installment, isFirstNormalInstallment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return balances;
|
||||
}
|
||||
|
||||
private Money[] retrieveIncomeOutstandingTillDate(final Loan loan, final LocalDate paymentDate) {
|
||||
Money[] balances = new Money[4];
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
Money interest = Money.zero(currency);
|
||||
Money paidFromFutureInstallments = Money.zero(currency);
|
||||
Money fee = Money.zero(currency);
|
||||
Money penalty = Money.zero(currency);
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments());
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||
boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
|
||||
if (!DateUtils.isBefore(paymentDate, installment.getDueDate())) {
|
||||
interest = interest.plus(installment.getInterestOutstanding(currency));
|
||||
penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency));
|
||||
fee = fee.plus(installment.getFeeChargesOutstanding(currency));
|
||||
} else if (DateUtils.isAfter(paymentDate, installment.getFromDate())) {
|
||||
Money[] balancesForCurrentPeriod = fetchInterestFeeAndPenaltyTillDate(loan, paymentDate, currency, installment,
|
||||
isFirstNormalInstallment);
|
||||
if (balancesForCurrentPeriod[0].isGreaterThan(balancesForCurrentPeriod[5])) {
|
||||
interest = interest.plus(balancesForCurrentPeriod[0]).minus(balancesForCurrentPeriod[5]);
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeriod[5])
|
||||
.minus(balancesForCurrentPeriod[0]);
|
||||
}
|
||||
if (balancesForCurrentPeriod[1].isGreaterThan(balancesForCurrentPeriod[3])) {
|
||||
fee = fee.plus(balancesForCurrentPeriod[1].minus(balancesForCurrentPeriod[3]));
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments
|
||||
.plus(balancesForCurrentPeriod[3].minus(balancesForCurrentPeriod[1]));
|
||||
}
|
||||
if (balancesForCurrentPeriod[2].isGreaterThan(balancesForCurrentPeriod[4])) {
|
||||
penalty = penalty.plus(balancesForCurrentPeriod[2].minus(balancesForCurrentPeriod[4]));
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeriod[4])
|
||||
.minus(balancesForCurrentPeriod[2]);
|
||||
}
|
||||
} else {
|
||||
paidFromFutureInstallments = paidFromFutureInstallments.plus(installment.getInterestPaid(currency))
|
||||
.plus(installment.getPenaltyChargesPaid(currency)).plus(installment.getFeeChargesPaid(currency));
|
||||
}
|
||||
|
||||
}
|
||||
balances[0] = interest;
|
||||
balances[1] = fee;
|
||||
balances[2] = penalty;
|
||||
balances[3] = paidFromFutureInstallments;
|
||||
return balances;
|
||||
}
|
||||
|
||||
private Money[] fetchInterestFeeAndPenaltyTillDate(final Loan loan, final LocalDate paymentDate, final MonetaryCurrency currency,
|
||||
final LoanRepaymentScheduleInstallment installment, final boolean isFirstNormalInstallment) {
|
||||
Money penaltyForCurrentPeriod = Money.zero(loan.getCurrency());
|
||||
Money penaltyAccoutedForCurrentPeriod = Money.zero(loan.getCurrency());
|
||||
Money feeForCurrentPeriod = Money.zero(loan.getCurrency());
|
||||
Money feeAccountedForCurrentPeriod = Money.zero(loan.getCurrency());
|
||||
int totalPeriodDays = DateUtils.getExactDifferenceInDays(installment.getFromDate(), installment.getDueDate());
|
||||
int tillDays = DateUtils.getExactDifferenceInDays(installment.getFromDate(), paymentDate);
|
||||
Money interestForCurrentPeriod = Money.of(loan.getCurrency(), BigDecimal.valueOf(
|
||||
calculateInterestForDays(totalPeriodDays, installment.getInterestCharged(loan.getCurrency()).getAmount(), tillDays)));
|
||||
Money interestAccountedForCurrentPeriod = installment.getInterestWaived(loan.getCurrency())
|
||||
.plus(installment.getInterestPaid(loan.getCurrency()));
|
||||
for (LoanCharge loanCharge : loan.getLoanCharges()) {
|
||||
if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) {
|
||||
boolean isDue = loanCharge.isDueInPeriod(installment.getFromDate(), paymentDate, isFirstNormalInstallment);
|
||||
if (isDue) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyForCurrentPeriod = penaltyForCurrentPeriod.plus(loanCharge.getAmount(loan.getCurrency()));
|
||||
penaltyAccoutedForCurrentPeriod = penaltyAccoutedForCurrentPeriod
|
||||
.plus(loanCharge.getAmountWaived(loan.getCurrency()).plus(loanCharge.getAmountPaid(loan.getCurrency())));
|
||||
} else {
|
||||
feeForCurrentPeriod = feeForCurrentPeriod.plus(loanCharge.getAmount(currency));
|
||||
feeAccountedForCurrentPeriod = feeAccountedForCurrentPeriod
|
||||
.plus(loanCharge.getAmountWaived(loan.getCurrency()).plus(
|
||||
|
||||
loanCharge.getAmountPaid(loan.getCurrency())));
|
||||
}
|
||||
} else if (loanCharge.isInstalmentFee()) {
|
||||
LoanInstallmentCharge loanInstallmentCharge = loanCharge.getInstallmentLoanCharge(installment.getInstallmentNumber());
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyAccoutedForCurrentPeriod = penaltyAccoutedForCurrentPeriod
|
||||
.plus(loanInstallmentCharge.getAmountPaid(currency));
|
||||
} else {
|
||||
feeAccountedForCurrentPeriod = feeAccountedForCurrentPeriod.plus(loanInstallmentCharge.getAmountPaid(currency));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Money[] balances = new Money[6];
|
||||
balances[0] = interestForCurrentPeriod;
|
||||
balances[1] = feeForCurrentPeriod;
|
||||
balances[2] = penaltyForCurrentPeriod;
|
||||
balances[3] = feeAccountedForCurrentPeriod;
|
||||
balances[4] = penaltyAccoutedForCurrentPeriod;
|
||||
balances[5] = interestAccountedForCurrentPeriod;
|
||||
return balances;
|
||||
}
|
||||
|
||||
private double calculateInterestForDays(final int daysInPeriod, final BigDecimal interest, final int days) {
|
||||
if (interest.doubleValue() == 0) {
|
||||
return 0;
|
||||
}
|
||||
return interest.doubleValue() / daysInPeriod * days;
|
||||
}
|
||||
|
||||
}
|
||||
+496
-147
@@ -21,20 +21,30 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
||||
import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
@@ -52,6 +62,7 @@ public class LoanChargeService {
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanTransactionProcessingService loanTransactionProcessingService;
|
||||
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
|
||||
public void recalculateAllCharges(final Loan loan) {
|
||||
Set<LoanCharge> charges = loan.getActiveCharges();
|
||||
@@ -68,54 +79,25 @@ public class LoanChargeService {
|
||||
BigDecimal totalChargeAmt = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
if (loanCharge.isOverdueInstallmentCharge()) {
|
||||
amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod);
|
||||
amount = calculateOverdueAmountPercentageAppliedTo(loan, loanCharge, penaltyWaitPeriod);
|
||||
} else {
|
||||
amount = calculateAmountPercentageAppliedTo(loan, loanCharge);
|
||||
}
|
||||
chargeAmt = loanCharge.getPercentage();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
|
||||
totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge);
|
||||
}
|
||||
} else {
|
||||
chargeAmt = loanCharge.amountOrPercentage();
|
||||
}
|
||||
if (loanCharge.isActive()) {
|
||||
clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
|
||||
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
|
||||
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
|
||||
totalChargeAmt);
|
||||
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
|
||||
}
|
||||
}
|
||||
|
||||
private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) {
|
||||
/*
|
||||
* JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down
|
||||
* installment charges from the loanCharge and from each of the repayment installments and allow them to be
|
||||
* recalculated fully anew. This patch is to avoid the 'merging' of existing and regenerated installment charges
|
||||
* which results in the installment charges being deleted on loan approval if the schedule is regenerated. Not
|
||||
* pretty. updateInstallmentCharges in LoanCharge.java: the merging looks like it will work but doesn't so this
|
||||
* patch simply hits the part which 'adds all' rather than merge. Possibly an ORM issue. The issue could be to
|
||||
* do with the fact that, on approval, the "recalculateLoanCharge" happens twice (probably 2 schedule
|
||||
* regenerations) whereas it only happens once on Submit and Disburse (and no problems with them)
|
||||
*
|
||||
* if (this.loanInstallmentCharge.isEmpty()) { this.loanInstallmentCharge.addAll(newChargeInstallments);
|
||||
*/
|
||||
Loan loan = loanCharge.getLoan();
|
||||
if (!loan.isSubmittedAndPendingApproval() && !loan.isApproved()) {
|
||||
return;
|
||||
} // doing for both just in case status is not
|
||||
// updated at this points
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
loanCharge.clearLoanInstallmentCharges();
|
||||
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||
if (installment.isRecalculatedInterestComponent()) {
|
||||
continue; // JW: does this in generateInstallmentLoanCharges - but don't understand it
|
||||
}
|
||||
installment.getInstallmentCharges().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void makeChargePayment(final Loan loan, final Long chargeId, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
|
||||
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
|
||||
@@ -130,36 +112,6 @@ public class LoanChargeService {
|
||||
handleChargePaidTransaction(loan, charge, paymentTransaction, installmentNumber);
|
||||
}
|
||||
|
||||
public void handleChargePaidTransaction(final Loan loan, final LoanCharge charge, final LoanTransaction chargesPayment,
|
||||
final Integer installmentNumber) {
|
||||
chargesPayment.updateLoan(loan);
|
||||
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge,
|
||||
chargesPayment.getAmount(loan.getCurrency()).getAmount(), installmentNumber);
|
||||
chargesPayment.getLoanChargesPaid().add(loanChargePaidBy);
|
||||
loan.addLoanTransaction(chargesPayment);
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_PAYMENT, loan);
|
||||
|
||||
final List<LoanRepaymentScheduleInstallment> chargePaymentInstallments = new ArrayList<>();
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments());
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
|
||||
if (installment.getInstallmentNumber().equals(installmentNumber) || (installmentNumber == null
|
||||
&& charge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))) {
|
||||
chargePaymentInstallments.add(installment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
final Set<LoanCharge> loanCharges = new HashSet<>(1);
|
||||
loanCharges.add(charge);
|
||||
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargesPayment,
|
||||
new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges,
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The
|
||||
* newly created transaction is also added to the Loan on which this method is called.
|
||||
@@ -230,14 +182,14 @@ public class LoanChargeService {
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
chargeAmt = loanCharge.getPercentage();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
|
||||
totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge);
|
||||
} else if (loanCharge.isOverdueInstallmentCharge()) {
|
||||
totalChargeAmt = loanCharge.amountOutstanding();
|
||||
}
|
||||
} else {
|
||||
chargeAmt = loanCharge.amountOrPercentage();
|
||||
}
|
||||
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
|
||||
update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
|
||||
|
||||
// NOTE: must add new loan charge to set of loan charges before
|
||||
// reprocessing the repayment schedule.
|
||||
@@ -250,72 +202,11 @@ public class LoanChargeService {
|
||||
// store Id's of existing loan transactions and existing reversed loan transactions
|
||||
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loanCharge);
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, loan);
|
||||
}
|
||||
|
||||
public void updateLoanCharges(final Loan loan, final Set<LoanCharge> loanCharges) {
|
||||
List<Long> existingCharges = fetchAllLoanChargeIds(loan);
|
||||
|
||||
/* Process new and updated charges **/
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
LoanCharge charge = loanCharge;
|
||||
// add new charges
|
||||
if (loanCharge.getId() == null) {
|
||||
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
|
||||
loanCharge.update(loan);
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) {
|
||||
loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(loan);
|
||||
for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) {
|
||||
if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().getId() == null
|
||||
&& loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().equals(loanDisbursementDetails)) {
|
||||
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, loanDisbursementDetails);
|
||||
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
|
||||
}
|
||||
}
|
||||
}
|
||||
loan.addCharge(loanCharge);
|
||||
|
||||
} else {
|
||||
charge = fetchLoanChargesById(loan, charge.getId());
|
||||
if (charge != null) {
|
||||
existingCharges.remove(charge.getId());
|
||||
}
|
||||
}
|
||||
final BigDecimal amount = calculateAmountPercentageAppliedTo(loan, loanCharge);
|
||||
BigDecimal chargeAmt;
|
||||
BigDecimal totalChargeAmt = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
chargeAmt = loanCharge.getPercentage();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
|
||||
}
|
||||
} else {
|
||||
chargeAmt = loanCharge.amountOrPercentage();
|
||||
}
|
||||
if (charge != null) {
|
||||
charge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
|
||||
totalChargeAmt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Updated deleted charges **/
|
||||
for (Long id : existingCharges) {
|
||||
fetchLoanChargesById(loan, id).setActive(false);
|
||||
}
|
||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
||||
}
|
||||
|
||||
private List<Long> fetchAllLoanChargeIds(final Loan loan) {
|
||||
List<Long> list = new ArrayList<>();
|
||||
for (LoanCharge loanCharge : loan.getCharges()) {
|
||||
list.add(loanCharge.getId());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public BigDecimal calculateAmountPercentageAppliedTo(final Loan loan, final LoanCharge loanCharge) {
|
||||
if (loanCharge.isOverdueInstallmentCharge()) {
|
||||
return loanCharge.getAmountPercentageAppliedTo();
|
||||
@@ -343,38 +234,254 @@ public class LoanChargeService {
|
||||
};
|
||||
}
|
||||
|
||||
private BigDecimal getDerivedAmountForCharge(final Loan loan, final LoanCharge loanCharge) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loan.isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) {
|
||||
amount = loan.getApprovedPrincipal();
|
||||
} else {
|
||||
// If charge type is specified due date and loan is multi disburment loan.
|
||||
// Then we need to get as of this loan charge due date how much amount disbursed.
|
||||
if (loanCharge.isSpecifiedDueDate() && loan.isMultiDisburmentLoan()) {
|
||||
for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) {
|
||||
if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) {
|
||||
amount = amount.add(loanDisbursementDetails.principal());
|
||||
public void updateLoanCharges(final Loan loan, final Set<LoanCharge> loanCharges) {
|
||||
List<Long> existingCharges = fetchAllLoanChargeIds(loan);
|
||||
|
||||
/* Process new and updated charges **/
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
LoanCharge charge = loanCharge;
|
||||
// add new charges
|
||||
if (loanCharge.getId() == null) {
|
||||
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
|
||||
loanCharge.update(loan);
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) {
|
||||
loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(loan);
|
||||
for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) {
|
||||
if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().getId() == null
|
||||
&& loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().equals(loanDisbursementDetails)) {
|
||||
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, loanDisbursementDetails);
|
||||
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
|
||||
}
|
||||
}
|
||||
}
|
||||
loan.getLoanCharges().add(loanCharge);
|
||||
} else {
|
||||
amount = loan.getPrincipal().getAmount();
|
||||
charge = loan.fetchLoanChargesById(charge.getId());
|
||||
if (charge != null) {
|
||||
existingCharges.remove(charge.getId());
|
||||
}
|
||||
}
|
||||
final BigDecimal amount = calculateAmountPercentageAppliedTo(loan, loanCharge);
|
||||
BigDecimal chargeAmt;
|
||||
BigDecimal totalChargeAmt = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
chargeAmt = loanCharge.getPercentage();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge);
|
||||
}
|
||||
} else {
|
||||
chargeAmt = loanCharge.amountOrPercentage();
|
||||
}
|
||||
if (charge != null) {
|
||||
update(charge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
|
||||
totalChargeAmt);
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
|
||||
/* Updated deleted charges **/
|
||||
for (Long id : existingCharges) {
|
||||
loan.fetchLoanChargesById(id).setActive(false);
|
||||
}
|
||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
||||
}
|
||||
|
||||
private Money getTotalAllTrancheDisbursementAmount(final Loan loan) {
|
||||
public BigDecimal calculatePerInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType,
|
||||
final BigDecimal percentage) {
|
||||
Money amount = Money.zero(loan.getCurrency());
|
||||
if (loan.isMultiDisburmentLoan()) {
|
||||
for (final LoanDisbursementDetails loanDisbursementDetail : loan.getDisbursementDetails()) {
|
||||
amount = amount.plus(loanDisbursementDetail.principal());
|
||||
}
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
amount = amount.plus(calculateInstallmentChargeAmount(loan, calculationType, percentage, installment));
|
||||
}
|
||||
return amount;
|
||||
return amount.getAmount();
|
||||
}
|
||||
|
||||
private BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) {
|
||||
return loanCharge.calculatePerInstallmentChargeAmount();
|
||||
public Map<String, Object> update(final JsonCommand command, final BigDecimal amount, final LoanCharge loanCharge) {
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>(7);
|
||||
|
||||
final String dateFormatAsInput = command.dateFormat();
|
||||
final String localeAsInput = command.locale();
|
||||
|
||||
final String dueDateParamName = "dueDate";
|
||||
if (command.isChangeInLocalDateParameterNamed(dueDateParamName, loanCharge.getDueLocalDate())) {
|
||||
final String valueAsInput = command.stringValueOfParameterNamed(dueDateParamName);
|
||||
actualChanges.put(dueDateParamName, valueAsInput);
|
||||
actualChanges.put("dateFormat", dateFormatAsInput);
|
||||
actualChanges.put("locale", localeAsInput);
|
||||
|
||||
loanCharge.setDueDate(command.localDateValueOfParameterNamed(dueDateParamName));
|
||||
}
|
||||
|
||||
final String amountParamName = "amount";
|
||||
if (command.isChangeInBigDecimalParameterNamed(amountParamName, loanCharge.getAmount())) {
|
||||
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName);
|
||||
BigDecimal loanChargeAmount;
|
||||
actualChanges.put(amountParamName, newValue);
|
||||
actualChanges.put("locale", localeAsInput);
|
||||
switch (loanCharge.getChargeCalculation()) {
|
||||
case INVALID:
|
||||
break;
|
||||
case FLAT:
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
loanCharge.setAmount(
|
||||
newValue.multiply(BigDecimal.valueOf(loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions())));
|
||||
} else {
|
||||
loanCharge.setAmount(newValue);
|
||||
}
|
||||
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT:
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
case PERCENT_OF_INTEREST:
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
loanCharge.setPercentage(newValue);
|
||||
loanCharge.setAmountPercentageAppliedTo(amount);
|
||||
loanChargeAmount = BigDecimal.ZERO;
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
loanChargeAmount = calculatePerInstallmentChargeAmount(loanCharge.getLoan(), loanCharge.getChargeCalculation(),
|
||||
loanCharge.getPercentage());
|
||||
}
|
||||
if (loanChargeAmount.compareTo(BigDecimal.ZERO) == 0) {
|
||||
loanChargeAmount = loanCharge.percentageOf(loanCharge.getAmountPercentageAppliedTo());
|
||||
}
|
||||
loanCharge.setAmount(loanCharge.minimumAndMaximumCap(loanChargeAmount));
|
||||
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
|
||||
break;
|
||||
}
|
||||
loanCharge.setAmountOrPercentage(newValue);
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
updateInstallmentCharges(loanCharge);
|
||||
}
|
||||
}
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
public void populateDerivedFields(final LoanCharge loanCharge, final BigDecimal amountPercentageAppliedTo,
|
||||
final BigDecimal chargeAmount, Integer numberOfRepayments, BigDecimal loanChargeAmount) {
|
||||
switch (loanCharge.getChargeCalculation()) {
|
||||
case INVALID:
|
||||
loanCharge.setPercentage(null);
|
||||
loanCharge.setAmount(null);
|
||||
loanCharge.setAmountPercentageAppliedTo(null);
|
||||
loanCharge.setAmountPaid(null);
|
||||
loanCharge.setAmountOutstanding(BigDecimal.ZERO);
|
||||
loanCharge.setAmountWaived(null);
|
||||
loanCharge.setAmountWrittenOff(null);
|
||||
break;
|
||||
case FLAT:
|
||||
loanCharge.setPercentage(null);
|
||||
loanCharge.setAmountPercentageAppliedTo(null);
|
||||
loanCharge.setAmountPaid(null);
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
if (numberOfRepayments == null) {
|
||||
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions();
|
||||
}
|
||||
loanCharge.setAmount(chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments)));
|
||||
} else {
|
||||
loanCharge.setAmount(chargeAmount);
|
||||
}
|
||||
loanCharge.setAmountOutstanding(loanCharge.getAmount());
|
||||
loanCharge.setAmountWaived(null);
|
||||
loanCharge.setAmountWrittenOff(null);
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT:
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
case PERCENT_OF_INTEREST:
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
loanCharge.setPercentage(chargeAmount);
|
||||
loanCharge.setAmountPercentageAppliedTo(amountPercentageAppliedTo);
|
||||
if (loanChargeAmount.compareTo(BigDecimal.ZERO) == 0) {
|
||||
loanChargeAmount = loanCharge.percentageOf(loanCharge.getAmountPercentageAppliedTo());
|
||||
}
|
||||
loanCharge.setAmount(loanCharge.minimumAndMaximumCap(loanChargeAmount));
|
||||
loanCharge.setAmountPaid(null);
|
||||
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
|
||||
loanCharge.setAmountWaived(null);
|
||||
loanCharge.setAmountWrittenOff(null);
|
||||
break;
|
||||
}
|
||||
loanCharge.setAmountOrPercentage(chargeAmount);
|
||||
if (loanCharge.getLoan() != null && loanCharge.isInstalmentFee()) {
|
||||
updateInstallmentCharges(loanCharge);
|
||||
}
|
||||
}
|
||||
|
||||
public void update(final LoanCharge loanCharge, final BigDecimal amount, final LocalDate dueDate, final Integer numberOfRepayments) {
|
||||
BigDecimal amountPercentageAppliedTo = BigDecimal.ZERO;
|
||||
if (loanCharge.getLoan() != null) {
|
||||
switch (loanCharge.getChargeCalculation()) {
|
||||
case PERCENT_OF_AMOUNT:
|
||||
// If charge type is specified due date and loan is multi disburment loan.
|
||||
// Then we need to get as of this loan charge due date how much amount disbursed.
|
||||
if (loanCharge.getLoan().isMultiDisburmentLoan() && loanCharge.isSpecifiedDueDate()) {
|
||||
for (final LoanDisbursementDetails loanDisbursementDetails : loanCharge.getLoan().getDisbursementDetails()) {
|
||||
if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) {
|
||||
amountPercentageAppliedTo = amountPercentageAppliedTo.add(loanDisbursementDetails.principal());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
amountPercentageAppliedTo = loanCharge.getLoan().getPrincipal().getAmount();
|
||||
}
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
amountPercentageAppliedTo = loanCharge.getLoan().getPrincipal().getAmount()
|
||||
.add(loanCharge.getLoan().getTotalInterest());
|
||||
break;
|
||||
case PERCENT_OF_INTEREST:
|
||||
amountPercentageAppliedTo = loanCharge.getLoan().getTotalInterest();
|
||||
break;
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = loanCharge.getLoanTrancheDisbursementCharge();
|
||||
amountPercentageAppliedTo = loanTrancheDisbursementCharge.getloanDisbursementDetails().principal();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
update(loanCharge, amount, dueDate, amountPercentageAppliedTo, numberOfRepayments, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
public LoanCharge create(final Loan loan, final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount,
|
||||
final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate,
|
||||
final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final BigDecimal loanChargeAmount,
|
||||
final ExternalId externalId) {
|
||||
final LoanCharge loanCharge = new LoanCharge();
|
||||
loanCharge.setLoan(loan);
|
||||
loanCharge.setCharge(chargeDefinition);
|
||||
loanCharge.setSubmittedOnDate(DateUtils.getBusinessLocalDate());
|
||||
loanCharge.setPenaltyCharge(chargeDefinition.isPenalty());
|
||||
loanCharge.setMinCap(chargeDefinition.getMinCap());
|
||||
loanCharge.setMaxCap(chargeDefinition.getMaxCap());
|
||||
loanCharge.setChargeTime(chargeTime == null ? chargeDefinition.getChargeTimeType() : chargeTime.getValue());
|
||||
|
||||
if (loanCharge.getChargeTimeType().equals(ChargeTimeType.SPECIFIED_DUE_DATE)
|
||||
|| loanCharge.getChargeTimeType().equals(ChargeTimeType.OVERDUE_INSTALLMENT)) {
|
||||
|
||||
if (dueDate == null) {
|
||||
final String defaultUserMessage = "Loan charge is missing due date.";
|
||||
throw new LoanChargeWithoutMandatoryFieldException("loanChargeAmount", "dueDate", defaultUserMessage,
|
||||
chargeDefinition.getId(), chargeDefinition.getName());
|
||||
}
|
||||
|
||||
loanCharge.setDueDate(dueDate);
|
||||
} else {
|
||||
loanCharge.setDueDate(null);
|
||||
}
|
||||
|
||||
loanCharge.setChargeCalculation(chargeCalculation == null ? chargeDefinition.getChargeCalculation() : chargeCalculation.getValue());
|
||||
|
||||
BigDecimal chargeAmount = chargeDefinition.getAmount();
|
||||
if (amount != null) {
|
||||
chargeAmount = amount;
|
||||
}
|
||||
|
||||
loanCharge.setChargePaymentMode(chargePaymentMode == null ? chargeDefinition.getChargePaymentMode() : chargePaymentMode.getValue());
|
||||
|
||||
populateDerivedFields(loanCharge, loanPrincipal, chargeAmount, numberOfRepayments, loanChargeAmount);
|
||||
|
||||
loanCharge.setPaid(loanCharge.determineIfFullyPaid());
|
||||
loanCharge.setExternalId(externalId);
|
||||
|
||||
return loanCharge;
|
||||
}
|
||||
|
||||
public LoanCharge fetchLoanChargesById(final Loan loan, final Long id) {
|
||||
@@ -402,4 +509,246 @@ public class LoanChargeService {
|
||||
}
|
||||
}
|
||||
|
||||
private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) {
|
||||
/*
|
||||
* JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down
|
||||
* installment charges from the loanCharge and from each of the repayment installments and allow them to be
|
||||
* recalculated fully anew. This patch is to avoid the 'merging' of existing and regenerated installment charges
|
||||
* which results in the installment charges being deleted on loan approval if the schedule is regenerated. Not
|
||||
* pretty. updateInstallmentCharges in LoanCharge.java: the merging looks like it will work but doesn't so this
|
||||
* patch simply hits the part which 'adds all' rather than merge. Possibly an ORM issue. The issue could be to
|
||||
* do with the fact that, on approval, the "recalculateLoanCharge" happens twice (probably 2 schedule
|
||||
* regenerations) whereas it only happens once on Submit and Disburse (and no problems with them)
|
||||
*
|
||||
* if (this.loanInstallmentCharge.isEmpty()) { this.loanInstallmentCharge.addAll(newChargeInstallments);
|
||||
*/
|
||||
Loan loan = loanCharge.getLoan();
|
||||
if (!loan.isSubmittedAndPendingApproval() && !loan.isApproved()) {
|
||||
return;
|
||||
} // doing for both just in case status is not
|
||||
// updated at this points
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
loanCharge.clearLoanInstallmentCharges();
|
||||
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||
if (installment.isRecalculatedInterestComponent()) {
|
||||
continue; // JW: does this in generateInstallmentLoanCharges - but don't understand it
|
||||
}
|
||||
installment.getInstallmentCharges().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChargePaidTransaction(final Loan loan, final LoanCharge charge, final LoanTransaction chargesPayment,
|
||||
final Integer installmentNumber) {
|
||||
chargesPayment.updateLoan(loan);
|
||||
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge,
|
||||
chargesPayment.getAmount(loan.getCurrency()).getAmount(), installmentNumber);
|
||||
chargesPayment.getLoanChargesPaid().add(loanChargePaidBy);
|
||||
loan.addLoanTransaction(chargesPayment);
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_PAYMENT, loan);
|
||||
|
||||
final List<LoanRepaymentScheduleInstallment> chargePaymentInstallments = new ArrayList<>();
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments());
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
|
||||
if (installment.getInstallmentNumber().equals(installmentNumber) || (installmentNumber == null
|
||||
&& charge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))) {
|
||||
chargePaymentInstallments.add(installment);
|
||||
break;
|
||||
}
|
||||
}
|
||||
final Set<LoanCharge> loanCharges = new HashSet<>(1);
|
||||
loanCharges.add(charge);
|
||||
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargesPayment,
|
||||
new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges,
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
|
||||
|
||||
loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate());
|
||||
}
|
||||
|
||||
private BigDecimal calculatePerInstallmentChargeAmount(final Loan loan, final LoanCharge loanCharge) {
|
||||
return calculatePerInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage());
|
||||
}
|
||||
|
||||
private void updateInstallmentCharges(final LoanCharge loanCharge) {
|
||||
final Collection<LoanInstallmentCharge> remove = new HashSet<>();
|
||||
final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(loanCharge.getLoan(), loanCharge);
|
||||
if (loanCharge.getLoanInstallmentCharge().isEmpty()) {
|
||||
loanCharge.getLoanInstallmentCharge().addAll(newChargeInstallments);
|
||||
} else {
|
||||
int index = 0;
|
||||
final List<LoanInstallmentCharge> oldChargeInstallments = new ArrayList<>();
|
||||
if (loanCharge.getLoanInstallmentCharge() != null && !loanCharge.getLoanInstallmentCharge().isEmpty()) {
|
||||
oldChargeInstallments.addAll(loanCharge.getLoanInstallmentCharge());
|
||||
}
|
||||
Collections.sort(oldChargeInstallments);
|
||||
final LoanInstallmentCharge[] loanChargePerInstallmentArray = newChargeInstallments
|
||||
.toArray(new LoanInstallmentCharge[newChargeInstallments.size()]);
|
||||
for (final LoanInstallmentCharge chargePerInstallment : oldChargeInstallments) {
|
||||
if (index == loanChargePerInstallmentArray.length) {
|
||||
remove.add(chargePerInstallment);
|
||||
chargePerInstallment.getInstallment().getInstallmentCharges().remove(chargePerInstallment);
|
||||
} else {
|
||||
LoanInstallmentCharge newLoanInstallmentCharge = loanChargePerInstallmentArray[index++];
|
||||
newLoanInstallmentCharge.getInstallment().getInstallmentCharges().remove(newLoanInstallmentCharge);
|
||||
chargePerInstallment.copyFrom(newLoanInstallmentCharge);
|
||||
}
|
||||
}
|
||||
loanCharge.getLoanInstallmentCharge().removeAll(remove);
|
||||
while (index < loanChargePerInstallmentArray.length) {
|
||||
loanCharge.getLoanInstallmentCharge().add(loanChargePerInstallmentArray[index++]);
|
||||
}
|
||||
}
|
||||
Money amount = Money.zero(loanCharge.getLoan().getCurrency());
|
||||
for (LoanInstallmentCharge charge : loanCharge.getLoanInstallmentCharge()) {
|
||||
amount = amount.plus(charge.getAmount());
|
||||
}
|
||||
loanCharge.setAmount(amount.getAmount());
|
||||
}
|
||||
|
||||
private List<LoanInstallmentCharge> generateInstallmentLoanCharges(final Loan loan, final LoanCharge loanCharge) {
|
||||
final List<LoanInstallmentCharge> loanChargePerInstallments = new ArrayList<>();
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
if (installment.isRecalculatedInterestComponent()) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal amount;
|
||||
if (loanCharge.getChargeCalculation().isFlat()) {
|
||||
amount = loanCharge.amountOrPercentage();
|
||||
} else {
|
||||
amount = calculateInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage(),
|
||||
installment).getAmount();
|
||||
}
|
||||
final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment);
|
||||
installment.getInstallmentCharges().add(loanInstallmentCharge);
|
||||
loanChargePerInstallments.add(loanInstallmentCharge);
|
||||
}
|
||||
}
|
||||
return loanChargePerInstallments;
|
||||
}
|
||||
|
||||
private BigDecimal calculateOverdueAmountPercentageAppliedTo(final Loan loan, final LoanCharge loanCharge,
|
||||
final int penaltyWaitPeriod) {
|
||||
LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment();
|
||||
LocalDate graceDate = DateUtils.getBusinessLocalDate().minusDays(penaltyWaitPeriod);
|
||||
Money amount = Money.zero(loan.getCurrency());
|
||||
|
||||
if (DateUtils.isAfter(graceDate, installment.getDueDate())) {
|
||||
amount = calculateOverdueAmountPercentageAppliedTo(loan, installment, loanCharge.getChargeCalculation());
|
||||
if (!amount.isGreaterThanZero()) {
|
||||
loanCharge.setActive(false);
|
||||
}
|
||||
} else {
|
||||
loanCharge.setActive(false);
|
||||
}
|
||||
return amount.getAmount();
|
||||
}
|
||||
|
||||
private Money calculateOverdueAmountPercentageAppliedTo(final Loan loan, final LoanRepaymentScheduleInstallment installment,
|
||||
final ChargeCalculationType calculationType) {
|
||||
return switch (calculationType) {
|
||||
case PERCENT_OF_AMOUNT -> installment.getPrincipalOutstanding(loan.getCurrency());
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST ->
|
||||
installment.getPrincipalOutstanding(loan.getCurrency()).plus(installment.getInterestOutstanding(loan.getCurrency()));
|
||||
case PERCENT_OF_INTEREST -> installment.getInterestOutstanding(loan.getCurrency());
|
||||
default -> Money.zero(loan.getCurrency());
|
||||
};
|
||||
}
|
||||
|
||||
private void update(final LoanCharge loanCharge, final BigDecimal amount, final LocalDate dueDate, final BigDecimal loanPrincipal,
|
||||
Integer numberOfRepayments, BigDecimal loanChargeAmount) {
|
||||
if (dueDate != null) {
|
||||
loanCharge.setDueDate(dueDate);
|
||||
}
|
||||
|
||||
if (amount != null) {
|
||||
switch (loanCharge.getChargeCalculation()) {
|
||||
case INVALID:
|
||||
break;
|
||||
case FLAT:
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
if (numberOfRepayments == null) {
|
||||
numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions();
|
||||
}
|
||||
loanCharge.setAmount(amount.multiply(BigDecimal.valueOf(numberOfRepayments)));
|
||||
} else {
|
||||
loanCharge.setAmount(amount);
|
||||
}
|
||||
break;
|
||||
case PERCENT_OF_AMOUNT:
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST:
|
||||
case PERCENT_OF_INTEREST:
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT:
|
||||
loanCharge.setPercentage(amount);
|
||||
loanCharge.setAmountPercentageAppliedTo(loanPrincipal);
|
||||
if (loanChargeAmount.compareTo(BigDecimal.ZERO) == 0) {
|
||||
loanChargeAmount = loanCharge.percentageOf(loanCharge.getAmountPercentageAppliedTo());
|
||||
}
|
||||
loanCharge.setAmount(loanCharge.minimumAndMaximumCap(loanChargeAmount));
|
||||
break;
|
||||
}
|
||||
loanCharge.setAmountOrPercentage(amount);
|
||||
loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding());
|
||||
if (loanCharge.getLoan() != null && loanCharge.isInstalmentFee()) {
|
||||
updateInstallmentCharges(loanCharge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Money getTotalAllTrancheDisbursementAmount(final Loan loan) {
|
||||
Money amount = Money.zero(loan.getCurrency());
|
||||
if (loan.isMultiDisburmentLoan()) {
|
||||
for (final LoanDisbursementDetails loanDisbursementDetail : loan.getDisbursementDetails()) {
|
||||
amount = amount.plus(loanDisbursementDetail.principal());
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
private List<Long> fetchAllLoanChargeIds(final Loan loan) {
|
||||
List<Long> list = new ArrayList<>();
|
||||
for (LoanCharge loanCharge : loan.getLoanCharges()) {
|
||||
list.add(loanCharge.getId());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private Money calculateInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType,
|
||||
final BigDecimal percentage, final LoanRepaymentScheduleInstallment installment) {
|
||||
Money percentOf = switch (calculationType) {
|
||||
case PERCENT_OF_AMOUNT -> installment.getPrincipal(loan.getCurrency());
|
||||
case PERCENT_OF_AMOUNT_AND_INTEREST ->
|
||||
installment.getPrincipal(loan.getCurrency()).plus(installment.getInterestCharged(loan.getCurrency()));
|
||||
case PERCENT_OF_INTEREST -> installment.getInterestCharged(loan.getCurrency());
|
||||
case PERCENT_OF_DISBURSEMENT_AMOUNT, INVALID, FLAT -> Money.zero(loan.getCurrency());
|
||||
|
||||
};
|
||||
return Money.zero(loan.getCurrency()) //
|
||||
.plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage));
|
||||
}
|
||||
|
||||
private BigDecimal getDerivedAmountForCharge(final Loan loan, final LoanCharge loanCharge) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loan.isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) {
|
||||
amount = loan.getApprovedPrincipal();
|
||||
} else {
|
||||
// If charge type is specified due date and loan is multi disburment loan.
|
||||
// Then we need to get as of this loan charge due date how much amount disbursed.
|
||||
if (loanCharge.isSpecifiedDueDate() && loan.isMultiDisburmentLoan()) {
|
||||
for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) {
|
||||
if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) {
|
||||
amount = amount.add(loanDisbursementDetails.principal());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
amount = loan.getPrincipal().getAmount();
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-2
@@ -62,6 +62,8 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||
private final LoanTransactionProcessingService loanTransactionProcessingService;
|
||||
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
private final LoanTransactionService loanTransactionService;
|
||||
|
||||
@Override
|
||||
public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command,
|
||||
@@ -91,7 +93,8 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
|
||||
loanTransaction.updateLoan(loan);
|
||||
|
||||
final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(loanTransaction);
|
||||
final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan,
|
||||
loanTransaction);
|
||||
|
||||
if (loanTransaction.isNotZero()) {
|
||||
loan.addLoanTransaction(loanTransaction);
|
||||
@@ -128,6 +131,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
.fetchLoanRepaymentScheduleInstallmentByDueDate(loanTransaction.getTransactionDate());
|
||||
|
||||
boolean reprocessOnPostConditions = false;
|
||||
|
||||
boolean processLatest = isTransactionChronologicallyLatest //
|
||||
&& adjustedTransaction == null // covers reversals
|
||||
&& !loan.isForeclosure() //
|
||||
@@ -163,7 +167,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
if (loanTransaction.isNotRecoveryRepayment()) {
|
||||
loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
|
||||
} else {
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||
}
|
||||
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoanTransactionService {
|
||||
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
|
||||
public List<LoanTransaction> retrieveListOfTransactionsForReprocessing(final Loan loan) {
|
||||
return loan.getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate())
|
||||
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isChronologicallyLatestRepaymentOrWaiver(final Loan loan, final LoanTransaction loanTransaction) {
|
||||
final Optional<LocalDate> lastTransactionDateForReprocessing = loanTransactionRepository
|
||||
.findLastTransactionDateForReprocessing(loan);
|
||||
|
||||
return lastTransactionDateForReprocessing.isEmpty()
|
||||
|| !DateUtils.isAfter(lastTransactionDateForReprocessing.get(), loanTransaction.getTransactionDate());
|
||||
}
|
||||
|
||||
private Predicate<LoanTransaction> loanTransactionForReprocessingPredicate() {
|
||||
return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge()
|
||||
|| transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction());
|
||||
}
|
||||
|
||||
}
|
||||
+5
-2
@@ -25,12 +25,15 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LoanScheduleComponent {
|
||||
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
|
||||
public void updateLoanSchedule(Loan loan, final LoanScheduleModel modifiedLoanSchedule) {
|
||||
final List<LoanScheduleModelPeriod> periods = modifiedLoanSchedule.getPeriods();
|
||||
for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) {
|
||||
@@ -55,7 +58,7 @@ public class LoanScheduleComponent {
|
||||
loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(periods, i.getInstallmentNumber()));
|
||||
|
||||
loan.updateLoanScheduleDependentDerivedFields();
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||
}
|
||||
|
||||
public void updateLoanSchedule(Loan loan, final List<LoanRepaymentScheduleInstallment> installments) {
|
||||
@@ -72,7 +75,7 @@ public class LoanScheduleComponent {
|
||||
loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(installments, i.getInstallmentNumber()));
|
||||
|
||||
loan.updateLoanScheduleDependentDerivedFields();
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment findByInstallmentNumber(final Collection<LoanRepaymentScheduleInstallment> installments,
|
||||
|
||||
+9
-1
@@ -43,6 +43,10 @@ import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
@@ -64,6 +68,9 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
|
||||
private ArgumentCaptor<Money> penaltyChargesWaived = ArgumentCaptor.forClass(Money.class);
|
||||
private ArgumentCaptor<Money> penaltyChargesWrittenOff = ArgumentCaptor.forClass(Money.class);
|
||||
|
||||
private final LoanChargeService loanChargeService = new LoanChargeService(mock(LoanChargeValidator.class),
|
||||
mock(LoanTransactionProcessingService.class), mock(LoanLifecycleStateMachine.class), mock(LoanBalanceService.class));
|
||||
|
||||
@BeforeAll
|
||||
public static void init() {
|
||||
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
|
||||
@@ -141,7 +148,8 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
|
||||
when(charge.isPenalty()).thenReturn(penalty);
|
||||
Loan loan = mock(Loan.class);
|
||||
when(loan.isInterestBearing()).thenReturn(false);
|
||||
return new LoanCharge(loan, charge, new BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE,
|
||||
|
||||
return loanChargeService.create(loan, charge, new BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE,
|
||||
ChargeCalculationType.FLAT, LocalDate.of(2023, 1, 15), ChargePaymentMode.REGULAR, 1, null, null);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user