FINERACT-2338: Allow backdated interest change on progressive loans

- charge-off handling
- write-off handling
- closed loans
- external owner changes
This commit is contained in:
adam.magyari
2025-08-13 13:52:30 +02:00
committed by Adam Saghy
parent c2519c77c7
commit 6b7df527de
11 changed files with 998 additions and 37 deletions
@@ -62,6 +62,7 @@ public class ProgressivePossibleNextRepaymentCalculationServiceImpl extends Abst
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), repaymentScheduleInstallments, Set.of(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), scheduleModel);
ctx.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination());
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(nextPaymentDueDate, ctx, false);
RepaymentPeriod repaymentPeriod = scheduleModel.findRepaymentPeriodByDueDate(nextPaymentDueDate)
@@ -1495,6 +1495,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) {
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) {
updateInstallmentsPrincipalAndInterestByModel(progressiveTransactionCtx);
progressiveTransactionCtx.setChargedOff(true);
}
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
}
@@ -1548,8 +1552,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx, boolean updateInstallments) {
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) {
Loan loan = ctx.getInstallments().getFirst().getLoan();
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff()
&& !ctx.isContractTerminated() && !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
if (isInterestRecalculationSupported(ctx, loan) && !loan.isNpa()
&& !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
boolean modelHasUpdates = rework(targetDate, ctx);
if (modelHasUpdates && updateInstallments) {
@@ -2313,9 +2317,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
boolean interestBearingAndInterestRecalculationEnabled = loanTransaction.getLoan()
.isInterestBearingAndInterestRecalculationEnabled();
if (interestBearingAndInterestRecalculationEnabled && ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& !progressiveTransactionCtx.isChargedOff() && !progressiveTransactionCtx.isContractTerminated()) {
if (isInterestRecalculationSupported(ctx, loanTransaction.getLoan())) {
// Clear any previously skipped installments before re-evaluating
ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) ctx;
progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().clear();
paymentAllocationContext
.setInAdvanceInstallmentsFilteringRules(installment -> loanTransaction.isBefore(installment.getDueDate())
@@ -2389,15 +2393,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
context.getTransactionMappings(), context.getLoanTransaction(), oldestPastDueInstallment,
context.getCtx().getCurrency());
Loan loan = context.getLoanTransaction().getLoan();
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()
&& !progressiveTransactionCtx.isChargedOff()
&& !progressiveTransactionCtx.isContractTerminated()) {
context.setAllocatedAmount(
handlingPaymentAllocationForInterestBearingProgressiveLoan(context.getLoanTransaction(),
context.getTransactionAmountUnprocessed(), context.getBalances(),
paymentAllocationType, oldestPastDueInstallment, progressiveTransactionCtx,
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges));
if (isInterestRecalculationSupported(context.getCtx(), loan)) {
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
context.getBalances(), paymentAllocationType, oldestPastDueInstallment,
(ProgressiveTransactionCtx) context.getCtx(), loanTransactionToRepaymentScheduleMapping,
oldestPastDueInstallmentCharges));
} else {
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment,
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
@@ -2418,14 +2419,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
context.getTransactionMappings(), context.getLoanTransaction(), dueInstallment,
context.getCtx().getCurrency());
Loan loan = context.getLoanTransaction().getLoan();
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()
&& !progressiveTransactionCtx.isChargedOff()
&& !progressiveTransactionCtx.isContractTerminated()) {
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
context.getBalances(), paymentAllocationType, dueInstallment, progressiveTransactionCtx,
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges));
if (isInterestRecalculationSupported(context.getCtx(), loan)) {
context.setAllocatedAmount(
handlingPaymentAllocationForInterestBearingProgressiveLoan(context.getLoanTransaction(),
context.getTransactionAmountUnprocessed(), context.getBalances(),
paymentAllocationType, dueInstallment, (ProgressiveTransactionCtx) context.getCtx(),
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges));
} else {
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, dueInstallment,
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
@@ -2472,13 +2471,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment);
}
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()
&& !progressiveTransactionCtx.isChargedOff()
&& !progressiveTransactionCtx.isContractTerminated()) {
if (isInterestRecalculationSupported(context.getCtx(), loan)) {
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
context.getLoanTransaction(), evenPortion, context.getBalances(), paymentAllocationType,
inAdvanceInstallment, progressiveTransactionCtx,
inAdvanceInstallment, (ProgressiveTransactionCtx) context.getCtx(),
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges));
} else {
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, inAdvanceInstallment,
@@ -3070,4 +3066,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.fetchFirstNormalInstallmentNumber(getCtx().getInstallments());
}
}
private boolean isInterestRecalculationSupported(TransactionCtx ctx, Loan loan) {
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) {
return loan.isInterestBearingAndInterestRecalculationEnabled() && !progressiveTransactionCtx.isChargedOff()
&& !progressiveTransactionCtx.isWrittenOff() && !progressiveTransactionCtx.isContractTerminated();
} else {
return false;
}
}
}
@@ -37,16 +37,18 @@ import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanIntere
public class ProgressiveTransactionCtx extends TransactionCtx {
private final ProgressiveLoanInterestScheduleModel model;
private List<LoanTransaction> alreadyProcessedTransactions = new ArrayList<>();
private final List<LoanTransaction> alreadyProcessedTransactions = new ArrayList<>();
@Setter
private Money sumOfInterestRefundAmount;
@Setter
private boolean isChargedOff = false;
@Setter
private boolean isWrittenOff = false;
@Setter
private boolean isContractTerminated = false;
@Setter
private boolean isPrepayAttempt = false;
private List<LoanRepaymentScheduleInstallment> skipRepaymentScheduleInstallments = new ArrayList<>();
private final List<LoanRepaymentScheduleInstallment> skipRepaymentScheduleInstallments = new ArrayList<>();
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
@@ -25,6 +25,7 @@ import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.Loan
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateApprovalDate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateForOverdueCharges;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateLoanIsActive;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateLoanStatusIsActiveOrClosed;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonComment;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleRequestStatus;
@@ -47,7 +48,9 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRescheduleRequestToTermVariationMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
@@ -70,7 +73,6 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
final JsonElement jsonElement = jsonCommand.parsedJson();
validateLoanIsActive(loan, dataValidatorBuilder);
validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
@@ -81,6 +83,11 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
boolean hasInterestRateChange = interestRate != null;
boolean hasAdjustDueDateChange = adjustedDueDate != null;
if (hasInterestRateChange) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
} else {
validateLoanIsActive(loan, dataValidatorBuilder);
}
if (hasInterestRateChange && hasAdjustDueDateChange) {
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.adjustedDueDateParamName).failWithCode(
@@ -144,7 +151,20 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
LocalDate rescheduleFromDate = loanRescheduleRequest.getRescheduleFromDate();
final Loan loan = loanRescheduleRequest.getLoan();
LoanRepaymentScheduleInstallment installment;
validateLoanIsActive(loan, dataValidatorBuilder);
boolean hasInterestRateChange = false;
for (LoanRescheduleRequestToTermVariationMapping mapping : loanRescheduleRequest
.getLoanRescheduleRequestToTermVariationMappings()) {
LoanTermVariationType termType = mapping.getLoanTermVariations().getTermType();
if (termType.isInterestRateVariation() || termType.isInterestRateFromInstallment()) {
hasInterestRateChange = true;
}
}
if (hasInterestRateChange) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
} else {
validateLoanIsActive(loan, dataValidatorBuilder);
}
if (loanRescheduleRequest.getInterestRateFromInstallmentTermVariationIfExists() != null) {
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
@@ -113,6 +113,7 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get());
ctx.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination());
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(businessDate, ctx);
}