-
Notifications
You must be signed in to change notification settings - Fork 2.2k
FINERACT-2354: First step - basic implementation of re-aging for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change (without edge cases) #5053
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FINERACT-2354: First step - basic implementation of re-aging for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change (without edge cases) #5053
Conversation
2ca485f to
f9b159c
Compare
f9b159c to
966268f
Compare
913e0a7 to
a68ea39
Compare
e6d847a to
391fbda
Compare
391fbda to
c70e3c5
Compare
c70e3c5 to
c354140
Compare
| .isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero"); | ||
| } | ||
|
|
||
| @Disabled |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
...oanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
Outdated
Show resolved
Hide resolved
| 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, please check
There was a problem hiding this comment.
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 -> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
c354140 to
3329a93
Compare
|
PTAL |
| final Optional<LoanTransaction> chargeOffTransaction = ctx.getAlreadyProcessedTransactions().stream() | ||
| .filter(t -> ((t.isChargeOff() && (loan.hasAccelerateChargeOffStrategy() || loan.hasZeroInterestChargeOffStrategy())) | ||
| || t.isContractTermination()) && !t.isReversed() && t.getTransactionDate().isBefore(reAgingStartDate)) | ||
| .findFirst(); |
There was a problem hiding this comment.
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
| final Optional<LoanTransaction> chargeOffTransaction = ctx.getAlreadyProcessedTransactions().stream() | ||
| .filter(t -> ((t.isChargeOff() && (loan.hasAccelerateChargeOffStrategy() || loan.hasZeroInterestChargeOffStrategy())) | ||
| || t.isContractTermination()) && !t.isReversed() && t.getTransactionDate().isBefore(reAgingStartDate)) | ||
| .findFirst(); |
There was a problem hiding this comment.
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?
| loan.getLoanProduct().getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation()) | ||
| .mc(mc).build(); | ||
| final LoanScheduleModel loanScheduleModelForReAging = scheduleGenerator.generate(mc, loanApplicationTerms, null, null); |
There was a problem hiding this comment.
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.
688c242 to
ae93391
Compare
| LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, | ||
| reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount()); | ||
| insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); | ||
| private void removeOutstandingAmountsFromInstallment(final LoanRepaymentScheduleInstallment existingInstallment, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍
ae93391 to
e8aea1d
Compare
2a5c344 to
88cb761
Compare
4296417 to
23ec571
Compare
| LocalDate disbursementDate = transactionDate; | ||
| if (!reAgingStartDate.isAfter(transactionDate)) { | ||
| disbursementDate = periodStartDate; | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
| 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()); | ||
| }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| 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; | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| 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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
| 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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
@mariiaKraievska Please rebase. |
23ec571 to
abb13c1
Compare
…t bearing products with interest calculation: default behavior
…rest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change
abb13c1 to
04e51f8
Compare
|
PTAL |
adamsaghy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
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!
FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.