Skip to content

Commit

Permalink
FINERACT-1971: Fix EMI Calculator: Last Unpaid Repayment Period Handl…
Browse files Browse the repository at this point in the history
…ing.

call calculateLastUnpaidRepaymentPeriodEMI recursively when emi would be less than zero to adjust remaining emi correction for previous period.
  • Loading branch information
somasorosdpc committed Jan 10, 2025
1 parent 81883a1 commit 9a15bd0
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,13 @@ private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestSchedu
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.reduce((first, second) -> second);
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc)));
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc));
if (repaymentPeriod.getEmi().isLessThanZero()) {
repaymentPeriod.setEmi(repaymentPeriod.getEmi().zero());
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
}
});
}

private void calculateOutstandingBalance(ProgressiveLoanInterestScheduleModel scheduleModel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,64 @@ public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEver
checkPeriod(interestSchedule, 5, 0, 16.89, 0.003950916667, 0.07, 16.82, 0.0);
}

@Test
public void soma_test_disbursedAmt1000_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() {

final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)),
repayment(7, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 8, 1)),
repayment(8, LocalDate.of(2024, 8, 1), LocalDate.of(2024, 9, 1)),
repayment(9, LocalDate.of(2024, 9, 1), LocalDate.of(2024, 10, 1)),
repayment(10, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 11, 1)),
repayment(11, LocalDate.of(2024, 11, 1), LocalDate.of(2024, 12, 1)),
repayment(12, LocalDate.of(2024, 12, 1), LocalDate.of(2025, 1, 1)));

final BigDecimal interestRate = BigDecimal.valueOf(25);
final Integer installmentAmountInMultiplesOf = null;

Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);

final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);

final Money disbursedAmount = toMoney(1000.0);
LocalDate disbursementDate = LocalDate.of(2024, 1, 1);

emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount);

for (int i = 0; i < 12; i++) {
LocalDate dueDate = expectedRepaymentPeriods.get(i).getDueDate();
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, dueDate, disbursementDate);
Money duePrincipal = dueAmounts.getDuePrincipal();
if (duePrincipal.isGreaterThanZero()) {
emiCalculator.payPrincipal(interestSchedule, dueDate, disbursementDate, duePrincipal);
}
}

checkPeriod(interestSchedule, 0, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 1, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 2, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 3, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 4, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 5, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 6, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 7, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 8, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 9, 95.02, 0.0, 95.02, 0.0, true);
checkPeriod(interestSchedule, 10, 49.8, 0.0, 49.8, 0.0, true);
checkPeriod(interestSchedule, 11, 0.0, 0.0, 0.0, 0.0, true);
}

@Test
public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repayIn1Month() {

Expand Down Expand Up @@ -1298,6 +1356,18 @@ private static void checkPeriod(final ProgressiveLoanInterestScheduleModel inter
remaingBalance);
}

private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
final double emiValue, final double interestDueCumulated, final double principalDue, final double remainingBalance,
final boolean fullyRepaid) {
final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx);

Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi()));
Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest()));
Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal()));
Assertions.assertEquals(remainingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance()));
Assertions.assertEquals(fullyRepaid, repaymentPeriod.isFullyPaid());
}

private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
final int interestIdx, final double emiValue, final double rateFactor, final double interestDue,
final double interestDueCumulated, final double principalDue, final double remaingBalance) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,10 @@ protected Installment installment(double principalAmount, double interestAmount,
return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate, null, null);
}

protected Installment fullyRepaidInstallment(double principalAmount, double interestAmount, String dueDate) {
return new Installment(principalAmount, interestAmount, null, null, 0.0, true, dueDate, null, null);
}

protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double totalOutstandingAmount,
Boolean completed, String dueDate) {
return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,127 @@ public void verifyInterestRefundCreatedForMerchantIssuedRefund() {
});
}

@Test
public void verifyInterestRefundCreatedForMerchantIssuedRefundDay22HighInterest12month() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2021", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
.daysInYearType(DaysInYearType.ACTUAL) //
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) //
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 26.0,
12, null);
Assertions.assertNotNull(loanId);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
});
runAt("22 January 2021", () -> {
Long loanId = loanIdRef.get();
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
.makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue());
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());

verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(14.96, "Accrual", "22 January 2021"), //
transaction(14.96, "Interest Refund", "22 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 January 2021") //
);
verifyRepaymentSchedule(loanId, installment(1000.0, null, "01 January 2021"), //
fullyRepaidInstallment(80.52, 14.96, "01 February 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 March 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 April 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 May 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 June 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 July 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 August 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 September 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 October 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 November 2021"), //
fullyRepaidInstallment(60.16, 0.0, "01 December 2021"), //
fullyRepaidInstallment(0.0, 0.0, "01 January 2022") //
);
});
}

@Test
public void verifyFullMerchantIssuedRefundOnDay0HighInterest12month() {
runAt("1 January 2021", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
.daysInYearType(DaysInYearType.ACTUAL) //
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) //
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 26.0,
12, null);
Assertions.assertNotNull(loanId);
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
.makeLoanRepayment("MerchantIssuedRefund", "1 January 2021", 1000F, loanId.intValue());
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());

verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "01 January 2021") //
);
verifyRepaymentSchedule(loanId, installment(1000.0, null, "01 January 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 February 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 March 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 April 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 May 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 June 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 July 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 August 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 September 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 October 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 November 2021"), //
fullyRepaidInstallment(45.2, 0.0, "01 December 2021"), //
fullyRepaidInstallment(0.0, 0.0, "01 January 2022") //
);
});
}

@Test
public void verifyRepaymentDay0HighInterest12month() {
runAt("1 January 2021", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
.daysInYearType(DaysInYearType.ACTUAL) //
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) //
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 26.0,
12, null);
Assertions.assertNotNull(loanId);
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment("Repayment",
"1 January 2021", 1000F, loanId.intValue());
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());

verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Repayment", "01 January 2021") //
);
verifyRepaymentSchedule(loanId, installment(1000.0, null, "01 January 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 February 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 March 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 April 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 May 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 June 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 July 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 August 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 September 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 October 2021"), //
fullyRepaidInstallment(95.48, 0.0, "01 November 2021"), //
fullyRepaidInstallment(45.2, 0.0, "01 December 2021"), //
fullyRepaidInstallment(0.0, 0.0, "01 January 2022") //
);
});
}

@Test
public void verifyUC01() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
Expand Down

0 comments on commit 9a15bd0

Please sign in to comment.