Skip to content

Commit ebae206

Browse files
committed
FINERACT-2326: Do not remove external id if transaction got not replayed
1 parent b692248 commit ebae206

File tree

5 files changed

+123
-10
lines changed

5 files changed

+123
-10
lines changed

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,9 @@ private static LoanTransaction useOldTransactionIfApplicable(LoanTransaction old
12141214

12151215
protected void createNewTransaction(final LoanTransaction oldTransaction, final LoanTransaction newTransaction,
12161216
final TransactionCtx ctx) {
1217-
oldTransaction.updateExternalId(null);
1217+
if (newTransaction.isNotReversed()) {
1218+
oldTransaction.updateExternalId(null);
1219+
}
12181220
oldTransaction.getLoanChargesPaid().clear();
12191221

12201222
if (newTransaction.getTypeOf().isInterestRefund()) {

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,14 @@ public void recalculateAccrualActivityTransaction(Loan loan, ChangedTransactionD
125125
protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction,
126126
ChangedTransactionDetail changedTransactionDetail) {
127127
loanTransaction.reverse();
128-
loanTransaction.updateExternalId(null);
129-
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
130-
// Adding Replayed relation from newly created transaction to reversed transaction
131-
newLoanTransaction.getLoanTransactionRelations().add(
132-
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
128+
129+
if (newLoanTransaction.isNotReversed()) {
130+
loanTransaction.updateExternalId(null);
131+
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
132+
// Adding Replayed relation from newly created transaction to reversed transaction
133+
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
134+
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
135+
}
133136
changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, newLoanTransaction));
134137
}
135138

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private Money recalculateTotalInterest(AdvancedPaymentScheduleTransactionProcess
9292
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
9393
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
9494
final List<LoanTransaction> newTransactions = reprocessResult.getLeft().getTransactionChanges().stream()
95-
.map(TransactionChangeData::getNewTransaction).toList();
95+
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList();
9696
loan.getLoanTransactions().addAll(newTransactions);
9797
ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight();
9898

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ public void processLatestTransaction(final LoanTransaction loanTransaction, fina
175175
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService
176176
.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, transactionCtx);
177177
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
178-
.map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList();
178+
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed)
179+
.peek(transaction -> transaction.updateLoan(loan)).toList();
179180
loan.getLoanTransactions().addAll(newTransactions);
180181

181182
loanBalanceService.updateLoanSummaryDerivedFields(loan);
@@ -206,10 +207,12 @@ private void handleChangedDetail(final ChangedTransactionDetail changedTransacti
206207
: new LoanAccrualAdjustmentTransactionBusinessEvent(newTransaction);
207208
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
208209
}
210+
if (oldTransaction != null) {
211+
loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), newTransaction);
212+
}
209213
}
210214

211215
if (oldTransaction != null) {
212-
loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), newTransaction);
213216
// Create reversal journal entries for old transaction if it exists (reverse-replay scenario)
214217
loanJournalEntryPoster.postJournalEntriesForLoanTransaction(oldTransaction, false, false);
215218
}
@@ -226,7 +229,7 @@ private ChangedTransactionDetail reprocessTransactionsAndFetchChangedTransaction
226229
change.getNewTransaction().updateLoan(loan);
227230
}
228231
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
229-
.map(TransactionChangeData::getNewTransaction).toList();
232+
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList();
230233
loan.getLoanTransactions().addAll(newTransactions);
231234
loanBalanceService.updateLoanSummaryDerivedFields(loan);
232235
loanAccrualActivityProcessingService.recalculateAccrualActivityTransaction(loan, changedTransactionDetail);

integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
5555
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
5656
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
57+
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
5758
import org.apache.fineract.client.models.LoanProduct;
5859
import org.apache.fineract.client.models.PaymentAllocationOrder;
5960
import org.apache.fineract.client.models.PostClientsResponse;
@@ -69,10 +70,12 @@
6970
import org.apache.fineract.client.models.PostLoansRequest;
7071
import org.apache.fineract.client.models.PostLoansResponse;
7172
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
73+
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
7274
import org.apache.fineract.client.models.PutLoansLoanIdRequest;
7375
import org.apache.fineract.client.util.CallFailedRuntimeException;
7476
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
7577
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
78+
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
7679
import org.apache.fineract.integrationtests.common.ClientHelper;
7780
import org.apache.fineract.integrationtests.common.CommonConstants;
7881
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
@@ -136,6 +139,10 @@ public static void setup() {
136139
commonLoanProductId = createLoanProduct("500", "15", "4", true, "25", true, LoanScheduleType.PROGRESSIVE,
137140
LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
138141
client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
142+
// setup COB Business Steps to prevent test failing due other integration test configurations
143+
new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
144+
"CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
145+
"EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS", "ACCRUAL_ACTIVITY_POSTING", "LOAN_INTEREST_RECALCULATION");
139146
}
140147

141148
// UC1: Simple payments
@@ -6146,6 +6153,104 @@ public void uc156() {
61466153
});
61476154
}
61486155

6156+
// UC157: Progressive loan with Accrual Activity reverse-replay
6157+
@Test
6158+
public void uc157() {
6159+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID,
6160+
new PutGlobalConfigurationsRequest().enabled(true));
6161+
final String operationDate = "13 September 2025";
6162+
AtomicLong createdLoanId = new AtomicLong();
6163+
GetLoansLoanIdTransactions[] accrualActivityId = new GetLoansLoanIdTransactions[1];
6164+
final BigDecimal interestRatePerPeriod = BigDecimal.valueOf(11.32);
6165+
final BigDecimal principalAmount = BigDecimal.valueOf(135.94);
6166+
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(//
6167+
Pair.of(1, 10), //
6168+
Pair.of(11, 30), //
6169+
Pair.of(31, 60), //
6170+
Pair.of(61, null)//
6171+
));
6172+
6173+
runAt(operationDate, () -> {
6174+
final ArrayList<String> interestRefundTypes = new ArrayList<String>();
6175+
interestRefundTypes.add("PAYOUT_REFUND");
6176+
interestRefundTypes.add("MERCHANT_ISSUED_REFUND");
6177+
Long clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
6178+
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
6179+
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestRateFrequencyType(YEARS)//
6180+
.daysInMonthType(DaysInMonthType.DAYS_30)//
6181+
.daysInYearType(DaysInYearType.DAYS_360)//
6182+
.numberOfRepayments(6)//
6183+
.repaymentEvery(1)//
6184+
.repaymentFrequencyType(2L)//
6185+
.chargeOffBehaviour("ZERO_INTEREST")//
6186+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
6187+
.repaymentStartDateType(LoanProduct.RepaymentStartDateTypeEnum.SUBMITTED_ON_DATE.ordinal())//
6188+
.enableDownPayment(false)//
6189+
.enableAccrualActivityPosting(true)//
6190+
.allowPartialPeriodInterestCalcualtion(null)//
6191+
.enableAutoRepaymentForDownPayment(null)//
6192+
.isInterestRecalculationEnabled(true)//
6193+
.delinquencyBucketId(delinquencyBucketId.longValue())//
6194+
.enableInstallmentLevelDelinquency(true)//
6195+
.interestRecalculationCompoundingMethod(0)//
6196+
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
6197+
.installmentAmountInMultiplesOf(null)//
6198+
.supportedInterestRefundTypes(interestRefundTypes) //
6199+
.rescheduleStrategyMethod(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getValue())//
6200+
.recalculationRestFrequencyType(2)//
6201+
.recalculationRestFrequencyInterval(1)//
6202+
.enableAccrualActivityPosting(true);
6203+
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
6204+
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate,
6205+
principalAmount.doubleValue(), 6).interestCalculationPeriodType(DAYS)//
6206+
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
6207+
.interestRatePerPeriod(interestRatePerPeriod)//
6208+
.repaymentEvery(1)//
6209+
.repaymentFrequencyType(MONTHS)//
6210+
.loanTermFrequency(6)//
6211+
.loanTermFrequencyType(MONTHS);
6212+
6213+
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
6214+
createdLoanId.set(loanResponse.getLoanId());
6215+
6216+
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().approvedLoanAmount(principalAmount)
6217+
.dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
6218+
6219+
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
6220+
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(principalAmount));
6221+
6222+
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
6223+
assertTrue(loanDetails.getStatus().getActive());
6224+
});
6225+
6226+
runAt("22 October 2025", () -> {
6227+
6228+
executeInlineCOB(createdLoanId.get());
6229+
verifyTransactions(createdLoanId.get(), //
6230+
transaction(135.94, "Disbursement", "13 September 2025", 135.94, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
6231+
transaction(1.28, "Accrual Activity", "13 October 2025", 0.0, 0.0, 1.28, 0.0, 0.0, 0.0, 0.0),
6232+
transaction(1.61, "Accrual", "21 October 2025", 0.0, 0.0, 1.61, 0.0, 0.0, 0.0, 0.0));
6233+
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
6234+
loanDetails.getTransactions().stream().filter(t -> "loanTransactionType.accrualActivity".equals(t.getType().getCode()))
6235+
.findFirst().ifPresent(t -> {
6236+
accrualActivityId[0] = t;
6237+
});
6238+
assertNotNull(accrualActivityId[0]);
6239+
assertNotNull(accrualActivityId[0].getExternalId());
6240+
6241+
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() //
6242+
.transactionDate("13 September 2025") //
6243+
.transactionAmount(135.94) //
6244+
.locale("en") //
6245+
.dateFormat(DATETIME_PATTERN)); //
6246+
6247+
GetLoansLoanIdTransactionsTransactionIdResponse loanTransactionDetails = loanTransactionHelper
6248+
.getLoanTransactionDetails(createdLoanId.get(), accrualActivityId[0].getId());
6249+
assertNotNull(loanTransactionDetails.getExternalId());
6250+
assertEquals(LocalDate.of(2025, 10, 22), loanTransactionDetails.getReversedOnDate());
6251+
});
6252+
}
6253+
61496254
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
61506255
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
61516256
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");

0 commit comments

Comments
 (0)