This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 349021957 FINERACT-2148: Backdated Charge-off with interest 
recalculation
349021957 is described below

commit 3490219577e0d24c501c8b292c494583b28699b2
Author: Adam Saghy <[email protected]>
AuthorDate: Fri Dec 6 11:00:10 2024 +0100

    FINERACT-2148: Backdated Charge-off with interest recalculation
---
 .../test/resources/features/LoanChargeOff.feature  | 33 ++++++++++
 .../portfolio/loanaccount/domain/Loan.java         |  6 +-
 ...dvancedPaymentScheduleTransactionProcessor.java | 75 ++++++++++++----------
 .../LoanInterestRecalculationCOBBusinessStep.java  |  2 +-
 .../serialization/LoanForeclosureValidator.java    |  2 +-
 .../LoanWritePlatformServiceJpaRepositoryImpl.java | 20 +++++-
 .../ProgressiveLoanSummaryDataProvider.java        |  7 +-
 7 files changed, 105 insertions(+), 40 deletions(-)

diff --git 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature
index 87596c48f..546cdc657 100644
--- 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature
+++ 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeOff.feature
@@ -1578,3 +1578,36 @@ Feature: Charge-off
       | ASSET   | 112601       | Loans Receivable           | 650.0 |        |
       | EXPENSE | 744037       | Credit Loss/Bad Debt-Fraud |       | 650.0  |
 
+  @TestRailId:C3326 @AdvancedPaymentAllocation
+  Scenario: Verify the repayment schedule is updated before the Charge-off in 
case of interest recalculation = true
+    When Admin sets the business date to "01 January 2024"
+    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_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_PMT_ALLOC_1
  | 01 January 2024   | 1000           | 7                      | 
DECLINING_BALANCE       | DAILY                       | EQUAL_INSTALLMENTS | 3  
               | MONTHS                | 1              | MONTHS                
 | 3                  | 0                       | 0                      | 0    
                | ADVANCED_PAYMENT_ALLOCATION |
+    And Admin successfully approves the loan on "01 January 2024" with "1000" 
amount and expected disbursement date on "01 January 2024"
+    When Admin successfully disburse the loan on "01 January 2024" with "1000" 
EUR transaction amount
+    When Admin runs inline COB job for Loan
+    Then Loan Repayment schedule has 3 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 January 2024  |                 | 1000.0          |     
          |          | 0.0  |           | 0.0    | 0.0   |            |       | 
            |
+      | 1  | 31   | 01 February 2024 |                 | 668.6           | 
331.4         | 5.83     | 0.0  | 0.0       | 337.23 | 0.0   | 0.0        | 0.0 
  | 337.23      |
+      | 2  | 29   | 01 March 2024    |                 | 335.27          | 
333.33        | 3.9      | 0.0  | 0.0       | 337.23 | 0.0   | 0.0        | 0.0 
  | 337.23      |
+      | 3  | 31   | 01 April 2024    |                 | 0.0             | 
335.27        | 1.96     | 0.0  | 0.0       | 337.23 | 0.0   | 0.0        | 0.0 
  | 337.23      |
+    Then Loan Repayment schedule has the following data in Total row:
+      | Principal due | Interest | Fees | Penalties | Due     | Paid | In 
advance | Late | Outstanding |
+      | 1000          | 11.69    | 0    | 0         | 1011.69 | 0    | 0       
   | 0    | 1011.69     |
+    When Admin sets the business date to "15 February 2024"
+    When Admin runs inline COB job for Loan
+    # Move the current date into the middle of the 2nd period, so 1st period 
is past due
+    When Admin sets the business date to "15 August 2024"
+    And Admin does charge-off the loan on "09 February 2024"
+    Then Loan Repayment schedule has 3 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 January 2024  |                 | 1000.0          |     
          |          | 0.0  |           | 0.0    | 0.0   |            |       | 
            |
+      | 1  | 31   | 01 February 2024 |                 | 668.6           | 
331.4         | 5.83     | 0.0  | 0.0       | 337.23 | 0.0   | 0.0        | 0.0 
  | 337.23      |
+      | 2  | 29   | 01 March 2024    |                 | 335.8           | 
332.8         | 4.43     | 0.0  | 0.0       | 337.23 | 0.0   | 0.0        | 0.0 
  | 337.23      |
+      | 3  | 31   | 01 April 2024    |                 | 0.0             | 
335.8         | 1.96     | 0.0  | 0.0       | 337.76 | 0.0   | 0.0        | 0.0 
  | 337.76      |
+    Then Loan Repayment schedule has the following data in Total row:
+      | Principal due | Interest | Fees | Penalties | Due     | Paid | In 
advance | Late | Outstanding |
+      | 1000          | 12.22    | 0    | 0         | 1012.22 | 0    | 0       
   | 0    | 1012.22     |
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 ebc7d9552..cc8039f15 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
@@ -1187,7 +1187,7 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
         doPostLoanTransactionChecks(getLastUserTransactionDate(), 
loanLifecycleStateMachine);
     }
 
-    public boolean isInterestRecalculationEnabledForProduct() {
+    private boolean isInterestRecalculationEnabledForProduct() {
         return this.loanProduct.isInterestRecalculationEnabled();
     }
 
@@ -2647,6 +2647,10 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
         return 
BigDecimal.ZERO.compareTo(getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate())
 < 0;
     }
 
+    public boolean isInterestRecalculationEnabled() {
+        return 
this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled();
+    }
+
     public LocalDate getMaturityDate() {
         return this.actualMaturityDate;
     }
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 ae1f67eb6..c13b33e1f 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
@@ -281,6 +281,10 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
 
     @Override
     public void processLatestTransaction(LoanTransaction loanTransaction, 
TransactionCtx ctx) {
+        // If we are behind, we might need to first recalculate interest
+        if (ctx instanceof ProgressiveTransactionCtx 
progressiveTransactionCtx) {
+            recalculateInterestForDate(loanTransaction.getTransactionDate(), 
progressiveTransactionCtx);
+        }
         switch (loanTransaction.getTypeOf()) {
             case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx);
             case WRITEOFF -> handleWriteOff(loanTransaction, ctx);
@@ -963,40 +967,44 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
     }
 
     private void recalculateInterestForDate(LocalDate targetDate, 
ProgressiveTransactionCtx ctx) {
-        if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()
-                && 
ctx.getInstallments().get(0).getLoan().getLoanProductRelatedDetail().isInterestRecalculationEnabled()
-                && !ctx.getInstallments().get(0).getLoan().isNpa() && 
!ctx.getInstallments().get(0).getLoan().isChargedOff()) {
-            List<LoanRepaymentScheduleInstallment> 
overdueInstallmentsSortedByInstallmentNumber = 
findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
-                    targetDate, ctx);
-            if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
-                List<LoanRepaymentScheduleInstallment> normalInstallments = 
ctx.getInstallments().stream() //
-                        .filter(installment -> !installment.isAdditional() && 
!installment.isDownPayment()).toList();
-
-                Optional<LoanRepaymentScheduleInstallment> 
currentInstallmentOptional = normalInstallments.stream().filter(
-                        installment -> 
installment.getFromDate().isBefore(targetDate) && 
!installment.getDueDate().isBefore(targetDate))
-                        .findAny();
-
-                // get DUE installment or last installment
-                LoanRepaymentScheduleInstallment lastInstallment = 
normalInstallments.stream()
-                        
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).get();
-                LoanRepaymentScheduleInstallment currentInstallment = 
currentInstallmentOptional.orElse(lastInstallment);
-
-                Money overDuePrincipal = Money.zero(ctx.getCurrency());
-                Money aggregatedOverDuePrincipal = 
Money.zero(ctx.getCurrency());
-                for (LoanRepaymentScheduleInstallment processingInstallment : 
overdueInstallmentsSortedByInstallmentNumber) {
-                    // add and subtract outstanding principal
-                    if (!overDuePrincipal.isZero()) {
-                        adjustOverduePrincipalForInstallment(targetDate, 
processingInstallment, overDuePrincipal,
-                                aggregatedOverDuePrincipal, ctx);
-                    }
+        if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) 
{
+            Loan loan = ctx.getInstallments().get(0).getLoan();
+            if (loan.isInterestRecalculationEnabled() && !loan.isNpa()
+                    && (!loan.isChargedOff() || !DateUtils.isAfter(targetDate, 
loan.getChargedOffOnDate()))) {
+
+                List<LoanRepaymentScheduleInstallment> 
overdueInstallmentsSortedByInstallmentNumber = 
findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
+                        targetDate, ctx);
+                if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
+                    List<LoanRepaymentScheduleInstallment> normalInstallments 
= ctx.getInstallments().stream() //
+                            .filter(installment -> !installment.isAdditional() 
&& !installment.isDownPayment()).toList();
+
+                    Optional<LoanRepaymentScheduleInstallment> 
currentInstallmentOptional = normalInstallments.stream().filter(
+                            installment -> 
installment.getFromDate().isBefore(targetDate) && 
!installment.getDueDate().isBefore(targetDate))
+                            .findAny();
+
+                    // get DUE installment or last installment
+                    LoanRepaymentScheduleInstallment lastInstallment = 
normalInstallments.stream()
+                            
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).get();
+                    LoanRepaymentScheduleInstallment currentInstallment = 
currentInstallmentOptional.orElse(lastInstallment);
+
+                    Money overDuePrincipal = Money.zero(ctx.getCurrency());
+                    Money aggregatedOverDuePrincipal = 
Money.zero(ctx.getCurrency());
+                    for (LoanRepaymentScheduleInstallment 
processingInstallment : overdueInstallmentsSortedByInstallmentNumber) {
+                        // add and subtract outstanding principal
+                        if (!overDuePrincipal.isZero()) {
+                            adjustOverduePrincipalForInstallment(targetDate, 
processingInstallment, overDuePrincipal,
+                                    aggregatedOverDuePrincipal, ctx);
+                        }
 
-                    overDuePrincipal = 
processingInstallment.getPrincipalOutstanding(ctx.getCurrency());
-                    aggregatedOverDuePrincipal = 
aggregatedOverDuePrincipal.add(overDuePrincipal);
-                }
+                        overDuePrincipal = 
processingInstallment.getPrincipalOutstanding(ctx.getCurrency());
+                        aggregatedOverDuePrincipal = 
aggregatedOverDuePrincipal.add(overDuePrincipal);
+                    }
 
-                boolean adjustNeeded = 
!currentInstallment.equals(lastInstallment) || 
!lastInstallment.isOverdueOn(targetDate);
-                if (adjustNeeded) {
-                    adjustOverduePrincipalForInstallment(targetDate, 
currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal, ctx);
+                    boolean adjustNeeded = 
!currentInstallment.equals(lastInstallment) || 
!lastInstallment.isOverdueOn(targetDate);
+                    if (adjustNeeded) {
+                        adjustOverduePrincipalForInstallment(targetDate, 
currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal,
+                                ctx);
+                    }
                 }
             }
         }
@@ -1071,9 +1079,6 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
     }
 
     private void handleRepayment(LoanTransaction loanTransaction, 
TransactionCtx transactionCtx) {
-        if (transactionCtx instanceof ProgressiveTransactionCtx) {
-            recalculateInterestForDate(loanTransaction.getTransactionDate(), 
(ProgressiveTransactionCtx) transactionCtx);
-        }
         if (loanTransaction.isRepaymentLikeType() || 
loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
             loanTransaction.resetDerivedComponents();
         }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInterestRecalculationCOBBusinessStep.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInterestRecalculationCOBBusinessStep.java
index e19e25f75..26dceb627 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInterestRecalculationCOBBusinessStep.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanInterestRecalculationCOBBusinessStep.java
@@ -36,7 +36,7 @@ public class LoanInterestRecalculationCOBBusinessStep 
implements LoanCOBBusiness
     @Override
     public Loan execute(Loan loan) {
         if (!loan.isInterestBearing() || !loan.getStatus().isActive() || 
loan.isNpa() || loan.isChargedOff()
-                || !loan.isInterestRecalculationEnabledForProduct()
+                || !loan.isInterestRecalculationEnabled()
                 || 
loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue())
 {
             log.debug(
                     "Skip processing loan interest recalculation [{}] - 
Possible reasons: Loan is not an interest bearing loan, Loan is not active, 
Interest recalculation on past due is disabled on this loan",
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java
index a4e22403f..8eba2c376 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java
@@ -28,7 +28,7 @@ import org.springframework.stereotype.Component;
 public final class LoanForeclosureValidator {
 
     public void validateForForeclosure(final Loan loan, final LocalDate 
transactionDate) {
-        if (loan.isInterestRecalculationEnabledForProduct()) {
+        if (loan.isInterestRecalculationEnabled()) {
             final String defaultUserMessage = "The loan with interest 
recalculation enabled cannot be foreclosed.";
             throw new 
LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured",
 defaultUserMessage,
                     loan.getId());
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 38fe5ccbc..a747373c7 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
@@ -3261,8 +3261,26 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         final List<Long> existingReversedTransactionIds = 
loan.findExistingReversedTransactionIds();
 
         LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, 
transactionDate, txnExternalId);
+
+        if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()
+                && DateUtils.isBefore(loan.getInterestRecalculatedOn(), 
DateUtils.getBusinessLocalDate())) {
+            final ScheduleGeneratorDTO scheduleGeneratorDTO = 
this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
+            
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, 
scheduleGeneratorDTO);
+            loan.addLoanTransaction(chargeOffTransaction);
+            ChangedTransactionDetail changedTransactionDetail = 
loan.reprocessTransactions();
+            if (changedTransactionDetail != null) {
+                for (final Map.Entry<Long, LoanTransaction> mapEntry : 
changedTransactionDetail.getNewTransactionMappings().entrySet()) {
+                    
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
+                    
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), 
mapEntry.getValue());
+                }
+                // Trigger transaction replayed event
+                
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
+            }
+        } else {
+            loan.addLoanTransaction(chargeOffTransaction);
+        }
         loanTransactionRepository.saveAndFlush(chargeOffTransaction);
-        loan.addLoanTransaction(chargeOffTransaction);
+
         saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
 
         String noteText = 
command.stringValueOfParameterNamed(LoanApiConstants.noteParameterName);
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 6bddb5e95..e8b1b3a19 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
@@ -23,6 +23,7 @@ import java.time.LocalDate;
 import java.util.Collection;
 import java.util.List;
 import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.fineract.organisation.monetary.data.CurrencyData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
@@ -42,6 +43,7 @@ import org.springframework.stereotype.Component;
 
 @Component
 @AllArgsConstructor
+@Slf4j
 public class ProgressiveLoanSummaryDataProvider extends 
CommonLoanSummaryDataProvider {
 
     private final AdvancedPaymentScheduleTransactionProcessor 
advancedPaymentScheduleTransactionProcessor;
@@ -89,7 +91,10 @@ public class ProgressiveLoanSummaryDataProvider extends 
CommonLoanSummaryDataPro
             ProgressiveLoanInterestScheduleModel model = 
changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight();
             if 
(!changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getCurrentTransactionToOldId().isEmpty()
                     || 
!changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft().getNewTransactionMappings().isEmpty())
 {
-                throw new RuntimeException("Transactions should not be reverse 
replayed!");
+                List<Long> replayedTransactions = 
changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft()
+                        
.getNewTransactionMappings().keySet().stream().toList();
+                log.warn("Reprocessed transactions show differences: There are 
unsaved changes of the following transactions: {}",
+                        replayedTransactions);
             }
             if (model != null) {
                 PeriodDueDetails dueAmounts = 
emiCalculator.getDueAmounts(model, 
loanRepaymentScheduleInstallment.getDueDate(),

Reply via email to