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

This commit is contained in:
Adam Saghy
2025-10-08 21:33:04 +02:00
parent b692248a8a
commit ebae20654a
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, protected void createNewTransaction(final LoanTransaction oldTransaction, final LoanTransaction newTransaction,
final TransactionCtx ctx) { final TransactionCtx ctx) {
oldTransaction.updateExternalId(null); if (newTransaction.isNotReversed()) {
oldTransaction.updateExternalId(null);
}
oldTransaction.getLoanChargesPaid().clear(); oldTransaction.getLoanChargesPaid().clear();
if (newTransaction.getTypeOf().isInterestRefund()) { if (newTransaction.getTypeOf().isInterestRefund()) {
@@ -125,11 +125,14 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction, protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction,
ChangedTransactionDetail changedTransactionDetail) { ChangedTransactionDetail changedTransactionDetail) {
loanTransaction.reverse(); loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations()); if (newLoanTransaction.isNotReversed()) {
// Adding Replayed relation from newly created transaction to reversed transaction loanTransaction.updateExternalId(null);
newLoanTransaction.getLoanTransactionRelations().add( newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED)); // 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)); changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, newLoanTransaction));
} }
@@ -92,7 +92,7 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess, .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges()); loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
final List<LoanTransaction> newTransactions = reprocessResult.getLeft().getTransactionChanges().stream() 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); loan.getLoanTransactions().addAll(newTransactions);
ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight(); ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight();
@@ -175,7 +175,8 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService
.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, transactionCtx); .processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, transactionCtx);
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream() 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); loan.getLoanTransactions().addAll(newTransactions);
loanBalanceService.updateLoanSummaryDerivedFields(loan); loanBalanceService.updateLoanSummaryDerivedFields(loan);
@@ -206,10 +207,12 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
: new LoanAccrualAdjustmentTransactionBusinessEvent(newTransaction); : new LoanAccrualAdjustmentTransactionBusinessEvent(newTransaction);
businessEventNotifierService.notifyPostBusinessEvent(businessEvent); businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
} }
if (oldTransaction != null) {
loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), newTransaction);
}
} }
if (oldTransaction != null) { if (oldTransaction != null) {
loanAccountTransfersService.updateLoanTransaction(oldTransaction.getId(), newTransaction);
// Create reversal journal entries for old transaction if it exists (reverse-replay scenario) // Create reversal journal entries for old transaction if it exists (reverse-replay scenario)
loanJournalEntryPoster.postJournalEntriesForLoanTransaction(oldTransaction, false, false); loanJournalEntryPoster.postJournalEntriesForLoanTransaction(oldTransaction, false, false);
} }
@@ -226,7 +229,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
change.getNewTransaction().updateLoan(loan); change.getNewTransaction().updateLoan(loan);
} }
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream() final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList(); .map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList();
loan.getLoanTransactions().addAll(newTransactions); loan.getLoanTransactions().addAll(newTransactions);
loanBalanceService.updateLoanSummaryDerivedFields(loan); loanBalanceService.updateLoanSummaryDerivedFields(loan);
loanAccrualActivityProcessingService.recalculateAccrualActivityTransaction(loan, changedTransactionDetail); 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.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse; 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.LoanProduct;
import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostClientsResponse; 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.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest; 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.models.PutLoansLoanIdRequest;
import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.integrationtests.common.BusinessDateHelper; 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.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper; 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, commonLoanProductId = createLoanProduct("500", "15", "4", true, "25", true, LoanScheduleType.PROGRESSIVE,
LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); 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 // 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, private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) { Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");