FINERACT-1971: Fix EMI Calculator: Last Unpaid Repayment Period Handling.

call calculateLastUnpaidRepaymentPeriodEMI recursively when emi would be less than zero to adjust remaining emi correction for previous period.
This commit is contained in:
Soma Sörös
2025-01-10 13:53:07 +01:00
committed by Adam Saghy
parent cee5b43b71
commit 100405a579
4 changed files with 202 additions and 1 deletions
@@ -307,7 +307,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc); Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.reduce((first, second) -> second); .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) { private void calculateOutstandingBalance(ProgressiveLoanInterestScheduleModel scheduleModel) {
@@ -1132,6 +1132,64 @@ class ProgressiveEMICalculatorTest {
checkPeriod(interestSchedule, 5, 0, 16.89, 0.003950916667, 0.07, 16.82, 0.0); 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 @Test
public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repayIn1Month() { public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repayIn1Month() {
@@ -1298,6 +1356,18 @@ class ProgressiveEMICalculatorTest {
remaingBalance); 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, private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
final int interestIdx, final double emiValue, final double rateFactor, final double interestDue, final int interestIdx, final double emiValue, final double rateFactor, final double interestDue,
final double interestDueCumulated, final double principalDue, final double remaingBalance) { final double interestDueCumulated, final double principalDue, final double remaingBalance) {
@@ -1075,6 +1075,10 @@ public abstract class BaseLoanIntegrationTest {
return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate, null, null); 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, protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double totalOutstandingAmount,
Boolean completed, String dueDate) { Boolean completed, String dueDate) {
return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null, null); return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null, null);
@@ -218,6 +218,127 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
}); });
} }
@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 @Test
public void verifyUC01() { public void verifyUC01() {
AtomicReference<Long> loanIdRef = new AtomicReference<>(); AtomicReference<Long> loanIdRef = new AtomicReference<>();