-
Notifications
You must be signed in to change notification settings - Fork 2.2k
FINERACT-2312: full term calculation #5219
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-2312: full term calculation #5219
Conversation
c7d8a3b to
ed3c0ea
Compare
8503394 to
b677187
Compare
|
4327678 to
a05daaa
Compare
a05daaa to
65e7bac
Compare
65e7bac to
5a2503b
Compare
|
5a2503b to
b4ad7ae
Compare
| public BigDecimal getRateFactorPlus1FromDate(LocalDate fromDate) { | ||
| return interestPeriods.stream().filter(ip -> !ip.getFromDate().isBefore(fromDate)).map(InterestPeriod::getRateFactor) | ||
| .reduce(BigDecimal.ONE, BigDecimal::add); | ||
| } |
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.
getRateFactorPlus1FromDate
has implementation .reduce(BigDecimal.ONE, BigDecimal::add);
1 + rate1 + rate2 + rate3 ...
But the Javadoc says:
sum of (Rate Factor + 1) from interest periods
similar to.
(rate1 + 1) + (rate2 + 1) + ...
Needs some more clarification in this javadoc, it is ambiguous
| * the date from which to include interest periods | ||
| * @return rate factor plus 1 for filtered interest periods | ||
| */ | ||
| public BigDecimal getRateFactorPlus1FromDate(LocalDate fromDate) { |
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.
Are we sure that interestPeriods never be null?
| final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf(); | ||
| ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments, | ||
| LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc()); | ||
|
|
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 whole thing can be handled as part of emiCalculator.generateInstallmentInterestScheduleModel
| expectedRepaymentPeriods, loanApplicationTerms.toLoanConfigurationDetails(), | ||
| loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc); | ||
|
|
||
| interestScheduleModel.allowFullTermForTranche(loanApplicationTerms.isAllowFullTermForTranche()); |
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.
Same as above
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.
allowFullTermForTranche can be stored on the model as part of loanProductRelatedDetail.
originalNumberOfRepayments we don t need this. Its already there on the loanProductRelatedDetail
| emiCalculator.addRepaymentPeriods(interestScheduleModel, extensionResult.disbursementDate, | ||
| extensionResult.additionalPeriods, List.of()); | ||
| } | ||
| } |
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 at all. When a new disbursement added to the loan, we can handle the additional periods to be added.
| final List<LoanScheduleModelPeriod> periods, final BigDecimal chargesDueAtTimeOfDisbursement, | ||
| final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) { | ||
|
|
||
| // Check if any disbursement has actually occurred (only relevant for Full Term Tranche) |
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 do we need this?
|
|
||
| private ScheduleExtensionResult calculateAdditionalPeriodsForFullTermTranches(final List<DisbursementData> disbursementDataList, | ||
| final List<LoanScheduleModelRepaymentPeriod> existingPeriods, final LoanApplicationTerms loanApplicationTerms) { | ||
| if (disbursementDataList.size() <= 1) { | ||
| return new ScheduleExtensionResult(0, null); | ||
| } | ||
|
|
||
| int maxAdditionalPeriods = 0; | ||
| LocalDate maxDisbursementDate = null; | ||
| final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); | ||
| final int currentPeriodCount = existingPeriods.size(); | ||
|
|
||
| // For each subsequent tranche, calculate how many additional periods are needed | ||
| // Each tranche needs 'numberOfRepayments' periods starting from its disbursement period | ||
| for (int i = 1; i < disbursementDataList.size(); i++) { | ||
| DisbursementData disbursementData = disbursementDataList.get(i); | ||
| // Skip expected disbursements that haven't actually been disbursed yet | ||
| if (!disbursementData.isDisbursed()) { | ||
| continue; | ||
| } | ||
| LocalDate disbursementDate = disbursementData.disbursementDate(); | ||
| int periodIndex = findPeriodIndexForDate(disbursementDate, existingPeriods); | ||
| int lastRequiredPeriodIndex = periodIndex + numberOfRepayments - 1; | ||
| int additionalPeriodsForThisTranche = Math.max(0, lastRequiredPeriodIndex - currentPeriodCount + 1); | ||
| if (additionalPeriodsForThisTranche > maxAdditionalPeriods) { | ||
| maxAdditionalPeriods = additionalPeriodsForThisTranche; | ||
| maxDisbursementDate = disbursementDate; | ||
| } | ||
| } | ||
|
|
||
| return new ScheduleExtensionResult(maxAdditionalPeriods, maxDisbursementDate); | ||
| } | ||
|
|
||
| private record ScheduleExtensionResult(int additionalPeriods, LocalDate disbursementDate) { | ||
| } | ||
|
|
||
| private int findPeriodIndexForDate(final LocalDate date, final List<LoanScheduleModelRepaymentPeriod> periods) { | ||
| for (int i = 0; i < periods.size(); i++) { | ||
| LoanScheduleModelRepaymentPeriod period = periods.get(i); | ||
| if (!date.isBefore(period.getFromDate()) && date.isBefore(period.getDueDate())) { | ||
| return i; | ||
| } | ||
| } | ||
| return periods.size() - 1; | ||
| } | ||
|
|
||
| private List<LoanScheduleModelRepaymentPeriod> generateAdditionalPeriods(final MathContext mc, final int additionalPeriods, | ||
| final List<LoanScheduleModelRepaymentPeriod> existingPeriods, final LoanApplicationTerms loanApplicationTerms, | ||
| final HolidayDetailDTO holidayDetailDTO) { | ||
| final Money zeroAmount = Money.zero(loanApplicationTerms.getCurrency(), mc); | ||
| final List<LoanScheduleModelRepaymentPeriod> extensionPeriods = new ArrayList<>(additionalPeriods); | ||
|
|
||
| LoanScheduleModelRepaymentPeriod lastPeriod = existingPeriods.get(existingPeriods.size() - 1); | ||
| LocalDate lastRepaymentDate = lastPeriod.getDueDate(); | ||
| int startingPeriodNumber = existingPeriods.size() + 1; | ||
|
|
||
| for (int i = 0; i < additionalPeriods; i++) { | ||
| LocalDate nextRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, false); | ||
|
|
||
| if (i == additionalPeriods - 1) { | ||
| nextRepaymentDate = adjustRepaymentDate(nextRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate(); | ||
| } | ||
|
|
||
| extensionPeriods.add(LoanScheduleModelRepaymentPeriod.repayment(startingPeriodNumber + i, lastRepaymentDate, nextRepaymentDate, | ||
| zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, zeroAmount, false, mc)); | ||
| lastRepaymentDate = nextRepaymentDate; | ||
| } | ||
|
|
||
| return extensionPeriods; | ||
| } | ||
|
|
||
| private LocalDate generateNextRepaymentDate(final LocalDate lastRepaymentDate, final LoanApplicationTerms loanApplicationTerms, | ||
| final boolean isFirstRepayment) { | ||
| return scheduledDateGenerator.generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment); | ||
| } | ||
|
|
||
| private AdjustedDateDetailsDTO adjustRepaymentDate(final LocalDate repaymentDate, final LoanApplicationTerms loanApplicationTerms, | ||
| final HolidayDetailDTO holidayDetailDTO) { | ||
| return scheduledDateGenerator.adjustRepaymentDate(repaymentDate, loanApplicationTerms, holidayDetailDTO); | ||
| } |
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 any of these... its not part of the generator to handle it, but the EmiCalculator "addDisbursement". ProgressiveLoanScheduleGenerator should NOT modifiy directly the ProgressiveLoanScheduleModel. Its not its responsibility!
| private LocalDate lastOverdueBalanceChange; | ||
|
|
||
| @Setter | ||
| private boolean allowFullTermForTranche = false; |
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 should be part of loanProductRelatedDetail
| private boolean allowFullTermForTranche = false; | ||
|
|
||
| @Setter | ||
| private int originalNumberOfRepayments; |
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 already available on the loanProductRelatedDetail
| * the date from which to include interest periods | ||
| * @return rate factor plus 1 for filtered interest periods | ||
| */ | ||
| public BigDecimal getRateFactorPlus1FromDate(LocalDate fromDate) { |
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.
We dont need this. We are already supporting mid period disbursements.
| } | ||
| } | ||
|
|
||
| private void addFullTermTrancheDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, |
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. When we are disbursing, we are simply checking the "on the fly" period should use how many periods. If the allowFullTermForTranche is true, we should just simply provide to the on-the-fly" model the number of installments from the loan product related details.
| * Term Tranche calculations where a disbursement occurs mid-period, this ensures only interest periods from the | ||
| * disbursement date are included. | ||
| */ | ||
| private BigDecimal calculateRateFactorPlus1NFromDate(final List<RepaymentPeriod> periods, final LocalDate fromDate, |
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 either. The "addDisbursement" action should handle all.
| .ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors( | ||
| getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel, | ||
| operation)); | ||
| if (scheduleModel.allowFullTermForTranche() && scheduleModel.originalNumberOfRepayments() > 0) { |
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.
We dont need a separate logic for this.
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.
Add
//TODO: If the allow full tranche is TRUE, we should generate and the new repayment periods to the `relatedRepaymentPeriods`
to the org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator#calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod
| // Process disbursement data | ||
| DisbursementData data = (DisbursementData) dataItem.getData(); | ||
| periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount); | ||
| // For FTT: only add disbursed tranches to cumulative balance |
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 do we need this?
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.