diff --git a/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java b/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java index c8ae5147f..6054e2e0d 100644 --- a/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java +++ b/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java @@ -20,6 +20,8 @@ package com.acme.fineract.loan.processor; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.springframework.stereotype.Component; @Component @@ -29,8 +31,9 @@ public class AcmeLoanRepaymentScheduleTransactionProcessor extends FineractStyle public static final String STRATEGY_NAME = "ACME Corp.: standard loan transaction processing strategy"; - public AcmeLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public AcmeLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java index 8783e9251..07fad8281 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/serialization/serializer/investor/InvestorBusinessEventSerializerTest.java @@ -40,9 +40,10 @@ import static org.mockito.Mockito.when; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.time.LocalDate; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.fineract.avro.generator.ByteBufferSerializable; @@ -135,7 +136,7 @@ public class InvestorBusinessEventSerializerTest { .thenReturn(createTransferData(firstTransferStatus, null)); Loan loan = Mockito.mock(Loan.class); when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1)); - List loanCharges = createMockCharges(); + final Set loanCharges = createMockCharges(); when(loan.getLoanCharges()).thenReturn(loanCharges); LoanOwnershipTransferBusinessEvent loanOwnershipTransferBusinessEvent = new LoanOwnershipTransferBusinessEvent( createExternalAssetOwnerTransfer(status, subStatus), loan); @@ -153,8 +154,8 @@ public class InvestorBusinessEventSerializerTest { assertEquals(CUSTOM_DATA_PREFIX + "_2", new String(customData.get("test_key_2").array(), UTF_8)); } - private List createMockCharges() { - List loanCharges = new ArrayList<>(); + private Set createMockCharges() { + final Set loanCharges = new HashSet<>(); loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("10.00000"))); loanCharges.add(loanCharge(1L, "charge a", new BigDecimal("15.00000"))); loanCharges.add(loanCharge(2L, "charge b", BigDecimal.ZERO)); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index e99319a62..694e0bfef 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.portfolio.delinquency.service; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EARLIEST_UNPAID_DATE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.NEXT_UNPAID_DUE_DATE; + import java.time.LocalDate; import java.util.Collection; import java.util.Comparator; @@ -27,6 +30,7 @@ import java.util.stream.Collector; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; @@ -51,6 +55,7 @@ import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; import org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.jetbrains.annotations.NotNull; @@ -142,7 +147,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo // loans collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList); collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount())); - collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate(nextPaymentDueDateConfig)); + collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan)); final LoanTransaction lastPayment = loan.getLastPaymentTransaction(); if (lastPayment != null) { @@ -228,4 +233,63 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo return List.of(); } + private LocalDate possibleNextRepaymentDate(final String nextPaymentDueDateConfig, final Loan loan) { + if (nextPaymentDueDateConfig == null) { + return null; + } + return switch (nextPaymentDueDateConfig.toLowerCase()) { + case EARLIEST_UNPAID_DATE -> getEarliestUnpaidInstallmentDate(loan); + case NEXT_UNPAID_DUE_DATE -> getNextUnpaidInstallmentDueDate(loan); + default -> null; + }; + } + + private LocalDate getEarliestUnpaidInstallmentDate(final Loan loan) { + LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate(); + List 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 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; + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java index bccbe3f43..5872c4bbf 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java @@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.springframework.stereotype.Component; // TODO: introduce tests for the state machine @@ -36,6 +37,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi private static final List ALLOWED_LOAN_STATUSES = List.of(LoanStatus.values()); private final BusinessEventNotifierService businessEventNotifierService; + private final LoanBalanceService loanBalanceService; @Override public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) { @@ -45,7 +47,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi @Override public void transition(final LoanEvent loanEvent, final Loan loan) { - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); internalTransition(loanEvent, loan); } @@ -55,7 +57,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi return; } - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); final LoanStatusTransition transition = determineTransition(loan, loan.getStatus(), transactionDate); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 4f22c7489..a11d7c710 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -67,7 +67,6 @@ import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.charge.domain.Charge; -import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.collateral.domain.LoanCollateral; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -76,7 +75,6 @@ import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; -import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; @@ -275,6 +273,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "interest_calculated_from_date") private LocalDate interestChargedFromDate; + @Setter @Column(name = "total_overpaid_derived", scale = 6, precision = 19) private BigDecimal totalOverpaid; @@ -347,6 +346,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private List loanTermVariations = new ArrayList<>(); + @Setter @Column(name = "total_recovered_derived", scale = 6, precision = 19) private BigDecimal totalRecovered; @@ -716,28 +716,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } - public void updateLoanSummaryDerivedFields() { - if (isNotDisbursed()) { - if (this.summary != null) { - this.summary.zeroFields(); - } - this.totalOverpaid = null; - } else { - final Money overpaidBy = calculateTotalOverpayment(); - this.totalOverpaid = null; - if (!overpaidBy.isLessThanZero()) { - this.totalOverpaid = overpaidBy.getAmountDefaultedToNullIfZero(); - } - - final Money recoveredAmount = calculateTotalRecoveredPayments(); - this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero(); - - final Money principal = this.loanRepaymentScheduleDetail.getPrincipal(); - this.summary.updateSummary(getCurrency(), principal, getRepaymentScheduleInstallments(), this.charges); - updateLoanOutstandingBalances(); - } - } - private boolean isInterestRecalculationEnabledForProduct() { return this.loanProduct.isInterestRecalculationEnabled(); } @@ -746,33 +724,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 fetchLoanTrancheChargeIds() { return getCharges().stream()// .filter(charge -> charge.isTrancheDisbursementCharge() && charge.isActive()) // @@ -793,7 +744,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .collect(Collectors.toList()); } - private LocalDate determineExpectedMaturityDate() { + public LocalDate determineExpectedMaturityDate() { List installments = getRepaymentScheduleInstallments().stream() .filter(i -> !i.isDownPayment() && !i.isAdditional()).toList(); final int numberOfInstallments = installments.size(); @@ -918,16 +869,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.postDatedChecks = new ArrayList<>(); } - public List retrieveListOfTransactionsForReprocessing() { - return getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate()).sorted(LoanTransactionComparator.INSTANCE) - .collect(Collectors.toList()); - } - - private static Predicate loanTransactionForReprocessingPredicate() { - return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge() - || transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction()); - } - public List retrieveListOfTransactionsExcludeAccruals() { final List repaymentsOrWaivers = new ArrayList<>(); for (final LoanTransaction transaction : this.loanTransactions) { @@ -945,95 +886,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .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 loanTransactions) { return loanTransactions.stream() // .filter(t -> t.isRepaymentLikeType() && t.isNotReversed()) // .noneMatch(t -> DateUtils.isBefore(loanTransaction.getTransactionDate(), t.getTransactionDate())); } - public boolean isChronologicallyLatestTransaction(final LoanTransaction loanTransaction, final List 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 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 installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment installment : installments) { - if (installment.isNotFullyPaidOff()) { - earliestUnpaidInstallmentDate = installment.getDueDate(); - break; - } - } - - LocalDate lastTransactionDate = null; - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) { - lastTransactionDate = transaction.getTransactionDate(); - } - } - - LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate; - if (DateUtils.isAfter(lastTransactionDate, earliestUnpaidInstallmentDate)) { - possibleNextRepaymentDate = lastTransactionDate; - } - - return possibleNextRepaymentDate; - } - public LoanTransaction findWriteOffTransaction() { return this.loanTransactions.stream() // .filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) // @@ -1041,49 +899,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .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 installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { - cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments - .plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency))) - .plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency)); - - cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency)); - } - - for (final LoanTransaction loanTransaction : this.loanTransactions) { - if (loanTransaction.isReversed()) { - continue; - } - if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) { - totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency)); - } else if (loanTransaction.isCreditBalanceRefund()) { - if (loanTransaction.getPrincipalPortion(currency).isZero()) { - totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency)); - } - } else if (loanTransaction.isChargeback()) { - if (loanTransaction.getPrincipalPortion(currency).isZero() && getCreditAllocationRules().stream() // - .filter(car -> car.getTransactionType().equals(CreditAllocationTransactionType.CHARGEBACK)) // - .findAny() // - .isEmpty()) { - totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency)); - } - } - } - - // if total paid in transactions doesn't match repayment schedule then there's an overpayment. - return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments); - } - public Money calculateTotalRecoveredPayments() { // in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments return getTotalRecoveredPayments(); @@ -1392,30 +1207,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return isChargedOff() && getChargeOffReason() != null ? getChargeOffReason().getId() : null; } - public Money getReceivableInterest(final LocalDate tillDate) { - Money receivableInterest = Money.zero(getCurrency()); - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement() - && !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { - if (transaction.isAccrual()) { - receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency())); - } else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver() || transaction.isAccrualAdjustment()) { - receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency())); - } - } - if (receivableInterest.isLessThanZero()) { - receivableInterest = receivableInterest.zero(); - } - /* - * if (transaction.getTransactionDate().isAfter(tillDate) && transaction.isAccrual()) { final String - * errorMessage = "The date on which a loan is interest waived cannot be in after accrual transactions." ; - * throw new InvalidLoanStateTransitionException("waive", "cannot.be.after.accrual.date", errorMessage, - * tillDate); } - */ - } - return receivableInterest; - } - public boolean isSyncDisbursementWithMeeting() { return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting; } @@ -1480,13 +1271,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return currentTransactionDate; } - public LoanTransaction getLastTransactionForReprocessing() { - return loanTransactions.stream() // - .filter(Loan.loanTransactionForReprocessingPredicate()) // - .reduce((first, second) -> second) // - .orElse(null); - } - public LoanTransaction getLastPaymentTransaction() { return loanTransactions.stream() // .filter(loanTransaction -> !loanTransaction.isReversed()) // @@ -1507,6 +1291,17 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 getAllDisbursementDetails() { return this.disbursementDetails; } @@ -1800,21 +1595,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } - public void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursementDetail) { - for (final LoanCharge charge : getActiveCharges()) { - if (charge.isOverdueInstallmentCharge()) { - charge.setActive(false); - } else if (charge.isTrancheDisbursementCharge() && disbursementDetail.getDisbursementDate() - .equals(charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate())) { - charge.resetToOriginal(getCurrency()); - } - } - this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(disbursementDetail.principal())); - disbursementDetail.updateActualDisbursementDate(null); - disbursementDetail.reverse(); - updateLoanSummaryDerivedFields(); - } - private int adjustNumberOfRepayments() { int repaymetsForAdjust = 0; for (LoanTermVariations loanTermVariations : this.loanTermVariations) { @@ -1867,145 +1647,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 installments) { this.repaymentScheduleInstallments.clear(); for (final LoanRepaymentScheduleInstallment installment : installments) { @@ -2046,7 +1687,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.loanTopupDetails; } - public Collection getLoanCharges() { + public Set getLoanCharges() { return this.charges; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index bacf7f02f..b5c21a78a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -33,18 +33,13 @@ import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import lombok.Getter; import lombok.Setter; -import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -56,11 +51,11 @@ import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; -import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; +@Setter @Getter @Entity @Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") }) @@ -104,7 +99,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "amount_paid_derived", scale = 6, precision = 19) private BigDecimal amountPaid; - @Setter @Column(name = "amount_waived_derived", scale = 6, precision = 19) private BigDecimal amountWaived; @@ -149,111 +143,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { @OneToMany(mappedBy = "loanCharge", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private Set loanChargePaidBySet = new HashSet<>(); - protected LoanCharge() { - // - } - - public LoanCharge(final Loan loan, final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount, - final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate, - final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final BigDecimal loanCharge, - final ExternalId externalId) { - this.loan = loan; - this.charge = chargeDefinition; - this.submittedOnDate = DateUtils.getBusinessLocalDate(); - this.penaltyCharge = chargeDefinition.isPenalty(); - this.minCap = chargeDefinition.getMinCap(); - this.maxCap = chargeDefinition.getMaxCap(); - - this.chargeTime = chargeDefinition.getChargeTimeType(); - if (chargeTime != null) { - this.chargeTime = chargeTime.getValue(); - } - - if (ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.SPECIFIED_DUE_DATE) - || ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.OVERDUE_INSTALLMENT)) { - - if (dueDate == null) { - final String defaultUserMessage = "Loan charge is missing due date."; - throw new LoanChargeWithoutMandatoryFieldException("loanCharge", "dueDate", defaultUserMessage, chargeDefinition.getId(), - chargeDefinition.getName()); - } - - this.dueDate = dueDate; - } else { - this.dueDate = null; - } - - this.chargeCalculation = chargeDefinition.getChargeCalculation(); - if (chargeCalculation != null) { - this.chargeCalculation = chargeCalculation.getValue(); - } - - BigDecimal chargeAmount = chargeDefinition.getAmount(); - if (amount != null) { - chargeAmount = amount; - } - - this.chargePaymentMode = chargeDefinition.getChargePaymentMode(); - if (chargePaymentMode != null) { - this.chargePaymentMode = chargePaymentMode.getValue(); - } - - populateDerivedFields(loanPrincipal, chargeAmount, numberOfRepayments, loanCharge); - this.paid = determineIfFullyPaid(); - this.externalId = externalId; - } - - public void populateDerivedFields(final BigDecimal amountPercentageAppliedTo, final BigDecimal chargeAmount, Integer numberOfRepayments, - BigDecimal loanCharge) { - - switch (ChargeCalculationType.fromInt(this.chargeCalculation)) { - case INVALID: - this.percentage = null; - this.amount = null; - this.amountPercentageAppliedTo = null; - this.amountPaid = null; - this.amountOutstanding = BigDecimal.ZERO; - this.amountWaived = null; - this.amountWrittenOff = null; - break; - case FLAT: - this.percentage = null; - this.amountPercentageAppliedTo = null; - this.amountPaid = null; - if (isInstalmentFee()) { - if (numberOfRepayments == null) { - numberOfRepayments = this.loan.fetchNumberOfInstallmensAfterExceptions(); - } - this.amount = chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments)); - } else { - this.amount = chargeAmount; - } - this.amountOutstanding = this.amount; - this.amountWaived = null; - this.amountWrittenOff = null; - break; - case PERCENT_OF_AMOUNT: - case PERCENT_OF_AMOUNT_AND_INTEREST: - case PERCENT_OF_INTEREST: - case PERCENT_OF_DISBURSEMENT_AMOUNT: - this.percentage = chargeAmount; - this.amountPercentageAppliedTo = amountPercentageAppliedTo; - if (loanCharge.compareTo(BigDecimal.ZERO) == 0) { - loanCharge = percentageOf(this.amountPercentageAppliedTo); - } - this.amount = minimumAndMaximumCap(loanCharge); - this.amountPaid = null; - this.amountOutstanding = calculateOutstanding(); - this.amountWaived = null; - this.amountWrittenOff = null; - break; - } - this.amountOrPercentage = chargeAmount; - if (this.loan != null && isInstalmentFee()) { - updateInstallmentCharges(); - } - } - public void markAsFullyPaid() { this.amountPaid = this.amount; this.amountOutstanding = BigDecimal.ZERO; @@ -340,234 +229,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { 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 update(final JsonCommand command, final BigDecimal amount) { - - final Map 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 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 installments = loan.getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment installment : installments) { - amount = amount.plus(calculateInstallmentChargeAmount(loan, getChargeCalculation(), getPercentage(), installment)); - } - return amount.getAmount(); - } - - private List generateInstallmentLoanCharges(final LoanCharge loanCharge) { - Loan loan = this.loan; - final List loanChargePerInstallments = new ArrayList<>(); - if (loanCharge.isInstalmentFee()) { - List 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 remove = new HashSet<>(); - final List newChargeInstallments = generateInstallmentLoanCharges(this); - if (this.loanInstallmentCharge.isEmpty()) { - this.loanInstallmentCharge.addAll(newChargeInstallments); - } else { - int index = 0; - final List oldChargeInstallments = new ArrayList<>(); - if (this.loanInstallmentCharge != null && !this.loanInstallmentCharge.isEmpty()) { - oldChargeInstallments.addAll(this.loanInstallmentCharge); - } - Collections.sort(oldChargeInstallments); - final LoanInstallmentCharge[] loanChargePerInstallmentArray = newChargeInstallments - .toArray(new LoanInstallmentCharge[newChargeInstallments.size()]); - for (final LoanInstallmentCharge chargePerInstallment : oldChargeInstallments) { - if (index == loanChargePerInstallmentArray.length) { - remove.add(chargePerInstallment); - chargePerInstallment.getInstallment().getInstallmentCharges().remove(chargePerInstallment); - } else { - LoanInstallmentCharge newLoanInstallmentCharge = loanChargePerInstallmentArray[index++]; - newLoanInstallmentCharge.getInstallment().getInstallmentCharges().remove(newLoanInstallmentCharge); - chargePerInstallment.copyFrom(newLoanInstallmentCharge); - } - } - this.loanInstallmentCharge.removeAll(remove); - while (index < loanChargePerInstallmentArray.length) { - this.loanInstallmentCharge.add(loanChargePerInstallmentArray[index++]); - } - } - Money amount = Money.zero(this.loan.getCurrency()); - for (LoanInstallmentCharge charge : this.loanInstallmentCharge) { - amount = amount.plus(charge.getAmount()); - } - this.amount = amount.getAmount(); - } - public boolean isDueAtDisbursement() { return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT) || ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.TRANCHE_DISBURSEMENT); @@ -600,7 +261,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { return BigDecimal.ZERO.compareTo(calculateOutstanding()) == 0; } - private BigDecimal calculateOutstanding() { + public BigDecimal calculateOutstanding() { if (this.amount == null) { return null; } @@ -645,7 +306,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { * @returns a minimum cap or maximum cap set on charges if the criteria fits else it returns the percentageOf if the * amount is within min and max cap */ - private BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) { + public BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) { BigDecimal minMaxCap; if (this.minCap != null) { final int minimumCap = percentageOf.compareTo(this.minCap); @@ -817,11 +478,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { return ChargeCalculationType.fromInt(this.chargeCalculation); } - public void updateAmount(final BigDecimal amount) { - this.amount = amount; - calculateOutstanding(); - } - public LoanInstallmentCharge getUnpaidInstallmentLoanCharge() { LoanInstallmentCharge unpaidChargePerInstallment = null; for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 9ca1949e6..55a29f5e7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -925,4 +925,5 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom 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 findLastTransactionDateForReprocessing(@Param("loan") Loan loan); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index e271c1378..a6aaa89ca 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.springframework.util.CollectionUtils; /** @@ -76,8 +77,9 @@ import org.springframework.util.CollectionUtils; public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor { protected final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); - protected final LoanChargeValidator loanChargeValidator = new LoanChargeValidator(); protected final ExternalIdFactory externalIdFactory; + protected final LoanChargeValidator loanChargeValidator; + protected final LoanBalanceService loanBalanceService; @Override public boolean accept(String s) { @@ -256,7 +258,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } else { loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); final Loan loan = loanTransaction.getLoan(); - if ((loan.isClosedObligationsMet() || loan.isOverPaid()) && currentInstallment.isObligationsMet() + if ((loan.isClosedObligationsMet() || loanBalanceService.isOverPaid(loan)) && currentInstallment.isObligationsMet() && currentInstallment.isTransactionDateWithinPeriod(currentInstallment.getObligationsMetOnDate())) { loanTransaction.updateTransactionDate(currentInstallment.getObligationsMetOnDate()); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java index eed45dd84..2518955dd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java @@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * Creocore style {@link LoanRepaymentScheduleTransactionProcessor}. @@ -47,8 +49,9 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL public static final String STRATEGY_NAME = "Creocore Unique"; - public CreocoreLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public CreocoreLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java index fb82ae93b..e24e00f1d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java @@ -32,6 +32,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * `First due/late charges, interest, principal, after in advance principal, charges, interest` style @@ -48,8 +50,9 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio public static final String STRATEGY_NAME = "Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest"; - public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java index acdee6768..148c0f4a2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java @@ -32,6 +32,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * `First due/late penalty, interest, principal, fee, after in advance penalty, interest, principal, fee` style @@ -48,8 +50,9 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio public static final String STRATEGY_NAME = "Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee"; - public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java index 8d7794a57..8d2d11e07 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java @@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of Interest first, then @@ -41,8 +43,9 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr public static final String STRATEGY_NAME = "Early Repayment Strategy"; - public EarlyPaymentLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public EarlyPaymentLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java index 0b3de1210..371784261 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java @@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * Old style {@link LoanRepaymentScheduleTransactionProcessor}. @@ -47,8 +49,9 @@ public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends Abst public static final String STRATEGY_NAME = "Penalties, Fees, Interest, Principal order"; - public FineractStyleLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public FineractStyleLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java index 67356bc58..fbafe9620 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java @@ -31,6 +31,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * Heavensfamily style {@link LoanRepaymentScheduleTransactionProcessor}. @@ -49,8 +51,9 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst public static final String STRATEGY_NAME = "HeavensFamily Unique"; - public HeavensFamilyLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public HeavensFamilyLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java index 5ed1824f6..d82dc0b29 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of Interest first, then @@ -42,8 +44,9 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr public static final String STRATEGY_NAME = "Interest, Principal, Penalties, Fees Order"; - public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java index a134067fb..5e46fa0a7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -30,6 +30,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of principal first, then @@ -42,8 +44,9 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr public static final String STRATEGY_NAME = "Principal, Interest, Penalties, Fees Order"; - public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java index 47ee06451..bbeadc84a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java @@ -31,6 +31,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; /** * Adhikar/RBI style {@link LoanRepaymentScheduleTransactionProcessor}. @@ -51,8 +53,9 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe public static final String STRATEGY_NAME = "Overdue/Due Fee/Int,Principal"; - public RBILoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - super(externalIdFactory); + public RBILoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index 4bec98359..f92112531 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -35,6 +35,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.data.CurrencyData; @@ -61,10 +62,14 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.ScheduleDateException; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; +@RequiredArgsConstructor public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanScheduleGenerator { + private final LoanTransactionService loanTransactionService; + @Override public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { @@ -2796,7 +2801,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO, loanRepaymentScheduleTransactionProcessor, onDate, calculateTill); - List loanTransactions = loan.retrieveListOfTransactionsForReprocessing(); + final List loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges()); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java index 31c1bdfd3..a3ce6e25b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java @@ -25,10 +25,10 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; -import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.springframework.stereotype.Component; @@ -56,12 +56,19 @@ import org.springframework.stereotype.Component; *

*/ @Component -@RequiredArgsConstructor public class CumulativeDecliningBalanceInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator { private final ScheduledDateGenerator scheduledDateGenerator; private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator; + public CumulativeDecliningBalanceInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator, + final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator, + final LoanTransactionService loanTransactionService) { + super(loanTransactionService); + this.scheduledDateGenerator = scheduledDateGenerator; + this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator; + } + @Override public ScheduledDateGenerator getScheduledDateGenerator() { return scheduledDateGenerator; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java index 852bd9b12..ad58d1f2f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java @@ -24,18 +24,25 @@ import java.time.LocalDate; import java.util.Collection; import java.util.Map; import java.util.TreeMap; -import lombok.RequiredArgsConstructor; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.springframework.stereotype.Component; @Component -@RequiredArgsConstructor public class CumulativeFlatInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator { private final ScheduledDateGenerator scheduledDateGenerator; private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator; + public CumulativeFlatInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator, + final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator, + final LoanTransactionService loanTransactionService) { + super(loanTransactionService); + this.scheduledDateGenerator = scheduledDateGenerator; + this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator; + } + @Override public ScheduledDateGenerator getScheduledDateGenerator() { return scheduledDateGenerator; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java index 507741ee4..b900baa6a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.serialization; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.organisation.holiday.domain.Holiday; @@ -31,11 +32,15 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public final class LoanDownPaymentTransactionValidator { + private final LoanBalanceService loanBalanceService; + public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) { @@ -53,7 +58,7 @@ public final class LoanDownPaymentTransactionValidator { } public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) { - if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) { + if (!(loan.isOpen() || loan.isClosedObligationsMet() || loanBalanceService.isOverPaid(loan))) { final List dataValidationErrors = new ArrayList<>(); final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid"; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid", diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java new file mode 100644 index 000000000..cb69a73eb --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java @@ -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 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; + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java index e28faaf1b..4cd54591f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java @@ -21,20 +21,30 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -52,6 +62,7 @@ public class LoanChargeService { private final LoanChargeValidator loanChargeValidator; private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanLifecycleStateMachine loanLifecycleStateMachine; + private final LoanBalanceService loanBalanceService; public void recalculateAllCharges(final Loan loan) { Set charges = loan.getActiveCharges(); @@ -68,54 +79,25 @@ public class LoanChargeService { BigDecimal totalChargeAmt = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageBased()) { if (loanCharge.isOverdueInstallmentCharge()) { - amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod); + amount = calculateOverdueAmountPercentageAppliedTo(loan, loanCharge, penaltyWaitPeriod); } else { amount = calculateAmountPercentageAppliedTo(loan, loanCharge); } chargeAmt = loanCharge.getPercentage(); if (loanCharge.isInstalmentFee()) { - totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); + totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge); } } else { chargeAmt = loanCharge.amountOrPercentage(); } if (loanCharge.isActive()) { clearLoanInstallmentChargesBeforeRegeneration(loanCharge); - loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), + update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); } } - private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { - /* - * JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down - * installment charges from the loanCharge and from each of the repayment installments and allow them to be - * recalculated fully anew. This patch is to avoid the 'merging' of existing and regenerated installment charges - * which results in the installment charges being deleted on loan approval if the schedule is regenerated. Not - * pretty. updateInstallmentCharges in LoanCharge.java: the merging looks like it will work but doesn't so this - * patch simply hits the part which 'adds all' rather than merge. Possibly an ORM issue. The issue could be to - * do with the fact that, on approval, the "recalculateLoanCharge" happens twice (probably 2 schedule - * regenerations) whereas it only happens once on Submit and Disburse (and no problems with them) - * - * if (this.loanInstallmentCharge.isEmpty()) { this.loanInstallmentCharge.addAll(newChargeInstallments); - */ - Loan loan = loanCharge.getLoan(); - if (!loan.isSubmittedAndPendingApproval() && !loan.isApproved()) { - return; - } // doing for both just in case status is not - // updated at this points - if (loanCharge.isInstalmentFee()) { - loanCharge.clearLoanInstallmentCharges(); - for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { - if (installment.isRecalculatedInterestComponent()) { - continue; // JW: does this in generateInstallmentLoanCharges - but don't understand it - } - installment.getInstallmentCharges().clear(); - } - } - } - public void makeChargePayment(final Loan loan, final Long chargeId, final List existingTransactionIds, final List existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) { loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction); @@ -130,36 +112,6 @@ public class LoanChargeService { handleChargePaidTransaction(loan, charge, paymentTransaction, installmentNumber); } - public void handleChargePaidTransaction(final Loan loan, final LoanCharge charge, final LoanTransaction chargesPayment, - final Integer installmentNumber) { - chargesPayment.updateLoan(loan); - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, - chargesPayment.getAmount(loan.getCurrency()).getAmount(), installmentNumber); - chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); - loan.addLoanTransaction(chargesPayment); - loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_PAYMENT, loan); - - final List chargePaymentInstallments = new ArrayList<>(); - List 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 loanCharges = new HashSet<>(1); - loanCharges.add(charge); - loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargesPayment, - new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges, - new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); - - loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate()); - } - /** * Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The * newly created transaction is also added to the Loan on which this method is called. @@ -230,14 +182,14 @@ public class LoanChargeService { if (loanCharge.getChargeCalculation().isPercentageBased()) { chargeAmt = loanCharge.getPercentage(); if (loanCharge.isInstalmentFee()) { - totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); + totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge); } else if (loanCharge.isOverdueInstallmentCharge()) { totalChargeAmt = loanCharge.amountOutstanding(); } } else { chargeAmt = loanCharge.amountOrPercentage(); } - loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); + update(loanCharge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); // NOTE: must add new loan charge to set of loan charges before // reprocessing the repayment schedule. @@ -250,72 +202,11 @@ public class LoanChargeService { // store Id's of existing loan transactions and existing reversed loan transactions final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loanCharge); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, loan); } - public void updateLoanCharges(final Loan loan, final Set loanCharges) { - List 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 fetchAllLoanChargeIds(final Loan loan) { - List list = new ArrayList<>(); - for (LoanCharge loanCharge : loan.getCharges()) { - list.add(loanCharge.getId()); - } - return list; - } - public BigDecimal calculateAmountPercentageAppliedTo(final Loan loan, final LoanCharge loanCharge) { if (loanCharge.isOverdueInstallmentCharge()) { return loanCharge.getAmountPercentageAppliedTo(); @@ -343,38 +234,254 @@ public class LoanChargeService { }; } - private BigDecimal getDerivedAmountForCharge(final Loan loan, final LoanCharge loanCharge) { - BigDecimal amount = BigDecimal.ZERO; - if (loan.isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) { - amount = loan.getApprovedPrincipal(); - } else { - // If charge type is specified due date and loan is multi disburment loan. - // Then we need to get as of this loan charge due date how much amount disbursed. - if (loanCharge.isSpecifiedDueDate() && loan.isMultiDisburmentLoan()) { - for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) { - if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) { - amount = amount.add(loanDisbursementDetails.principal()); + public void updateLoanCharges(final Loan loan, final Set loanCharges) { + List existingCharges = fetchAllLoanChargeIds(loan); + + /* Process new and updated charges **/ + for (final LoanCharge loanCharge : loanCharges) { + LoanCharge charge = loanCharge; + // add new charges + if (loanCharge.getId() == null) { + LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; + loanCharge.update(loan); + if (loan.getLoanProduct().isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) { + loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(loan); + for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) { + if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().getId() == null + && loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().equals(loanDisbursementDetails)) { + loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, loanDisbursementDetails); + loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); + } } } + loan.getLoanCharges().add(loanCharge); } else { - amount = loan.getPrincipal().getAmount(); + charge = loan.fetchLoanChargesById(charge.getId()); + if (charge != null) { + existingCharges.remove(charge.getId()); + } + } + final BigDecimal amount = calculateAmountPercentageAppliedTo(loan, loanCharge); + BigDecimal chargeAmt; + BigDecimal totalChargeAmt = BigDecimal.ZERO; + if (loanCharge.getChargeCalculation().isPercentageBased()) { + chargeAmt = loanCharge.getPercentage(); + if (loanCharge.isInstalmentFee()) { + totalChargeAmt = calculatePerInstallmentChargeAmount(loan, loanCharge); + } + } else { + chargeAmt = loanCharge.amountOrPercentage(); + } + if (charge != null) { + update(charge, chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), + totalChargeAmt); } } - return amount; + + /* Updated deleted charges **/ + for (Long id : existingCharges) { + loan.fetchLoanChargesById(id).setActive(false); + } + loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); } - private Money getTotalAllTrancheDisbursementAmount(final Loan loan) { + public BigDecimal calculatePerInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType, + final BigDecimal percentage) { Money amount = Money.zero(loan.getCurrency()); - if (loan.isMultiDisburmentLoan()) { - for (final LoanDisbursementDetails loanDisbursementDetail : loan.getDisbursementDetails()) { - amount = amount.plus(loanDisbursementDetail.principal()); - } + List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment installment : installments) { + amount = amount.plus(calculateInstallmentChargeAmount(loan, calculationType, percentage, installment)); } - return amount; + return amount.getAmount(); } - private BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) { - return loanCharge.calculatePerInstallmentChargeAmount(); + public Map update(final JsonCommand command, final BigDecimal amount, final LoanCharge loanCharge) { + final Map actualChanges = new LinkedHashMap<>(7); + + final String dateFormatAsInput = command.dateFormat(); + final String localeAsInput = command.locale(); + + final String dueDateParamName = "dueDate"; + if (command.isChangeInLocalDateParameterNamed(dueDateParamName, loanCharge.getDueLocalDate())) { + final String valueAsInput = command.stringValueOfParameterNamed(dueDateParamName); + actualChanges.put(dueDateParamName, valueAsInput); + actualChanges.put("dateFormat", dateFormatAsInput); + actualChanges.put("locale", localeAsInput); + + loanCharge.setDueDate(command.localDateValueOfParameterNamed(dueDateParamName)); + } + + final String amountParamName = "amount"; + if (command.isChangeInBigDecimalParameterNamed(amountParamName, loanCharge.getAmount())) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(amountParamName); + BigDecimal loanChargeAmount; + actualChanges.put(amountParamName, newValue); + actualChanges.put("locale", localeAsInput); + switch (loanCharge.getChargeCalculation()) { + case INVALID: + break; + case FLAT: + if (loanCharge.isInstalmentFee()) { + loanCharge.setAmount( + newValue.multiply(BigDecimal.valueOf(loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions()))); + } else { + loanCharge.setAmount(newValue); + } + loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding()); + break; + case PERCENT_OF_AMOUNT: + case PERCENT_OF_AMOUNT_AND_INTEREST: + case PERCENT_OF_INTEREST: + case PERCENT_OF_DISBURSEMENT_AMOUNT: + loanCharge.setPercentage(newValue); + loanCharge.setAmountPercentageAppliedTo(amount); + loanChargeAmount = BigDecimal.ZERO; + if (loanCharge.isInstalmentFee()) { + loanChargeAmount = calculatePerInstallmentChargeAmount(loanCharge.getLoan(), loanCharge.getChargeCalculation(), + loanCharge.getPercentage()); + } + if (loanChargeAmount.compareTo(BigDecimal.ZERO) == 0) { + loanChargeAmount = loanCharge.percentageOf(loanCharge.getAmountPercentageAppliedTo()); + } + loanCharge.setAmount(loanCharge.minimumAndMaximumCap(loanChargeAmount)); + loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding()); + break; + } + loanCharge.setAmountOrPercentage(newValue); + if (loanCharge.isInstalmentFee()) { + updateInstallmentCharges(loanCharge); + } + } + return actualChanges; + } + + public void populateDerivedFields(final LoanCharge loanCharge, final BigDecimal amountPercentageAppliedTo, + final BigDecimal chargeAmount, Integer numberOfRepayments, BigDecimal loanChargeAmount) { + switch (loanCharge.getChargeCalculation()) { + case INVALID: + loanCharge.setPercentage(null); + loanCharge.setAmount(null); + loanCharge.setAmountPercentageAppliedTo(null); + loanCharge.setAmountPaid(null); + loanCharge.setAmountOutstanding(BigDecimal.ZERO); + loanCharge.setAmountWaived(null); + loanCharge.setAmountWrittenOff(null); + break; + case FLAT: + loanCharge.setPercentage(null); + loanCharge.setAmountPercentageAppliedTo(null); + loanCharge.setAmountPaid(null); + if (loanCharge.isInstalmentFee()) { + if (numberOfRepayments == null) { + numberOfRepayments = loanCharge.getLoan().fetchNumberOfInstallmensAfterExceptions(); + } + loanCharge.setAmount(chargeAmount.multiply(BigDecimal.valueOf(numberOfRepayments))); + } else { + loanCharge.setAmount(chargeAmount); + } + loanCharge.setAmountOutstanding(loanCharge.getAmount()); + loanCharge.setAmountWaived(null); + loanCharge.setAmountWrittenOff(null); + break; + case PERCENT_OF_AMOUNT: + case PERCENT_OF_AMOUNT_AND_INTEREST: + case PERCENT_OF_INTEREST: + case PERCENT_OF_DISBURSEMENT_AMOUNT: + loanCharge.setPercentage(chargeAmount); + loanCharge.setAmountPercentageAppliedTo(amountPercentageAppliedTo); + if (loanChargeAmount.compareTo(BigDecimal.ZERO) == 0) { + loanChargeAmount = loanCharge.percentageOf(loanCharge.getAmountPercentageAppliedTo()); + } + loanCharge.setAmount(loanCharge.minimumAndMaximumCap(loanChargeAmount)); + loanCharge.setAmountPaid(null); + loanCharge.setAmountOutstanding(loanCharge.calculateOutstanding()); + loanCharge.setAmountWaived(null); + loanCharge.setAmountWrittenOff(null); + break; + } + loanCharge.setAmountOrPercentage(chargeAmount); + if (loanCharge.getLoan() != null && loanCharge.isInstalmentFee()) { + updateInstallmentCharges(loanCharge); + } + } + + public void update(final LoanCharge loanCharge, final BigDecimal amount, final LocalDate dueDate, final Integer numberOfRepayments) { + BigDecimal amountPercentageAppliedTo = BigDecimal.ZERO; + if (loanCharge.getLoan() != null) { + switch (loanCharge.getChargeCalculation()) { + case PERCENT_OF_AMOUNT: + // If charge type is specified due date and loan is multi disburment loan. + // Then we need to get as of this loan charge due date how much amount disbursed. + if (loanCharge.getLoan().isMultiDisburmentLoan() && loanCharge.isSpecifiedDueDate()) { + for (final LoanDisbursementDetails loanDisbursementDetails : loanCharge.getLoan().getDisbursementDetails()) { + if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) { + amountPercentageAppliedTo = amountPercentageAppliedTo.add(loanDisbursementDetails.principal()); + } + } + } else { + amountPercentageAppliedTo = loanCharge.getLoan().getPrincipal().getAmount(); + } + break; + case PERCENT_OF_AMOUNT_AND_INTEREST: + amountPercentageAppliedTo = loanCharge.getLoan().getPrincipal().getAmount() + .add(loanCharge.getLoan().getTotalInterest()); + break; + case PERCENT_OF_INTEREST: + amountPercentageAppliedTo = loanCharge.getLoan().getTotalInterest(); + break; + case PERCENT_OF_DISBURSEMENT_AMOUNT: + LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = loanCharge.getLoanTrancheDisbursementCharge(); + amountPercentageAppliedTo = loanTrancheDisbursementCharge.getloanDisbursementDetails().principal(); + break; + default: + break; + } + } + update(loanCharge, amount, dueDate, amountPercentageAppliedTo, numberOfRepayments, BigDecimal.ZERO); + } + + public LoanCharge create(final Loan loan, final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount, + final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate, + final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final BigDecimal loanChargeAmount, + final ExternalId externalId) { + final LoanCharge loanCharge = new LoanCharge(); + loanCharge.setLoan(loan); + loanCharge.setCharge(chargeDefinition); + loanCharge.setSubmittedOnDate(DateUtils.getBusinessLocalDate()); + loanCharge.setPenaltyCharge(chargeDefinition.isPenalty()); + loanCharge.setMinCap(chargeDefinition.getMinCap()); + loanCharge.setMaxCap(chargeDefinition.getMaxCap()); + loanCharge.setChargeTime(chargeTime == null ? chargeDefinition.getChargeTimeType() : chargeTime.getValue()); + + if (loanCharge.getChargeTimeType().equals(ChargeTimeType.SPECIFIED_DUE_DATE) + || loanCharge.getChargeTimeType().equals(ChargeTimeType.OVERDUE_INSTALLMENT)) { + + if (dueDate == null) { + final String defaultUserMessage = "Loan charge is missing due date."; + throw new LoanChargeWithoutMandatoryFieldException("loanChargeAmount", "dueDate", defaultUserMessage, + chargeDefinition.getId(), chargeDefinition.getName()); + } + + loanCharge.setDueDate(dueDate); + } else { + loanCharge.setDueDate(null); + } + + loanCharge.setChargeCalculation(chargeCalculation == null ? chargeDefinition.getChargeCalculation() : chargeCalculation.getValue()); + + BigDecimal chargeAmount = chargeDefinition.getAmount(); + if (amount != null) { + chargeAmount = amount; + } + + loanCharge.setChargePaymentMode(chargePaymentMode == null ? chargeDefinition.getChargePaymentMode() : chargePaymentMode.getValue()); + + populateDerivedFields(loanCharge, loanPrincipal, chargeAmount, numberOfRepayments, loanChargeAmount); + + loanCharge.setPaid(loanCharge.determineIfFullyPaid()); + loanCharge.setExternalId(externalId); + + return loanCharge; } public LoanCharge fetchLoanChargesById(final Loan loan, final Long id) { @@ -402,4 +509,246 @@ public class LoanChargeService { } } + private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { + /* + * JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down + * installment charges from the loanCharge and from each of the repayment installments and allow them to be + * recalculated fully anew. This patch is to avoid the 'merging' of existing and regenerated installment charges + * which results in the installment charges being deleted on loan approval if the schedule is regenerated. Not + * pretty. updateInstallmentCharges in LoanCharge.java: the merging looks like it will work but doesn't so this + * patch simply hits the part which 'adds all' rather than merge. Possibly an ORM issue. The issue could be to + * do with the fact that, on approval, the "recalculateLoanCharge" happens twice (probably 2 schedule + * regenerations) whereas it only happens once on Submit and Disburse (and no problems with them) + * + * if (this.loanInstallmentCharge.isEmpty()) { this.loanInstallmentCharge.addAll(newChargeInstallments); + */ + Loan loan = loanCharge.getLoan(); + if (!loan.isSubmittedAndPendingApproval() && !loan.isApproved()) { + return; + } // doing for both just in case status is not + // updated at this points + if (loanCharge.isInstalmentFee()) { + loanCharge.clearLoanInstallmentCharges(); + for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { + if (installment.isRecalculatedInterestComponent()) { + continue; // JW: does this in generateInstallmentLoanCharges - but don't understand it + } + installment.getInstallmentCharges().clear(); + } + } + } + + private void handleChargePaidTransaction(final Loan loan, final LoanCharge charge, final LoanTransaction chargesPayment, + final Integer installmentNumber) { + chargesPayment.updateLoan(loan); + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, + chargesPayment.getAmount(loan.getCurrency()).getAmount(), installmentNumber); + chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); + loan.addLoanTransaction(chargesPayment); + loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_PAYMENT, loan); + + final List chargePaymentInstallments = new ArrayList<>(); + List 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 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 remove = new HashSet<>(); + final List newChargeInstallments = generateInstallmentLoanCharges(loanCharge.getLoan(), loanCharge); + if (loanCharge.getLoanInstallmentCharge().isEmpty()) { + loanCharge.getLoanInstallmentCharge().addAll(newChargeInstallments); + } else { + int index = 0; + final List 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 generateInstallmentLoanCharges(final Loan loan, final LoanCharge loanCharge) { + final List loanChargePerInstallments = new ArrayList<>(); + if (loanCharge.isInstalmentFee()) { + List 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 fetchAllLoanChargeIds(final Loan loan) { + List 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; + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java index 4595f6e04..c7cd77ae7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java @@ -62,6 +62,8 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanLifecycleStateMachine loanLifecycleStateMachine; + private final LoanBalanceService loanBalanceService; + private final LoanTransactionService loanTransactionService; @Override public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command, @@ -91,7 +93,8 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler loanTransaction.updateLoan(loan); - final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(loanTransaction); + final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan, + loanTransaction); if (loanTransaction.isNotZero()) { loan.addLoanTransaction(loanTransaction); @@ -128,6 +131,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler .fetchLoanRepaymentScheduleInstallmentByDueDate(loanTransaction.getTransactionDate()); boolean reprocessOnPostConditions = false; + boolean processLatest = isTransactionChronologicallyLatest // && adjustedTransaction == null // covers reversals && !loan.isForeclosure() // @@ -163,7 +167,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler if (loanTransaction.isNotRecoveryRepayment()) { loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate()); } else { - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } if (loan.getLoanProduct().isMultiDisburseLoan()) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionService.java new file mode 100644 index 000000000..bc3e2f582 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionService.java @@ -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 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 lastTransactionDateForReprocessing = loanTransactionRepository + .findLastTransactionDateForReprocessing(loan); + + return lastTransactionDateForReprocessing.isEmpty() + || !DateUtils.isAfter(lastTransactionDateForReprocessing.get(), loanTransaction.getTransactionDate()); + } + + private Predicate loanTransactionForReprocessingPredicate() { + return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge() + || transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction()); + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/schedule/LoanScheduleComponent.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/schedule/LoanScheduleComponent.java index 48029d58a..4688594c7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/schedule/LoanScheduleComponent.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/schedule/LoanScheduleComponent.java @@ -25,12 +25,15 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class LoanScheduleComponent { + private final LoanBalanceService loanBalanceService; + public void updateLoanSchedule(Loan loan, final LoanScheduleModel modifiedLoanSchedule) { final List periods = modifiedLoanSchedule.getPeriods(); for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { @@ -55,7 +58,7 @@ public class LoanScheduleComponent { loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(periods, i.getInstallmentNumber())); loan.updateLoanScheduleDependentDerivedFields(); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } public void updateLoanSchedule(Loan loan, final List installments) { @@ -72,7 +75,7 @@ public class LoanScheduleComponent { loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(installments, i.getInstallmentNumber())); loan.updateLoanScheduleDependentDerivedFields(); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } private LoanRepaymentScheduleInstallment findByInstallmentNumber(final Collection installments, diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java index 2e8203134..290b3e733 100644 --- a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java @@ -43,6 +43,10 @@ import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -64,6 +68,9 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest { private ArgumentCaptor penaltyChargesWaived = ArgumentCaptor.forClass(Money.class); private ArgumentCaptor penaltyChargesWrittenOff = ArgumentCaptor.forClass(Money.class); + private final LoanChargeService loanChargeService = new LoanChargeService(mock(LoanChargeValidator.class), + mock(LoanTransactionProcessingService.class), mock(LoanLifecycleStateMachine.class), mock(LoanBalanceService.class)); + @BeforeAll public static void init() { MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); @@ -141,7 +148,8 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest { when(charge.isPenalty()).thenReturn(penalty); Loan loan = mock(Loan.class); when(loan.isInterestBearing()).thenReturn(false); - return new LoanCharge(loan, charge, new BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE, + + return loanChargeService.create(loan, charge, new BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE, ChargeCalculationType.FLAT, LocalDate.of(2023, 1, 15), ChargePaymentMode.REGULAR, 1, null, null); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index c2fffc12c..fcfa66de1 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -90,7 +90,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Abs import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; @@ -118,13 +121,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private final LoanRepositoryWrapper loanRepositoryWrapper; private final InterestRefundService interestRefundService; private final LoanScheduleComponent loanSchedule; + private final LoanTransactionService loanTransactionService; - public AdvancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, LoanRepositoryWrapper loanRepositoryWrapper, - InterestRefundService interestRefundService, ExternalIdFactory externalIdFactory, LoanScheduleComponent loanSchedule) { - super(externalIdFactory); + public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper, + final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory, + final LoanScheduleComponent loanSchedule, final LoanTransactionService loanTransactionRepository, + final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) { + super(externalIdFactory, loanChargeValidator, loanBalanceService); this.emiCalculator = emiCalculator; this.loanRepositoryWrapper = loanRepositoryWrapper; this.interestRefundService = interestRefundService; + this.loanTransactionService = loanTransactionRepository; this.loanSchedule = loanSchedule; } @@ -269,7 +276,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) { Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); - List transactions = loan.retrieveListOfTransactionsForReprocessing(); + final List transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency(); List installments = loan.getRepaymentScheduleInstallments(); Set charges = loan.getActiveCharges(); @@ -1514,7 +1521,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep newTransaction.updateLoan(loan); loan.getLoanTransactions().add(newTransaction); } - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java index dac63b237..7485d1411 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java @@ -62,6 +62,7 @@ public class InternalProgressiveLoanApiResource implements InitializingBean { private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService; private final InterestScheduleModelRepositoryWrapper writePlatformService; + private final LoanTransactionService loanTransactionService; @Override @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") @@ -94,7 +95,7 @@ public class InternalProgressiveLoanApiResource implements InitializingBean { } private ProgressiveLoanInterestScheduleModel reprocessTransactionsAndGetModel(final Loan loan) { - final List transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing(); + final List transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); final LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate(); final Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor .reprocessProgressiveLoanTransactionsTransactional(loan.getDisbursementDate(), businessDate, transactionsToReprocess, diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index ea8a7f4f7..46b4243cb 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -49,6 +49,7 @@ import java.util.Set; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -69,6 +70,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; @@ -98,8 +103,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest { private static final MockedStatic MONEY_HELPER = mockStatic(MoneyHelper.class); private AdvancedPaymentScheduleTransactionProcessor underTest; private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class); - private static final LoanRepositoryWrapper loanRepositoryWrapper = Mockito.mock(LoanRepositoryWrapper.class); - private static final LoanScheduleComponent loanSchedule = Mockito.mock(LoanScheduleComponent.class); @BeforeAll public static void init() { @@ -114,7 +117,10 @@ class AdvancedPaymentScheduleTransactionProcessorTest { @BeforeEach public void setUp() { - underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null, null, loanSchedule); + underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class), + Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class), + Mockito.mock(LoanTransactionService.class), Mockito.mock(LoanChargeValidator.class), + Mockito.mock(LoanBalanceService.class)); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 83edbd7e3..081853410 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -104,11 +104,13 @@ import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDe import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService; import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService; import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService; import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; @@ -147,7 +149,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final LoanAccrualsProcessingService loanAccrualsProcessingService; - private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; private final InterestRefundServiceDelegate interestRefundServiceDelegate; private final LoanTransactionValidator loanTransactionValidator; private final LoanForeclosureValidator loanForeclosureValidator; @@ -161,6 +162,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanTransactionProcessingService loanTransactionProcessingService; + private final LoanBalanceService loanBalanceService; + private final LoanTransactionService loanTransactionService; @Transactional @Override @@ -767,7 +770,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); final ScheduleGeneratorDTO scheduleGeneratorDTO = null; - final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); + final LoanRepaymentScheduleInstallment foreCloseDetail = loanBalanceService.fetchLoanForeclosureDetail(loan, foreClosureDate); loanAccrualsProcessingService.processAccrualsOnLoanForeClosure(loan, foreClosureDate, newTransactions); @@ -875,7 +878,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { LoanTransaction refundTransaction = LoanTransaction.refund(loan, loanTransactionType, transactionAmount, paymentDetail, transactionDate, txnExternalId); - final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(refundTransaction); + final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan, + refundTransaction); boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream() .map(LoanSupportedInterestRefundTypes::getTransactionType) @@ -1002,7 +1006,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final List newInstallments = new ArrayList<>(loan.getRepaymentScheduleInstallments()); final MonetaryCurrency currency = loan.getCurrency(); Money totalPrincipal = Money.zero(currency); - final Money[] balances = loan.retrieveIncomeForOverlappingPeriod(transactionDate); + final Money[] balances = loanBalanceService.retrieveIncomeForOverlappingPeriod(loan, transactionDate); boolean isInterestComponent = true; for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 54003773d..7afe2bb2c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -1571,7 +1571,8 @@ public class LoanScheduleAssembler { for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) { if (loanScheduleModelPeriod instanceof LoanScheduleModelDisbursementPeriod) { for (final LoanCharge loanCharge : nonCompoundingCharges) { - loanCharge.populateDerivedFields(totalInterest, loanCharge.amountOrPercentage(), null, BigDecimal.ZERO); + loanChargeService.populateDerivedFields(loanCharge, totalInterest, loanCharge.amountOrPercentage(), null, + BigDecimal.ZERO); loanScheduleModelPeriod.addLoanCharges(loanCharge.getAmountOutstanding(), BigDecimal.ZERO); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java index ea0417740..58c2927f0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java @@ -55,6 +55,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi private final BusinessEventNotifierService businessEventNotifierService; private final LoanTransactionAssembler loanTransactionAssembler; private final LoanAccountService loanAccountService; + private final LoanBalanceService loanBalanceService; @Override @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -93,7 +94,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi return; } - LocalDate closureDate = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate(); + LocalDate closureDate = loanBalanceService.isOverPaid(loan) ? loan.getOverpaidOnDate() : loan.getClosedOnDate(); // Reverse accrual activities posted after the closure date loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(closureDate)) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index 976835c2a..b50865a53 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -113,6 +113,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing private final TransactionTemplate transactionTemplate; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanChargeService loanChargeService; + private final LoanBalanceService loanBalanceService; /** * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan @@ -307,7 +308,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing // TODO implement progressive accrual case if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { - final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); + final LoanRepaymentScheduleInstallment foreCloseDetail = loanBalanceService.fetchLoanForeclosureDetail(loan, foreClosureDate); MonetaryCurrency currency = loan.getCurrency(); reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate, false); @@ -494,7 +495,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing Money interest = null; final boolean isPastPeriod = isAfterPeriod(tillDate, installment); final boolean isInPeriod = isInPeriod(tillDate, installment, false); - if (isPastPeriod || loan.isClosed() || loan.isOverPaid()) { + if (isPastPeriod || loan.isClosed() || loanBalanceService.isOverPaid(loan)) { interest = installment.getInterestCharged(currency).minus(installment.getCreditedInterest()); } else { if (isInPeriod) { // first period first day is not accrued diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java index 98ff8f82e..9ad6a79aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java @@ -61,6 +61,7 @@ public class LoanChargeAssembler { private final LoanChargeRepository loanChargeRepository; private final LoanProductRepository loanProductRepository; private final ExternalIdFactory externalIdFactory; + private final LoanChargeService loanChargeService; public Set fromParsedJson(final JsonElement element, List disbursementDetails) { JsonArray jsonDisbursement = this.fromApiJsonHelper.extractJsonArrayNamed("disbursementData", element); @@ -210,7 +211,7 @@ public class LoanChargeAssembler { .orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId)); if (!loanCharge.isTrancheDisbursementCharge() || disbursementChargeIds.contains(loanChargeId)) { - loanCharge.update(amount, dueDate, numberOfRepayments); + loanChargeService.update(loanCharge, amount, dueDate, numberOfRepayments); loanCharges.add(loanCharge); } @@ -296,7 +297,7 @@ public class LoanChargeAssembler { if (percentage == null) { percentage = chargeDefinition.getAmount(); } - loanCharge = LoanCharge.calculatePerInstallmentChargeAmount(loan, + loanCharge = loanChargeService.calculatePerInstallmentChargeAmount(loan, ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()), percentage); } @@ -314,7 +315,7 @@ public class LoanChargeAssembler { } ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId"); - return new LoanCharge(loan, chargeDefinition, amountPercentageAppliedTo, amount, chargeTime, chargeCalculation, dueDate, + return loanChargeService.create(loan, chargeDefinition, amountPercentageAppliedTo, amount, chargeTime, chargeCalculation, dueDate, chargePaymentMode, null, loanCharge, externalId); } @@ -324,7 +325,7 @@ public class LoanChargeAssembler { public LoanCharge createNewWithoutLoan(final Charge chargeDefinition, final BigDecimal loanPrincipal, final BigDecimal amount, final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate, final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final ExternalId externalId) { - return new LoanCharge(null, chargeDefinition, loanPrincipal, amount, chargeTime, chargeCalculation, dueDate, chargePaymentMode, - numberOfRepayments, BigDecimal.ZERO, externalId); + return loanChargeService.create(null, chargeDefinition, loanPrincipal, amount, chargeTime, chargeCalculation, dueDate, + chargePaymentMode, numberOfRepayments, BigDecimal.ZERO, externalId); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 48ab671b5..aa18fdc61 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -288,9 +288,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo .equals(loan.transactionProcessingStrategy())) { // [For Adv payment allocation strategy] check if charge due date is earlier than last transaction // date, if yes trigger reprocess else no reprocessing - LoanTransaction lastPaymentTransaction = loan.getLastTransactionForReprocessing(); - if (lastPaymentTransaction != null - && DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) { + final Optional lastPaymentTransactionDate = loanTransactionRepository.findLastTransactionDateForReprocessing(loan); + if (lastPaymentTransactionDate.isPresent() + && DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransactionDate.get())) { reprocessRequired = false; } } @@ -475,7 +475,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo final Map changes = new LinkedHashMap<>(3); if (loan.getActiveCharges().contains(loanCharge)) { final BigDecimal amount = loanChargeService.calculateAmountPercentageAppliedTo(loan, loanCharge); - final Map loanChargeChanges = loanCharge.update(command, amount); + final Map loanChargeChanges = loanChargeService.update(command, amount, loanCharge); changes.putAll(loanChargeChanges); loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java index 9edd3b22c..82d73a91d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java @@ -244,8 +244,8 @@ public class LoanDisbursementService { if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { externalId = ExternalId.generate(); } - final LoanCharge loanCharge = new LoanCharge(loan, chargeDefinition, principal, null, null, null, expectedDisbursementDate, - null, null, BigDecimal.ZERO, externalId); + final LoanCharge loanCharge = loanChargeService.create(loan, chargeDefinition, principal, null, null, null, + expectedDisbursementDate, null, null, BigDecimal.ZERO, externalId); LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetails); loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 0a6a793f1..c7b4a45ea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -179,6 +179,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa private final LoanForeclosureValidator loanForeclosureValidator; private final LoanTransactionMapper loanTransactionMapper; private final LoanTransactionProcessingService loadTransactionProcessingService; + private final LoanBalanceService loanBalanceService; @Override public LoanAccountData retrieveOne(final Long loanId) { @@ -2088,7 +2089,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final LocalDate earliestUnpaidInstallmentDate = DateUtils.getBusinessLocalDate(); - final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loan.fetchLoanForeclosureDetail(transactionDate); + final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanBalanceService.fetchLoanForeclosureDetail(loan, + transactionDate); BigDecimal unrecognizedIncomePortion = null; final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT); final Collection paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); @@ -2236,7 +2238,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa return loanRepositoryWrapper.existsByLoanId(loanId); } - private LoanTransaction deriveDefaultInterestWaiverTransaction(Loan loan) { + private LoanTransaction deriveDefaultInterestWaiverTransaction(final Loan loan) { final Money totalInterestOutstanding = loan.getTotalInterestOutstandingOnLoan(); Money possibleInterestToWaive = totalInterestOutstanding.copy(); LocalDate transactionDate = DateUtils.getBusinessLocalDate(); @@ -2260,4 +2262,5 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa return LoanTransaction.waiver(loan.getOffice(), loan, possibleInterestToWaive, transactionDate, possibleInterestToWaive, possibleInterestToWaive.zero(), ExternalId.empty()); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java index ed1eedc90..1da92d4a3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java @@ -61,6 +61,8 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; private final LoanTermVariationsMapper loanMapper; private final InterestScheduleModelRepositoryWrapper modelRepository; + private final LoanTransactionService loanTransactionService; + private final LoanBalanceService loanBalanceService; @Override public boolean canProcessLatestTransactionOnly(Loan loan, LoanTransaction loanTransaction, @@ -86,7 +88,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.orElse(null), getTotalRefundInterestAmount(loan)); - progressiveContext.getAlreadyProcessedTransactions().addAll(loan.retrieveListOfTransactionsForReprocessing()); + progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan)); progressiveContext.setChargedOff(loan.isChargedOff()); ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext); if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { @@ -158,7 +160,9 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc public Optional processPostDisbursementTransactions(Loan loan) { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( loan.getTransactionProcessingStrategyCode()); - final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); + final List allNonContraTransactionsPostDisbursement = loanTransactionService + .retrieveListOfTransactionsForReprocessing(loan); + final List copyTransactions = new ArrayList<>(); if (allNonContraTransactionsPostDisbursement.isEmpty()) { @@ -173,7 +177,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc loan.getDisbursementDate(), copyTransactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); return Optional.of(changedTransactionDetail); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 51ce03452..dae94b210 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -287,6 +287,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanMapper loanMapper; private final LoanTransactionProcessingService loanTransactionProcessingService; + private final LoanBalanceService loanBalanceService; + private final LoanTransactionService loanTransactionService; @Transactional @Override @@ -572,7 +574,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSED, actualDisbursementDate1); loanDisbursementService.handleDisbursementTransaction(loan, actualDisbursementDate1, paymentDetail1); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged()); /* @@ -591,11 +593,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } if (loan.getLoanProduct().isMultiDisburseLoan() || loan.isProgressiveSchedule()) { - final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); + final List allNonContraTransactionsPostDisbursement = loanTransactionService + .retrieveListOfTransactionsForReprocessing(loan); if (!allNonContraTransactionsPostDisbursement.isEmpty()) { reprocessLoanTransactionsService.reprocessTransactions(loan); } - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, loan); @@ -972,8 +975,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf newInterestPaymentWaiverTransaction.updateLoan(loan); - final boolean isTransactionChronologicallyLatest = loan - .isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction); + final boolean isTransactionChronologicallyLatest = loanTransactionService.isChronologicallyLatestRepaymentOrWaiver(loan, + newInterestPaymentWaiverTransaction); if (newInterestPaymentWaiverTransaction.isNotZero()) { loan.addLoanTransaction(newInterestPaymentWaiverTransaction); @@ -1412,7 +1415,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf Money unrecognizedIncome = transactionAmountAsMoney.zero(); Money interestComponent = transactionAmountAsMoney; if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - Money receivableInterest = loan.getReceivableInterest(transactionDate); + Money receivableInterest = loanBalanceService.getReceivableInterest(loan, transactionDate); if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) { interestComponent = receivableInterest; unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest); @@ -2024,119 +2027,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } - public void updateLoanRepaymentScheduleDates(final Loan loan, final LocalDate meetingStartDate, final String recuringRule, - final boolean isHolidayEnabled, final List 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 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 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 installments = loan.getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) { - LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); - if (!DateUtils.isBefore(oldDueDate, presentMeetingDate)) { - if (isFirstTime) { - isFirstTime = false; - newRepaymentDate = newMeetingDate; - } else { - // tmpFromDate.plusDays(1) is done to make sure - // getNewRepaymentMeetingDate method returns next meeting - // date and not the same as tmpFromDate - newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recurringRule, tmpFromDate, tmpFromDate.plusDays(1), - loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays); - } - - if (isHolidayEnabled) { - newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); - } - if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) { - latestRepaymentDate = newRepaymentDate; - } - loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); - // reset from date to get actual daysInPeriod - - loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); - - tmpFromDate = newRepaymentDate;// update with new repayment date - } else { - tmpFromDate = oldDueDate; - } - } - if (latestRepaymentDate != null) { - loan.setExpectedMaturityDate(latestRepaymentDate); - } - } - private void removeLoanCycle(final Loan loan) { final List loansToUpdate; if (loan.isGroupLoan()) { @@ -2924,7 +2814,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null); loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } - final List loanTransactions = loan.retrieveListOfTransactionsForReprocessing(); + final List loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); loanTransactions.add(chargeOffTransaction); reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions); loan.addLoanTransaction(chargeOffTransaction); @@ -3117,9 +3007,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.addLoanTransaction(chargebackTransaction); if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) { + final List transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan); loanTransactionProcessingService.reprocessLoanTransactions(loan.getTransactionProcessingStrategyCode(), - loan.getDisbursementDate(), loan.retrieveListOfTransactionsForReprocessing(), loan.getCurrency(), - loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + loan.getDisbursementDate(), transactions, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), + loan.getActiveCharges()); } else { loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargebackTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), @@ -3215,7 +3106,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.adjustNetDisbursalAmount(loan.getApprovedPrincipal()); actualChanges.put(ACTUAL_DISBURSEMENT_DATE, ""); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } return actualChanges; @@ -3246,7 +3137,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } private void reverseExistingTransactions(final Loan loan) { @@ -3319,7 +3210,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); return Optional.of(loanTransaction); } @@ -3383,7 +3274,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } changes.put("externalId", externalId); loanTransaction = LoanTransaction.writeoff(loan, loan.getOffice(), closureDate, externalId); - final boolean isLastTransaction = loan.isChronologicallyLatestTransaction(loanTransaction, loan.getLoanTransactions()); + final boolean isLastTransaction = loanTransactionRepository.isChronologicallyLatest(loanTransaction.getTransactionDate(), + loan); if (!isLastTransaction) { final String errorMessage = "The closing date of the loan must be on or after latest transaction date."; throw new InvalidLoanStateTransitionException("close.loan", "must.occur.on.or.after.latest.transaction.date", @@ -3395,7 +3287,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } else if (totalOutstanding.isGreaterThanZero()) { final String errorMessage = "A loan with money outstanding cannot be closed"; throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage, @@ -3403,8 +3295,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } - if (loan.isOverPaid()) { - final Money totalLoanOverpayment = loan.calculateTotalOverpayment(); + if (loanBalanceService.isOverPaid(loan)) { + final Money totalLoanOverpayment = loanBalanceService.calculateTotalOverpayment(loan); if (totalLoanOverpayment.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) { // TODO - KW - technically should set somewhere that this loan // has 'overpaid' amount @@ -3474,7 +3366,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate, lastDisbursalTransaction.getAmount()); - loan.updateLoanToLastDisbursalState(disbursementDetail); + loanBalanceService.updateLoanToLastDisbursalState(loan, disbursementDetail); loan.getLoanTermVariations() .removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation() && DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate)) @@ -3579,4 +3471,118 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } return !statusEnum.hasStateOf(actualLoanStatus) || isMultiTrancheDisburse; } + + private void updateLoanRepaymentScheduleDates(final Loan loan, final LocalDate meetingStartDate, final String recuringRule, + final boolean isHolidayEnabled, final List 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 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 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 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); + } + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java index 2e9d4823c..42753862b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -53,6 +53,7 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; private final EMICalculator emiCalculator; private final LoanRepositoryWrapper loanRepository; + private final LoanTransactionService loanTransactionService; private final InterestScheduleModelRepositoryWrapper modelRepository; @Override @@ -80,8 +81,8 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro } private ProgressiveLoanInterestScheduleModel calculateModel(Loan loan, LocalDate businessDate) { - List transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream() - .filter(t -> !t.isAccrualActivity()).toList(); + final List transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan) + .stream().filter(t -> !t.isAccrualActivity()).toList(); Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index 6e9757e7e..1f26b8519 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -22,6 +22,8 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -35,6 +37,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; @@ -49,15 +53,27 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; private final LoanTransactionProcessingService loanTransactionProcessingService; private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepositoryWrapper; + private final LoanBalanceService loanBalanceService; + private final LoanTransactionRepository loanTransactionRepository; @Override public void reprocessTransactions(final Loan loan) { - final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); + final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(loan); final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, allNonContraTransactionsPostDisbursement); handleChangedDetail(changedTransactionDetail); } + private List retrieveListOfTransactionsForReprocessing(final Loan loan) { + return loan.getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate()) + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); + } + + private Predicate loanTransactionForReprocessingPredicate() { + return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge() + || transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction()); + } + @Override public void reprocessParticularTransactions(final Loan loan, final List loanTransactions) { final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, loanTransactions); @@ -66,8 +82,8 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa @Override public void reprocessTransactionsWithPostTransactionChecks(final Loan loan, final LocalDate transactionDate) { - final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, - loan.retrieveListOfTransactionsForReprocessing()); + final List transactions = retrieveListOfTransactionsForReprocessing(loan); + final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, transactions); handleChangedDetail(changedTransactionDetail); } @@ -101,7 +117,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa return; } loan.getLoanCharges().remove(loanCharge); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); } private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final Loan loan, final LoanCharge loanCharge) { @@ -147,7 +163,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa .map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList(); loan.getLoanTransactions().addAll(newTransactions); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); handleChangedDetail(changedTransactionDetail); } @@ -185,7 +201,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa final List newTransactions = changedTransactionDetail.getTransactionChanges().stream() .map(TransactionChangeData::getNewTransaction).toList(); loan.getLoanTransactions().addAll(newTransactions); - loan.updateLoanSummaryDerivedFields(); + loanBalanceService.updateLoanSummaryDerivedFields(loan); return changedTransactionDetail; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java index 1992eef92..0311d062b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java @@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidat import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService; import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; @@ -91,6 +92,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService { private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanChargeValidator loanChargeValidator; private final LoanJournalEntryPoster journalEntryPoster; + private final LoanBalanceService loanBalanceService; @Override public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter, @@ -112,7 +114,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService { Money unrecognizedIncome = transactionAmountAsMoney.zero(); Money interestComponent = transactionAmountAsMoney; if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - Money receivableInterest = loan.getReceivableInterest(transactionDate); + Money receivableInterest = loanBalanceService.getReceivableInterest(loan, transactionDate); if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) { interestComponent = receivableInterest; unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java index 2901e0716..04dd3aecb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java @@ -33,6 +33,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; @@ -48,63 +51,77 @@ public class LoanAccountAutoStarter { @Bean @Conditional(CreocoreLoanRepaymentScheduleTransactionProcessorCondition.class) public CreocoreLoanRepaymentScheduleTransactionProcessor creocoreLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new CreocoreLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new CreocoreLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService); } @Bean @Conditional(EarlyRepaymentLoanRepaymentScheduleTransactionProcessorCondition.class) public EarlyPaymentLoanRepaymentScheduleTransactionProcessor earlyPaymentLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new EarlyPaymentLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new EarlyPaymentLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService); } @Bean @Conditional(MifosStandardLoanRepaymentScheduleTransactionProcessorCondition.class) public FineractStyleLoanRepaymentScheduleTransactionProcessor fineractStyleLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new FineractStyleLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new FineractStyleLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService); } @Bean @Conditional(HeavensFamilyLoanRepaymentScheduleTransactionProcessorCondition.class) public HeavensFamilyLoanRepaymentScheduleTransactionProcessor heavensFamilyLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new HeavensFamilyLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new HeavensFamilyLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService); } @Bean @Conditional(InterestPrincipalPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class) public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor interestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, + loanBalanceService); } @Bean @Conditional(PrincipalInterestPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class) public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor principalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, + loanBalanceService); } @Bean @Conditional(RBIIndiaLoanRepaymentScheduleTransactionProcessorCondition.class) - public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor(ExternalIdFactory externalIdFactory) { - return new RBILoanRepaymentScheduleTransactionProcessor(externalIdFactory); + public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor( + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new RBILoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, loanBalanceService); } @Bean @Conditional(DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorCondition.class) public DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor duePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, + loanBalanceService); } @Bean @Conditional(DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorCondition.class) public DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor duePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor( - ExternalIdFactory externalIdFactory) { - return new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(externalIdFactory); + final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { + return new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator, + loanBalanceService); } @Bean @@ -117,11 +134,13 @@ public class LoanAccountAutoStarter { @Bean @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) - public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, - LoanRepositoryWrapper loanRepositoryWrapper, - @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, ExternalIdFactory externalIdFactory, - LoanScheduleComponent loanSchedule) { + public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, + final LoanRepositoryWrapper loanRepositoryWrapper, + final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, + final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule, + final LoanTransactionService loanTransactionService, final LoanChargeValidator loanChargeValidator, + final LoanBalanceService loanBalanceService) { return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService, - externalIdFactory, loanSchedule); + externalIdFactory, loanSchedule, loanTransactionService, loanChargeValidator, loanBalanceService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index 2d4b51cf5..47f02d53e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -118,6 +118,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService; import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService; @@ -143,6 +144,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatfor import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionRelationReadService; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl; @@ -268,11 +270,11 @@ public class LoanAccountConfiguration { @Bean @ConditionalOnMissingBean(LoanChargeAssembler.class) - public LoanChargeAssembler loanChargeAssembler( - - FromJsonHelper fromApiJsonHelper, ChargeRepositoryWrapper chargeRepository, LoanChargeRepository loanChargeRepository, - LoanProductRepository loanProductRepository, ExternalIdFactory externalIdFactory) { - return new LoanChargeAssembler(fromApiJsonHelper, chargeRepository, loanChargeRepository, loanProductRepository, externalIdFactory); + public LoanChargeAssembler loanChargeAssembler(final FromJsonHelper fromApiJsonHelper, final ChargeRepositoryWrapper chargeRepository, + final LoanChargeRepository loanChargeRepository, final LoanProductRepository loanProductRepository, + final ExternalIdFactory externalIdFactory, final LoanChargeService loanChargeService) { + return new LoanChargeAssembler(fromApiJsonHelper, chargeRepository, loanChargeRepository, loanProductRepository, externalIdFactory, + loanChargeService); } @Bean @@ -331,14 +333,14 @@ public class LoanAccountConfiguration { DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository, LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService, LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper, - LoanTransactionProcessingService loanTransactionProcessingService) { + LoanTransactionProcessingService loanTransactionProcessingService, LoanBalanceService loanBalanceService) { return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository, loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService, fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService, staffReadPlatformService, paginationHelper, paymentTypeReadPlatformService, floatingRatesReadPlatformService, loanUtilService, configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator, delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService, - loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService); + loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService, loanBalanceService); } @Bean @@ -402,7 +404,8 @@ public class LoanAccountConfiguration { ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService, LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService, LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper, - LoanTransactionProcessingService loanTransactionProcessingService) { + LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService, + LoanTransactionService loanTransactionService) { return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository, @@ -417,7 +420,7 @@ public class LoanAccountConfiguration { loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService, reprocessLoanTransactionsService, loanAccountService, journalEntryPoster, loanAdjustmentService, loanAccountingBridgeMapper, loanMapper, - loanTransactionProcessingService); + loanTransactionProcessingService, loanBalanceService, loanTransactionService); } @Bean @@ -434,10 +437,12 @@ public class LoanAccountConfiguration { LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService, LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator, ReprocessLoanTransactionsService reprocessLoanTransactionsService, - LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) { + LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine, + LoanBalanceService loanBalanceService, LoanTransactionService loanTransactionService) { return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator, - reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine); + reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService, + loanTransactionService); } @Bean @@ -457,9 +462,10 @@ public class LoanAccountConfiguration { @Bean @ConditionalOnMissingBean(LoanChargeService.class) - public LoanChargeService loanChargeService(LoanChargeValidator loanChargeValidator, - LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) { - return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService, loanLifecycleStateMachine); + public LoanChargeService loanChargeService(final LoanChargeValidator loanChargeValidator, + final LoanTransactionProcessingService loanTransactionProcessingService, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanBalanceService loanBalanceService) { + return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService); } @Bean diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java index 07a9a070f..7f19ed333 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java @@ -87,12 +87,17 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -122,6 +127,9 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { @Mock private AvroDateTimeMapper mapper; + private final LoanChargeService loanChargeService = new LoanChargeService(mock(LoanChargeValidator.class), + mock(LoanTransactionProcessingService.class), mock(LoanLifecycleStateMachine.class), mock(LoanBalanceService.class)); + private MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); private static final String CUSTOM_DATA_PREFIX = "test_data_loan_delinquency_range_business_event"; @@ -275,7 +283,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { .thenReturn(installmentDelinquencyTags); when(loanForProcessing.getLoanCharges()).thenAnswer(a -> repaymentScheduleInstallments.get(0).getInstallmentCharges().stream() - .map(c -> c.getLoanCharge()).collect(Collectors.toList())); + .map(LoanInstallmentCharge::getLoanCharge).collect(Collectors.toSet())); // when LoanAccountDelinquencyRangeDataV1 data = (LoanAccountDelinquencyRangeDataV1) serializer.toAvroDTO(event); @@ -431,8 +439,9 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { } private LoanCharge buildLoanCharge(Loan loan, BigDecimal amount, Charge charge) { - LoanCharge loanCharge = new LoanCharge(loan, charge, amount, amount, ChargeTimeType.SPECIFIED_DUE_DATE, ChargeCalculationType.FLAT, - LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, new BigDecimal(100), ExternalId.generate()); + LoanCharge loanCharge = loanChargeService.create(loan, charge, amount, amount, ChargeTimeType.SPECIFIED_DUE_DATE, + ChargeCalculationType.FLAT, LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, new BigDecimal(100), + ExternalId.generate()); ReflectionTestUtils.setField(loanCharge, "id", 1L); return loanCharge; } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java index 32ac828bf..ac92c81a8 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachineTest.java @@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -48,6 +49,9 @@ class DefaultLoanLifecycleStateMachineTest { @Mock private BusinessEventNotifierService businessEventNotifierService; + @Mock + private LoanBalanceService loanBalanceService; + private DefaultLoanLifecycleStateMachine underTest; private MockedStatic moneyHelperStatic; @@ -58,7 +62,7 @@ class DefaultLoanLifecycleStateMachineTest { moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelperStatic.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); - underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService); + underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService, loanBalanceService); } @AfterEach diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java index 7cd31e6ab..fa7807957 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java @@ -41,6 +41,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.rate.domain.Rate; @@ -61,7 +63,7 @@ public class LoanBuilder { private Staff loanOfficer; private CodeValue loanPurpose; private LoanRepaymentScheduleTransactionProcessor transactionProcessor = new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor( - mock(ExternalIdFactory.class)); + mock(ExternalIdFactory.class), mock(LoanChargeValidator.class), mock(LoanBalanceService.class)); private LoanProductRelatedDetail loanRepaymentScheduleDetail; private LoanStatus loanStatus = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL; private LoanSubStatus loanSubStatus; diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java index e0f725be5..06c8e218e 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java @@ -265,8 +265,23 @@ public class LoanTest { * @return the {@link LoanCharge} */ private LoanCharge buildLoanCharge() { - return new LoanCharge(mock(Loan.class), mock(Charge.class), new BigDecimal(100), new BigDecimal(100), - ChargeTimeType.TRANCHE_DISBURSEMENT, ChargeCalculationType.FLAT, LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, - new BigDecimal(100), ExternalId.generate()); + final LoanCharge loanCharge = new LoanCharge(); + + loanCharge.setLoan(mock(Loan.class)); + loanCharge.setCharge(mock(Charge.class)); + loanCharge.setAmount(new BigDecimal(100)); + loanCharge.setAmountOutstanding(new BigDecimal(100)); + loanCharge.setChargeTime(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()); + loanCharge.setChargeCalculation(ChargeCalculationType.FLAT.getValue()); + loanCharge.setDueDate(LocalDate.of(2022, 6, 27)); + loanCharge.setChargePaymentMode(ChargePaymentMode.REGULAR.getValue()); + loanCharge.setPercentage(null); + loanCharge.setAmountPercentageAppliedTo(null); + loanCharge.setAmountPaid(null); + loanCharge.setAmountWaived(null); + loanCharge.setAmountWrittenOff(null); + loanCharge.setExternalId(ExternalId.generate()); + + return loanCharge; } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java index 474db7b09..00c3467f9 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.mock; import java.math.BigDecimal; import java.math.MathContext; @@ -34,6 +35,7 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -44,6 +46,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -99,7 +103,8 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio @BeforeEach public void setUp() { - underTest = new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(null); + underTest = new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(mock(ExternalIdFactory.class), + mock(LoanChargeValidator.class), mock(LoanBalanceService.class)); Mockito.reset(charges, transactionMappings); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java index 94f2d609e..0b0d212b7 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.mock; import java.math.BigDecimal; import java.math.MathContext; @@ -34,6 +35,7 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -44,6 +46,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -100,7 +104,8 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio @BeforeEach public void setUp() { - underTest = new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(null); + underTest = new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(mock(ExternalIdFactory.class), + mock(LoanChargeValidator.class), mock(LoanBalanceService.class)); Mockito.reset(charges, transactionMappings); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java index 77c74d56c..7323ea61b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java @@ -111,13 +111,20 @@ public class LoanDownPaymentHandlerServiceImplTest { @Mock private LoanLifecycleStateMachine loanLifecycleStateMachine; + @Mock + private LoanBalanceService loanBalanceService; + + @Mock + private LoanTransactionService loanTransactionService; + private LoanDownPaymentHandlerServiceImpl underTest; @BeforeEach public void setUp() { underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator, - reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine); + reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine, loanBalanceService, + loanTransactionService); moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true);