Skip to content

Commit

Permalink
FINERACT-1958: Fix repayment schedule generation
Browse files Browse the repository at this point in the history
  • Loading branch information
adamsaghy committed May 2, 2024
1 parent a022a51 commit 967a4d4
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1417,10 +1417,14 @@ public boolean isMultiDisburseLoan() {
}

@NotNull
public Money getMaxOutstandingBalance() {
public Money getMaxOutstandingBalanceMoney() {
return Money.of(getCurrency(), this.maxOutstandingBalance);
}

public BigDecimal getMaxOutstandingBalance() {
return maxOutstandingBalance;
}

public BigDecimal getFixedEmiAmount() {
BigDecimal fixedEmiAmount = this.fixedEmiAmount;
if (getCurrentPeriodFixedEmiAmount() != null) {
Expand Down Expand Up @@ -1882,4 +1886,7 @@ public void updateVariationDays(final long daysToAdd) {
this.variationDays += daysToAdd;
}

public LocalDate getLoanEndDate() {
return loanEndDate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1005,11 +1005,13 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm
&& !DateUtils.isAfter(disburseDetail.getKey(), scheduledDueDate)) {
// validation check for amount not exceeds specified max
// amount as per the configuration
Money maxOutstandingBalance = loanApplicationTerms.getMaxOutstandingBalance();
if (scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue()).isGreaterThan(maxOutstandingBalance)) {
String errorMsg = "Outstanding balance must not exceed the amount: " + maxOutstandingBalance;
throw new MultiDisbursementOutstandingAmoutException(errorMsg, maxOutstandingBalance.getAmount(),
disburseDetail.getValue());
if (loanApplicationTerms.getMaxOutstandingBalance() != null) {
Money maxOutstandingBalance = loanApplicationTerms.getMaxOutstandingBalanceMoney();
if (scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue()).isGreaterThan(maxOutstandingBalance)) {
String errorMsg = "Outstanding balance must not exceed the amount: " + maxOutstandingBalance;
throw new MultiDisbursementOutstandingAmoutException(errorMsg, maxOutstandingBalance.getAmount(),
disburseDetail.getValue());
}
}

// creates and add disbursement detail to the repayments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -77,8 +78,7 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer
: loanApplicationTerms.getSubmittedOnDate();

LoanScheduleParams scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency,
Money.of(currency, chargesDueAtTimeOfDisbursement), periodStartDate,
getPrincipalToBeScheduled(loanApplicationTerms, periodStartDate));
Money.of(currency, chargesDueAtTimeOfDisbursement), periodStartDate, Money.zero(currency));

List<LoanScheduleModelPeriod> periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, scheduleParams,
chargesDueAtTimeOfDisbursement);
Expand All @@ -90,7 +90,9 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer
final Set<LoanCharge> nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges);
boolean isNextRepaymentAvailable = true;

while (!scheduleParams.getOutstandingBalance().isZero()) {
boolean thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream()
.anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate()));
while (!scheduleParams.getOutstandingBalance().isZero() || thereIsDisbursementBeforeOrOnLoanEndDate) {
scheduleParams.setActualRepaymentDate(getScheduledDateGenerator().generateNextRepaymentDate(
scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment, scheduleParams.getPeriodNumber()));
AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator()
Expand All @@ -109,9 +111,7 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer

ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, BigDecimal.ZERO);

if (loanApplicationTerms.isMultiDisburseLoan()) {
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, scheduledDueDate);
}
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, scheduledDueDate);

// 5 determine principal,interest of repayment period
PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(loanApplicationTerms,
Expand Down Expand Up @@ -179,6 +179,8 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer
// adjustInstallmentOrPrincipalAmount(loanApplicationTerms, scheduleParams.getTotalCumulativePrincipal(),
// scheduleParams.getPeriodNumber(), mc);
// }
thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream()
.anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate()));
}

// If the disbursement happened after maturity date
Expand Down Expand Up @@ -237,28 +239,6 @@ private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharg
return chargesDueAtTimeOfDisbursement;
}

/**
* this method calculates the principal amount for generating the repayment schedule.
*/
private Money getPrincipalToBeScheduled(final LoanApplicationTerms loanApplicationTerms, LocalDate periodStartDate) {
Money principalToBeScheduled;
if (loanApplicationTerms.isMultiDisburseLoan()) {
if (loanApplicationTerms.getTotalDisbursedAmount().isGreaterThanZero()) {
BigDecimal totalDisbursalAmountsOnThe = loanApplicationTerms.getDisbursementDatas().stream()
.filter(d -> d.getActualDisbursementDate().equals(periodStartDate)).map(DisbursementData::getPrincipal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
principalToBeScheduled = Money.of(loanApplicationTerms.getCurrency(), totalDisbursalAmountsOnThe);
} else if (loanApplicationTerms.getApprovedPrincipal().isGreaterThanZero()) {
principalToBeScheduled = loanApplicationTerms.getApprovedPrincipal();
} else {
principalToBeScheduled = loanApplicationTerms.getPrincipal();
}
} else {
principalToBeScheduled = loanApplicationTerms.getPrincipal();
}
return principalToBeScheduled;
}

private List<LoanScheduleModelPeriod> createNewLoanScheduleListWithDisbursementDetails(final LoanApplicationTerms loanApplicationTerms,
final LoanScheduleParams loanScheduleParams, final BigDecimal chargesDueAtTimeOfDisbursement) {
List<LoanScheduleModelPeriod> periods = new ArrayList<>();
Expand All @@ -271,11 +251,17 @@ private List<LoanScheduleModelPeriod> createNewLoanScheduleListWithDisbursementD
null, null));
}
for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) {
Money principalMoney = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal());
if (disbursementData.disbursementDate().equals(loanScheduleParams.getPeriodStartDate())) {
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement(
disbursementData.disbursementDate(), Money.of(loanScheduleParams.getCurrency(), disbursementData.getPrincipal()),
chargesDueAtTimeOfDisbursement);
periods.add(disbursementPeriod);
loanScheduleParams.addOutstandingBalance(principalMoney);
loanScheduleParams.addOutstandingBalanceAsPerRest(principalMoney);
loanScheduleParams.addPrincipalToBeScheduled(principalMoney);
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(principalMoney));
loanApplicationTerms.resetFixedEmiAmount();
if (loanApplicationTerms.isDownPaymentEnabled()) {
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms,
loanScheduleParams, loanApplicationTerms.getExpectedDisbursementDate(), disbursementData.getPrincipal());
Expand All @@ -284,8 +270,7 @@ private List<LoanScheduleModelPeriod> createNewLoanScheduleListWithDisbursementD
} else {
Money disbursedAmount = loanScheduleParams.getDisburseDetailMap().getOrDefault(disbursementData.disbursementDate(),
Money.zero(loanApplicationTerms.getCurrency()));
loanScheduleParams.getDisburseDetailMap().put(disbursementData.disbursementDate(),
disbursedAmount.add(Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal())));
loanScheduleParams.getDisburseDetailMap().put(disbursementData.disbursementDate(), disbursedAmount.add(principalMoney));
}
}

Expand Down Expand Up @@ -335,18 +320,21 @@ private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(fin
*/
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement,
LoanScheduleParams scheduleParams, final Collection<LoanScheduleModelPeriod> periods, final LocalDate scheduledDueDate) {
for (Map.Entry<LocalDate, Money> disburseDetail : scheduleParams.getDisburseDetailMap().entrySet()) {
Iterator<Map.Entry<LocalDate, Money>> iter = scheduleParams.getDisburseDetailMap().entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<LocalDate, Money> disburseDetail = iter.next();
if ((disburseDetail.getKey().isEqual(scheduleParams.getPeriodStartDate())
|| disburseDetail.getKey().isAfter(scheduleParams.getPeriodStartDate()))
&& disburseDetail.getKey().isBefore(scheduledDueDate)) {
// validation check for amount not exceeds specified max
// amount as per the configuration
loanApplicationTerms.getMaxOutstandingBalance();
if (scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue())
.isGreaterThan(loanApplicationTerms.getMaxOutstandingBalance())) {
String errorMsg = "Outstanding balance must not exceed the amount: " + loanApplicationTerms.getMaxOutstandingBalance();
throw new MultiDisbursementOutstandingAmoutException(errorMsg,
loanApplicationTerms.getMaxOutstandingBalance().getAmount(), disburseDetail.getValue());
if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getMaxOutstandingBalance() != null) {
Money maxOutstandingBalance = loanApplicationTerms.getMaxOutstandingBalanceMoney();
if (scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue()).isGreaterThan(maxOutstandingBalance)) {
String errorMsg = "Outstanding balance must not exceed the amount: " + maxOutstandingBalance;
throw new MultiDisbursementOutstandingAmoutException(errorMsg, loanApplicationTerms.getMaxOutstandingBalance(),
disburseDetail.getValue());
}
}

// creates and add disbursement detail to the repayments
Expand All @@ -356,6 +344,13 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm
periods.add(disbursementPeriod);

BigDecimal downPaymentAmt = BigDecimal.ZERO;
// updates actual outstanding balance with new
// disbursement detail
scheduleParams.addOutstandingBalance(disburseDetail.getValue());
scheduleParams.addOutstandingBalanceAsPerRest(disburseDetail.getValue());
scheduleParams.addPrincipalToBeScheduled(disburseDetail.getValue());
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(disburseDetail.getValue()));
loanApplicationTerms.resetFixedEmiAmount();
if (loanApplicationTerms.isDownPaymentEnabled()) {
// get list of disbursements done on same day and create down payment periods
List<DisbursementData> disbursementsOnSameDate = loanApplicationTerms.getDisbursementDatas().stream()
Expand All @@ -368,13 +363,7 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm
downPaymentAmt = downPaymentAmt.add(downPaymentPeriod.principalDue());
}
}
// updates actual outstanding balance with new
// disbursement detail
scheduleParams.addOutstandingBalance(disburseDetail.getValue());
scheduleParams.addOutstandingBalanceAsPerRest(disburseDetail.getValue());
scheduleParams.addPrincipalToBeScheduled(disburseDetail.getValue());
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(disburseDetail.getValue()));
loanApplicationTerms.resetFixedEmiAmount();
iter.remove();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ protected void verifyRepaymentSchedule(Long loanId, Installment... installments)
"Expected installments are not matching with the installments configured on the loan");

int installmentNumber = 0;
for (int i = 1; i < installments.length; i++) {
for (int i = 0; i < installments.length; i++) {
GetLoansLoanIdRepaymentPeriod period = loanResponse.getRepaymentSchedule().getPeriods().get(i);
Double principalDue = period.getPrincipalDue();
Double amount = installments[i].principalAmount;
Expand Down Expand Up @@ -673,6 +673,11 @@ protected PostLoansLoanIdRequest approveLoanRequest(Double amount, String approv
.approvedOnDate(approvalDate).locale("en");
}

protected PostLoansLoanIdRequest approveLoanRequest(Double amount, String approvalDate, String expectedDisbursementDate) {
return new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(amount))
.expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN).approvedOnDate(approvalDate).locale("en");
}

protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
int numberOfRepayments) {
return applyAndApproveLoan(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void testSnapshotEventGenerationWhenLoanInstallmentIsNotPayed() {

// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(1250.0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
Expand Down Expand Up @@ -123,7 +123,7 @@ public void testNoSnapshotEventGenerationWhenLoanInstallmentIsPayed() {

// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(1250.0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
Expand All @@ -134,7 +134,7 @@ public void testNoSnapshotEventGenerationWhenLoanInstallmentIsPayed() {

// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(1250.0, null, "01 January 2023"), //
installment(313.0, true, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
Expand Down Expand Up @@ -180,7 +180,7 @@ public void testNoSnapshotEventGenerationWhenWhenCustomSnapshotEventCOBTaskIsNot

// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(1250.0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
Expand Down Expand Up @@ -226,7 +226,7 @@ public void testNoSnapshotEventGenerationWhenCOBDateIsNotMatchingWithInstallment

// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(1250.0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
Expand Down Expand Up @@ -270,7 +270,7 @@ public void testNoSnapshotEventGenerationWhenCustomSnapshotEventIsDisabled() {

// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(1250.0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
Expand Down
Loading

0 comments on commit 967a4d4

Please sign in to comment.