FINERACT-2421: Adjust interest with MIR and CBR results in improper allocations

This commit is contained in:
Jose Alberto Hernandez
2026-02-02 18:52:59 +01:00
committed by Adam Saghy
parent cc372286fa
commit 7b322481b8
23 changed files with 146 additions and 66 deletions
@@ -57,7 +57,8 @@ public class ProgressivePossibleNextRepaymentCalculationServiceImpl extends Abst
ProgressiveLoanInterestScheduleModel scheduleModel = optionalScheduleModel.get();
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments();
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), repaymentScheduleInstallments, Set.of(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), scheduleModel);
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), scheduleModel,
loan.getActiveLoanTermVariations());
ctx.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination());
@@ -229,7 +229,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
changedTransactionDetail, scheduleModel, loan.getActiveLoanTermVariations());
List<ChangeOperation> changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges);
@@ -571,7 +571,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList());
if (validateInterestRefundTransactionRelation(loanTransaction)) {
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
modifiedTransactions, unmodifiedTransactionIds);
modifiedTransactions, unmodifiedTransactionIds, ctx.getActiveLoanTermVariations());
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
loanTransaction.updateAmount(newAmount.getAmount());
}
@@ -28,6 +28,7 @@ import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
@@ -52,14 +53,16 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
ProgressiveLoanInterestScheduleModel model) {
this(currency, installments, charges, overpaymentHolder, changedTransactionDetail, model, Money.zero(currency));
ProgressiveLoanInterestScheduleModel model, List<LoanTermVariations> activeLoanTermVariations) {
this(currency, installments, charges, overpaymentHolder, changedTransactionDetail, model, Money.zero(currency),
activeLoanTermVariations);
}
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount) {
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail);
ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount,
List<LoanTermVariations> activeLoanTermVariations) {
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail, activeLoanTermVariations);
this.sumOfInterestRefundAmount = sumOfInterestRefundAmount;
this.model = model;
}
@@ -112,7 +112,8 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
savedModel = extractModel(progressiveLoanModel);
if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) {
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get());
Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get(),
loan.getActiveLoanTermVariations());
ctx.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination());
@@ -164,8 +164,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null));
underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge),
new MoneyHolder(overpaidAmount), null, loan.getActiveLoanTermVariations()));
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
@@ -208,8 +208,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null));
underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge),
new MoneyHolder(overpaidAmount), null, loan.getActiveLoanTermVariations()));
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), refEq(zero));
@@ -260,8 +260,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
when(loanTransaction.isOn(transactionDate)).thenReturn(true);
underTest.processLatestTransaction(loanTransaction,
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null));
underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge),
new MoneyHolder(overpaidAmount), null, loan.getActiveLoanTermVariations()));
Mockito.verify(installment, times(1)).payFeeChargesComponent(transactionDate, chargeAmountMoney);
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
@@ -289,7 +289,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
installments.add(installment);
// when
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null,
loan.getActiveLoanTermVariations());
underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify principal
@@ -346,7 +347,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
installments.add(installment);
// when
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null,
loan.getActiveLoanTermVariations());
underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify charges on installment
@@ -398,7 +400,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
installments.add(installment2);
// when
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null,
loan.getActiveLoanTermVariations());
underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify principal
@@ -491,7 +494,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
// Set up TransactionCtx with installments and charges
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, Set.of(), overpaymentHolder, changedTransactionDetail,
model, null);
model, loan.getActiveLoanTermVariations());
// Mock additional necessary methods
LoanCharge loanCharge = mock(LoanCharge.class);
@@ -570,7 +573,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(model.getMaturityDate()).thenReturn(LocalDate.of(2023, 12, 31));
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, spyInstallments, Set.of(), new MoneyHolder(Money.zero(currency)),
mock(ChangedTransactionDetail.class), model, Money.zero(currency));
mock(ChangedTransactionDetail.class), model, Money.zero(currency), loan.getActiveLoanTermVariations());
underTest.processLatestTransaction(disbursementTransaction, ctx);