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.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor; 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; import org.springframework.stereotype.Component;
@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 static final String STRATEGY_NAME = "ACME Corp.: standard loan transaction processing strategy";
public AcmeLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public AcmeLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @Override
@@ -40,9 +40,10 @@ import static org.mockito.Mockito.when;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.fineract.avro.generator.ByteBufferSerializable; import org.apache.fineract.avro.generator.ByteBufferSerializable;
@@ -135,7 +136,7 @@ public class InvestorBusinessEventSerializerTest {
.thenReturn(createTransferData(firstTransferStatus, null)); .thenReturn(createTransferData(firstTransferStatus, null));
Loan loan = Mockito.mock(Loan.class); Loan loan = Mockito.mock(Loan.class);
when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1)); when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1));
List<LoanCharge> loanCharges = createMockCharges(); final Set<LoanCharge> loanCharges = createMockCharges();
when(loan.getLoanCharges()).thenReturn(loanCharges); when(loan.getLoanCharges()).thenReturn(loanCharges);
LoanOwnershipTransferBusinessEvent loanOwnershipTransferBusinessEvent = new LoanOwnershipTransferBusinessEvent( LoanOwnershipTransferBusinessEvent loanOwnershipTransferBusinessEvent = new LoanOwnershipTransferBusinessEvent(
createExternalAssetOwnerTransfer(status, subStatus), loan); 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)); assertEquals(CUSTOM_DATA_PREFIX + "_2", new String(customData.get("test_key_2").array(), UTF_8));
} }
private List<LoanCharge> createMockCharges() { private Set<LoanCharge> createMockCharges() {
List<LoanCharge> loanCharges = new ArrayList<>(); final Set<LoanCharge> loanCharges = new HashSet<>();
loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("10.00000"))); loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("10.00000")));
loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("15.00000"))); loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("15.00000")));
loanCharges.add(loanCharge(2L, "charge b", BigDecimal.ZERO)); loanCharges.add(loanCharge(2L, "charge b", BigDecimal.ZERO));
@@ -18,6 +18,9 @@
*/ */
package org.apache.fineract.portfolio.delinquency.service; 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.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
@@ -27,6 +30,7 @@ import java.util.stream.Collector;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; 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.MathUtil;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; 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.DelinquencyPausePeriod;
import org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency; import org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -142,7 +147,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
// loans // loans
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList); collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount())); collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()));
collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate(nextPaymentDueDateConfig)); collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
final LoanTransaction lastPayment = loan.getLastPaymentTransaction(); final LoanTransaction lastPayment = loan.getLastPaymentTransaction();
if (lastPayment != null) { if (lastPayment != null) {
@@ -228,4 +233,63 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
return List.of(); 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.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
// TODO: introduce tests for the state machine // 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 static final List<LoanStatus> ALLOWED_LOAN_STATUSES = List.of(LoanStatus.values());
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanBalanceService loanBalanceService;
@Override @Override
public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) { public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) {
@@ -45,7 +47,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
@Override @Override
public void transition(final LoanEvent loanEvent, final Loan loan) { public void transition(final LoanEvent loanEvent, final Loan loan) {
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
internalTransition(loanEvent, loan); internalTransition(loanEvent, loan);
} }
@@ -55,7 +57,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
return; return;
} }
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
final LoanStatusTransition transition = determineTransition(loan, loan.getStatus(), transactionDate); 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.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
import org.apache.fineract.portfolio.charge.domain.Charge; 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.client.domain.Client;
import org.apache.fineract.portfolio.collateral.domain.LoanCollateral; import org.apache.fineract.portfolio.collateral.domain.LoanCollateral;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; 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.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; 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.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
@@ -275,6 +273,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "interest_calculated_from_date") @Column(name = "interest_calculated_from_date")
private LocalDate interestChargedFromDate; private LocalDate interestChargedFromDate;
@Setter
@Column(name = "total_overpaid_derived", scale = 6, precision = 19) @Column(name = "total_overpaid_derived", scale = 6, precision = 19)
private BigDecimal totalOverpaid; private BigDecimal totalOverpaid;
@@ -347,6 +346,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanTermVariations> loanTermVariations = new ArrayList<>(); private List<LoanTermVariations> loanTermVariations = new ArrayList<>();
@Setter
@Column(name = "total_recovered_derived", scale = 6, precision = 19) @Column(name = "total_recovered_derived", scale = 6, precision = 19)
private BigDecimal totalRecovered; 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() { private boolean isInterestRecalculationEnabledForProduct() {
return this.loanProduct.isInterestRecalculationEnabled(); return this.loanProduct.isInterestRecalculationEnabled();
} }
@@ -746,33 +724,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.loanProduct.isMultiDisburseLoan(); 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() { public List<Long> fetchLoanTrancheChargeIds() {
return getCharges().stream()// return getCharges().stream()//
.filter(charge -> charge.isTrancheDisbursementCharge() && charge.isActive()) // .filter(charge -> charge.isTrancheDisbursementCharge() && charge.isActive()) //
@@ -793,7 +744,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private LocalDate determineExpectedMaturityDate() { public LocalDate determineExpectedMaturityDate() {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments().stream() List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments().stream()
.filter(i -> !i.isDownPayment() && !i.isAdditional()).toList(); .filter(i -> !i.isDownPayment() && !i.isAdditional()).toList();
final int numberOfInstallments = installments.size(); final int numberOfInstallments = installments.size();
@@ -918,16 +869,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.postDatedChecks = new ArrayList<>(); 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() { public List<LoanTransaction> retrieveListOfTransactionsExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>(); final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) { for (final LoanTransaction transaction : this.loanTransactions) {
@@ -945,95 +886,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); .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) { public boolean isAfterLastRepayment(final LoanTransaction loanTransaction, final List<LoanTransaction> loanTransactions) {
return loanTransactions.stream() // return loanTransactions.stream() //
.filter(t -> t.isRepaymentLikeType() && t.isNotReversed()) // .filter(t -> t.isRepaymentLikeType() && t.isNotReversed()) //
.noneMatch(t -> DateUtils.isBefore(loanTransaction.getTransactionDate(), t.getTransactionDate())); .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() { public LoanTransaction findWriteOffTransaction() {
return this.loanTransactions.stream() // return this.loanTransactions.stream() //
.filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) // .filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) //
@@ -1041,49 +899,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.orElse(null); .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() { public Money calculateTotalRecoveredPayments() {
// in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments // in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments
return getTotalRecoveredPayments(); return getTotalRecoveredPayments();
@@ -1392,30 +1207,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return isChargedOff() && getChargeOffReason() != null ? getChargeOffReason().getId() : null; 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() { public boolean isSyncDisbursementWithMeeting() {
return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting; return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting;
} }
@@ -1480,13 +1271,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return currentTransactionDate; return currentTransactionDate;
} }
public LoanTransaction getLastTransactionForReprocessing() {
return loanTransactions.stream() //
.filter(Loan.loanTransactionForReprocessingPredicate()) //
.reduce((first, second) -> second) //
.orElse(null);
}
public LoanTransaction getLastPaymentTransaction() { public LoanTransaction getLastPaymentTransaction() {
return loanTransactions.stream() // return loanTransactions.stream() //
.filter(loanTransaction -> !loanTransaction.isReversed()) // .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()); 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() { public List<LoanDisbursementDetails> getAllDisbursementDetails() {
return this.disbursementDetails; 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() { private int adjustNumberOfRepayments() {
int repaymetsForAdjust = 0; int repaymetsForAdjust = 0;
for (LoanTermVariations loanTermVariations : this.loanTermVariations) { for (LoanTermVariations loanTermVariations : this.loanTermVariations) {
@@ -1867,145 +1647,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.writeOffReason = writeOffReason; 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) { public void updateLoanScheduleOnForeclosure(final Collection<LoanRepaymentScheduleInstallment> installments) {
this.repaymentScheduleInstallments.clear(); this.repaymentScheduleInstallments.clear();
for (final LoanRepaymentScheduleInstallment installment : installments) { for (final LoanRepaymentScheduleInstallment installment : installments) {
@@ -2046,7 +1687,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.loanTopupDetails; return this.loanTopupDetails;
} }
public Collection<LoanCharge> getLoanCharges() { public Set<LoanCharge> getLoanCharges() {
return this.charges; return this.charges;
} }
@@ -33,18 +33,13 @@ import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; 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.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId; 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.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; 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.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
@Setter
@Getter @Getter
@Entity @Entity
@Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") }) @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) @Column(name = "amount_paid_derived", scale = 6, precision = 19)
private BigDecimal amountPaid; private BigDecimal amountPaid;
@Setter
@Column(name = "amount_waived_derived", scale = 6, precision = 19) @Column(name = "amount_waived_derived", scale = 6, precision = 19)
private BigDecimal amountWaived; private BigDecimal amountWaived;
@@ -149,111 +143,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanChargePaidBy> loanChargePaidBySet = new HashSet<>(); 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() { public void markAsFullyPaid() {
this.amountPaid = this.amount; this.amountPaid = this.amount;
this.amountOutstanding = BigDecimal.ZERO; this.amountOutstanding = BigDecimal.ZERO;
@@ -340,234 +229,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.loan = loan; 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() { public boolean isDueAtDisbursement() {
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT) return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT)
|| ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.TRANCHE_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; return BigDecimal.ZERO.compareTo(calculateOutstanding()) == 0;
} }
private BigDecimal calculateOutstanding() { public BigDecimal calculateOutstanding() {
if (this.amount == null) { if (this.amount == null) {
return 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 * @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 * amount is within min and max cap
*/ */
private BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) { public BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) {
BigDecimal minMaxCap; BigDecimal minMaxCap;
if (this.minCap != null) { if (this.minCap != null) {
final int minimumCap = percentageOf.compareTo(this.minCap); final int minimumCap = percentageOf.compareTo(this.minCap);
@@ -817,11 +478,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return ChargeCalculationType.fromInt(this.chargeCalculation); return ChargeCalculationType.fromInt(this.chargeCalculation);
} }
public void updateAmount(final BigDecimal amount) {
this.amount = amount;
calculateOutstanding();
}
public LoanInstallmentCharge getUnpaidInstallmentLoanCharge() { public LoanInstallmentCharge getUnpaidInstallmentLoanCharge() {
LoanInstallmentCharge unpaidChargePerInstallment = null; LoanInstallmentCharge unpaidChargePerInstallment = null;
for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) {
@@ -925,4 +925,5 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public void updateTransactionDate(final LocalDate transactionDate) { public void updateTransactionDate(final LocalDate transactionDate) {
this.dateOf = transactionDate; this.dateOf = transactionDate;
} }
} }
@@ -70,4 +70,32 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
@Query(FIND_LOAN_ID_BY_ID) @Query(FIND_LOAN_ID_BY_ID)
Optional<Long> findLoanIdById(@Param("id") Long 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.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
/** /**
@@ -76,8 +77,9 @@ import org.springframework.util.CollectionUtils;
public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor { public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
protected final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); protected final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
protected final LoanChargeValidator loanChargeValidator = new LoanChargeValidator();
protected final ExternalIdFactory externalIdFactory; protected final ExternalIdFactory externalIdFactory;
protected final LoanChargeValidator loanChargeValidator;
protected final LoanBalanceService loanBalanceService;
@Override @Override
public boolean accept(String s) { public boolean accept(String s) {
@@ -256,7 +258,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} else { } else {
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
final Loan loan = loanTransaction.getLoan(); final Loan loan = loanTransaction.getLoan();
if ((loan.isClosedObligationsMet() || loan.isOverPaid()) && currentInstallment.isObligationsMet() if ((loan.isClosedObligationsMet() || loanBalanceService.isOverPaid(loan)) && currentInstallment.isObligationsMet()
&& currentInstallment.isTransactionDateWithinPeriod(currentInstallment.getObligationsMetOnDate())) { && currentInstallment.isTransactionDateWithinPeriod(currentInstallment.getObligationsMetOnDate())) {
loanTransaction.updateTransactionDate(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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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}. * Creocore style {@link LoanRepaymentScheduleTransactionProcessor}.
@@ -47,8 +49,9 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL
public static final String STRATEGY_NAME = "Creocore Unique"; public static final String STRATEGY_NAME = "Creocore Unique";
public CreocoreLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public CreocoreLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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 * `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 static final String STRATEGY_NAME = "Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest";
public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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 * `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 static final String STRATEGY_NAME = "Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee";
public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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 * 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 static final String STRATEGY_NAME = "Early Repayment Strategy";
public EarlyPaymentLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public EarlyPaymentLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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}. * 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 static final String STRATEGY_NAME = "Penalties, Fees, Interest, Principal order";
public FineractStyleLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public FineractStyleLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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}. * Heavensfamily style {@link LoanRepaymentScheduleTransactionProcessor}.
@@ -49,8 +51,9 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
public static final String STRATEGY_NAME = "HeavensFamily Unique"; public static final String STRATEGY_NAME = "HeavensFamily Unique";
public HeavensFamilyLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public HeavensFamilyLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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 * 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 static final String STRATEGY_NAME = "Interest, Principal, Penalties, Fees Order";
public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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 * 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 static final String STRATEGY_NAME = "Principal, Interest, Penalties, Fees Order";
public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; 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}. * 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 static final String STRATEGY_NAME = "Overdue/Due Fee/Int,Principal";
public RBILoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public RBILoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Override @Override
@@ -35,6 +35,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData; 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.MultiDisbursementEmiAmountException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.ScheduleDateException; 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; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
@RequiredArgsConstructor
public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanScheduleGenerator { public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanScheduleGenerator {
private final LoanTransactionService loanTransactionService;
@Override @Override
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) { final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
@@ -2796,7 +2801,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO, LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor, onDate, calculateTill); loanRepaymentScheduleTransactionProcessor, onDate, calculateTill);
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsForReprocessing(); final List<LoanTransaction> loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(), loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(),
loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges()); loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges());
@@ -25,10 +25,10 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; 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.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -56,12 +56,19 @@ import org.springframework.stereotype.Component;
* </p> * </p>
*/ */
@Component @Component
@RequiredArgsConstructor
public class CumulativeDecliningBalanceInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator { public class CumulativeDecliningBalanceInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator; private final ScheduledDateGenerator scheduledDateGenerator;
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator; private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
public CumulativeDecliningBalanceInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator,
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator,
final LoanTransactionService loanTransactionService) {
super(loanTransactionService);
this.scheduledDateGenerator = scheduledDateGenerator;
this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator;
}
@Override @Override
public ScheduledDateGenerator getScheduledDateGenerator() { public ScheduledDateGenerator getScheduledDateGenerator() {
return scheduledDateGenerator; return scheduledDateGenerator;
@@ -24,18 +24,25 @@ import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@RequiredArgsConstructor
public class CumulativeFlatInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator { public class CumulativeFlatInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator; private final ScheduledDateGenerator scheduledDateGenerator;
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator; private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
public CumulativeFlatInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator,
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator,
final LoanTransactionService loanTransactionService) {
super(loanTransactionService);
this.scheduledDateGenerator = scheduledDateGenerator;
this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator;
}
@Override @Override
public ScheduledDateGenerator getScheduledDateGenerator() { public ScheduledDateGenerator getScheduledDateGenerator() {
return scheduledDateGenerator; return scheduledDateGenerator;
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.serialization;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.organisation.holiday.domain.Holiday; 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.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@RequiredArgsConstructor
public final class LoanDownPaymentTransactionValidator { public final class LoanDownPaymentTransactionValidator {
private final LoanBalanceService loanBalanceService;
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays, public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) { final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) { if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
@@ -53,7 +58,7 @@ public final class LoanDownPaymentTransactionValidator {
} }
public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) { 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 List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid"; 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", 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.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; 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.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.Money; 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.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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; 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.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -52,6 +62,7 @@ public class LoanChargeService {
private final LoanChargeValidator loanChargeValidator; private final LoanChargeValidator loanChargeValidator;
private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final LoanBalanceService loanBalanceService;
public void recalculateAllCharges(final Loan loan) { public void recalculateAllCharges(final Loan loan) {
Set<LoanCharge> charges = loan.getActiveCharges(); Set<LoanCharge> charges = loan.getActiveCharges();
@@ -68,54 +79,25 @@ public class LoanChargeService {
BigDecimal totalChargeAmt = BigDecimal.ZERO; BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) { if (loanCharge.getChargeCalculation().isPercentageBased()) {
if (loanCharge.isOverdueInstallmentCharge()) { if (loanCharge.isOverdueInstallmentCharge()) {
amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod); amount = calculateOverdueAmountPercentageAppliedTo(loan, loanCharge, penaltyWaitPeriod);
} else { } else {
amount = calculateAmountPercentageAppliedTo(loan, loanCharge); amount = calculateAmountPercentageAppliedTo(loan, loanCharge);
} }
chargeAmt = loanCharge.getPercentage(); chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee()) {
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge);
} }
} else { } else {
chargeAmt = loanCharge.amountOrPercentage(); chargeAmt = loanCharge.amountOrPercentage();
} }
if (loanCharge.isActive()) { if (loanCharge.isActive()) {
clearLoanInstallmentChargesBeforeRegeneration(loanCharge); clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
totalChargeAmt); totalChargeAmt);
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); 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, public void makeChargePayment(final Loan loan, final Long chargeId, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) { final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction); loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
@@ -130,36 +112,6 @@ public class LoanChargeService {
handleChargePaidTransaction(loan, charge, paymentTransaction, installmentNumber); 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 * 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. * 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()) { if (loanCharge.getChargeCalculation().isPercentageBased()) {
chargeAmt = loanCharge.getPercentage(); chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) { if (loanCharge.isInstalmentFee()) {
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge);
} else if (loanCharge.isOverdueInstallmentCharge()) { } else if (loanCharge.isOverdueInstallmentCharge()) {
totalChargeAmt = loanCharge.amountOutstanding(); totalChargeAmt = loanCharge.amountOutstanding();
} }
} else { } else {
chargeAmt = loanCharge.amountOrPercentage(); 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 // NOTE: must add new loan charge to set of loan charges before
// reprocessing the repayment schedule. // reprocessing the repayment schedule.
@@ -250,72 +202,11 @@ public class LoanChargeService {
// store Id's of existing loan transactions and existing reversed loan transactions // store Id's of existing loan transactions and existing reversed loan transactions
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loanCharge); wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loanCharge);
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, 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) { public BigDecimal calculateAmountPercentageAppliedTo(final Loan loan, final LoanCharge loanCharge) {
if (loanCharge.isOverdueInstallmentCharge()) { if (loanCharge.isOverdueInstallmentCharge()) {
return loanCharge.getAmountPercentageAppliedTo(); return loanCharge.getAmountPercentageAppliedTo();
@@ -343,38 +234,254 @@ public class LoanChargeService {
}; };
} }
private BigDecimal getDerivedAmountForCharge(final Loan loan, final LoanCharge loanCharge) { public void updateLoanCharges(final Loan loan, final Set<LoanCharge> loanCharges) {
BigDecimal amount = BigDecimal.ZERO; List<Long> existingCharges = fetchAllLoanChargeIds(loan);
if (loan.isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) {
amount = loan.getApprovedPrincipal(); /* Process new and updated charges **/
} else { for (final LoanCharge loanCharge : loanCharges) {
// If charge type is specified due date and loan is multi disburment loan. LoanCharge charge = loanCharge;
// Then we need to get as of this loan charge due date how much amount disbursed. // add new charges
if (loanCharge.isSpecifiedDueDate() && loan.isMultiDisburmentLoan()) { if (loanCharge.getId() == null) {
for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) { LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) { loanCharge.update(loan);
amount = amount.add(loanDisbursementDetails.principal()); 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 { } 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()); Money amount = Money.zero(loan.getCurrency());
if (loan.isMultiDisburmentLoan()) { List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanDisbursementDetails loanDisbursementDetail : loan.getDisbursementDetails()) { for (final LoanRepaymentScheduleInstallment installment : installments) {
amount = amount.plus(loanDisbursementDetail.principal()); amount = amount.plus(calculateInstallmentChargeAmount(loan, calculationType, percentage, installment));
}
} }
return amount; return amount.getAmount();
} }
private BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) { public Map<String, Object> update(final JsonCommand command, final BigDecimal amount, final LoanCharge loanCharge) {
return loanCharge.calculatePerInstallmentChargeAmount(); 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) { 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 ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionService loanTransactionService;
@Override @Override
public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command, public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command,
@@ -91,7 +93,8 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
loanTransaction.updateLoan(loan); loanTransaction.updateLoan(loan);
final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(loanTransaction); final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan,
loanTransaction);
if (loanTransaction.isNotZero()) { if (loanTransaction.isNotZero()) {
loan.addLoanTransaction(loanTransaction); loan.addLoanTransaction(loanTransaction);
@@ -128,6 +131,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
.fetchLoanRepaymentScheduleInstallmentByDueDate(loanTransaction.getTransactionDate()); .fetchLoanRepaymentScheduleInstallmentByDueDate(loanTransaction.getTransactionDate());
boolean reprocessOnPostConditions = false; boolean reprocessOnPostConditions = false;
boolean processLatest = isTransactionChronologicallyLatest // boolean processLatest = isTransactionChronologicallyLatest //
&& adjustedTransaction == null // covers reversals && adjustedTransaction == null // covers reversals
&& !loan.isForeclosure() // && !loan.isForeclosure() //
@@ -163,7 +167,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
if (loanTransaction.isNotRecoveryRepayment()) { if (loanTransaction.isNotRecoveryRepayment()) {
loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate()); loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
} else { } else {
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
if (loan.getLoanProduct().isMultiDisburseLoan()) { 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.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanScheduleComponent { public class LoanScheduleComponent {
private final LoanBalanceService loanBalanceService;
public void updateLoanSchedule(Loan loan, final LoanScheduleModel modifiedLoanSchedule) { public void updateLoanSchedule(Loan loan, final LoanScheduleModel modifiedLoanSchedule) {
final List<LoanScheduleModelPeriod> periods = modifiedLoanSchedule.getPeriods(); final List<LoanScheduleModelPeriod> periods = modifiedLoanSchedule.getPeriods();
for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) {
@@ -55,7 +58,7 @@ public class LoanScheduleComponent {
loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(periods, i.getInstallmentNumber())); loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(periods, i.getInstallmentNumber()));
loan.updateLoanScheduleDependentDerivedFields(); loan.updateLoanScheduleDependentDerivedFields();
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
public void updateLoanSchedule(Loan loan, final List<LoanRepaymentScheduleInstallment> installments) { 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.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(installments, i.getInstallmentNumber()));
loan.updateLoanScheduleDependentDerivedFields(); loan.updateLoanScheduleDependentDerivedFields();
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
private LoanRepaymentScheduleInstallment findByInstallmentNumber(final Collection<LoanRepaymentScheduleInstallment> installments, 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.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; 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.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@@ -64,6 +68,9 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
private ArgumentCaptor<Money> penaltyChargesWaived = ArgumentCaptor.forClass(Money.class); private ArgumentCaptor<Money> penaltyChargesWaived = ArgumentCaptor.forClass(Money.class);
private ArgumentCaptor<Money> penaltyChargesWrittenOff = 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 @BeforeAll
public static void init() { public static void init() {
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
@@ -141,7 +148,8 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
when(charge.isPenalty()).thenReturn(penalty); when(charge.isPenalty()).thenReturn(penalty);
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
when(loan.isInterestBearing()).thenReturn(false); 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); 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.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; 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.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.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -118,13 +121,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanRepositoryWrapper loanRepositoryWrapper;
private final InterestRefundService interestRefundService; private final InterestRefundService interestRefundService;
private final LoanScheduleComponent loanSchedule; private final LoanScheduleComponent loanSchedule;
private final LoanTransactionService loanTransactionService;
public AdvancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, LoanRepositoryWrapper loanRepositoryWrapper, public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper,
InterestRefundService interestRefundService, ExternalIdFactory externalIdFactory, LoanScheduleComponent loanSchedule) { final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory,
super(externalIdFactory); final LoanScheduleComponent loanSchedule, final LoanTransactionService loanTransactionRepository,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
this.emiCalculator = emiCalculator; this.emiCalculator = emiCalculator;
this.loanRepositoryWrapper = loanRepositoryWrapper; this.loanRepositoryWrapper = loanRepositoryWrapper;
this.interestRefundService = interestRefundService; this.interestRefundService = interestRefundService;
this.loanTransactionService = loanTransactionRepository;
this.loanSchedule = loanSchedule; this.loanSchedule = loanSchedule;
} }
@@ -269,7 +276,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) { public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) {
Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
List<LoanTransaction> transactions = loan.retrieveListOfTransactionsForReprocessing(); final List<LoanTransaction> transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency(); MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency();
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
Set<LoanCharge> charges = loan.getActiveCharges(); Set<LoanCharge> charges = loan.getActiveCharges();
@@ -1514,7 +1521,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
newTransaction.updateLoan(loan); newTransaction.updateLoan(loan);
loan.getLoanTransactions().add(newTransaction); loan.getLoanTransactions().add(newTransaction);
} }
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
} }
} }
@@ -62,6 +62,7 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService; private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService;
private final InterestScheduleModelRepositoryWrapper writePlatformService; private final InterestScheduleModelRepositoryWrapper writePlatformService;
private final LoanTransactionService loanTransactionService;
@Override @Override
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
@@ -94,7 +95,7 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
} }
private ProgressiveLoanInterestScheduleModel reprocessTransactionsAndGetModel(final Loan loan) { 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 LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate();
final Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor final Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactionsTransactional(loan.getDisbursementDate(), businessDate, transactionsToReprocess, .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.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; 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.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; 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.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; 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.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; 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 static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest; private AdvancedPaymentScheduleTransactionProcessor underTest;
private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class); 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 @BeforeAll
public static void init() { public static void init() {
@@ -114,7 +117,10 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
@BeforeEach @BeforeEach
public void setUp() { 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.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); 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.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; 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.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService; import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService; import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService; 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.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
@@ -147,7 +149,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final InterestRefundServiceDelegate interestRefundServiceDelegate; private final InterestRefundServiceDelegate interestRefundServiceDelegate;
private final LoanTransactionValidator loanTransactionValidator; private final LoanTransactionValidator loanTransactionValidator;
private final LoanForeclosureValidator loanForeclosureValidator; private final LoanForeclosureValidator loanForeclosureValidator;
@@ -161,6 +162,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionService loanTransactionService;
@Transactional @Transactional
@Override @Override
@@ -767,7 +770,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
final ScheduleGeneratorDTO scheduleGeneratorDTO = null; final ScheduleGeneratorDTO scheduleGeneratorDTO = null;
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); final LoanRepaymentScheduleInstallment foreCloseDetail = loanBalanceService.fetchLoanForeclosureDetail(loan, foreClosureDate);
loanAccrualsProcessingService.processAccrualsOnLoanForeClosure(loan, foreClosureDate, newTransactions); loanAccrualsProcessingService.processAccrualsOnLoanForeClosure(loan, foreClosureDate, newTransactions);
@@ -875,7 +878,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
LoanTransaction refundTransaction = LoanTransaction.refund(loan, loanTransactionType, transactionAmount, paymentDetail, LoanTransaction refundTransaction = LoanTransaction.refund(loan, loanTransactionType, transactionAmount, paymentDetail,
transactionDate, txnExternalId); transactionDate, txnExternalId);
final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(refundTransaction); final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan,
refundTransaction);
boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream() boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream()
.map(LoanSupportedInterestRefundTypes::getTransactionType) .map(LoanSupportedInterestRefundTypes::getTransactionType)
@@ -1002,7 +1006,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(loan.getRepaymentScheduleInstallments()); final List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(loan.getRepaymentScheduleInstallments());
final MonetaryCurrency currency = loan.getCurrency(); final MonetaryCurrency currency = loan.getCurrency();
Money totalPrincipal = Money.zero(currency); Money totalPrincipal = Money.zero(currency);
final Money[] balances = loan.retrieveIncomeForOverlappingPeriod(transactionDate); final Money[] balances = loanBalanceService.retrieveIncomeForOverlappingPeriod(loan, transactionDate);
boolean isInterestComponent = true; boolean isInterestComponent = true;
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) { if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) {
@@ -1571,7 +1571,8 @@ public class LoanScheduleAssembler {
for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) { for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) {
if (loanScheduleModelPeriod instanceof LoanScheduleModelDisbursementPeriod) { if (loanScheduleModelPeriod instanceof LoanScheduleModelDisbursementPeriod) {
for (final LoanCharge loanCharge : nonCompoundingCharges) { 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); loanScheduleModelPeriod.addLoanCharges(loanCharge.getAmountOutstanding(), BigDecimal.ZERO);
} }
} }
@@ -55,6 +55,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionAssembler loanTransactionAssembler; private final LoanTransactionAssembler loanTransactionAssembler;
private final LoanAccountService loanAccountService; private final LoanAccountService loanAccountService;
private final LoanBalanceService loanBalanceService;
@Override @Override
@Transactional(propagation = Propagation.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
@@ -93,7 +94,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
return; 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 // Reverse accrual activities posted after the closure date
loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(closureDate)) 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 TransactionTemplate transactionTemplate;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanChargeService loanChargeService; private final LoanChargeService loanChargeService;
private final LoanBalanceService loanBalanceService;
/** /**
* method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan * 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 // TODO implement progressive accrual case
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
&& (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { && (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(); MonetaryCurrency currency = loan.getCurrency();
reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate, false); reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate, false);
@@ -494,7 +495,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
Money interest = null; Money interest = null;
final boolean isPastPeriod = isAfterPeriod(tillDate, installment); final boolean isPastPeriod = isAfterPeriod(tillDate, installment);
final boolean isInPeriod = isInPeriod(tillDate, installment, false); 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()); interest = installment.getInterestCharged(currency).minus(installment.getCreditedInterest());
} else { } else {
if (isInPeriod) { // first period first day is not accrued if (isInPeriod) { // first period first day is not accrued
@@ -61,6 +61,7 @@ public class LoanChargeAssembler {
private final LoanChargeRepository loanChargeRepository; private final LoanChargeRepository loanChargeRepository;
private final LoanProductRepository loanProductRepository; private final LoanProductRepository loanProductRepository;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final LoanChargeService loanChargeService;
public Set<LoanCharge> fromParsedJson(final JsonElement element, List<LoanDisbursementDetails> disbursementDetails) { public Set<LoanCharge> fromParsedJson(final JsonElement element, List<LoanDisbursementDetails> disbursementDetails) {
JsonArray jsonDisbursement = this.fromApiJsonHelper.extractJsonArrayNamed("disbursementData", element); JsonArray jsonDisbursement = this.fromApiJsonHelper.extractJsonArrayNamed("disbursementData", element);
@@ -210,7 +211,7 @@ public class LoanChargeAssembler {
.orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId)); .orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId));
if (!loanCharge.isTrancheDisbursementCharge() || disbursementChargeIds.contains(loanChargeId)) { if (!loanCharge.isTrancheDisbursementCharge() || disbursementChargeIds.contains(loanChargeId)) {
loanCharge.update(amount, dueDate, numberOfRepayments); loanChargeService.update(loanCharge, amount, dueDate, numberOfRepayments);
loanCharges.add(loanCharge); loanCharges.add(loanCharge);
} }
@@ -296,7 +297,7 @@ public class LoanChargeAssembler {
if (percentage == null) { if (percentage == null) {
percentage = chargeDefinition.getAmount(); percentage = chargeDefinition.getAmount();
} }
loanCharge = LoanCharge.calculatePerInstallmentChargeAmount(loan, loanCharge = loanChargeService.calculatePerInstallmentChargeAmount(loan,
ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()), percentage); ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()), percentage);
} }
@@ -314,7 +315,7 @@ public class LoanChargeAssembler {
} }
ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId"); 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); chargePaymentMode, null, loanCharge, externalId);
} }
@@ -324,7 +325,7 @@ public class LoanChargeAssembler {
public LoanCharge createNewWithoutLoan(final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount, public LoanCharge createNewWithoutLoan(final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount,
final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate, final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate,
final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final ExternalId externalId) { final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final ExternalId externalId) {
return new LoanCharge(null, chargeDefinition, loanPrincipal, amount, chargeTime, chargeCalculation, dueDate, chargePaymentMode, return loanChargeService.create(null, chargeDefinition, loanPrincipal, amount, chargeTime, chargeCalculation, dueDate,
numberOfRepayments, BigDecimal.ZERO, externalId); chargePaymentMode, numberOfRepayments, BigDecimal.ZERO, externalId);
} }
} }
@@ -288,9 +288,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
.equals(loan.transactionProcessingStrategy())) { .equals(loan.transactionProcessingStrategy())) {
// [For Adv payment allocation strategy] check if charge due date is earlier than last transaction // [For Adv payment allocation strategy] check if charge due date is earlier than last transaction
// date, if yes trigger reprocess else no reprocessing // date, if yes trigger reprocess else no reprocessing
LoanTransaction lastPaymentTransaction = loan.getLastTransactionForReprocessing(); final Optional<LocalDate> lastPaymentTransactionDate = loanTransactionRepository.findLastTransactionDateForReprocessing(loan);
if (lastPaymentTransaction != null if (lastPaymentTransactionDate.isPresent()
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) { && DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransactionDate.get())) {
reprocessRequired = false; reprocessRequired = false;
} }
} }
@@ -475,7 +475,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final Map<String, Object> changes = new LinkedHashMap<>(3); final Map<String, Object> changes = new LinkedHashMap<>(3);
if (loan.getActiveCharges().contains(loanCharge)) { if (loan.getActiveCharges().contains(loanCharge)) {
final BigDecimal amount = loanChargeService.calculateAmountPercentageAppliedTo(loan, 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); changes.putAll(loanChargeChanges);
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
} }
@@ -244,8 +244,8 @@ public class LoanDisbursementService {
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate(); externalId = ExternalId.generate();
} }
final LoanCharge loanCharge = new LoanCharge(loan, chargeDefinition, principal, null, null, null, expectedDisbursementDate, final LoanCharge loanCharge = loanChargeService.create(loan, chargeDefinition, principal, null, null, null,
null, null, BigDecimal.ZERO, externalId); expectedDisbursementDate, null, null, BigDecimal.ZERO, externalId);
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge,
disbursementDetails); disbursementDetails);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
@@ -179,6 +179,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
private final LoanForeclosureValidator loanForeclosureValidator; private final LoanForeclosureValidator loanForeclosureValidator;
private final LoanTransactionMapper loanTransactionMapper; private final LoanTransactionMapper loanTransactionMapper;
private final LoanTransactionProcessingService loadTransactionProcessingService; private final LoanTransactionProcessingService loadTransactionProcessingService;
private final LoanBalanceService loanBalanceService;
@Override @Override
public LoanAccountData retrieveOne(final Long loanId) { public LoanAccountData retrieveOne(final Long loanId) {
@@ -2088,7 +2089,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate(); final LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate();
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate); final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanBalanceService.fetchLoanForeclosureDetail(loan,
transactionDate);
BigDecimal unrecognizedIncomePortion = null; BigDecimal unrecognizedIncomePortion = null;
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT); final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
@@ -2236,7 +2238,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return loanRepositoryWrapper.existsByLoanId(loanId); return loanRepositoryWrapper.existsByLoanId(loanId);
} }
private LoanTransaction deriveDefaultInterestWaiverTransaction(Loan loan) { private LoanTransaction deriveDefaultInterestWaiverTransaction(final Loan loan) {
final Money totalInterestOutstanding = loan.getTotalInterestOutstandingOnLoan(); final Money totalInterestOutstanding = loan.getTotalInterestOutstandingOnLoan();
Money possibleInterestToWaive = totalInterestOutstanding.copy(); Money possibleInterestToWaive = totalInterestOutstanding.copy();
LocalDate transactionDate = DateUtils.getBusinessLocalDate(); LocalDate transactionDate = DateUtils.getBusinessLocalDate();
@@ -2260,4 +2262,5 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return LoanTransaction.waiver(loan.getOffice(), loan, possibleInterestToWaive, transactionDate, possibleInterestToWaive, return LoanTransaction.waiver(loan.getOffice(), loan, possibleInterestToWaive, transactionDate, possibleInterestToWaive,
possibleInterestToWaive.zero(), ExternalId.empty()); possibleInterestToWaive.zero(), ExternalId.empty());
} }
} }
@@ -61,6 +61,8 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final LoanTermVariationsMapper loanMapper; private final LoanTermVariationsMapper loanMapper;
private final InterestScheduleModelRepositoryWrapper modelRepository; private final InterestScheduleModelRepositoryWrapper modelRepository;
private final LoanTransactionService loanTransactionService;
private final LoanBalanceService loanBalanceService;
@Override @Override
public boolean canProcessLatestTransactionOnly(Loan loan, LoanTransaction loanTransaction, public boolean canProcessLatestTransactionOnly(Loan loan, LoanTransaction loanTransaction,
@@ -86,7 +88,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(), ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), savedModel.orElse(null), getTotalRefundInterestAmount(loan)); new ChangedTransactionDetail(), savedModel.orElse(null), getTotalRefundInterestAmount(loan));
progressiveContext.getAlreadyProcessedTransactions().addAll(loan.retrieveListOfTransactionsForReprocessing()); progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan));
progressiveContext.setChargedOff(loan.isChargedOff()); progressiveContext.setChargedOff(loan.isChargedOff());
ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext); ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext);
if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
@@ -158,7 +160,9 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
public Optional<ChangedTransactionDetail> processPostDisbursementTransactions(Loan loan) { public Optional<ChangedTransactionDetail> processPostDisbursementTransactions(Loan loan) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(
loan.getTransactionProcessingStrategyCode()); loan.getTransactionProcessingStrategyCode());
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loanTransactionService
.retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> copyTransactions = new ArrayList<>(); final List<LoanTransaction> copyTransactions = new ArrayList<>();
if (allNonContraTransactionsPostDisbursement.isEmpty()) { if (allNonContraTransactionsPostDisbursement.isEmpty()) {
@@ -173,7 +177,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
loan.getDisbursementDate(), copyTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getDisbursementDate(), copyTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges()); loan.getActiveCharges());
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
return Optional.of(changedTransactionDetail); return Optional.of(changedTransactionDetail);
} }
@@ -287,6 +287,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanMapper loanMapper; private final LoanMapper loanMapper;
private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionService loanTransactionService;
@Transactional @Transactional
@Override @Override
@@ -572,7 +574,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSED, loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSED,
actualDisbursementDate1); actualDisbursementDate1);
loanDisbursementService.handleDisbursementTransaction(loan, actualDisbursementDate1, paymentDetail1); loanDisbursementService.handleDisbursementTransaction(loan, actualDisbursementDate1, paymentDetail1);
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged()); 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()) { if (loan.getLoanProduct().isMultiDisburseLoan() || loan.isProgressiveSchedule()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loanTransactionService
.retrieveListOfTransactionsForReprocessing(loan);
if (!allNonContraTransactionsPostDisbursement.isEmpty()) { if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
reprocessLoanTransactionsService.reprocessTransactions(loan); reprocessLoanTransactionsService.reprocessTransactions(loan);
} }
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, loan);
@@ -972,8 +975,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
newInterestPaymentWaiverTransaction.updateLoan(loan); newInterestPaymentWaiverTransaction.updateLoan(loan);
final boolean isTransactionChronologicallyLatest = loan final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan,
.isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction); newInterestPaymentWaiverTransaction);
if (newInterestPaymentWaiverTransaction.isNotZero()) { if (newInterestPaymentWaiverTransaction.isNotZero()) {
loan.addLoanTransaction(newInterestPaymentWaiverTransaction); loan.addLoanTransaction(newInterestPaymentWaiverTransaction);
@@ -1412,7 +1415,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
Money unrecognizedIncome = transactionAmountAsMoney.zero(); Money unrecognizedIncome = transactionAmountAsMoney.zero();
Money interestComponent = transactionAmountAsMoney; Money interestComponent = transactionAmountAsMoney;
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableInterest = loan.getReceivableInterest(transactionDate); Money receivableInterest = loanBalanceService.getReceivableInterest(loan, transactionDate);
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) { if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
interestComponent = receivableInterest; interestComponent = receivableInterest;
unrecognizedIncome = transactionAmountAsMoney.minus(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) { private void removeLoanCycle(final Loan loan) {
final List<Loan> loansToUpdate; final List<Loan> loansToUpdate;
if (loan.isGroupLoan()) { if (loan.isGroupLoan()) {
@@ -2924,7 +2814,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null); final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} }
final List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsForReprocessing(); final List<LoanTransaction> loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
loanTransactions.add(chargeOffTransaction); loanTransactions.add(chargeOffTransaction);
reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions); reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions);
loan.addLoanTransaction(chargeOffTransaction); loan.addLoanTransaction(chargeOffTransaction);
@@ -3117,9 +3007,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.addLoanTransaction(chargebackTransaction); loan.addLoanTransaction(chargebackTransaction);
if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) { if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) {
final List<LoanTransaction> transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
loanTransactionProcessingService.reprocessLoanTransactions(loan.getTransactionProcessingStrategyCode(), loanTransactionProcessingService.reprocessLoanTransactions(loan.getTransactionProcessingStrategyCode(),
loan.getDisbursementDate(), loan.retrieveListOfTransactionsForReprocessing(), loan.getCurrency(), loan.getDisbursementDate(), transactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); loan.getActiveCharges());
} else { } else {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargebackTransaction, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargebackTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
@@ -3215,7 +3106,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.adjustNetDisbursalAmount(loan.getApprovedPrincipal()); loan.adjustNetDisbursalAmount(loan.getApprovedPrincipal());
actualChanges.put(ACTUAL_DISBURSEMENT_DATE, ""); actualChanges.put(ACTUAL_DISBURSEMENT_DATE, "");
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
return actualChanges; return actualChanges;
@@ -3246,7 +3137,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
private void reverseExistingTransactions(final Loan 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 TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
return Optional.of(loanTransaction); return Optional.of(loanTransaction);
} }
@@ -3383,7 +3274,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
changes.put("externalId", externalId); changes.put("externalId", externalId);
loanTransaction = LoanTransaction.writeoff(loan, loan.getOffice(), closureDate, 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) { if (!isLastTransaction) {
final String errorMessage = "The closing date of the loan must be on or after latest transaction date."; 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", 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 TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} else if (totalOutstanding.isGreaterThanZero()) { } else if (totalOutstanding.isGreaterThanZero()) {
final String errorMessage = "A loan with money outstanding cannot be closed"; final String errorMessage = "A loan with money outstanding cannot be closed";
throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage, throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage,
@@ -3403,8 +3295,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
} }
if (loan.isOverPaid()) { if (loanBalanceService.isOverPaid(loan)) {
final Money totalLoanOverpayment = loan.calculateTotalOverpayment(); final Money totalLoanOverpayment = loanBalanceService.calculateTotalOverpayment(loan);
if (totalLoanOverpayment.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) { if (totalLoanOverpayment.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) {
// TODO - KW - technically should set somewhere that this loan // TODO - KW - technically should set somewhere that this loan
// has 'overpaid' amount // has 'overpaid' amount
@@ -3474,7 +3366,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate, final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate,
lastDisbursalTransaction.getAmount()); lastDisbursalTransaction.getAmount());
loan.updateLoanToLastDisbursalState(disbursementDetail); loanBalanceService.updateLoanToLastDisbursalState(loan, disbursementDetail);
loan.getLoanTermVariations() loan.getLoanTermVariations()
.removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation() .removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation()
&& DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate)) && DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate))
@@ -3579,4 +3471,118 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
return !statusEnum.hasStateOf(actualLoanStatus) || isMultiTrancheDisburse; 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 AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
private final EMICalculator emiCalculator; private final EMICalculator emiCalculator;
private final LoanRepositoryWrapper loanRepository; private final LoanRepositoryWrapper loanRepository;
private final LoanTransactionService loanTransactionService;
private final InterestScheduleModelRepositoryWrapper modelRepository; private final InterestScheduleModelRepositoryWrapper modelRepository;
@Override @Override
@@ -80,8 +81,8 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
} }
private ProgressiveLoanInterestScheduleModel calculateModel(Loan loan, LocalDate businessDate) { private ProgressiveLoanInterestScheduleModel calculateModel(Loan loan, LocalDate businessDate) {
List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream() final List<LoanTransaction> transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan)
.filter(t -> !t.isAccrualActivity()).toList(); .stream().filter(t -> !t.isAccrualActivity()).toList();
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(), .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
@@ -22,6 +22,8 @@ import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; 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.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; 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.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -49,15 +53,27 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanTransactionProcessingService loanTransactionProcessingService;
private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepositoryWrapper; private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepositoryWrapper;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionRepository loanTransactionRepository;
@Override @Override
public void reprocessTransactions(final Loan loan) { public void reprocessTransactions(final Loan loan) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(loan);
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan,
allNonContraTransactionsPostDisbursement); allNonContraTransactionsPostDisbursement);
handleChangedDetail(changedTransactionDetail); 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 @Override
public void reprocessParticularTransactions(final Loan loan, final List<LoanTransaction> loanTransactions) { public void reprocessParticularTransactions(final Loan loan, final List<LoanTransaction> loanTransactions) {
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, loanTransactions); final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, loanTransactions);
@@ -66,8 +82,8 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
@Override @Override
public void reprocessTransactionsWithPostTransactionChecks(final Loan loan, final LocalDate transactionDate) { public void reprocessTransactionsWithPostTransactionChecks(final Loan loan, final LocalDate transactionDate) {
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, final List<LoanTransaction> transactions = retrieveListOfTransactionsForReprocessing(loan);
loan.retrieveListOfTransactionsForReprocessing()); final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, transactions);
handleChangedDetail(changedTransactionDetail); handleChangedDetail(changedTransactionDetail);
} }
@@ -101,7 +117,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
return; return;
} }
loan.getLoanCharges().remove(loanCharge); loan.getLoanCharges().remove(loanCharge);
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} }
private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final Loan loan, final LoanCharge loanCharge) { 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(); .map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList();
loan.getLoanTransactions().addAll(newTransactions); loan.getLoanTransactions().addAll(newTransactions);
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
handleChangedDetail(changedTransactionDetail); handleChangedDetail(changedTransactionDetail);
} }
@@ -185,7 +201,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream() final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList(); .map(TransactionChangeData::getNewTransaction).toList();
loan.getLoanTransactions().addAll(newTransactions); loan.getLoanTransactions().addAll(newTransactions);
loan.updateLoanSummaryDerivedFields(); loanBalanceService.updateLoanSummaryDerivedFields(loan);
return changedTransactionDetail; 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.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; 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.LoanDownPaymentHandlerService;
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster; import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
@@ -91,6 +92,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanChargeValidator loanChargeValidator; private final LoanChargeValidator loanChargeValidator;
private final LoanJournalEntryPoster journalEntryPoster; private final LoanJournalEntryPoster journalEntryPoster;
private final LoanBalanceService loanBalanceService;
@Override @Override
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter, public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
@@ -112,7 +114,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
Money unrecognizedIncome = transactionAmountAsMoney.zero(); Money unrecognizedIncome = transactionAmountAsMoney.zero();
Money interestComponent = transactionAmountAsMoney; Money interestComponent = transactionAmountAsMoney;
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableInterest = loan.getReceivableInterest(transactionDate); Money receivableInterest = loanBalanceService.getReceivableInterest(loan, transactionDate);
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) { if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
interestComponent = receivableInterest; interestComponent = receivableInterest;
unrecognizedIncome = transactionAmountAsMoney.minus(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.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; 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.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.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -48,63 +51,77 @@ public class LoanAccountAutoStarter {
@Bean @Bean
@Conditional(CreocoreLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(CreocoreLoanRepaymentScheduleTransactionProcessorCondition.class)
public CreocoreLoanRepaymentScheduleTransactionProcessor creocoreLoanRepaymentScheduleTransactionProcessor( public CreocoreLoanRepaymentScheduleTransactionProcessor creocoreLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new CreocoreLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new CreocoreLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Bean @Bean
@Conditional(EarlyRepaymentLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(EarlyRepaymentLoanRepaymentScheduleTransactionProcessorCondition.class)
public EarlyPaymentLoanRepaymentScheduleTransactionProcessor earlyPaymentLoanRepaymentScheduleTransactionProcessor( public EarlyPaymentLoanRepaymentScheduleTransactionProcessor earlyPaymentLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new EarlyPaymentLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new EarlyPaymentLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Bean @Bean
@Conditional(MifosStandardLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(MifosStandardLoanRepaymentScheduleTransactionProcessorCondition.class)
public FineractStyleLoanRepaymentScheduleTransactionProcessor fineractStyleLoanRepaymentScheduleTransactionProcessor( public FineractStyleLoanRepaymentScheduleTransactionProcessor fineractStyleLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new FineractStyleLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new FineractStyleLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Bean @Bean
@Conditional(HeavensFamilyLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(HeavensFamilyLoanRepaymentScheduleTransactionProcessorCondition.class)
public HeavensFamilyLoanRepaymentScheduleTransactionProcessor heavensFamilyLoanRepaymentScheduleTransactionProcessor( public HeavensFamilyLoanRepaymentScheduleTransactionProcessor heavensFamilyLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new HeavensFamilyLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new HeavensFamilyLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Bean @Bean
@Conditional(InterestPrincipalPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(InterestPrincipalPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class)
public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor interestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor( public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor interestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
} }
@Bean @Bean
@Conditional(PrincipalInterestPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(PrincipalInterestPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class)
public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor principalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor( public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor principalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
} }
@Bean @Bean
@Conditional(RBIIndiaLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(RBIIndiaLoanRepaymentScheduleTransactionProcessorCondition.class)
public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor(
return new RBILoanRepaymentScheduleTransactionProcessor(externalIdFactory); final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new RBILoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService);
} }
@Bean @Bean
@Conditional(DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorCondition.class)
public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor duePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor( public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor duePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
} }
@Bean @Bean
@Conditional(DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorCondition.class) @Conditional(DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorCondition.class)
public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor duePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor( public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor duePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(
ExternalIdFactory externalIdFactory) { final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
return new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(externalIdFactory); final LoanBalanceService loanBalanceService) {
return new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
loanBalanceService);
} }
@Bean @Bean
@@ -117,11 +134,13 @@ public class LoanAccountAutoStarter {
@Bean @Bean
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator,
LoanRepositoryWrapper loanRepositoryWrapper, final LoanRepositoryWrapper loanRepositoryWrapper,
@Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, ExternalIdFactory externalIdFactory, final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService,
LoanScheduleComponent loanSchedule) { final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanTransactionService loanTransactionService, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService, 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.LoanArrearsAgingServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl; 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.LoanCalculateRepaymentPastDueService;
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService; 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.LoanTransactionAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionRelationReadService; 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.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
@@ -268,11 +270,11 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanChargeAssembler.class) @ConditionalOnMissingBean(LoanChargeAssembler.class)
public LoanChargeAssembler loanChargeAssembler( public LoanChargeAssembler loanChargeAssembler(final FromJsonHelper fromApiJsonHelper, final ChargeRepositoryWrapper chargeRepository,
final LoanChargeRepository loanChargeRepository, final LoanProductRepository loanProductRepository,
FromJsonHelper fromApiJsonHelper, ChargeRepositoryWrapper chargeRepository, LoanChargeRepository loanChargeRepository, final ExternalIdFactory externalIdFactory, final LoanChargeService loanChargeService) {
LoanProductRepository loanProductRepository, ExternalIdFactory externalIdFactory) { return new LoanChargeAssembler(fromApiJsonHelper, chargeRepository, loanChargeRepository, loanProductRepository, externalIdFactory,
return new LoanChargeAssembler(fromApiJsonHelper, chargeRepository, loanChargeRepository, loanProductRepository, externalIdFactory); loanChargeService);
} }
@Bean @Bean
@@ -331,14 +333,14 @@ public class LoanAccountConfiguration {
DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository, DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository,
LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService, LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService,
LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper, LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper,
LoanTransactionProcessingService loanTransactionProcessingService) { LoanTransactionProcessingService loanTransactionProcessingService, LoanBalanceService loanBalanceService) {
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository, return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService, loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService, fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
staffReadPlatformService, paginationHelper, paymentTypeReadPlatformService, floatingRatesReadPlatformService, staffReadPlatformService, paginationHelper, paymentTypeReadPlatformService, floatingRatesReadPlatformService,
loanUtilService, configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator, loanUtilService, configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator,
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService, delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService); loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService, loanBalanceService);
} }
@Bean @Bean
@@ -402,7 +404,8 @@ public class LoanAccountConfiguration {
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService, ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService,
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService, LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper, LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
LoanTransactionProcessingService loanTransactionProcessingService) { LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService,
LoanTransactionService loanTransactionService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
@@ -417,7 +420,7 @@ public class LoanAccountConfiguration {
loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator,
loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService, reprocessLoanTransactionsService, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService, reprocessLoanTransactionsService,
loanAccountService, journalEntryPoster, loanAdjustmentService, loanAccountingBridgeMapper, loanMapper, loanAccountService, journalEntryPoster, loanAdjustmentService, loanAccountingBridgeMapper, loanMapper,
loanTransactionProcessingService); loanTransactionProcessingService, loanBalanceService, loanTransactionService);
} }
@Bean @Bean
@@ -434,10 +437,12 @@ public class LoanAccountConfiguration {
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService,
LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator, LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) { LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine,
LoanBalanceService loanBalanceService, LoanTransactionService loanTransactionService) {
return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator, loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator,
reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine); reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService,
loanTransactionService);
} }
@Bean @Bean
@@ -457,9 +462,10 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanChargeService.class) @ConditionalOnMissingBean(LoanChargeService.class)
public LoanChargeService loanChargeService(LoanChargeValidator loanChargeValidator, public LoanChargeService loanChargeService(final LoanChargeValidator loanChargeValidator,
LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) { final LoanTransactionProcessingService loanTransactionProcessingService,
return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService, loanLifecycleStateMachine); final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanBalanceService loanBalanceService) {
return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService);
} }
@Bean @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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; 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.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; 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.LoanChargeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; 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.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -122,6 +127,9 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
@Mock @Mock
private AvroDateTimeMapper mapper; 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 MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static final String CUSTOM_DATA_PREFIX = "test_data_loan_delinquency_range_business_event"; private static final String CUSTOM_DATA_PREFIX = "test_data_loan_delinquency_range_business_event";
@@ -275,7 +283,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
.thenReturn(installmentDelinquencyTags); .thenReturn(installmentDelinquencyTags);
when(loanForProcessing.getLoanCharges()).thenAnswer(a -> repaymentScheduleInstallments.get(0).getInstallmentCharges().stream() when(loanForProcessing.getLoanCharges()).thenAnswer(a -> repaymentScheduleInstallments.get(0).getInstallmentCharges().stream()
.map(c -> c.getLoanCharge()).collect(Collectors.toList())); .map(LoanInstallmentCharge::getLoanCharge).collect(Collectors.toSet()));
// when // when
LoanAccountDelinquencyRangeDataV1 data = (LoanAccountDelinquencyRangeDataV1) serializer.toAvroDTO(event); LoanAccountDelinquencyRangeDataV1 data = (LoanAccountDelinquencyRangeDataV1) serializer.toAvroDTO(event);
@@ -431,8 +439,9 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
} }
private LoanCharge buildLoanCharge(Loan loan, BigDecimal amount, Charge charge) { private LoanCharge buildLoanCharge(Loan loan, BigDecimal amount, Charge charge) {
LoanCharge loanCharge = new LoanCharge(loan, charge, amount, amount, ChargeTimeType.SPECIFIED_DUE_DATE, ChargeCalculationType.FLAT, LoanCharge loanCharge = loanChargeService.create(loan, charge, amount, amount, ChargeTimeType.SPECIFIED_DUE_DATE,
LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, new BigDecimal(100), ExternalId.generate()); ChargeCalculationType.FLAT, LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, new BigDecimal(100),
ExternalId.generate());
ReflectionTestUtils.setField(loanCharge, "id", 1L); ReflectionTestUtils.setField(loanCharge, "id", 1L);
return loanCharge; 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.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper; 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.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -48,6 +49,9 @@ class DefaultLoanLifecycleStateMachineTest {
@Mock @Mock
private BusinessEventNotifierService businessEventNotifierService; private BusinessEventNotifierService businessEventNotifierService;
@Mock
private LoanBalanceService loanBalanceService;
private DefaultLoanLifecycleStateMachine underTest; private DefaultLoanLifecycleStateMachine underTest;
private MockedStatic<MoneyHelper> moneyHelperStatic; private MockedStatic<MoneyHelper> moneyHelperStatic;
@@ -58,7 +62,7 @@ class DefaultLoanLifecycleStateMachineTest {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelperStatic.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelperStatic.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); moneyHelperStatic.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService); underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService, loanBalanceService);
} }
@AfterEach @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.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; 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.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.rate.domain.Rate; import org.apache.fineract.portfolio.rate.domain.Rate;
@@ -61,7 +63,7 @@ public class LoanBuilder {
private Staff loanOfficer; private Staff loanOfficer;
private CodeValue loanPurpose; private CodeValue loanPurpose;
private LoanRepaymentScheduleTransactionProcessor transactionProcessor = new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor( private LoanRepaymentScheduleTransactionProcessor transactionProcessor = new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(
mock(ExternalIdFactory.class)); mock(ExternalIdFactory.class), mock(LoanChargeValidator.class), mock(LoanBalanceService.class));
private LoanProductRelatedDetail loanRepaymentScheduleDetail; private LoanProductRelatedDetail loanRepaymentScheduleDetail;
private LoanStatus loanStatus = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL; private LoanStatus loanStatus = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL;
private LoanSubStatus loanSubStatus; private LoanSubStatus loanSubStatus;
@@ -265,8 +265,23 @@ public class LoanTest {
* @return the {@link LoanCharge} * @return the {@link LoanCharge}
*/ */
private LoanCharge buildLoanCharge() { private LoanCharge buildLoanCharge() {
return new LoanCharge(mock(Loan.class), mock(Charge.class), new BigDecimal(100), new BigDecimal(100), final LoanCharge loanCharge = new LoanCharge();
ChargeTimeType.TRANCHE_DISBURSEMENT, ChargeCalculationType.FLAT, LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1,
new BigDecimal(100), ExternalId.generate()); 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.eq;
import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.mock;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; 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.ActionContext;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; 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.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; 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.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; 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.AfterAll;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@@ -99,7 +103,8 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
underTest = new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(null); underTest = new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(mock(ExternalIdFactory.class),
mock(LoanChargeValidator.class), mock(LoanBalanceService.class));
Mockito.reset(charges, transactionMappings); Mockito.reset(charges, transactionMappings);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); 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.eq;
import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.mock;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; 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.ActionContext;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; 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.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; 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.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; 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.AfterAll;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@@ -100,7 +104,8 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
underTest = new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(null); underTest = new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(mock(ExternalIdFactory.class),
mock(LoanChargeValidator.class), mock(LoanBalanceService.class));
Mockito.reset(charges, transactionMappings); Mockito.reset(charges, transactionMappings);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
@@ -111,13 +111,20 @@ public class LoanDownPaymentHandlerServiceImplTest {
@Mock @Mock
private LoanLifecycleStateMachine loanLifecycleStateMachine; private LoanLifecycleStateMachine loanLifecycleStateMachine;
@Mock
private LoanBalanceService loanBalanceService;
@Mock
private LoanTransactionService loanTransactionService;
private LoanDownPaymentHandlerServiceImpl underTest; private LoanDownPaymentHandlerServiceImpl underTest;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator, 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::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true); tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true);