FINERACT-2326: Do not remove external id if transaction got not replayed

This commit is contained in:
Adam Saghy
2025-10-08 21:38:32 +02:00
parent 6066c0d97f
commit 7791939860
5 changed files with 123 additions and 10 deletions
@@ -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()) {
@@ -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));
}
@@ -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();
@@ -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);
@@ -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 ---------------------------------------");