FINERACT-2080: Extract calculations from loan entity

This commit is contained in:
Oleksii Novikov
2025-05-22 13:58:44 +02:00
committed by Adam Saghy
parent 18a55874ba
commit 47d5330802
53 changed files with 1403 additions and 1128 deletions
@@ -20,6 +20,8 @@ package com.acme.fineract.loan.processor;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.springframework.stereotype.Component;
@Component
@@ -29,8 +31,9 @@ public class AcmeLoanRepaymentScheduleTransactionProcessor extends FineractStyle
public static final String STRATEGY_NAME = "ACME Corp.: standard loan transaction processing strategy";
public AcmeLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
super(externalIdFactory);
public AcmeLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
}
@Override
@@ -40,9 +40,10 @@ import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.fineract.avro.generator.ByteBufferSerializable;
@@ -135,7 +136,7 @@ public class InvestorBusinessEventSerializerTest {
.thenReturn(createTransferData(firstTransferStatus, null));
Loan loan = Mockito.mock(Loan.class);
when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1));
List<LoanCharge> loanCharges = createMockCharges();
final Set<LoanCharge> loanCharges = createMockCharges();
when(loan.getLoanCharges()).thenReturn(loanCharges);
LoanOwnershipTransferBusinessEvent loanOwnershipTransferBusinessEvent = new LoanOwnershipTransferBusinessEvent(
createExternalAssetOwnerTransfer(status, subStatus), loan);
@@ -153,8 +154,8 @@ public class InvestorBusinessEventSerializerTest {
assertEquals(CUSTOM_DATA_PREFIX + "_2", new String(customData.get("test_key_2").array(), UTF_8));
}
private List<LoanCharge> createMockCharges() {
List<LoanCharge> loanCharges = new ArrayList<>();
private Set<LoanCharge> createMockCharges() {
final Set<LoanCharge> loanCharges = new HashSet<>();
loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("10.00000")));
loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("15.00000")));
loanCharges.add(loanCharge(2L, "charge b", BigDecimal.ZERO));
@@ -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;
}
}
@@ -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);
@@ -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;
}
@@ -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) {
@@ -925,4 +925,5 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public void updateTransactionDate(final LocalDate transactionDate) {
this.dateOf = transactionDate;
}
}
@@ -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);
}
@@ -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());
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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());
@@ -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;
@@ -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;
@@ -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",
@@ -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;
}
}
@@ -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;
}
}
@@ -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()) {
@@ -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());
}
}
@@ -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,
@@ -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);
}
@@ -90,7 +90,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Abs
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -118,13 +121,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final InterestRefundService interestRefundService;
private final LoanScheduleComponent loanSchedule;
private final LoanTransactionService loanTransactionService;
public AdvancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, LoanRepositoryWrapper loanRepositoryWrapper,
InterestRefundService interestRefundService, ExternalIdFactory externalIdFactory, LoanScheduleComponent loanSchedule) {
super(externalIdFactory);
public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper,
final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory,
final LoanScheduleComponent loanSchedule, final LoanTransactionService loanTransactionRepository,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
this.emiCalculator = emiCalculator;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.interestRefundService = interestRefundService;
this.loanTransactionService = loanTransactionRepository;
this.loanSchedule = loanSchedule;
}
@@ -269,7 +276,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) {
Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
List<LoanTransaction> transactions = loan.retrieveListOfTransactionsForReprocessing();
final List<LoanTransaction> transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency();
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
Set<LoanCharge> charges = loan.getActiveCharges();
@@ -1514,7 +1521,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
newTransaction.updateLoan(loan);
loan.getLoanTransactions().add(newTransaction);
}
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
}
}
}
@@ -62,6 +62,7 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService;
private final InterestScheduleModelRepositoryWrapper writePlatformService;
private final LoanTransactionService loanTransactionService;
@Override
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
@@ -94,7 +95,7 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
}
private ProgressiveLoanInterestScheduleModel reprocessTransactionsAndGetModel(final Loan loan) {
final List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing();
final List<LoanTransaction> transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
final LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate();
final Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactionsTransactional(loan.getDisbursementDate(), businessDate, transactionsToReprocess,
@@ -49,6 +49,7 @@ import java.util.Set;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -69,6 +70,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -98,8 +103,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest;
private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
private static final LoanRepositoryWrapper loanRepositoryWrapper = Mockito.mock(LoanRepositoryWrapper.class);
private static final LoanScheduleComponent loanSchedule = Mockito.mock(LoanScheduleComponent.class);
@BeforeAll
public static void init() {
@@ -114,7 +117,10 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
@BeforeEach
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null, null, loanSchedule);
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class),
Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class),
Mockito.mock(LoanTransactionService.class), Mockito.mock(LoanChargeValidator.class),
Mockito.mock(LoanBalanceService.class));
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
@@ -104,11 +104,13 @@ import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDe
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
@@ -147,7 +149,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
private final LoanTransactionValidator loanTransactionValidator;
private final LoanForeclosureValidator loanForeclosureValidator;
@@ -161,6 +162,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionService loanTransactionService;
@Transactional
@Override
@@ -767,7 +770,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
final ScheduleGeneratorDTO scheduleGeneratorDTO = null;
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
final LoanRepaymentScheduleInstallment foreCloseDetail = loanBalanceService.fetchLoanForeclosureDetail(loan, foreClosureDate);
loanAccrualsProcessingService.processAccrualsOnLoanForeClosure(loan, foreClosureDate, newTransactions);
@@ -875,7 +878,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
LoanTransaction refundTransaction = LoanTransaction.refund(loan, loanTransactionType, transactionAmount, paymentDetail,
transactionDate, txnExternalId);
final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(refundTransaction);
final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan,
refundTransaction);
boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream()
.map(LoanSupportedInterestRefundTypes::getTransactionType)
@@ -1002,7 +1006,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(loan.getRepaymentScheduleInstallments());
final MonetaryCurrency currency = loan.getCurrency();
Money totalPrincipal = Money.zero(currency);
final Money[] balances = loan.retrieveIncomeForOverlappingPeriod(transactionDate);
final Money[] balances = loanBalanceService.retrieveIncomeForOverlappingPeriod(loan, transactionDate);
boolean isInterestComponent = true;
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) {
@@ -1571,7 +1571,8 @@ public class LoanScheduleAssembler {
for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) {
if (loanScheduleModelPeriod instanceof LoanScheduleModelDisbursementPeriod) {
for (final LoanCharge loanCharge : nonCompoundingCharges) {
loanCharge.populateDerivedFields(totalInterest, loanCharge.amountOrPercentage(), null, BigDecimal.ZERO);
loanChargeService.populateDerivedFields(loanCharge, totalInterest, loanCharge.amountOrPercentage(), null,
BigDecimal.ZERO);
loanScheduleModelPeriod.addLoanCharges(loanCharge.getAmountOutstanding(), BigDecimal.ZERO);
}
}
@@ -55,6 +55,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionAssembler loanTransactionAssembler;
private final LoanAccountService loanAccountService;
private final LoanBalanceService loanBalanceService;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
@@ -93,7 +94,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
return;
}
LocalDate closureDate = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
LocalDate closureDate = loanBalanceService.isOverPaid(loan) ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
// Reverse accrual activities posted after the closure date
loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(closureDate))
@@ -113,6 +113,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
private final TransactionTemplate transactionTemplate;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanChargeService loanChargeService;
private final LoanBalanceService loanBalanceService;
/**
* method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan
@@ -307,7 +308,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
// TODO implement progressive accrual case
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
&& (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) {
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
final LoanRepaymentScheduleInstallment foreCloseDetail = loanBalanceService.fetchLoanForeclosureDetail(loan, foreClosureDate);
MonetaryCurrency currency = loan.getCurrency();
reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate, false);
@@ -494,7 +495,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
Money interest = null;
final boolean isPastPeriod = isAfterPeriod(tillDate, installment);
final boolean isInPeriod = isInPeriod(tillDate, installment, false);
if (isPastPeriod || loan.isClosed() || loan.isOverPaid()) {
if (isPastPeriod || loan.isClosed() || loanBalanceService.isOverPaid(loan)) {
interest = installment.getInterestCharged(currency).minus(installment.getCreditedInterest());
} else {
if (isInPeriod) { // first period first day is not accrued
@@ -61,6 +61,7 @@ public class LoanChargeAssembler {
private final LoanChargeRepository loanChargeRepository;
private final LoanProductRepository loanProductRepository;
private final ExternalIdFactory externalIdFactory;
private final LoanChargeService loanChargeService;
public Set<LoanCharge> fromParsedJson(final JsonElement element, List<LoanDisbursementDetails> disbursementDetails) {
JsonArray jsonDisbursement = this.fromApiJsonHelper.extractJsonArrayNamed("disbursementData", element);
@@ -210,7 +211,7 @@ public class LoanChargeAssembler {
.orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId));
if (!loanCharge.isTrancheDisbursementCharge() || disbursementChargeIds.contains(loanChargeId)) {
loanCharge.update(amount, dueDate, numberOfRepayments);
loanChargeService.update(loanCharge, amount, dueDate, numberOfRepayments);
loanCharges.add(loanCharge);
}
@@ -296,7 +297,7 @@ public class LoanChargeAssembler {
if (percentage == null) {
percentage = chargeDefinition.getAmount();
}
loanCharge = LoanCharge.calculatePerInstallmentChargeAmount(loan,
loanCharge = loanChargeService.calculatePerInstallmentChargeAmount(loan,
ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()), percentage);
}
@@ -314,7 +315,7 @@ public class LoanChargeAssembler {
}
ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId");
return new LoanCharge(loan, chargeDefinition, amountPercentageAppliedTo, amount, chargeTime, chargeCalculation, dueDate,
return loanChargeService.create(loan, chargeDefinition, amountPercentageAppliedTo, amount, chargeTime, chargeCalculation, dueDate,
chargePaymentMode, null, loanCharge, externalId);
}
@@ -324,7 +325,7 @@ public class LoanChargeAssembler {
public LoanCharge createNewWithoutLoan(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 ExternalId externalId) {
return new LoanCharge(null, chargeDefinition, loanPrincipal, amount, chargeTime, chargeCalculation, dueDate, chargePaymentMode,
numberOfRepayments, BigDecimal.ZERO, externalId);
return loanChargeService.create(null, chargeDefinition, loanPrincipal, amount, chargeTime, chargeCalculation, dueDate,
chargePaymentMode, numberOfRepayments, BigDecimal.ZERO, externalId);
}
}
@@ -288,9 +288,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
.equals(loan.transactionProcessingStrategy())) {
// [For Adv payment allocation strategy] check if charge due date is earlier than last transaction
// date, if yes trigger reprocess else no reprocessing
LoanTransaction lastPaymentTransaction = loan.getLastTransactionForReprocessing();
if (lastPaymentTransaction != null
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) {
final Optional<LocalDate> lastPaymentTransactionDate = loanTransactionRepository.findLastTransactionDateForReprocessing(loan);
if (lastPaymentTransactionDate.isPresent()
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransactionDate.get())) {
reprocessRequired = false;
}
}
@@ -475,7 +475,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final Map<String, Object> changes = new LinkedHashMap<>(3);
if (loan.getActiveCharges().contains(loanCharge)) {
final BigDecimal amount = loanChargeService.calculateAmountPercentageAppliedTo(loan, loanCharge);
final Map<String, Object> loanChargeChanges = loanCharge.update(command, amount);
final Map<String, Object> loanChargeChanges = loanChargeService.update(command, amount, loanCharge);
changes.putAll(loanChargeChanges);
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
}
@@ -244,8 +244,8 @@ public class LoanDisbursementService {
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate();
}
final LoanCharge loanCharge = new LoanCharge(loan, chargeDefinition, principal, null, null, null, expectedDisbursementDate,
null, null, BigDecimal.ZERO, externalId);
final LoanCharge loanCharge = loanChargeService.create(loan, chargeDefinition, principal, null, null, null,
expectedDisbursementDate, null, null, BigDecimal.ZERO, externalId);
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge,
disbursementDetails);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
@@ -179,6 +179,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
private final LoanForeclosureValidator loanForeclosureValidator;
private final LoanTransactionMapper loanTransactionMapper;
private final LoanTransactionProcessingService loadTransactionProcessingService;
private final LoanBalanceService loanBalanceService;
@Override
public LoanAccountData retrieveOne(final Long loanId) {
@@ -2088,7 +2089,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate();
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate);
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanBalanceService.fetchLoanForeclosureDetail(loan,
transactionDate);
BigDecimal unrecognizedIncomePortion = null;
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
@@ -2236,7 +2238,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return loanRepositoryWrapper.existsByLoanId(loanId);
}
private LoanTransaction deriveDefaultInterestWaiverTransaction(Loan loan) {
private LoanTransaction deriveDefaultInterestWaiverTransaction(final Loan loan) {
final Money totalInterestOutstanding = loan.getTotalInterestOutstandingOnLoan();
Money possibleInterestToWaive = totalInterestOutstanding.copy();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
@@ -2260,4 +2262,5 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return LoanTransaction.waiver(loan.getOffice(), loan, possibleInterestToWaive, transactionDate, possibleInterestToWaive,
possibleInterestToWaive.zero(), ExternalId.empty());
}
}
@@ -61,6 +61,8 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final LoanTermVariationsMapper loanMapper;
private final InterestScheduleModelRepositoryWrapper modelRepository;
private final LoanTransactionService loanTransactionService;
private final LoanBalanceService loanBalanceService;
@Override
public boolean canProcessLatestTransactionOnly(Loan loan, LoanTransaction loanTransaction,
@@ -86,7 +88,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), savedModel.orElse(null), getTotalRefundInterestAmount(loan));
progressiveContext.getAlreadyProcessedTransactions().addAll(loan.retrieveListOfTransactionsForReprocessing());
progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan));
progressiveContext.setChargedOff(loan.isChargedOff());
ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext);
if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
@@ -158,7 +160,9 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
public Optional<ChangedTransactionDetail> processPostDisbursementTransactions(Loan loan) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(
loan.getTransactionProcessingStrategyCode());
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loanTransactionService
.retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> copyTransactions = new ArrayList<>();
if (allNonContraTransactionsPostDisbursement.isEmpty()) {
@@ -173,7 +177,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
loan.getDisbursementDate(), copyTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges());
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
return Optional.of(changedTransactionDetail);
}
@@ -287,6 +287,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanMapper loanMapper;
private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionService loanTransactionService;
@Transactional
@Override
@@ -572,7 +574,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSED,
actualDisbursementDate1);
loanDisbursementService.handleDisbursementTransaction(loan, actualDisbursementDate1, paymentDetail1);
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged());
/*
@@ -591,11 +593,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
if (loan.getLoanProduct().isMultiDisburseLoan() || loan.isProgressiveSchedule()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loanTransactionService
.retrieveListOfTransactionsForReprocessing(loan);
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
}
loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, loan);
@@ -972,8 +975,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
newInterestPaymentWaiverTransaction.updateLoan(loan);
final boolean isTransactionChronologicallyLatest = loan
.isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction);
final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan,
newInterestPaymentWaiverTransaction);
if (newInterestPaymentWaiverTransaction.isNotZero()) {
loan.addLoanTransaction(newInterestPaymentWaiverTransaction);
@@ -1412,7 +1415,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
Money unrecognizedIncome = transactionAmountAsMoney.zero();
Money interestComponent = transactionAmountAsMoney;
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableInterest = loan.getReceivableInterest(transactionDate);
Money receivableInterest = loanBalanceService.getReceivableInterest(loan, transactionDate);
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
interestComponent = receivableInterest;
unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest);
@@ -2024,119 +2027,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
public void updateLoanRepaymentScheduleDates(final Loan loan, final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final boolean isSkipRepaymentonfirstdayofmonth, final Integer numberofDays) {
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = loan.getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType();
final Integer loanRepaymentInterval = loan.getLoanRepaymentScheduleDetail().getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
LocalDate newRepaymentDate;
LocalDate latestRepaymentDate = null;
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
// FIXME: AA this won't update repayment dates before current date.
if (DateUtils.isAfter(oldDueDate, meetingStartDate) && DateUtils.isDateInTheFuture(oldDueDate)) {
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate,
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType,
loanRepaymentInterval, tmpFromDate);
if (DateUtils.isAfter(newRepaymentDate, maxDateLimitForNewRepayment)) {
newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, meetingStartDate, tmpFromDate,
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
}
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
}
if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) {
latestRepaymentDate = newRepaymentDate;
}
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
tmpFromDate = newRepaymentDate;// update with new repayment date
} else {
tmpFromDate = oldDueDate;
}
}
if (latestRepaymentDate != null) {
loan.setExpectedMaturityDate(latestRepaymentDate);
}
}
private LocalDate getMaxDateLimitForNewRepayment(final PeriodFrequencyType periodFrequencyType, final Integer loanRepaymentInterval,
final LocalDate startDate) {
LocalDate dueRepaymentPeriodDate = startDate;
final int repaidEvery = 2 * loanRepaymentInterval;
switch (periodFrequencyType) {
case DAYS -> dueRepaymentPeriodDate = startDate.plusDays(repaidEvery);
case WEEKS -> dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery);
case MONTHS -> dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery);
case YEARS -> dueRepaymentPeriodDate = startDate.plusYears(repaidEvery);
case INVALID, WHOLE_TERM -> {
}
}
return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate
}
private void updateLoanRepaymentScheduleDates(final Loan loan, final String recurringRule, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final LocalDate presentMeetingDate, final LocalDate newMeetingDate,
final boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays) {
// first repayment's from date is same as disbursement date.
// meetingStartDate is used as seedDate Capture the seedDate from user and use the seedDate as meetingStart date
LocalDate tmpFromDate = loan.getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType();
final Integer loanRepaymentInterval = loan.getLoanRepaymentScheduleDetail().getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
LocalDate newRepaymentDate;
boolean isFirstTime = true;
LocalDate latestRepaymentDate = null;
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
if (!DateUtils.isBefore(oldDueDate, presentMeetingDate)) {
if (isFirstTime) {
isFirstTime = false;
newRepaymentDate = newMeetingDate;
} else {
// tmpFromDate.plusDays(1) is done to make sure
// getNewRepaymentMeetingDate method returns next meeting
// date and not the same as tmpFromDate
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recurringRule, tmpFromDate, tmpFromDate.plusDays(1),
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays);
}
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
}
if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) {
latestRepaymentDate = newRepaymentDate;
}
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
tmpFromDate = newRepaymentDate;// update with new repayment date
} else {
tmpFromDate = oldDueDate;
}
}
if (latestRepaymentDate != null) {
loan.setExpectedMaturityDate(latestRepaymentDate);
}
}
private void removeLoanCycle(final Loan loan) {
final List<Loan> loansToUpdate;
if (loan.isGroupLoan()) {
@@ -2924,7 +2814,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
}
final List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsForReprocessing();
final List<LoanTransaction> loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
loanTransactions.add(chargeOffTransaction);
reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions);
loan.addLoanTransaction(chargeOffTransaction);
@@ -3117,9 +3007,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.addLoanTransaction(chargebackTransaction);
if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) {
final List<LoanTransaction> transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
loanTransactionProcessingService.reprocessLoanTransactions(loan.getTransactionProcessingStrategyCode(),
loan.getDisbursementDate(), loan.retrieveListOfTransactionsForReprocessing(), loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
loan.getDisbursementDate(), transactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges());
} else {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargebackTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
@@ -3215,7 +3106,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.adjustNetDisbursalAmount(loan.getApprovedPrincipal());
actualChanges.put(ACTUAL_DISBURSEMENT_DATE, "");
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
}
return actualChanges;
@@ -3246,7 +3137,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
}
private void reverseExistingTransactions(final Loan loan) {
@@ -3319,7 +3210,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
return Optional.of(loanTransaction);
}
@@ -3383,7 +3274,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
changes.put("externalId", externalId);
loanTransaction = LoanTransaction.writeoff(loan, loan.getOffice(), closureDate, externalId);
final boolean isLastTransaction = loan.isChronologicallyLatestTransaction(loanTransaction, loan.getLoanTransactions());
final boolean isLastTransaction = loanTransactionRepository.isChronologicallyLatest(loanTransaction.getTransactionDate(),
loan);
if (!isLastTransaction) {
final String errorMessage = "The closing date of the loan must be on or after latest transaction date.";
throw new InvalidLoanStateTransitionException("close.loan", "must.occur.on.or.after.latest.transaction.date",
@@ -3395,7 +3287,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
} else if (totalOutstanding.isGreaterThanZero()) {
final String errorMessage = "A loan with money outstanding cannot be closed";
throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage,
@@ -3403,8 +3295,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
if (loan.isOverPaid()) {
final Money totalLoanOverpayment = loan.calculateTotalOverpayment();
if (loanBalanceService.isOverPaid(loan)) {
final Money totalLoanOverpayment = loanBalanceService.calculateTotalOverpayment(loan);
if (totalLoanOverpayment.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) {
// TODO - KW - technically should set somewhere that this loan
// has 'overpaid' amount
@@ -3474,7 +3366,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate,
lastDisbursalTransaction.getAmount());
loan.updateLoanToLastDisbursalState(disbursementDetail);
loanBalanceService.updateLoanToLastDisbursalState(loan, disbursementDetail);
loan.getLoanTermVariations()
.removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation()
&& DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate))
@@ -3579,4 +3471,118 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
return !statusEnum.hasStateOf(actualLoanStatus) || isMultiTrancheDisburse;
}
private void updateLoanRepaymentScheduleDates(final Loan loan, final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final boolean isSkipRepaymentonfirstdayofmonth, final Integer numberofDays) {
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = loan.getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType();
final Integer loanRepaymentInterval = loan.getLoanRepaymentScheduleDetail().getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
LocalDate newRepaymentDate;
LocalDate latestRepaymentDate = null;
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
// FIXME: AA this won't update repayment dates before current date.
if (DateUtils.isAfter(oldDueDate, meetingStartDate) && DateUtils.isDateInTheFuture(oldDueDate)) {
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate,
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType,
loanRepaymentInterval, tmpFromDate);
if (DateUtils.isAfter(newRepaymentDate, maxDateLimitForNewRepayment)) {
newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, meetingStartDate, tmpFromDate,
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
}
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
}
if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) {
latestRepaymentDate = newRepaymentDate;
}
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
tmpFromDate = newRepaymentDate;// update with new repayment date
} else {
tmpFromDate = oldDueDate;
}
}
if (latestRepaymentDate != null) {
loan.setExpectedMaturityDate(latestRepaymentDate);
}
}
private LocalDate getMaxDateLimitForNewRepayment(final PeriodFrequencyType periodFrequencyType, final Integer loanRepaymentInterval,
final LocalDate startDate) {
LocalDate dueRepaymentPeriodDate = startDate;
final int repaidEvery = 2 * loanRepaymentInterval;
switch (periodFrequencyType) {
case DAYS -> dueRepaymentPeriodDate = startDate.plusDays(repaidEvery);
case WEEKS -> dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery);
case MONTHS -> dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery);
case YEARS -> dueRepaymentPeriodDate = startDate.plusYears(repaidEvery);
case INVALID, WHOLE_TERM -> {
}
}
return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate
}
private void updateLoanRepaymentScheduleDates(final Loan loan, final String recurringRule, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final LocalDate presentMeetingDate, final LocalDate newMeetingDate,
final boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays) {
// first repayment's from date is same as disbursement date.
// meetingStartDate is used as seedDate Capture the seedDate from user and use the seedDate as meetingStart date
LocalDate tmpFromDate = loan.getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType();
final Integer loanRepaymentInterval = loan.getLoanRepaymentScheduleDetail().getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
LocalDate newRepaymentDate;
boolean isFirstTime = true;
LocalDate latestRepaymentDate = null;
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
if (!DateUtils.isBefore(oldDueDate, presentMeetingDate)) {
if (isFirstTime) {
isFirstTime = false;
newRepaymentDate = newMeetingDate;
} else {
// tmpFromDate.plusDays(1) is done to make sure
// getNewRepaymentMeetingDate method returns next meeting
// date and not the same as tmpFromDate
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recurringRule, tmpFromDate, tmpFromDate.plusDays(1),
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays);
}
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
}
if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) {
latestRepaymentDate = newRepaymentDate;
}
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
tmpFromDate = newRepaymentDate;// update with new repayment date
} else {
tmpFromDate = oldDueDate;
}
}
if (latestRepaymentDate != null) {
loan.setExpectedMaturityDate(latestRepaymentDate);
}
}
}
@@ -53,6 +53,7 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
private final EMICalculator emiCalculator;
private final LoanRepositoryWrapper loanRepository;
private final LoanTransactionService loanTransactionService;
private final InterestScheduleModelRepositoryWrapper modelRepository;
@Override
@@ -80,8 +81,8 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
}
private ProgressiveLoanInterestScheduleModel calculateModel(Loan loan, LocalDate businessDate) {
List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream()
.filter(t -> !t.isAccrualActivity()).toList();
final List<LoanTransaction> transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan)
.stream().filter(t -> !t.isAccrualActivity()).toList();
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
@@ -22,6 +22,8 @@ import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -35,6 +37,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
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.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -49,15 +53,27 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
private final LoanTransactionProcessingService loanTransactionProcessingService;
private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepositoryWrapper;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionRepository loanTransactionRepository;
@Override
public void reprocessTransactions(final Loan loan) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(loan);
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan,
allNonContraTransactionsPostDisbursement);
handleChangedDetail(changedTransactionDetail);
}
private List<LoanTransaction> retrieveListOfTransactionsForReprocessing(final Loan loan) {
return loan.getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate())
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
}
private Predicate<LoanTransaction> loanTransactionForReprocessingPredicate() {
return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge()
|| transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction());
}
@Override
public void reprocessParticularTransactions(final Loan loan, final List<LoanTransaction> loanTransactions) {
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, loanTransactions);
@@ -66,8 +82,8 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
@Override
public void reprocessTransactionsWithPostTransactionChecks(final Loan loan, final LocalDate transactionDate) {
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan,
loan.retrieveListOfTransactionsForReprocessing());
final List<LoanTransaction> transactions = retrieveListOfTransactionsForReprocessing(loan);
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, transactions);
handleChangedDetail(changedTransactionDetail);
}
@@ -101,7 +117,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
return;
}
loan.getLoanCharges().remove(loanCharge);
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
}
private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final Loan loan, final LoanCharge loanCharge) {
@@ -147,7 +163,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
.map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList();
loan.getLoanTransactions().addAll(newTransactions);
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
handleChangedDetail(changedTransactionDetail);
}
@@ -185,7 +201,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList();
loan.getLoanTransactions().addAll(newTransactions);
loan.updateLoanSummaryDerivedFields();
loanBalanceService.updateLoanSummaryDerivedFields(loan);
return changedTransactionDetail;
}
}
@@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidat
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
@@ -91,6 +92,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanChargeValidator loanChargeValidator;
private final LoanJournalEntryPoster journalEntryPoster;
private final LoanBalanceService loanBalanceService;
@Override
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
@@ -112,7 +114,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
Money unrecognizedIncome = transactionAmountAsMoney.zero();
Money interestComponent = transactionAmountAsMoney;
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableInterest = loan.getReceivableInterest(transactionDate);
Money receivableInterest = loanBalanceService.getReceivableInterest(loan, transactionDate);
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
interestComponent = receivableInterest;
unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest);
@@ -33,6 +33,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -48,63 +51,77 @@ public class LoanAccountAutoStarter {
@Bean
@Conditional(CreocoreLoanRepaymentScheduleTransactionProcessorCondition.class)
public CreocoreLoanRepaymentScheduleTransactionProcessor creocoreLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new CreocoreLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new CreocoreLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
}
@Bean
@Conditional(EarlyRepaymentLoanRepaymentScheduleTransactionProcessorCondition.class)
public EarlyPaymentLoanRepaymentScheduleTransactionProcessor earlyPaymentLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new EarlyPaymentLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new EarlyPaymentLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
}
@Bean
@Conditional(MifosStandardLoanRepaymentScheduleTransactionProcessorCondition.class)
public FineractStyleLoanRepaymentScheduleTransactionProcessor fineractStyleLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new FineractStyleLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new FineractStyleLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
}
@Bean
@Conditional(HeavensFamilyLoanRepaymentScheduleTransactionProcessorCondition.class)
public HeavensFamilyLoanRepaymentScheduleTransactionProcessor heavensFamilyLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new HeavensFamilyLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new HeavensFamilyLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
}
@Bean
@Conditional(InterestPrincipalPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class)
public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor interestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
}
@Bean
@Conditional(PrincipalInterestPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class)
public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor principalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
}
@Bean
@Conditional(RBIIndiaLoanRepaymentScheduleTransactionProcessorCondition.class)
public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) {
return new RBILoanRepaymentScheduleTransactionProcessor(externalIdFactory);
public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor(
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new RBILoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
}
@Bean
@Conditional(DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorCondition.class)
public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor duePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
}
@Bean
@Conditional(DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorCondition.class)
public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor duePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) {
return new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(externalIdFactory);
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
}
@Bean
@@ -117,11 +134,13 @@ public class LoanAccountAutoStarter {
@Bean
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator,
LoanRepositoryWrapper loanRepositoryWrapper,
@Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, ExternalIdFactory externalIdFactory,
LoanScheduleComponent loanSchedule) {
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator,
final LoanRepositoryWrapper loanRepositoryWrapper,
final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanTransactionService loanTransactionService, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService,
externalIdFactory, loanSchedule);
externalIdFactory, loanSchedule, loanTransactionService, loanChargeValidator, loanBalanceService);
}
}
@@ -118,6 +118,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService
import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService;
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService;
@@ -143,6 +144,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatfor
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionRelationReadService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
@@ -268,11 +270,11 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanChargeAssembler.class)
public LoanChargeAssembler loanChargeAssembler(
FromJsonHelper fromApiJsonHelper, ChargeRepositoryWrapper chargeRepository, LoanChargeRepository loanChargeRepository,
LoanProductRepository loanProductRepository, ExternalIdFactory externalIdFactory) {
return new LoanChargeAssembler(fromApiJsonHelper, chargeRepository, loanChargeRepository, loanProductRepository, externalIdFactory);
public LoanChargeAssembler loanChargeAssembler(final FromJsonHelper fromApiJsonHelper, final ChargeRepositoryWrapper chargeRepository,
final LoanChargeRepository loanChargeRepository, final LoanProductRepository loanProductRepository,
final ExternalIdFactory externalIdFactory, final LoanChargeService loanChargeService) {
return new LoanChargeAssembler(fromApiJsonHelper, chargeRepository, loanChargeRepository, loanProductRepository, externalIdFactory,
loanChargeService);
}
@Bean
@@ -331,14 +333,14 @@ public class LoanAccountConfiguration {
DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository,
LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService,
LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper,
LoanTransactionProcessingService loanTransactionProcessingService) {
LoanTransactionProcessingService loanTransactionProcessingService, LoanBalanceService loanBalanceService) {
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
staffReadPlatformService, paginationHelper, paymentTypeReadPlatformService, floatingRatesReadPlatformService,
loanUtilService, configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator,
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService);
loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService, loanBalanceService);
}
@Bean
@@ -402,7 +404,8 @@ public class LoanAccountConfiguration {
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService,
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
LoanTransactionProcessingService loanTransactionProcessingService) {
LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService,
LoanTransactionService loanTransactionService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
@@ -417,7 +420,7 @@ public class LoanAccountConfiguration {
loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator,
loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService, reprocessLoanTransactionsService,
loanAccountService, journalEntryPoster, loanAdjustmentService, loanAccountingBridgeMapper, loanMapper,
loanTransactionProcessingService);
loanTransactionProcessingService, loanBalanceService, loanTransactionService);
}
@Bean
@@ -434,10 +437,12 @@ public class LoanAccountConfiguration {
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService,
LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator,
ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine,
LoanBalanceService loanBalanceService, LoanTransactionService loanTransactionService) {
return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator,
reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine);
reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService,
loanTransactionService);
}
@Bean
@@ -457,9 +462,10 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanChargeService.class)
public LoanChargeService loanChargeService(LoanChargeValidator loanChargeValidator,
LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) {
return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService, loanLifecycleStateMachine);
public LoanChargeService loanChargeService(final LoanChargeValidator loanChargeValidator,
final LoanTransactionProcessingService loanTransactionProcessingService,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanBalanceService loanBalanceService) {
return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService);
}
@Bean
@@ -87,12 +87,17 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -122,6 +127,9 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
@Mock
private AvroDateTimeMapper mapper;
private final LoanChargeService loanChargeService = new LoanChargeService(mock(LoanChargeValidator.class),
mock(LoanTransactionProcessingService.class), mock(LoanLifecycleStateMachine.class), mock(LoanBalanceService.class));
private MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static final String CUSTOM_DATA_PREFIX = "test_data_loan_delinquency_range_business_event";
@@ -275,7 +283,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
.thenReturn(installmentDelinquencyTags);
when(loanForProcessing.getLoanCharges()).thenAnswer(a -> repaymentScheduleInstallments.get(0).getInstallmentCharges().stream()
.map(c -> c.getLoanCharge()).collect(Collectors.toList()));
.map(LoanInstallmentCharge::getLoanCharge).collect(Collectors.toSet()));
// when
LoanAccountDelinquencyRangeDataV1 data = (LoanAccountDelinquencyRangeDataV1) serializer.toAvroDTO(event);
@@ -431,8 +439,9 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
}
private LoanCharge buildLoanCharge(Loan loan, BigDecimal amount, Charge charge) {
LoanCharge loanCharge = new LoanCharge(loan, charge, amount, amount, ChargeTimeType.SPECIFIED_DUE_DATE, ChargeCalculationType.FLAT,
LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, new BigDecimal(100), ExternalId.generate());
LoanCharge loanCharge = loanChargeService.create(loan, charge, amount, amount, ChargeTimeType.SPECIFIED_DUE_DATE,
ChargeCalculationType.FLAT, LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, new BigDecimal(100),
ExternalId.generate());
ReflectionTestUtils.setField(loanCharge, "id", 1L);
return loanCharge;
}
@@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -48,6 +49,9 @@ class DefaultLoanLifecycleStateMachineTest {
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@Mock
private LoanBalanceService loanBalanceService;
private DefaultLoanLifecycleStateMachine underTest;
private MockedStatic<MoneyHelper> moneyHelperStatic;
@@ -58,7 +62,7 @@ class DefaultLoanLifecycleStateMachineTest {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelperStatic.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService);
underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService, loanBalanceService);
}
@AfterEach
@@ -41,6 +41,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.rate.domain.Rate;
@@ -61,7 +63,7 @@ public class LoanBuilder {
private Staff loanOfficer;
private CodeValue loanPurpose;
private LoanRepaymentScheduleTransactionProcessor transactionProcessor = new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(
mock(ExternalIdFactory.class));
mock(ExternalIdFactory.class), mock(LoanChargeValidator.class), mock(LoanBalanceService.class));
private LoanProductRelatedDetail loanRepaymentScheduleDetail;
private LoanStatus loanStatus = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL;
private LoanSubStatus loanSubStatus;
@@ -265,8 +265,23 @@ public class LoanTest {
* @return the {@link LoanCharge}
*/
private LoanCharge buildLoanCharge() {
return new LoanCharge(mock(Loan.class), mock(Charge.class), new BigDecimal(100), new BigDecimal(100),
ChargeTimeType.TRANCHE_DISBURSEMENT, ChargeCalculationType.FLAT, LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1,
new BigDecimal(100), ExternalId.generate());
final LoanCharge loanCharge = new LoanCharge();
loanCharge.setLoan(mock(Loan.class));
loanCharge.setCharge(mock(Charge.class));
loanCharge.setAmount(new BigDecimal(100));
loanCharge.setAmountOutstanding(new BigDecimal(100));
loanCharge.setChargeTime(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue());
loanCharge.setChargeCalculation(ChargeCalculationType.FLAT.getValue());
loanCharge.setDueDate(LocalDate.of(2022, 6, 27));
loanCharge.setChargePaymentMode(ChargePaymentMode.REGULAR.getValue());
loanCharge.setPercentage(null);
loanCharge.setAmountPercentageAppliedTo(null);
loanCharge.setAmountPaid(null);
loanCharge.setAmountWaived(null);
loanCharge.setAmountWrittenOff(null);
loanCharge.setExternalId(ExternalId.generate());
return loanCharge;
}
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.mock;
import java.math.BigDecimal;
import java.math.MathContext;
@@ -34,6 +35,7 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -44,6 +46,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
@@ -99,7 +103,8 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
@BeforeEach
public void setUp() {
underTest = new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(null);
underTest = new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(mock(ExternalIdFactory.class),
mock(LoanChargeValidator.class), mock(LoanBalanceService.class));
Mockito.reset(charges, transactionMappings);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.mock;
import java.math.BigDecimal;
import java.math.MathContext;
@@ -34,6 +35,7 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -44,6 +46,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
@@ -100,7 +104,8 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
@BeforeEach
public void setUp() {
underTest = new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(null);
underTest = new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(mock(ExternalIdFactory.class),
mock(LoanChargeValidator.class), mock(LoanBalanceService.class));
Mockito.reset(charges, transactionMappings);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
@@ -111,13 +111,20 @@ public class LoanDownPaymentHandlerServiceImplTest {
@Mock
private LoanLifecycleStateMachine loanLifecycleStateMachine;
@Mock
private LoanBalanceService loanBalanceService;
@Mock
private LoanTransactionService loanTransactionService;
private LoanDownPaymentHandlerServiceImpl underTest;
@BeforeEach
public void setUp() {
underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator,
reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine);
reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService,
loanTransactionService);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true);