Skip to content

Commit b677187

Browse files
committed
FINERACT-2412: add full term tranche calculations
1 parent be63177 commit b677187

File tree

9 files changed

+274
-41
lines changed

9 files changed

+274
-41
lines changed

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,16 @@ public final class LoanSchedulePeriodData {
8181

8282
public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed,
8383
final BigDecimal feeChargesDueAtTimeOfDisbursement, final boolean isDisbursed) {
84+
return disbursementOnlyPeriod(disbursementDate, principalDisbursed, feeChargesDueAtTimeOfDisbursement, isDisbursed,
85+
principalDisbursed);
86+
}
87+
88+
public static LoanSchedulePeriodData disbursementOnlyPeriod(final LocalDate disbursementDate, final BigDecimal principalDisbursed,
89+
final BigDecimal feeChargesDueAtTimeOfDisbursement, final boolean isDisbursed,
90+
final BigDecimal principalLoanBalanceOutstanding) {
8491
return builder().dueDate(disbursementDate) //
8592
.principalDisbursed(principalDisbursed) //
86-
.principalLoanBalanceOutstanding(principalDisbursed) //
93+
.principalLoanBalanceOutstanding(principalLoanBalanceOutstanding) //
8794
.feeChargesDue(feeChargesDueAtTimeOfDisbursement) //
8895
.feeChargesPaid(isDisbursed ? feeChargesDueAtTimeOfDisbursement : null) //
8996
.feeChargesOutstanding(isDisbursed ? null : feeChargesDueAtTimeOfDisbursement) //

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> repr
227227
final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf();
228228
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
229229
LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
230+
231+
scheduleModel.allowFullTermForTranche(loan.isAllowFullTermForTranche());
232+
if (loan.isAllowFullTermForTranche()) {
233+
scheduleModel.originalNumberOfRepayments(loan.getNumberOfRepayments());
234+
}
235+
230236
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
231237
changedTransactionDetail, scheduleModel);
232238

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.List;
3232
import java.util.Optional;
3333
import java.util.Set;
34+
import lombok.extern.slf4j.Slf4j;
3435
import org.apache.fineract.infrastructure.core.service.MathUtil;
3536
import org.apache.fineract.organisation.monetary.data.CurrencyData;
3637
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -58,6 +59,7 @@
5859
import org.springframework.beans.factory.annotation.Autowired;
5960
import org.springframework.stereotype.Component;
6061

62+
@Slf4j
6163
@Component
6264
public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
6365

@@ -109,6 +111,10 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer
109111
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel(
110112
expectedRepaymentPeriods, loanApplicationTerms.toLoanConfigurationDetails(),
111113
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
114+
115+
interestScheduleModel.allowFullTermForTranche(loanApplicationTerms.isAllowFullTermForTranche());
116+
interestScheduleModel.originalNumberOfRepayments(loanApplicationTerms.getNumberOfRepayments());
117+
112118
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
113119

114120
prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms);
@@ -309,7 +315,15 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm
309315
final List<LoanScheduleModelPeriod> periods, final BigDecimal chargesDueAtTimeOfDisbursement,
310316
final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) {
311317

318+
// Check if any disbursement has actually occurred
319+
boolean hasAnyDisbursement = disbursementDataList.stream().anyMatch(DisbursementData::isDisbursed);
320+
312321
for (DisbursementData disbursementData : disbursementDataList) {
322+
// If at least one disbursement has occurred, skip expected (undisbursed) tranches.
323+
// If no disbursements have occurred yet, process all expected tranches for EMI calculation.
324+
if (hasAnyDisbursement && !disbursementData.isDisbursed()) {
325+
continue;
326+
}
313327
final LocalDate disbursementDate = disbursementData.disbursementDate();
314328
final LocalDate periodFromDate = scheduleParams.getPeriodStartDate();
315329
final LocalDate periodDueDate = scheduleParams.getActualRepaymentDate();
@@ -525,11 +539,24 @@ private ScheduleExtensionResult calculateAdditionalPeriodsForFullTermTranches(fi
525539
int maxAdditionalPeriods = 0;
526540
LocalDate maxDisbursementDate = null;
527541
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
542+
final int currentPeriodCount = existingPeriods.size();
528543

544+
// For each subsequent tranche, calculate how many additional periods are needed
545+
// Each tranche needs 'numberOfRepayments' periods starting from its disbursement period
529546
for (int i = 1; i < disbursementDataList.size(); i++) {
530-
LocalDate disbursementDate = disbursementDataList.get(i).disbursementDate();
547+
DisbursementData disbursementData = disbursementDataList.get(i);
548+
// Skip expected disbursements that haven't actually been disbursed yet
549+
if (!disbursementData.isDisbursed()) {
550+
continue;
551+
}
552+
LocalDate disbursementDate = disbursementData.disbursementDate();
531553
int periodIndex = findPeriodIndexForDate(disbursementDate, existingPeriods);
532-
int additionalPeriodsForThisTranche = periodIndex;
554+
// This tranche needs 'numberOfRepayments' periods. The first repayment for this tranche
555+
// is at the due date of the period containing the disbursement. Since the base schedule
556+
// already has the first tranche's periods, subsequent tranches need periods extending
557+
// from (periodIndex + 1) through (periodIndex + numberOfRepayments).
558+
int lastRequiredPeriodIndex = periodIndex + numberOfRepayments;
559+
int additionalPeriodsForThisTranche = Math.max(0, lastRequiredPeriodIndex - currentPeriodCount + 1);
533560
if (additionalPeriodsForThisTranche > maxAdditionalPeriods) {
534561
maxAdditionalPeriods = additionalPeriodsForThisTranche;
535562
maxDisbursementDate = disbursementDate;

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,86 @@ public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleM
130130
}
131131

132132
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
133-
scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate()))
134-
.forEach(rp -> rp.setTotalDisbursedAmount(rp.getTotalDisbursedAmount().add(operation.getAmount())));
133+
scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())).forEach(rp -> {
134+
rp.setTotalDisbursedAmount(rp.getTotalDisbursedAmount().add(operation.getAmount()));
135+
});
135136

136-
scheduleModel
137-
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
138-
scheduleModel.zero(), scheduleModel.zero())
139-
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
140-
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
141-
operation));
137+
if (scheduleModel.allowFullTermForTranche() && scheduleModel.originalNumberOfRepayments() > 0) {
138+
addFullTermTrancheDisbursement(scheduleModel, operation);
139+
} else {
140+
scheduleModel
141+
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
142+
scheduleModel.zero(), scheduleModel.zero())
143+
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
144+
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
145+
operation));
146+
}
147+
}
148+
149+
private void addFullTermTrancheDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel,
150+
final EmiChangeOperation operation) {
151+
final MathContext mc = scheduleModel.mc();
152+
final LocalDate disbursementDate = operation.getSubmittedOnDate();
153+
final Money disbursedAmount = operation.getAmount();
154+
final int originalNumberOfRepayments = scheduleModel.originalNumberOfRepayments();
155+
156+
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDate, disbursedAmount, scheduleModel.zero(),
157+
scheduleModel.zero());
158+
159+
int disbursementPeriodIndex = findDisbursementPeriodIndex(scheduleModel, disbursementDate);
160+
if (disbursementPeriodIndex < 0) {
161+
disbursementPeriodIndex = 0;
162+
}
163+
164+
int requiredEndIndex = disbursementPeriodIndex + originalNumberOfRepayments;
165+
int currentPeriodCount = scheduleModel.repaymentPeriods().size();
166+
if (requiredEndIndex > currentPeriodCount) {
167+
extendScheduleForFullTermTranche(scheduleModel, requiredEndIndex - currentPeriodCount);
168+
}
169+
170+
List<RepaymentPeriod> trancheRepaymentPeriods = scheduleModel.repaymentPeriods().subList(disbursementPeriodIndex,
171+
disbursementPeriodIndex + originalNumberOfRepayments);
172+
173+
if (trancheRepaymentPeriods.isEmpty()) {
174+
return;
175+
}
176+
177+
calculateRateFactorForPeriods(trancheRepaymentPeriods, scheduleModel);
178+
179+
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(trancheRepaymentPeriods, mc));
180+
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(trancheRepaymentPeriods, mc));
181+
182+
final Money trancheEmi = Money.of(disbursedAmount.getCurrencyData(),
183+
calculateEMIValue(rateFactorN, disbursedAmount.getAmount(), fnResult, mc), mc);
184+
final Money finalTrancheEmi = applyInstallmentAmountInMultiplesOf(scheduleModel, trancheEmi);
185+
186+
for (RepaymentPeriod period : trancheRepaymentPeriods) {
187+
Money existingEmi = period.getEmi() != null ? period.getEmi() : scheduleModel.zero();
188+
Money newEmi = existingEmi.plus(finalTrancheEmi, mc);
189+
period.setEmi(newEmi);
190+
period.setOriginalEmi(newEmi);
191+
}
192+
193+
calculateOutstandingBalance(scheduleModel);
194+
}
195+
196+
private void extendScheduleForFullTermTranche(final ProgressiveLoanInterestScheduleModel scheduleModel, int periodsToAdd) {
197+
final int existingCount = scheduleModel.repaymentPeriods().size();
198+
final LocalDate startDate = scheduleModel.getStartDate();
199+
final List<LocalDateInterval> newPeriodDates = generateAdditionalRepaymentPeriodDueDates(scheduleModel, periodsToAdd, existingCount,
200+
scheduleModel.resolveRepaymentPEriodLengthGeneratorFunction(startDate));
201+
updateModel(scheduleModel, newPeriodDates, LocalDateInterval::startDate, LocalDateInterval::endDate);
202+
}
203+
204+
private int findDisbursementPeriodIndex(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDate) {
205+
List<RepaymentPeriod> periods = scheduleModel.repaymentPeriods();
206+
for (int i = 0; i < periods.size(); i++) {
207+
RepaymentPeriod period = periods.get(i);
208+
if (!disbursementDate.isBefore(period.getFromDate()) && disbursementDate.isBefore(period.getDueDate())) {
209+
return i;
210+
}
211+
}
212+
return periods.size() - 1;
142213
}
143214

144215
private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestScheduleModel scheduleModel,

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ public class ProgressiveLoanInterestScheduleModel {
6969
@Setter
7070
private LocalDate lastOverdueBalanceChange;
7171

72+
@Setter
73+
private boolean allowFullTermForTranche = false;
74+
75+
@Setter
76+
private int originalNumberOfRepayments;
77+
7278
public ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods,
7379
final ILoanConfigurationDetails loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
7480
this.repaymentPeriods = new ArrayList<>(repaymentPeriods);
@@ -96,15 +102,21 @@ private ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repayme
96102
}
97103

98104
public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
99-
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail,
100-
installmentAmountInMultiplesOf, mc, false);
105+
ProgressiveLoanInterestScheduleModel copy = new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates,
106+
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc, false);
107+
copy.allowFullTermForTranche(this.allowFullTermForTranche);
108+
copy.originalNumberOfRepayments(this.originalNumberOfRepayments);
109+
return copy;
101110
}
102111

103112
public ProgressiveLoanInterestScheduleModel copyWithoutPaidAmounts() {
104113
final List<RepaymentPeriod> repaymentPeriodCopies = copyRepaymentPeriods(repaymentPeriods,
105114
(previousPeriod, repaymentPeriod) -> RepaymentPeriod.copyWithoutPaidAmounts(previousPeriod, repaymentPeriod, mc));
106-
return new ProgressiveLoanInterestScheduleModel(repaymentPeriodCopies, interestRates, loanProductRelatedDetail,
107-
installmentAmountInMultiplesOf, mc, true);
115+
ProgressiveLoanInterestScheduleModel copy = new ProgressiveLoanInterestScheduleModel(repaymentPeriodCopies, interestRates,
116+
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc, true);
117+
copy.allowFullTermForTranche(this.allowFullTermForTranche);
118+
copy.originalNumberOfRepayments(this.originalNumberOfRepayments);
119+
return copy;
108120
}
109121

110122
private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods,

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ public Money getCreditedAmounts() {
344344
public Money getOutstandingLoanBalance() {
345345
if (outstandingBalanceCalculation == null) {
346346
outstandingBalanceCalculation = Memo.of(() -> {
347+
if (getInterestPeriods().isEmpty()) {
348+
return getZero();
349+
}
347350
InterestPeriod lastInterestPeriod = getInterestPeriods().getLast();
348351
Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() //
349352
.plus(lastInterestPeriod.getBalanceCorrectionAmount(), getMc()) //

0 commit comments

Comments
 (0)