From c33f27093025261e122fb1ed2f6ce34fe62c14f3 Mon Sep 17 00:00:00 2001 From: Faheem Ahmad Date: Wed, 20 Nov 2024 11:27:54 +0500 Subject: [PATCH] SU-320 (#1298) * SU-320 --- .../portfolio/loanaccount/domain/Loan.java | 25 +++++++- .../LoanRepaymentScheduleInstallment.java | 15 ++++- .../domain/LoanSummaryWrapper.java | 8 ++- ...edPaymentScheduleTransactionProcessor.java | 61 +++++++++++++++---- ...stractCumulativeLoanScheduleGenerator.java | 21 +++++++ .../service/LoanReadPlatformServiceImpl.java | 6 +- 6 files changed, 116 insertions(+), 20 deletions(-) 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 5877d88a3ca..39ed1b99e0c 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 @@ -4219,9 +4219,17 @@ private Money calculateTotalOverpayment() { .plus(scheduledRepayment.getPenaltyChargesWrittenOff(currency)); cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments .plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency))) - .plus(scheduledRepayment.getFeeChargesPaid(currency)) - .plus(scheduledRepayment.getPenaltyChargesPaid(currency).plus(scheduledRepayment.getAdvancePrincipalAmount())) + .plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency)) .plus(scheduleWrittenOffValue); + if (scheduledRepayment.isLastInstallment(installments) && scheduledRepayment.isOverpaidInAdvance(currency) + && scheduledRepayment.getAdvancePrincipalAmount().compareTo(BigDecimal.ZERO) > 0) { + cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments + .plus(scheduledRepayment.getInterestWaived(currency)); + // Do not add advance payment amount if installment was overpaid + continue; + } else { + cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments.plus(scheduledRepayment.getAdvancePrincipalAmount()); + } cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency)); } @@ -4239,7 +4247,18 @@ private Money calculateTotalOverpayment() { // if total paid in transactions doesnt match repayment schedule then // theres an overpayment. - return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments); + Money overpaid = totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments); + if (overpaid.isZero()) { + Money totalPrincipalPaid = Money.zero(this.getCurrency()); + for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { + totalPrincipalPaid = totalPrincipalPaid.add( + scheduledRepayment.getPrincipalCompleted(this.getCurrency()).plus(scheduledRepayment.getAdvancePrincipalAmount())); + } + if (totalPrincipalPaid.isGreaterThan(this.getPrincipal())) { + overpaid = totalPrincipalPaid.minus(this.getPrincipal()); + } + } + return overpaid; } public Money calculateTotalRecoveredPayments() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index ce617d24b67..2817e35ca92 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -990,6 +990,8 @@ public Money payInterestComponent(final LocalDate transactionDate, final Money t this.originalInterestChargedAmount = this.interestCharged; this.interestCharged = getInterestPaid(currency).plus(getInterestWaived(currency)).plus(getInterestWrittenOff(currency)) .plus(interestDue).getAmount(); + } else { + this.originalInterestChargedAmount = BigDecimal.ZERO; } } else { @@ -1261,7 +1263,7 @@ public void updateDerivedFields(final MonetaryCurrency currency, final LocalDate } } - private void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency, + public void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency, final Money amountPaidInRepaymentPeriod) { if (isInAdvance(transactionDate)) { this.totalPaidInAdvance = asMoney(this.totalPaidInAdvance, currency).plus(amountPaidInRepaymentPeriod).getAmount(); @@ -1330,6 +1332,7 @@ public void updateInterestCharged(final BigDecimal interestCharged) { } public void updateObligationMet(final Boolean obligationMet) { + this.obligationsMet = obligationMet; } @@ -1668,4 +1671,14 @@ public void setInterestRecalculatedOnDate(LocalDate interestRecalculatedOnDate) this.interestRecalculatedOnDate = interestRecalculatedOnDate; } + public boolean isLastInstallment(List installments) { + return this.installmentNumber.equals(installments.get(installments.size() - 1).getInstallmentNumber()); + } + + public boolean isOverpaidInAdvance(MonetaryCurrency currency) { + return this.getPrincipal(currency).isGreaterThanZero() && this.getInterestCharged(currency).isZero() + && this.getFeeChargesCharged(currency).isZero() && this.getPenaltyChargesCharged(currency).isZero() + && this.isRecalculatedInterestComponent(); + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java index e357139d7dc..0bcc70ea391 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryWrapper.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.Set; @@ -37,7 +38,12 @@ public Money calculateTotalPrincipalRepaid(final List 0) { + continue; + } else { + total = total.plus(installment.getAdvancePrincipalAmount()); + } } return total; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index f30802f6bf6..ceea2793cd5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -1172,10 +1172,14 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo } // For having similar logic we are populating installment list even when the future installment // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element. + // As per SU+ requirements, advance payment goes to outstanding balance so first immediate advance + // installment + // will always be seleted List inAdvanceInstallments = new ArrayList<>(); if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getFromDate())).toList(); + .filter(e -> loanTransaction.isBefore(e.getFromDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) .filter(e -> loanTransaction.isBefore(e.getFromDate())) @@ -1187,7 +1191,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo } int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); - + boolean stopProcessingAdvanceInstallment = false; for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { @@ -1220,7 +1224,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo } } case IN_ADVANCE -> { - if (loanTransaction.doNotProcessAdvanceInstallments()) { + if (loanTransaction.doNotProcessAdvanceInstallments() || stopProcessingAdvanceInstallment) { // This condition will only be true if loan processing type is VERTICAL. // For vertical payments, Past Due and Due installments MUST be processed Horizontally exit = true; @@ -1252,18 +1256,49 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo Money zero = transactionAmountUnprocessed.zero(); for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { if (transactionAmountUnprocessed.isGreaterThanZero()) { - loanTransaction.updateComponents(transactionAmountUnprocessed, Money.zero(currency), - Money.zero(currency), Money.zero(currency)); - inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount() - .add(transactionAmountUnprocessed.getAmount())); - inAdvanceInstallment.setRecalculateEMI(loanTransaction.recalculateEMI()); - LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( - transactionMappings, loanTransaction, inAdvanceInstallment, currency); - addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed, - zero, zero, zero); + if (inAdvanceInstallment.isLastInstallment(installments) + && inAdvanceInstallment.isOverpaidInAdvance(currency) && transactionAmountUnprocessed + .isGreaterThan(inAdvanceInstallment.getPrincipal(currency))) { + // This MUST be true only in case of advance overpayment after repayment + // schedule is regenerated + // Process principal and move the remaining amount to overpaid + + Money paidPrincipalComponent = inAdvanceInstallment.payPrincipalComponent( + loanTransaction.getTransactionDate(), transactionAmountUnprocessed, false, + loanTransaction); + + inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount() + .add(transactionAmountUnprocessed.getAmount())); + + balances.setAggregatedPrincipalPortion( + balances.getAggregatedPrincipalPortion().add(transactionAmountUnprocessed)); + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, inAdvanceInstallment, currency); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed, + zero, zero, zero); + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPrincipalComponent); + stopProcessingAdvanceInstallment = true; + + } else { + balances.setAggregatedPrincipalPortion( + balances.getAggregatedPrincipalPortion().add(transactionAmountUnprocessed)); + inAdvanceInstallment.checkIfRepaymentPeriodObligationsAreMet( + loanTransaction.getTransactionDate(), currency); + + inAdvanceInstallment.trackAdvanceAndLateTotalsForRepaymentPeriod( + loanTransaction.getTransactionDate(), currency, transactionAmountUnprocessed); + inAdvanceInstallment.setAdvancePrincipalAmount(inAdvanceInstallment.getAdvancePrincipalAmount() + .add(transactionAmountUnprocessed.getAmount())); + inAdvanceInstallment.setRecalculateEMI(loanTransaction.recalculateEMI()); + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, inAdvanceInstallment, currency); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, transactionAmountUnprocessed, + zero, zero, zero); + + transactionAmountUnprocessed = Money.zero(currency); + } } } - transactionAmountUnprocessed = Money.zero(currency); exit = true; } else { exit = true; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index 947195ac20a..6e8611b9889 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -3070,6 +3070,27 @@ private LoanScheduleDTO rescheduleNextInstallmentsForProgressiveLoans(final Math advancePayments.add(new RecalculationDetail(loanTransaction.getTransactionDate(), loanTransaction)); } } + // Extra amount paid in advance + if (outstandingBalance.isLessThanZero()) { + // If overpaid in advance then add the last installment to keep track of advance payment amount + outstandingBalanceAsPerRest = outstandingBalance.zero(); + outstandingBalance = outstandingBalance.zero(); + BigDecimal remainingLoanPrincipal = principalToBeScheduled.getAmount(); + for (LoanRepaymentScheduleInstallment inst : loan.getRepaymentScheduleInstallments()) { + if (inst.getInstallmentNumber().intValue() < installment.getInstallmentNumber()) { + remainingLoanPrincipal = remainingLoanPrincipal + .subtract(inst.getPrincipal(loanApplicationTerms.getCurrency()).getAmount() + .subtract(inst.getAdvancePrincipalAmount())); + } + } + installment.setPrincipal(remainingLoanPrincipal); + installment.setInterestCharged(BigDecimal.ZERO); + installment.setFeeChargesCharged(BigDecimal.ZERO); + installment.setPenaltyCharges(BigDecimal.ZERO); + installment.setRecalculatedInterestComponent(true); + installment.getInstallmentCharges().clear(); + newRepaymentScheduleInstallments.add(installment); + } //// break; } 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 ece1e27004d..9d7b51d05cb 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 @@ -1525,8 +1525,10 @@ public LoanScheduleData extractData(@NotNull final ResultSet rs) throws SQLExcep this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(principalDue); } BigDecimal advancePrincipalAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "advancePrincipalAmount"); - this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(advancePrincipalAmount); - outstandingPrincipalBalanceOfLoan = outstandingPrincipalBalanceOfLoan.subtract(advancePrincipalAmount); + if (this.outstandingLoanPrincipalBalance.compareTo(BigDecimal.ZERO) > 0) { + this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.subtract(advancePrincipalAmount); + outstandingPrincipalBalanceOfLoan = outstandingPrincipalBalanceOfLoan.subtract(advancePrincipalAmount); + } final boolean isDownPayment = rs.getBoolean("isDownPayment"); LoanSchedulePeriodData periodData;