Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@ruzeynalov
Copy link
Contributor

Description

Describe the changes made and why they were made.

Ignore if these details are present on the associated Apache Fineract JIRA ticket.

Checklist

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

  • Write the commit message as per https://github.com/apache/fineract/#pull-requests
  • 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 coding conventions at https://cwiki.apache.org/confluence/display/FINERACT/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
  • Submission is not a "code dump". (Large changes can be made "in repository" via a branch. Ask on the developer mailing list for guidance, if required.)

FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.

@ruzeynalov ruzeynalov marked this pull request as draft September 24, 2025 11:58
@ruzeynalov ruzeynalov force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch 4 times, most recently from 2ca485f to f9b159c Compare October 2, 2025 11:22
@ruzeynalov ruzeynalov force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from f9b159c to 966268f Compare October 7, 2025 12:51
@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch 2 times, most recently from 913e0a7 to a68ea39 Compare October 13, 2025 08:23
@ruzeynalov ruzeynalov force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from e6d847a to 391fbda Compare October 15, 2025 08:00
@ruzeynalov ruzeynalov marked this pull request as ready for review October 15, 2025 08:01
@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from 391fbda to c70e3c5 Compare October 15, 2025 08:07
@adamsaghy adamsaghy force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from c70e3c5 to c354140 Compare October 15, 2025 09:45
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
}

@Disabled
Copy link
Contributor

@adamsaghy adamsaghy Oct 15, 2025

Choose a reason for hiding this comment

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

If this test is not valid anymore, remove it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

.isEqualTo("error.msg.loan.reage.supported.only.for.progressive.loan.schedule.type");
}

@Disabled
Copy link
Contributor

@adamsaghy adamsaghy Oct 15, 2025

Choose a reason for hiding this comment

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

If this test is not valid anymore, remove it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

Comment on lines 3061 to 3084
final LoanApplicationTerms loanApplicationTerms = new LoanApplicationTerms.Builder().submittedOnDate(expectedDisbursementDate)
.currency(currency.getCurrencyData()).repaymentsStartingFromDate(reAgingStartDate)
.expectedDisbursementDate(expectedDisbursementDate).principal(outstandingPrincipalBalance.get())
.loanTermFrequency(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
.loanTermPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType())
.numberOfRepayments(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
.repaymentEvery(loanTransaction.getLoanReAgeParameter().getFrequencyNumber())
.repaymentPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType())
.interestRatePerPeriod(interestRate)
.interestRatePeriodFrequencyType(loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType())
.annualNominalInterestRate(interestRate).daysInMonthType(loan.getLoanProduct().fetchDaysInMonthType())
.daysInYearType(loan.getLoanProduct().fetchDaysInYearType()).inArrearsTolerance(Money.zero(currency, mc))
.disbursementDatas(disbursementData).isDownPaymentEnabled(false).downPaymentPercentage(ZERO).seedDate(reAgingStartDate)
.interestRecognitionOnDisbursementDate(
loan.getLoanProduct().getLoanProductRelatedDetail().isInterestRecognitionOnDisbursementDate())
.daysInYearCustomStrategy(loan.getLoanProduct().getLoanProductRelatedDetail().getDaysInYearCustomStrategy())
.interestMethod(loan.getLoanProductRelatedDetail().getInterestMethod()).allowPartialPeriodInterestCalculation(
loan.getLoanProduct().getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation())
.mc(mc).build();
final LoanScheduleModel loanScheduleModelForReAging = scheduleGenerator.generate(mc, loanApplicationTerms, null, null);

// Now merge the temporary schedule with existing installments
mergeTemporaryScheduleWithExistingInstallments(loanScheduleModelForReAging, installments, repaymentPeriods, currency,
transactionDate, ctx, loan);
Copy link
Contributor

Choose a reason for hiding this comment

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

@mariiaKraievska Do we need this? I mean ProgressiveEMICalculator and existing ProgressiveLoanInterestScheduleModel + reage parameters would be enough to generate and update the existing ProgressiveLoanInterestScheduleModel and once its done, just update the LoanRepaymentScheduleInstallment based on the model. what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Done, please check

Copy link
Contributor

Choose a reason for hiding this comment

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

what changed here?

if (chargeOffTransaction.isPresent()) {
repaymentPeriods.stream().filter(rp -> !rp.getDueDate().isBefore(reAgingStartDate) && !rp.isFullyPaid())
.forEach(repaymentPeriod -> {
repaymentPeriod.getInterestPeriods().forEach(interestPeriod -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

why to set 0 rate factor and mark them as paused?

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right, it's enough to pause them.

Copy link
Contributor

Choose a reason for hiding this comment

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

why to pause them? :D

@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from c354140 to 3329a93 Compare October 15, 2025 12:00
@mariiaKraievska
Copy link
Contributor

PTAL

Comment on lines 2989 to 2992
final Optional<LoanTransaction> chargeOffTransaction = ctx.getAlreadyProcessedTransactions().stream()
.filter(t -> ((t.isChargeOff() && (loan.hasAccelerateChargeOffStrategy() || loan.hasZeroInterestChargeOffStrategy()))
|| t.isContractTermination()) && !t.isReversed() && t.getTransactionDate().isBefore(reAgingStartDate))
.findFirst();
Copy link
Contributor

Choose a reason for hiding this comment

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

Its 2 types of transactions: chargeoff and contract termination

Comment on lines 2989 to 2992
final Optional<LoanTransaction> chargeOffTransaction = ctx.getAlreadyProcessedTransactions().stream()
.filter(t -> ((t.isChargeOff() && (loan.hasAccelerateChargeOffStrategy() || loan.hasZeroInterestChargeOffStrategy()))
|| t.isContractTermination()) && !t.isReversed() && t.getTransactionDate().isBefore(reAgingStartDate))
.findFirst();
Copy link
Contributor

Choose a reason for hiding this comment

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

what if txn date and reage start date are matching?

Comment on lines 3078 to 3080
loan.getLoanProduct().getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation())
.mc(mc).build();
final LoanScheduleModel loanScheduleModelForReAging = scheduleGenerator.generate(mc, loanApplicationTerms, null, null);
Copy link
Contributor

Choose a reason for hiding this comment

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

I dont think we need this. scheduleGenerator we should use the existing model + the EmiCalculator and introduce a new action: "reage" and provide its parameters. THe EmiCalculator can calculate interim models and update existing one based on that. After the model is updated we can use to update the existing repayment installments based on the model alone.

@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch 5 times, most recently from 688c242 to ae93391 Compare October 17, 2025 09:05
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount());
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
private void removeOutstandingAmountsFromInstallment(final LoanRepaymentScheduleInstallment existingInstallment,
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be better to use updateInstallmentsByModelForReAging and set principal and interest from the model directly?

Copy link
Contributor

Choose a reason for hiding this comment

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

Done 👍

@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from ae93391 to e8aea1d Compare October 17, 2025 10:26
@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch 5 times, most recently from 2a5c344 to 88cb761 Compare October 20, 2025 14:40
@ruzeynalov ruzeynalov changed the title FINERACT-2354: Re-aging for Interest bearing products- Interest calculation: Default Behavior FINERACT-2354: First step - basic implementation of re-aging for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change (without edge cases) Oct 20, 2025
@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch 2 times, most recently from 4296417 to 23ec571 Compare October 22, 2025 10:18
Comment on lines 595 to 598
LocalDate disbursementDate = transactionDate;
if (!reAgingStartDate.isAfter(transactionDate)) {
disbursementDate = periodStartDate;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This might not needed anymore. I believe we always need to go with transaction date.

Copy link
Contributor

Choose a reason for hiding this comment

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

Removed

Comment on lines 577 to 584
final List<RepaymentPeriod> periodsBeforeReAging = existingRepaymentPeriods.stream()
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList();

periodsBeforeReAging.forEach(rp -> {
final InterestPeriod lastInterestPeriod = rp.getInterestPeriods().getLast();
lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated());
rp.setEmi(rp.getTotalPaidAmount());
});
Copy link
Contributor

@adamsaghy adamsaghy Oct 27, 2025

Choose a reason for hiding this comment

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

Please extract into a method and leave a short description what it does, like why we are negating the balance correction, etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

Comment on lines 586 to 598
final LocalDate periodStartDate = switch (loanReAgeParameter.getFrequencyType()) {
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
case MONTHS -> reAgingStartDate.minusMonths(loanReAgeParameter.getFrequencyNumber());
case YEARS -> reAgingStartDate.minusYears(loanReAgeParameter.getFrequencyNumber());
case WHOLE_TERM -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WHOLE_TERM");
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
};

LocalDate disbursementDate = transactionDate;
if (!reAgingStartDate.isAfter(transactionDate)) {
disbursementDate = periodStartDate;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets extract into a method and leave a short description what it does, like: calculating the reage start date which is used to calculate the due date, etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

Comment on lines 601 to 609
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, null);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), null,
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);

addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(disbursementDate, loanApplicationTerms.getPrincipal()));

final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();
Copy link
Contributor

Choose a reason for hiding this comment

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

Extract into a method (generate reage submodel, or something like that) and leave a short description. Return the new model.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

Comment on lines 616 to 639
final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream()
.filter(period -> period.getDueDate().equals(reAgeDate)).findFirst();

for (final RepaymentPeriod newPeriod : newPeriods) {
final Optional<RepaymentPeriod> existingRepaymentPeriodOpt = existingRepaymentPeriods.stream().filter(
period -> period.getFromDate().equals(newPeriod.getFromDate()) && period.getDueDate().equals(newPeriod.getDueDate()))
.findFirst();
Optional<RepaymentPeriod> previousExistingRepaymentPeriodOpt = Optional.empty();
if (existingRepaymentPeriodOpt.isPresent() && firstExistingRepaymentPeriodOpt.isPresent()
&& existingRepaymentPeriodOpt.get().equals(firstExistingRepaymentPeriodOpt.get())) {
previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious();
}

final Money newPrincipal = newPeriod.getDuePrincipal();
final Money newInterest = newPeriod.getDueInterest();

final RepaymentPeriod rp = RepaymentPeriod.create(
previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(),
newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(),
loanTransaction.getLoan().getLoanProductRelatedDetail());
rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount());

existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove);
existingRepaymentPeriods.add(rp);
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
}

final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast();
final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream()
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate)
|| repaymentPeriod.getDueDate().isEqual(reAgingStartDate))
&& !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate()))
.toList();

calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, transactionDate);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods);
Copy link
Contributor

Choose a reason for hiding this comment

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

Extract into a method and leave a short description what it does (like merging the newmodel and existing one together and after recalculate the balances).

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

Comment on lines +1307 to +1314
if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) {
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be moved into the org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl#handleChargebackTransaction method.

Copy link
Contributor

Choose a reason for hiding this comment

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

I tried to make this changes, but now I get this error "During synchronization a new object was found through a relationship that was not marked cascade PERSIST: org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction"

Comment on lines 98 to 111
loan.addLoanTransaction(reAgeTransaction);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());
if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())
&& !loan.isInterestBearingAndInterestRecalculationEnabled()) {
reprocessLoanTransactionsService.reprocessTransactionsWithPostTransactionChecks(loan, reAgeTransaction.getTransactionDate());
} else if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
reprocessLoanTransactionsService.reprocessTransactions(loan);
Copy link
Contributor

Choose a reason for hiding this comment

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

I dont think we should add reage txn to the loan before the reprocessing, hence it might be flushed during the loanUtilService.buildScheduleGeneratorDTO or loanScheduleService.regenerateRepaymentSchedule and in that case, it will be immediately reverse-replayed.

I would rather provide transaction list to the reprocessLoanTransactionsService.reprocessTransactions and only after it was reprocessed to be added to the loan (if needed)

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

@adamsaghy
Copy link
Contributor

@mariiaKraievska Please rebase.

@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from 23ec571 to abb13c1 Compare October 27, 2025 12:17
ruzeynalov and others added 2 commits October 27, 2025 14:22
…t bearing products with interest calculation: default behavior
…rest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change
@mariiaKraievska mariiaKraievska force-pushed the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch from abb13c1 to 04e51f8 Compare October 27, 2025 12:28
@mariiaKraievska
Copy link
Contributor

PTAL

Copy link
Contributor

@adamsaghy adamsaghy left a comment

Choose a reason for hiding this comment

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

LGTM

@adamsaghy adamsaghy merged commit d21a1a9 into apache:develop Oct 27, 2025
33 checks passed
@adamsaghy adamsaghy deleted the FINERACT-2354/reaging-for-interest-bearing-products-interest-calculation-default-behaviour branch October 27, 2025 13:22
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.

4 participants