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 4afa5e881..cbbac987c 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 2d531c9a7..0d64ed973 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 b35746d99..ec4696893 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 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 4b7322937..91b2940ca 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 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 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 62cdc035c..540a37267 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 interestRefundTypes = new ArrayList(); + 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 ---------------------------------------");