Skip to content

FINERACT-1153: Split oversized last reschedule installment#5523

Open
airajena wants to merge 1 commit intoapache:developfrom
airajena:FINERACT-1153/split-large-last-installment-lf
Open

FINERACT-1153: Split oversized last reschedule installment#5523
airajena wants to merge 1 commit intoapache:developfrom
airajena:FINERACT-1153/split-large-last-installment-lf

Conversation

@airajena
Copy link
Contributor

Summary

This PR implements FINERACT-1153 by adding configurable handling for loan reschedule cases where the final installment becomes much larger than prior installments.

Problem

For some loan reschedule flows (for example reduced EMI / interest appropriation scenarios), the generated schedule can produce an oversized last installment, which is not desirable operationally.

Solution

Introduced a new global configuration and reschedule logic to automatically split an oversized last installment by extending repayment periods until the last installment is in line.

New global configuration

  • Key: split-large-last-installment-on-loan-reschedule
  • Default: false (disabled)
  • Scope: tenant global configuration

Behavior

  • When config is false: existing behavior is unchanged.
  • When config is true:
    • During loan reschedule approval, schedule generation checks whether the final installment is larger than prior installments.
    • If yes, it auto-adds repayment-period extension variation(s), regenerates the schedule, and repeats until:
      • last installment is no longer oversized, or
      • safety cap is reached.

Safety guard

  • Added max extension loop guard to prevent infinite adjustments.

Checklist

Please make sure these boxes are checked before submitting your pull request - thanks!

  • Write the commit message as per our guidelines
  • Acknowledge that we will not review PRs that are not passing the build ("green") - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.
  • Create/update unit or integration tests for verifying the changes made.
  • Follow our coding conventions.
  • Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes
  • This PR must not be a "code dump". Large changes can be made in a branch, with assistance. Ask for help on the developer mailing list.

@airajena airajena force-pushed the FINERACT-1153/split-large-last-installment-lf branch from 56530e1 to 85639c5 Compare February 21, 2026 19:47
@adamsaghy
Copy link
Contributor

@airajena Please rebase the PR (sorry we have a high number of PRs nowadays)!

@airajena airajena force-pushed the FINERACT-1153/split-large-last-installment-lf branch 2 times, most recently from c989939 to 9ec5131 Compare February 24, 2026 17:54
@airajena airajena force-pushed the FINERACT-1153/split-large-last-installment-lf branch from 9ec5131 to 99e0bd7 Compare February 28, 2026 17:57
Comment on lines +524 to +554
private boolean isLastInstallmentAmountTooLarge(final LoanScheduleDTO loanScheduleDTO, final MonetaryCurrency currency,
final LocalDate rescheduleFromDate) {
final List<LoanRepaymentScheduleInstallment> installments = loanScheduleDTO.getInstallments();
if (installments == null || installments.size() < 2) {
return false;
}

final List<LoanRepaymentScheduleInstallment> rescheduledInstallments = new ArrayList<>();
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isDownPayment() || DateUtils.isBefore(installment.getDueDate(), rescheduleFromDate)) {
continue;
}
rescheduledInstallments.add(installment);
}

if (rescheduledInstallments.size() < 2) {
return false;
}

final LoanRepaymentScheduleInstallment lastInstallment = rescheduledInstallments.get(rescheduledInstallments.size() - 1);
BigDecimal maxPriorDueAmount = BigDecimal.ZERO;
for (int i = 0; i < rescheduledInstallments.size() - 1; i++) {
final BigDecimal dueAmount = rescheduledInstallments.get(i).getDue(currency).getAmount();
if (dueAmount.compareTo(maxPriorDueAmount) > 0) {
maxPriorDueAmount = dueAmount;
}
}

final BigDecimal lastInstallmentDueAmount = lastInstallment.getDue(currency).getAmount();
final BigDecimal minimumMaterialDifference = BigDecimal.ONE.movePointLeft(currency.getDigitsAfterDecimal());
return lastInstallmentDueAmount.subtract(maxPriorDueAmount).compareTo(minimumMaterialDifference) > 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you help me understand the logic behind this?

I’m curious about what constitutes “too large” in this context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants