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

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


The following commit(s) were added to refs/heads/release/1.13.0 by this push:
     new 7791939860 FINERACT-2326: Do not remove external id if transaction got 
not replayed
7791939860 is described below

commit 779193986049d874278ce61010982c13ff6af71a
Author: Adam Saghy <[email protected]>
AuthorDate: Wed Oct 8 16:27:12 2025 +0200

    FINERACT-2326: Do not remove external id if transaction got not replayed
---
 ...dvancedPaymentScheduleTransactionProcessor.java |   4 +-
 .../LoanAccrualActivityProcessingServiceImpl.java  |  13 ++-
 .../ProgressiveLoanInterestRefundServiceImpl.java  |   2 +-
 .../ReprocessLoanTransactionsServiceImpl.java      |   9 +-
 ...PaymentAllocationLoanRepaymentScheduleTest.java | 105 +++++++++++++++++++++
 5 files changed, 123 insertions(+), 10 deletions(-)

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 4afa5e881e..cbbac987c5 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
@@ -1214,7 +1214,9 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
 
     protected void createNewTransaction(final LoanTransaction oldTransaction, 
final LoanTransaction newTransaction,
             final TransactionCtx ctx) {
-        oldTransaction.updateExternalId(null);
+        if (newTransaction.isNotReversed()) {
+            oldTransaction.updateExternalId(null);
+        }
         oldTransaction.getLoanChargesPaid().clear();
 
         if (newTransaction.getTypeOf().isInterestRefund()) {
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 2d531c9a7c..0d64ed973f 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
@@ -125,11 +125,14 @@ public class LoanAccrualActivityProcessingServiceImpl 
implements LoanAccrualActi
     protected void createNewTransaction(LoanTransaction loanTransaction, 
LoanTransaction newLoanTransaction,
             ChangedTransactionDetail changedTransactionDetail) {
         loanTransaction.reverse();
-        loanTransaction.updateExternalId(null);
-        
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
-        // Adding Replayed relation from newly created transaction to reversed 
transaction
-        newLoanTransaction.getLoanTransactionRelations().add(
-                LoanTransactionRelation.linkToTransaction(newLoanTransaction, 
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
+
+        if (newLoanTransaction.isNotReversed()) {
+            loanTransaction.updateExternalId(null);
+            
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
+            // Adding Replayed relation from newly created transaction to 
reversed transaction
+            
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
+                    loanTransaction, 
LoanTransactionRelationTypeEnum.REPLAYED));
+        }
         changedTransactionDetail.addTransactionChange(new 
TransactionChangeData(loanTransaction, newLoanTransaction));
     }
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java
index b35746d991..ec46968932 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java
@@ -92,7 +92,7 @@ public class ProgressiveLoanInterestRefundServiceImpl 
implements InterestRefundS
                 
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), 
relatedRefundTransactionDate, transactionsToReprocess,
                         loan.getCurrency(), installmentsToReprocess, 
loan.getActiveCharges());
         final List<LoanTransaction> newTransactions = 
reprocessResult.getLeft().getTransactionChanges().stream()
-                .map(TransactionChangeData::getNewTransaction).toList();
+                
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList();
         loan.getLoanTransactions().addAll(newTransactions);
         ProgressiveLoanInterestScheduleModel modelAfter = 
reprocessResult.getRight();
 
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 4b73229378..91b2940ca5 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
@@ -175,7 +175,8 @@ public class ReprocessLoanTransactionsServiceImpl 
implements ReprocessLoanTransa
         final ChangedTransactionDetail changedTransactionDetail = 
loanTransactionProcessingService
                 
.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), 
loanTransaction, transactionCtx);
         final List<LoanTransaction> newTransactions = 
changedTransactionDetail.getTransactionChanges().stream()
-                
.map(TransactionChangeData::getNewTransaction).peek(transaction -> 
transaction.updateLoan(loan)).toList();
+                
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed)
+                .peek(transaction -> transaction.updateLoan(loan)).toList();
         loan.getLoanTransactions().addAll(newTransactions);
 
         loanBalanceService.updateLoanSummaryDerivedFields(loan);
@@ -206,10 +207,12 @@ public class ReprocessLoanTransactionsServiceImpl 
implements ReprocessLoanTransa
                             : new 
LoanAccrualAdjustmentTransactionBusinessEvent(newTransaction);
                     
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
                 }
+                if (oldTransaction != null) {
+                    
loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), 
newTransaction);
+                }
             }
 
             if (oldTransaction != null) {
-                
loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), 
newTransaction);
                 // Create reversal journal entries for old transaction if it 
exists (reverse-replay scenario)
                 
loanJournalEntryPoster.postJournalEntriesForLoanTransaction(oldTransaction, 
false, false);
             }
@@ -226,7 +229,7 @@ public class ReprocessLoanTransactionsServiceImpl 
implements ReprocessLoanTransa
             change.getNewTransaction().updateLoan(loan);
         }
         final List<LoanTransaction> newTransactions = 
changedTransactionDetail.getTransactionChanges().stream()
-                .map(TransactionChangeData::getNewTransaction).toList();
+                
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList();
         loan.getLoanTransactions().addAll(newTransactions);
         loanBalanceService.updateLoanSummaryDerivedFields(loan);
         
loanAccrualActivityProcessingService.recalculateAccrualActivityTransaction(loan,
 changedTransactionDetail);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java
index 62cdc035c6..540a37267a 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java
@@ -54,6 +54,7 @@ import 
org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
 import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
 import 
org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
+import 
org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
 import org.apache.fineract.client.models.LoanProduct;
 import org.apache.fineract.client.models.PaymentAllocationOrder;
 import org.apache.fineract.client.models.PostClientsResponse;
@@ -69,10 +70,12 @@ import 
org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionI
 import org.apache.fineract.client.models.PostLoansRequest;
 import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
+import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
 import org.apache.fineract.client.models.PutLoansLoanIdRequest;
 import org.apache.fineract.client.util.CallFailedRuntimeException;
 import 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
 import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.BusinessStepHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
 import org.apache.fineract.integrationtests.common.CommonConstants;
 import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
@@ -136,6 +139,10 @@ public class 
AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
         commonLoanProductId = createLoanProduct("500", "15", "4", true, "25", 
true, LoanScheduleType.PROGRESSIVE,
                 LoanScheduleProcessingType.HORIZONTAL, assetAccount, 
incomeAccount, expenseAccount, overpaymentAccount);
         client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        // setup COB Business Steps to prevent test failing due other 
integration test configurations
+        new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", 
"APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
+                "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", 
"UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS", 
"ACCRUAL_ACTIVITY_POSTING", "LOAN_INTEREST_RECALCULATION");
     }
 
     // UC1: Simple payments
@@ -6146,6 +6153,104 @@ public class 
AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
         });
     }
 
+    // UC157: Progressive loan with Accrual Activity reverse-replay
+    @Test
+    public void uc157() {
+        
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID,
+                new PutGlobalConfigurationsRequest().enabled(true));
+        final String operationDate = "13 September 2025";
+        AtomicLong createdLoanId = new AtomicLong();
+        GetLoansLoanIdTransactions[] accrualActivityId = new 
GetLoansLoanIdTransactions[1];
+        final BigDecimal interestRatePerPeriod = BigDecimal.valueOf(11.32);
+        final BigDecimal principalAmount = BigDecimal.valueOf(135.94);
+        final Integer delinquencyBucketId = 
DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, 
List.of(//
+                Pair.of(1, 10), //
+                Pair.of(11, 30), //
+                Pair.of(31, 60), //
+                Pair.of(61, null)//
+        ));
+
+        runAt(operationDate, () -> {
+            final ArrayList<String> interestRefundTypes = new 
ArrayList<String>();
+            interestRefundTypes.add("PAYOUT_REFUND");
+            interestRefundTypes.add("MERCHANT_ISSUED_REFUND");
+            Long clientId = 
ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+                    
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestRateFrequencyType(YEARS)//
+                    .daysInMonthType(DaysInMonthType.DAYS_30)//
+                    .daysInYearType(DaysInYearType.DAYS_360)//
+                    .numberOfRepayments(6)//
+                    .repaymentEvery(1)//
+                    .repaymentFrequencyType(2L)//
+                    .chargeOffBehaviour("ZERO_INTEREST")//
+                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
+                    
.repaymentStartDateType(LoanProduct.RepaymentStartDateTypeEnum.SUBMITTED_ON_DATE.ordinal())//
+                    .enableDownPayment(false)//
+                    .enableAccrualActivityPosting(true)//
+                    .allowPartialPeriodInterestCalcualtion(null)//
+                    .enableAutoRepaymentForDownPayment(null)//
+                    .isInterestRecalculationEnabled(true)//
+                    .delinquencyBucketId(delinquencyBucketId.longValue())//
+                    .enableInstallmentLevelDelinquency(true)//
+                    .interestRecalculationCompoundingMethod(0)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+                    .installmentAmountInMultiplesOf(null)//
+                    .supportedInterestRefundTypes(interestRefundTypes) //
+                    
.rescheduleStrategyMethod(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getValue())//
+                    .recalculationRestFrequencyType(2)//
+                    .recalculationRestFrequencyInterval(1)//
+                    .enableAccrualActivityPosting(true);
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductResponse.getResourceId(), operationDate,
+                    principalAmount.doubleValue(), 
6).interestCalculationPeriodType(DAYS)//
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
+                    .interestRatePerPeriod(interestRatePerPeriod)//
+                    .repaymentEvery(1)//
+                    .repaymentFrequencyType(MONTHS)//
+                    .loanTermFrequency(6)//
+                    .loanTermFrequencyType(MONTHS);
+
+            PostLoansResponse loanResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+            createdLoanId.set(loanResponse.getLoanId());
+
+            loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new 
PostLoansLoanIdRequest().approvedLoanAmount(principalAmount)
+                    
.dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
+
+            loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new 
PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
+                    
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(principalAmount));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(createdLoanId.get());
+            assertTrue(loanDetails.getStatus().getActive());
+        });
+
+        runAt("22 October 2025", () -> {
+
+            executeInlineCOB(createdLoanId.get());
+            verifyTransactions(createdLoanId.get(), //
+                    transaction(135.94, "Disbursement", "13 September 2025", 
135.94, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+                    transaction(1.28, "Accrual Activity", "13 October 2025", 
0.0, 0.0, 1.28, 0.0, 0.0, 0.0, 0.0),
+                    transaction(1.61, "Accrual", "21 October 2025", 0.0, 0.0, 
1.61, 0.0, 0.0, 0.0, 0.0));
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(createdLoanId.get());
+            loanDetails.getTransactions().stream().filter(t -> 
"loanTransactionType.accrualActivity".equals(t.getType().getCode()))
+                    .findFirst().ifPresent(t -> {
+                        accrualActivityId[0] = t;
+                    });
+            assertNotNull(accrualActivityId[0]);
+            assertNotNull(accrualActivityId[0].getExternalId());
+
+            loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new 
PostLoansLoanIdTransactionsRequest() //
+                    .transactionDate("13 September 2025") //
+                    .transactionAmount(135.94) //
+                    .locale("en") //
+                    .dateFormat(DATETIME_PATTERN)); //
+
+            GetLoansLoanIdTransactionsTransactionIdResponse 
loanTransactionDetails = loanTransactionHelper
+                    .getLoanTransactionDetails(createdLoanId.get(), 
accrualActivityId[0].getId());
+            assertNotNull(loanTransactionDetails.getExternalId());
+            assertEquals(LocalDate.of(2025, 10, 22), 
loanTransactionDetails.getReversedOnDate());
+        });
+    }
+
     private Long 
applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long
 clientId, Long loanProductId,
             Integer numberOfRepayments, String loanDisbursementDate, double 
amount) {
         LOG.info("------------------------------APPLY AND APPROVE LOAN 
---------------------------------------");

Reply via email to