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
@@ -1636,6 +1636,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.loanTermVariations.stream().filter(LoanTermVariations::isActive).collect(Collectors.toList()); return this.loanTermVariations.stream().filter(LoanTermVariations::isActive).collect(Collectors.toList());
} }
public void setLoanTermVariations(List<LoanTermVariations> loanTermVariations) {
this.loanTermVariations = loanTermVariations;
}
public boolean isTopup() { public boolean isTopup() {
return this.isTopup; return this.isTopup;
} }
@@ -179,9 +179,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
Loan loan = loanTransaction.getLoan();
// pass through for new transactions // pass through for new transactions
if (loanTransaction.getId() == null) { if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder, null)); processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder, null,
loan.getActiveLoanTermVariations()));
loanTransaction.adjustInterestComponent(); loanTransaction.adjustInterestComponent();
} else { } else {
/** /**
@@ -192,8 +194,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// Reset derived component of new loan transaction and // Reset derived component of new loan transaction and
// re-process transaction // re-process transaction
processLatestTransaction(newLoanTransaction, processLatestTransaction(newLoanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder,
new TransactionCtx(currency, installments, charges, overpaymentHolder, null)); null, loan.getActiveLoanTermVariations()));
newLoanTransaction.adjustInterestComponent(); newLoanTransaction.adjustInterestComponent();
/** /**
* Check if the transaction amounts have changed. If so, reverse the original transaction and update * Check if the transaction amounts have changed. If so, reverse the original transaction and update
@@ -26,6 +26,7 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@@ -36,4 +37,5 @@ public class TransactionCtx {
private final Set<LoanCharge> charges; private final Set<LoanCharge> charges;
private final MoneyHolder overpaymentHolder; private final MoneyHolder overpaymentHolder;
private final ChangedTransactionDetail changedTransactionDetail; private final ChangedTransactionDetail changedTransactionDetail;
private final List<LoanTermVariations> activeLoanTermVariations;
} }
@@ -18,26 +18,22 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.handler; package org.apache.fineract.portfolio.loanaccount.rescheduleloan.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanRescheduleRequestWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanRescheduleRequestWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor
@CommandType(entity = "RESCHEDULELOAN", action = "APPROVE") @CommandType(entity = "RESCHEDULELOAN", action = "APPROVE")
public class ApproveLoanRescheduleRequestCommandHandler implements NewCommandSourceHandler { public class ApproveLoanRescheduleRequestCommandHandler implements NewCommandSourceHandler {
private final LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService; private final LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService;
@Autowired
public ApproveLoanRescheduleRequestCommandHandler(LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService) {
this.loanRescheduleRequestWritePlatformService = loanRescheduleRequestWritePlatformService;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processCommand(JsonCommand jsonCommand) { public CommandProcessingResult processCommand(JsonCommand jsonCommand) {
@@ -18,26 +18,22 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.handler; package org.apache.fineract.portfolio.loanaccount.rescheduleloan.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanRescheduleRequestWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanRescheduleRequestWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor
@CommandType(entity = "RESCHEDULELOAN", action = "CREATE") @CommandType(entity = "RESCHEDULELOAN", action = "CREATE")
public class CreateLoanRescheduleRequestCommandHandler implements NewCommandSourceHandler { public class CreateLoanRescheduleRequestCommandHandler implements NewCommandSourceHandler {
private final LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService; private final LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService;
@Autowired
public CreateLoanRescheduleRequestCommandHandler(LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService) {
this.loanRescheduleRequestWritePlatformService = loanRescheduleRequestWritePlatformService;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processCommand(JsonCommand jsonCommand) { public CommandProcessingResult processCommand(JsonCommand jsonCommand) {
@@ -18,26 +18,22 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.handler; package org.apache.fineract.portfolio.loanaccount.rescheduleloan.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanRescheduleRequestWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.service.LoanRescheduleRequestWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor
@CommandType(entity = "RESCHEDULELOAN", action = "REJECT") @CommandType(entity = "RESCHEDULELOAN", action = "REJECT")
public class RejectLoanRescheduleRequestCommandHandler implements NewCommandSourceHandler { public class RejectLoanRescheduleRequestCommandHandler implements NewCommandSourceHandler {
private final LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService; private final LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService;
@Autowired
public RejectLoanRescheduleRequestCommandHandler(LoanRescheduleRequestWritePlatformService loanRescheduleRequestWritePlatformService) {
this.loanRescheduleRequestWritePlatformService = loanRescheduleRequestWritePlatformService;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processCommand(JsonCommand jsonCommand) { public CommandProcessingResult processCommand(JsonCommand jsonCommand) {
@@ -22,6 +22,7 @@ import java.time.LocalDate;
import java.util.List; import java.util.List;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
@@ -33,5 +34,6 @@ public interface InterestRefundService {
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
Money totalInterestByTransactions(LoanRepaymentScheduleTransactionProcessor processor, Long loanId, Money totalInterestByTransactions(LoanRepaymentScheduleTransactionProcessor processor, Long loanId,
LocalDate relatedRefundTransactionDate, List<LoanTransaction> newTransactions, List<Long> oldTransactionIds); LocalDate relatedRefundTransactionDate, List<LoanTransaction> newTransactions, List<Long> oldTransactionIds,
List<LoanTermVariations> activeLoanTermVariations);
} }
@@ -597,7 +597,7 @@ public class LoanChargeService {
loanCharges.add(charge); loanCharges.add(charge);
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargesPayment, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargesPayment,
new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges, new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges,
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate()); loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate());
} }
@@ -140,7 +140,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
if (processLatest) { if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
if (!loan.isProgressiveSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { if (!loan.isProgressiveSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) { if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
reprocessOnPostConditions = true; reprocessOnPostConditions = true;
@@ -83,7 +83,7 @@ public class LoanRefundService {
loadTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, loadTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate()); loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
} }
@@ -57,7 +57,8 @@ public class ProgressivePossibleNextRepaymentCalculationServiceImpl extends Abst
ProgressiveLoanInterestScheduleModel scheduleModel = optionalScheduleModel.get(); ProgressiveLoanInterestScheduleModel scheduleModel = optionalScheduleModel.get();
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments();
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), repaymentScheduleInstallments, Set.of(), 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.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff()); ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination()); ctx.setContractTerminated(loan.isContractTermination());
@@ -229,7 +229,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments, ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc()); LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel); changedTransactionDetail, scheduleModel, loan.getActiveLoanTermVariations());
List<ChangeOperation> changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges); 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()); .filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList());
if (validateInterestRefundTransactionRelation(loanTransaction)) { if (validateInterestRefundTransactionRelation(loanTransaction)) {
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate, final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
modifiedTransactions, unmodifiedTransactionIds); modifiedTransactions, unmodifiedTransactionIds, ctx.getActiveLoanTermVariations());
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund); final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
loanTransaction.updateAmount(newAmount.getAmount()); 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.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; 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.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; 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, public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail, Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
ProgressiveLoanInterestScheduleModel model) { ProgressiveLoanInterestScheduleModel model, List<LoanTermVariations> activeLoanTermVariations) {
this(currency, installments, charges, overpaymentHolder, changedTransactionDetail, model, Money.zero(currency)); this(currency, installments, charges, overpaymentHolder, changedTransactionDetail, model, Money.zero(currency),
activeLoanTermVariations);
} }
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail, Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount) { ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount,
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail); List<LoanTermVariations> activeLoanTermVariations) {
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail, activeLoanTermVariations);
this.sumOfInterestRefundAmount = sumOfInterestRefundAmount; this.sumOfInterestRefundAmount = sumOfInterestRefundAmount;
this.model = model; this.model = model;
} }
@@ -112,7 +112,8 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
savedModel = extractModel(progressiveLoanModel); savedModel = extractModel(progressiveLoanModel);
if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) { if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) {
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), 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.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff()); ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination()); ctx.setContractTerminated(loan.isContractTermination());
@@ -164,8 +164,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
when(loanTransaction.isPenaltyPayment()).thenReturn(false); when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction, underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge),
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null)); new MoneyHolder(overpaidAmount), null, loan.getActiveLoanTermVariations()));
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney)); Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); 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(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
when(loanTransaction.isPenaltyPayment()).thenReturn(false); when(loanTransaction.isPenaltyPayment()).thenReturn(false);
underTest.processLatestTransaction(loanTransaction, underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge),
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null)); new MoneyHolder(overpaidAmount), null, loan.getActiveLoanTermVariations()));
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney)); Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), refEq(zero)); 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(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
when(loanTransaction.isOn(transactionDate)).thenReturn(true); when(loanTransaction.isOn(transactionDate)).thenReturn(true);
underTest.processLatestTransaction(loanTransaction, underTest.processLatestTransaction(loanTransaction, new TransactionCtx(currency, List.of(installment), Set.of(charge),
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount), null)); new MoneyHolder(overpaidAmount), null, loan.getActiveLoanTermVariations()));
Mockito.verify(installment, times(1)).payFeeChargesComponent(transactionDate, chargeAmountMoney); Mockito.verify(installment, times(1)).payFeeChargesComponent(transactionDate, chargeAmountMoney);
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
@@ -289,7 +289,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
installments.add(installment); installments.add(installment);
// when // 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); underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify principal // verify principal
@@ -346,7 +347,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
installments.add(installment); installments.add(installment);
// when // 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); underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify charges on installment // verify charges on installment
@@ -398,7 +400,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
installments.add(installment2); installments.add(installment2);
// when // 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); underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify principal // verify principal
@@ -491,7 +494,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
// Set up TransactionCtx with installments and charges // Set up TransactionCtx with installments and charges
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, Set.of(), overpaymentHolder, changedTransactionDetail, TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, Set.of(), overpaymentHolder, changedTransactionDetail,
model, null); model, loan.getActiveLoanTermVariations());
// Mock additional necessary methods // Mock additional necessary methods
LoanCharge loanCharge = mock(LoanCharge.class); LoanCharge loanCharge = mock(LoanCharge.class);
@@ -570,7 +573,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
when(model.getMaturityDate()).thenReturn(LocalDate.of(2023, 12, 31)); when(model.getMaturityDate()).thenReturn(LocalDate.of(2023, 12, 31));
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, spyInstallments, Set.of(), new MoneyHolder(Money.zero(currency)), 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); underTest.processLatestTransaction(disbursementTransaction, ctx);
@@ -189,10 +189,11 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} }
Money totalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), refundTransaction.getTransactionDate(), Money totalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), refundTransaction.getTransactionDate(),
List.of(), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList()); List.of(), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList(),
loan.getActiveLoanTermVariations());
Money newTotalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), Money newTotalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(),
refundTransaction.getTransactionDate(), List.of(refundTransaction), refundTransaction.getTransactionDate(), List.of(refundTransaction),
loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList()); loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList(), loan.getActiveLoanTermVariations());
BigDecimal interestRefundAmount = totalInterest.minus(newTotalInterest).getAmount(); BigDecimal interestRefundAmount = totalInterest.minus(newTotalInterest).getAmount();
if (MathUtil.isZero(interestRefundAmount)) { if (MathUtil.isZero(interestRefundAmount)) {
@@ -846,12 +847,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
if (processLatest) { if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), refundTransaction, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), refundTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
loan.getLoanTransactions().add(refundTransaction); loan.getLoanTransactions().add(refundTransaction);
if (interestRefundTransaction != null) { if (interestRefundTransaction != null) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(),
interestRefundTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), interestRefundTransaction,
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
loan.addLoanTransaction(interestRefundTransaction); loan.addLoanTransaction(interestRefundTransaction);
} }
} else { } else {
@@ -444,11 +444,6 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
} else { } else {
loanSchedule.updateLoanSchedule(loan, loanScheduleDTO.getLoanScheduleModel()); loanSchedule.updateLoanSchedule(loan, loanScheduleDTO.getLoanScheduleModel());
} }
loanAccrualsProcessingService.reprocessExistingAccruals(loan, true);
loanChargeService.recalculateAllCharges(loan);
reprocessLoanTransactionsService.reprocessTransactions(loan);
this.loanRepaymentScheduleHistoryRepository.saveAll(loanRepaymentScheduleHistoryList);
loan.updateRescheduledByUser(appUser); loan.updateRescheduledByUser(appUser);
loan.updateRescheduledOnDate(DateUtils.getBusinessLocalDate()); loan.updateRescheduledOnDate(DateUtils.getBusinessLocalDate());
@@ -456,6 +451,12 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
// update the status of the request // update the status of the request
loanRescheduleRequest.approve(appUser, approvedOnDate); loanRescheduleRequest.approve(appUser, approvedOnDate);
loanAccrualsProcessingService.reprocessExistingAccruals(loan, true);
loanChargeService.recalculateAllCharges(loan);
reprocessLoanTransactionsService.reprocessTransactions(loan);
this.loanRepaymentScheduleHistoryRepository.saveAll(loanRepaymentScheduleHistoryList);
Optional<LocalDate> lastTransactionDateForReprocessing = loanTransactionRepository.findLastTransactionDateForReprocessing(loan); Optional<LocalDate> lastTransactionDateForReprocessing = loanTransactionRepository.findLastTransactionDateForReprocessing(loan);
if (lastTransactionDateForReprocessing.isPresent()) { if (lastTransactionDateForReprocessing.isPresent()) {
loanLifecycleStateMachine.determineAndTransition(loan, lastTransactionDateForReprocessing.get()); loanLifecycleStateMachine.determineAndTransition(loan, lastTransactionDateForReprocessing.get());
@@ -2244,9 +2244,11 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan); final InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan);
final Money totalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), targetTxn.getTransactionDate(), final Money totalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), targetTxn.getTransactionDate(),
List.of(), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList()); List.of(), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList(),
loan.getActiveLoanTermVariations());
final Money newTotalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), targetTxn.getTransactionDate(), final Money newTotalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), targetTxn.getTransactionDate(),
List.of(targetTxn), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList()); List.of(targetTxn), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList(),
loan.getActiveLoanTermVariations());
final BigDecimal interestRefundAmount = totalInterest.minus(newTotalInterest).getAmount(); final BigDecimal interestRefundAmount = totalInterest.minus(newTotalInterest).getAmount();
final Collection<PaymentTypeData> paymentTypeOptions = paymentTypeReadPlatformService.retrieveAllPaymentTypes(); final Collection<PaymentTypeData> paymentTypeOptions = paymentTypeReadPlatformService.retrieveAllPaymentTypes();
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.INTEREST_REFUND); final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.INTEREST_REFUND);
@@ -211,7 +211,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
ProgressiveLoanInterestScheduleModel model = savedModel.get(); ProgressiveLoanInterestScheduleModel model = savedModel.get();
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(), ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), model, getTotalRefundInterestAmount(loan)); new ChangedTransactionDetail(), model, getTotalRefundInterestAmount(loan), loan.getActiveLoanTermVariations());
progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan)); progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan));
progressiveContext.setChargedOff(loan.isChargedOff()); progressiveContext.setChargedOff(loan.isChargedOff());
progressiveContext.setWrittenOff(loan.isClosedWrittenOff()); progressiveContext.setWrittenOff(loan.isClosedWrittenOff());
@@ -964,8 +964,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (isTransactionChronologicallyLatest && (!reprocess || !loan.isInterestBearingAndInterestRecalculationEnabled()) if (isTransactionChronologicallyLatest && (!reprocess || !loan.isInterestBearingAndInterestRecalculationEnabled())
&& !loan.isForeclosure()) { && !loan.isForeclosure()) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(),
newInterestPaymentWaiverTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), newInterestPaymentWaiverTransaction,
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
reprocess = false; reprocess = false;
if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
@@ -2993,7 +2994,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (processLatest) { if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), interestRefundTxn, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), interestRefundTxn,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
loan.addLoanTransaction(interestRefundTxn); loan.addLoanTransaction(interestRefundTxn);
} else { } else {
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
@@ -3053,7 +3054,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.addLoanTransaction(chargebackTransaction); loan.addLoanTransaction(chargebackTransaction);
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargebackTransaction, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), chargebackTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
} }
loanLifecycleStateMachine.determineAndTransition(loan, chargebackTransaction.getTransactionDate()); loanLifecycleStateMachine.determineAndTransition(loan, chargebackTransaction.getTransactionDate());
} }
@@ -3253,7 +3254,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.addLoanTransaction(loanTransaction); loan.addLoanTransaction(loanTransaction);
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
} }
loanBalanceService.updateLoanSummaryDerivedFields(loan); loanBalanceService.updateLoanSummaryDerivedFields(loan);
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING, loan); loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING, loan);
@@ -3325,7 +3326,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.addLoanTransaction(loanTransaction); loan.addLoanTransaction(loanTransaction);
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null, loan.getActiveLoanTermVariations()));
loanBalanceService.updateLoanSummaryDerivedFields(loan); loanBalanceService.updateLoanSummaryDerivedFields(loan);
} else if (totalOutstanding.isGreaterThanZero()) { } else if (totalOutstanding.isGreaterThanZero()) {
@@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
@@ -119,8 +120,10 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
@Override @Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public Money totalInterestByTransactions(LoanRepaymentScheduleTransactionProcessor processor, final Long loanId, public Money totalInterestByTransactions(LoanRepaymentScheduleTransactionProcessor processor, final Long loanId,
LocalDate relatedRefundTransactionDate, List<LoanTransaction> newTransactions, List<Long> oldTransactionIds) { LocalDate relatedRefundTransactionDate, List<LoanTransaction> newTransactions, List<Long> oldTransactionIds,
List<LoanTermVariations> activeLoanTermVariations) {
Loan loan = loanAssembler.assembleFrom(loanId); Loan loan = loanAssembler.assembleFrom(loanId);
loan.setLoanTermVariations(activeLoanTermVariations);
if (processor == null) { if (processor == null) {
processor = loanTransactionProcessingService.getTransactionProcessor(loan.getTransactionProcessingStrategyCode()); processor = loanTransactionProcessingService.getTransactionProcessor(loan.getTransactionProcessingStrategyCode());
} }
@@ -100,7 +100,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
final ProgressiveTransactionCtx progressiveTransactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(), final ProgressiveTransactionCtx progressiveTransactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), savedModel.get()); new ChangedTransactionDetail(), savedModel.get(), loan.getActiveLoanTermVariations());
progressiveTransactionCtx.setChargedOff(loan.isChargedOff()); progressiveTransactionCtx.setChargedOff(loan.isChargedOff());
progressiveTransactionCtx.setWrittenOff(loan.isClosedWrittenOff()); progressiveTransactionCtx.setWrittenOff(loan.isClosedWrittenOff());
progressiveTransactionCtx.setContractTerminated(loan.isContractTermination()); progressiveTransactionCtx.setContractTerminated(loan.isContractTermination());
@@ -108,7 +108,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
} }
} else { } else {
transactionCtx = new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), transactionCtx = new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail()); new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), loan.getActiveLoanTermVariations());
} }
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.integrationtests; package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder; import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
@@ -25,11 +27,15 @@ import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse;
import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
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.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper; import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
@@ -153,6 +159,65 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
}); });
} }
@Test
public void testMerchantIssuedRefundAndCreditBalanceRefundWithAdjustSchedule() {
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("24 September 2025", () -> {
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
final Long loanId = createLoanForRefundWithInterestRefund(clientId, "MERCHANT_ISSUED_REFUND", "24 September 2025", 116.89,
35.99, 3);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(116.89), "24 September 2025");
});
runAt("26 September 2025", () -> {
executeInlineCOB(loanIdRef.get());
addRepaymentForLoan(loanIdRef.get(), 117.12, "26 September 2025");
});
runAt("06 October 2025", () -> {
executeInlineCOB(loanIdRef.get());
loanTransactionHelper.makeMerchantIssuedRefund(loanIdRef.get(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("06 October 2025").locale(LOCALE).transactionAmount(8.13));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanIdRef.get());
// Validate Loan is Overpaid
assertTrue(loanDetails.getStatus().getOverpaid());
validateLoanSummaryBalances(loanDetails, 0.00, 117.12, 0.00, 116.89, 8.14);
});
runAt("07 October 2025", () -> {
executeInlineCOB(loanIdRef.get());
loanTransactionHelper.makeCreditBalanceRefund(loanIdRef.get(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("07 October 2025").locale(LOCALE).transactionAmount(8.14));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanIdRef.get());
// Validate Loan is Closed
assertTrue(loanDetails.getStatus().getClosedObligationsMet());
validateLoanSummaryBalances(loanDetails, 0.00, 117.12, 0.00, 116.89, null);
PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper//
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()//
.loanId(loanIdRef.get())//
.rescheduleReasonId(1L)//
.rescheduleFromDate("25 September 2025").dateFormat(DATETIME_PATTERN).locale(LOCALE)//
.submittedOnDate("07 October 2025")//
.newInterestRate(BigDecimal.valueOf(25.99)));//
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), //
new PostUpdateRescheduleLoansRequest()//
.approvedOnDate("07 October 2025").locale(LOCALE).dateFormat(DATETIME_PATTERN));//
loanDetails = loanTransactionHelper.getLoanDetails(loanIdRef.get());
// Validate Loan is Overpaid
assertTrue(loanDetails.getStatus().getOverpaid());
validateLoanSummaryBalances(loanDetails, 0.00, 117.06, 0.00, 116.89, 0.06);
});
}
private Long createAndDisburseLoanForMerchantIssuedRefundWithInterestRefund(Long clientId) { private Long createAndDisburseLoanForMerchantIssuedRefundWithInterestRefund(Long clientId) {
return createAndDisburseLoanForRefundWithInterestRefund(clientId, "MERCHANT_ISSUED_REFUND"); return createAndDisburseLoanForRefundWithInterestRefund(clientId, "MERCHANT_ISSUED_REFUND");
} }