diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature index 0f92124a450..3746a453889 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature @@ -3166,3 +3166,79 @@ Feature: LoanRepayment Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 1250.0 | 39.33 | 0.0 | 0.0 | 1289.33 | 0.0 | 0.0 | 0.0 | 1289.33 | + + @AdvancedPaymentAllocation @ProgressiveLoanSchedule + Scenario: Verify AdvancedPaymentAllocation behaviour: loanScheduleProcessingType-horizontal, charge after maturity, in advanced repayment (future installment type: NEXT_INSTALLMENT) + When Global config "charge-accrual-date" value set to "submitted-date" + When Admin sets the business date to "01 September 2023" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 September 2023 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 September 2023" with "1000" amount and expected disbursement date on "01 September 2023" + When Admin successfully disburse the loan on "01 September 2023" with "1000" EUR transaction amount + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 September 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 01 September 2023 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 2 | 15 | 16 September 2023 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 3 | 15 | 01 October 2023 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 4 | 15 | 16 October 2023 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | + | 01 September 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | +# Add charge after maturity + When Admin adds "LOAN_NSF_FEE" due date charge with "17 October 2023" due date and 20 EUR transaction amount + Then Loan Repayment schedule has 5 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 September 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 01 September 2023 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 2 | 15 | 16 September 2023 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 3 | 15 | 01 October 2023 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 4 | 15 | 16 October 2023 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 5 | 1 | 17 October 2023 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 0.0 | 0.0 | 0.0 | 1020.0 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | + | 01 September 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | + Then Loan Charges tab has the following data: + | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | + | NSF fee | true | Specified due date | 17 October 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | +# Make due date repayments + And Customer makes "AUTOPAY" repayment on "01 September 2023" with 250 EUR transaction amount + When Admin sets the business date to "02 September 2023" + When Admin runs inline COB job for Loan + When Admin sets the business date to "02 September 2023" + And Customer makes "AUTOPAY" repayment on "02 September 2023" with 250 EUR transaction amount + Then Loan Repayment schedule has 5 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 September 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 01 September 2023 | 01 September 2023 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | + | 2 | 15 | 16 September 2023 | 02 September 2023 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 250.0 | 0.0 | 0.0 | + | 3 | 15 | 01 October 2023 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 4 | 15 | 16 October 2023 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | + | 5 | 1 | 17 October 2023 | | 0.0 | 0.0 | 0.0 | 0.0 | 20.0 | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 1000.0 | 0.0 | 0.0 | 20.0 | 1020.0 | 500.0 | 250.0 | 0.0 | 520.0 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | + | 01 September 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | + | 01 September 2023 | Repayment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 750.0 | + | 01 September 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | + | 02 September 2023 | Repayment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 500.0 | + Then Loan Charges tab has the following data: + | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | + | NSF fee | true | Specified due date | 17 October 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 | + When Admin sets the business date to "03 September 2023" + When Admin runs inline COB job for Loan + #Make backdated repayment to trigger loan transaction reprocessing + And Customer makes "AUTOPAY" repayment on "01 September 2023" with 250 EUR transaction amount + When Admin sets the business date to "04 September 2023" + #Run COB to check there is no accounting meltdown and accrual is handled properly + When Admin runs inline COB job for Loan \ No newline at end of file diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java index e70a9f521b9..fb102fd5818 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidBy.java @@ -60,18 +60,10 @@ public LoanTransaction getLoanTransaction() { return this.loanTransaction; } - public void setLoanTransaction(final LoanTransaction loanTransaction) { - this.loanTransaction = loanTransaction; - } - public LoanCharge getLoanCharge() { return this.loanCharge; } - public void setLoanCharge(final LoanCharge loanCharge) { - this.loanCharge = loanCharge; - } - public BigDecimal getAmount() { return this.amount; } @@ -83,4 +75,8 @@ public void setAmount(final BigDecimal amount) { public Integer getInstallmentNumber() { return this.installmentNumber; } + + public void setInstallmentNumber(Integer installmentNumber) { + this.installmentNumber = installmentNumber; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 6444da3e071..fef7d0f6635 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -245,10 +245,14 @@ public static int fetchFirstNormalInstallmentNumber(List installments) { int firstPeriod = fetchFirstNormalInstallmentNumber(installments); - return (targetInstallment.getInstallmentNumber().equals(firstPeriod) - ? !DateUtils.isBefore(transactionDate, targetInstallment.getFromDate()) - : DateUtils.isAfter(transactionDate, targetInstallment.getFromDate())) - && !DateUtils.isAfter(transactionDate, targetInstallment.getDueDate()); + return isInPeriod(transactionDate, targetInstallment, targetInstallment.getInstallmentNumber().equals(firstPeriod)); } + private static boolean isInPeriod(LocalDate transactionDate, LoanRepaymentScheduleInstallment targetInstallment, + boolean isFirstPeriod) { + LocalDate fromDate = targetInstallment.getFromDate(); + LocalDate dueDate = targetInstallment.getDueDate(); + return isFirstPeriod ? DateUtils.occursOnDayFromAndUpToAndIncluding(fromDate, dueDate, transactionDate) + : DateUtils.occursOnDayFromExclusiveAndUpToAndIncluding(fromDate, dueDate, transactionDate); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java index 506ae8a108d..83c0bc07f31 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java @@ -18,14 +18,16 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.function.Predicate; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; -import org.jetbrains.annotations.NotNull; +import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; /** * A wrapper around loan schedule related data exposing needed behaviour by loan. @@ -34,42 +36,61 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper { public void reprocess(final MonetaryCurrency currency, final LocalDate disbursementDate, final List repaymentPeriods, LoanCharge loanCharge) { - - Money totalInterest = Money.zero(currency); - Money totalPrincipal = Money.zero(currency); + Loan loan = loanCharge.getLoan(); + Money zero = Money.zero(currency); + Money totalInterest = zero; + Money totalPrincipal = zero; for (final LoanRepaymentScheduleInstallment installment : repaymentPeriods) { totalInterest = totalInterest.plus(installment.getInterestCharged(currency)); totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); } + LoanChargePaidBy accrualBy = null; + if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate()) { // TODO: why only if not interest bearing + LoanRepaymentScheduleInstallment addedPeriod = addChargeOnlyRepaymentInstallmentIfRequired(loanCharge, repaymentPeriods); + if (addedPeriod != null) { + addedPeriod.updateObligationsMet(currency, disbursementDate); + } + accrualBy = loanCharge.getLoanChargePaidBySet().stream().filter(e -> e.getLoanTransaction().isAccrual()).findFirst() + .orElse(null); + } LocalDate startDate = disbursementDate; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(repaymentPeriods); for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { + if (period.isDownPayment()) { + continue; + } + boolean installmentChargeApplicable = !period.isRecalculatedInterestComponent(); + boolean isFirstNonDownPaymentPeriod = period.getInstallmentNumber().equals(firstNormalInstallmentNumber); + LocalDate dueDate = period.getDueDate(); + final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, dueDate, loanCharge, currency, period, + totalPrincipal, totalInterest, installmentChargeApplicable, isFirstNonDownPaymentPeriod); + final Money feeChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, dueDate, loanCharge, currency, + installmentChargeApplicable, isFirstNonDownPaymentPeriod, feeCharge()); + final Money feeChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, dueDate, loanCharge, currency, + installmentChargeApplicable, isFirstNonDownPaymentPeriod, feeCharge()); + + Predicate penaltyPredicate = LoanCharge::isPenaltyCharge; + final Money penaltyChargesDueForRepaymentPeriod = penaltyChargesDueWithin(startDate, dueDate, loanCharge, currency, period, + totalPrincipal, totalInterest, installmentChargeApplicable, isFirstNonDownPaymentPeriod); + final Money penaltyChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, dueDate, loanCharge, currency, + installmentChargeApplicable, isFirstNonDownPaymentPeriod, penaltyPredicate); + final Money penaltyChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, dueDate, loanCharge, currency, + installmentChargeApplicable, isFirstNonDownPaymentPeriod, penaltyPredicate); - if (!period.isDownPayment()) { - boolean isFirstNonDownPaymentPeriod = period.getInstallmentNumber().equals(firstNormalInstallmentNumber); - - final Money feeChargesDueForRepaymentPeriod = feeChargesDueWithin(startDate, period.getDueDate(), loanCharge, currency, - period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); - final Money feeChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, period.getDueDate(), loanCharge, currency, - !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, feeCharge()); - final Money feeChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, period.getDueDate(), loanCharge, - currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, feeCharge()); - - final Money penaltyChargesDueForRepaymentPeriod = penaltyChargesDueWithin(startDate, period.getDueDate(), loanCharge, - currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), - isFirstNonDownPaymentPeriod); - final Money penaltyChargesWaivedForRepaymentPeriod = chargesWaivedWithin(startDate, period.getDueDate(), loanCharge, - currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, LoanCharge::isPenaltyCharge); - final Money penaltyChargesWrittenOffForRepaymentPeriod = loanChargesWrittenOffWithin(startDate, period.getDueDate(), - loanCharge, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod, - LoanCharge::isPenaltyCharge); - - period.addToChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, - feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, - penaltyChargesWrittenOffForRepaymentPeriod); - - startDate = period.getDueDate(); + period.addToChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, + feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, + penaltyChargesWrittenOffForRepaymentPeriod); + + if (accrualBy != null && period.isAdditional() + && loanChargeIsDue(startDate, dueDate, isFirstNonDownPaymentPeriod, loanCharge)) { + Money amount = Money.of(currency, accrualBy.getAmount()); + boolean isFee = loanCharge.isFeeCharge(); + period.updateAccrualPortion(period.getInterestAccrued(currency), + MathUtil.plus(period.getFeeAccrued(currency), (isFee ? amount : null)), + MathUtil.plus(period.getPenaltyAccrued(currency), (isFee ? null : amount))); + accrualBy.setInstallmentNumber(period.getInstallmentNumber()); } + startDate = dueDate; } } @@ -198,17 +219,53 @@ private BigDecimal getInstallmentFee(MonetaryCurrency currency, LoanRepaymentSch } @NotNull - private BigDecimal getBaseAmount(MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge, + private BigDecimal getBaseAmount(MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, LoanCharge loanCharge, BigDecimal amount) { - if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { - amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()) - .add(period.getInterestCharged(monetaryCurrency).getAmount()); - } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { - amount = amount.add(period.getInterestCharged(monetaryCurrency).getAmount()); - } else { - amount = amount.add(period.getPrincipal(monetaryCurrency).getAmount()); + BigDecimal baseAmount = getBaseAmount(loanCharge, period.getPrincipal(currency).getAmount(), + period.getInterestCharged(currency).getAmount()); + return MathUtil.add(amount, baseAmount); + } + + @NotNull + private BigDecimal getBaseAmount(LoanCharge loanCharge, BigDecimal principal, BigDecimal interest) { + ChargeCalculationType calcType = loanCharge.getChargeCalculation(); + if (calcType.isPercentageOfAmountAndInterest()) { + return MathUtil.add(principal, interest); + } + if (calcType.isPercentageOfInterest()) { + return interest; } - return amount; + return principal; } + /** + * @return newly added period if there is any + */ + public LoanRepaymentScheduleInstallment addChargeOnlyRepaymentInstallmentIfRequired(@NotNull LoanCharge loanCharge, + List installments) { + if (installments == null) { + return null; + } + if (!loanCharge.isSpecifiedDueDate()) { + return null; + } + LocalDate chargeDueDate = loanCharge.getEffectiveDueDate(); + LoanRepaymentScheduleInstallment latestInstallment = installments.stream().filter(i -> !i.isDownPayment()) + .reduce((first, second) -> second).orElseThrow(); + if (!DateUtils.isAfter(chargeDueDate, latestInstallment.getDueDate())) { + return null; + } + if (latestInstallment.isAdditional()) { + latestInstallment.updateDueDate(chargeDueDate); + return null; + } else { + Loan loan = loanCharge.getLoan(); + final LoanRepaymentScheduleInstallment additionalInstallment = new LoanRepaymentScheduleInstallment(loan, + (loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestInstallment.getDueDate(), chargeDueDate, BigDecimal.ZERO, + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); + additionalInstallment.markAsAdditional(); + loan.addLoanRepaymentScheduleInstallment(additionalInstallment); + return additionalInstallment; + } + } } 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 e5978412eba..73d909b05bc 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 @@ -46,6 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; @@ -62,6 +63,8 @@ */ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor { + public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); + @Override public boolean accept(String s) { return getCode().equalsIgnoreCase(s) || getName().equalsIgnoreCase(s); @@ -510,7 +513,6 @@ protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransac newLoanTransaction.getLoanTransactionRelations().add( LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED)); changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction); - } protected void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency, @@ -887,33 +889,17 @@ private LoanCharge findLatestPaidChargeFromUnOrderedSet(final Set ch protected void addChargeOnlyRepaymentInstallmentIfRequired(Set charges, List installments) { - if (!CollectionUtils.isEmpty(charges) && !CollectionUtils.isEmpty(installments)) { - LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = installments.stream().filter(i -> !i.isDownPayment()) - .reduce((first, second) -> second).orElseThrow(); - LocalDate installmentDueDate = null; - - LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges); - if (latestCharge != null - && DateUtils.isAfter(latestCharge.getEffectiveDueDate(), latestRepaymentScheduleInstalment.getDueDate())) { - installmentDueDate = latestCharge.getEffectiveDueDate(); - } - - if (installmentDueDate != null) { - if (latestRepaymentScheduleInstalment.isAdditional()) { - latestRepaymentScheduleInstalment.updateDueDate(installmentDueDate); - } else { - Loan loan = latestCharge.getLoan(); - final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, - (installments.size() + 1), latestRepaymentScheduleInstalment.getDueDate(), installmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); - installment.markAsAdditional(); - loan.addLoanRepaymentScheduleInstallment(installment); - } - } + LoanCharge latestCharge = getLatestLoanChargeWithSpecificDueDate(charges); + if (latestCharge == null) { + return; } + loanChargeProcessor.addChargeOnlyRepaymentInstallmentIfRequired(latestCharge, installments); } - private LoanCharge getLatestLoanChargeWithSpecificDueDate(Set charges) { + protected LoanCharge getLatestLoanChargeWithSpecificDueDate(Set charges) { + if (charges == null) { + return null; + } LoanCharge latestCharge = null; List chargesWithSpecificDueDate = new ArrayList<>(); chargesWithSpecificDueDate.addAll(charges.stream().filter(charge -> charge.isSpecifiedDueDate()).toList()); 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 31c2ca10fa2..ada2be619ff 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 @@ -132,7 +132,9 @@ private static LoanCharge createCharge(boolean penalty) { when(charge.getName()).thenReturn("charge a"); when(charge.getCurrencyCode()).thenReturn("UDS"); when(charge.isPenalty()).thenReturn(penalty); - LoanCharge loanCharge = new LoanCharge(null, charge, new BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE, + Loan loan = mock(Loan.class); + when(loan.isInterestBearing()).thenReturn(false); + LoanCharge loanCharge = new LoanCharge(loan, charge, new BigDecimal(1000), new BigDecimal(10), ChargeTimeType.SPECIFIED_DUE_DATE, ChargeCalculationType.FLAT, LocalDate.of(2023, 01, 15), ChargePaymentMode.REGULAR, 1, null, null); return loanCharge; } @@ -150,5 +152,4 @@ private LoanRepaymentScheduleInstallment createPeriod(int periodId, LocalDate st Mockito.when(period.getInterestCharged(eq(currency))).thenReturn(interest); return period; } - } 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 a9b5773b94e..9353dc1ce69 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 @@ -72,7 +72,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; -import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; @@ -95,8 +94,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy"; - public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); - public final EMICalculator emiCalculator; @Override @@ -162,8 +159,6 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement installments.removeIf(LoanRepaymentScheduleInstallment::isReAged); installments.removeIf(LoanRepaymentScheduleInstallment::isAdditional); - addChargeOnlyRepaymentInstallmentIfRequired(charges, installments); - for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { currentInstallment.resetBalances(); currentInstallment.updateObligationsMet(currency, disbursementDate); 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 2b9855d0d25..ece6bcdd8bf 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 @@ -838,7 +838,7 @@ private void reprocessPeriodicAccruals(Loan loan, final Collection installments = loan.getRepaymentScheduleInstallments(); boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge() - .equalsIgnoreCase("submitted-date"); + .equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE); for (LoanRepaymentScheduleInstallment installment : installments) { checkAndUpdateAccrualsForInstallment(loan, accruals, installments, isBasedOnSubmittedOnDate, installment); } 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 12295fd0574..34598a69d37 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 @@ -1006,7 +1006,6 @@ private void validateAddLoanCharge(final Loan loan, final Charge chargeDefinitio } private boolean addCharge(final Loan loan, final Charge chargeDefinition, LoanCharge loanCharge) { - if (!loan.hasCurrencyCodeOf(chargeDefinition.getCurrencyCode())) { final String errorMessage = "Charge and Loan must have the same currency."; throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage); @@ -1021,24 +1020,7 @@ private boolean addCharge(final Loan loan, final Charge chargeDefinition, LoanCh } } - if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate()) { - LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = loan.getRepaymentScheduleInstallments() - .get(loan.getLoanRepaymentScheduleInstallmentsSize() - 1); - if (DateUtils.isAfter(loanCharge.getDueDate(), latestRepaymentScheduleInstalment.getDueDate())) { - if (latestRepaymentScheduleInstalment.isAdditional()) { - latestRepaymentScheduleInstalment.updateDueDate(loanCharge.getDueDate()); - } else { - final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, - (loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestRepaymentScheduleInstalment.getDueDate(), - loanCharge.getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); - installment.markAsAdditional(); - loan.addLoanRepaymentScheduleInstallment(installment); - } - } - } - loan.addLoanCharge(loanCharge); - loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge); // we want to apply charge transactions only for those loans charges that are applied when a loan is active and