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 9, 2025
1 parent e1d043b commit 87886c1
Show file tree
Hide file tree
Showing 3 changed files with 158 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 @@ -49,10 +49,13 @@
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ExtendWith(MockitoExtension.class)
class ProgressiveEMICalculatorTest {

private static final Logger log = LoggerFactory.getLogger(ProgressiveEMICalculatorTest.class);
private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();

private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
Expand Down Expand Up @@ -1132,6 +1135,54 @@ 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);
}
}

Assertions.assertTrue(interestSchedule.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()).toList().isEmpty());

}

@Test
public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repayIn1Month() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,94 @@ 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());

logLoanTransactions(loanId);
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") //
);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logInstallmentsOfLoanDetailsFull(loanDetails);
});
}

@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());

logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "01 January 2021") //
);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logInstallmentsOfLoanDetailsFull(loanDetails);
});
}

@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());

logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Repayment", "01 January 2021") //
);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logInstallmentsOfLoanDetailsFull(loanDetails);
});
}

@Test
public void verifyUC01() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
Expand Down Expand Up @@ -1285,6 +1373,18 @@ public void verifyUC19() {
});
}

private void logInstallmentsOfLoanDetailsFull(GetLoansLoanIdResponse loanDetails) {
log.info("index, dueDate, principal, fee, penalty, interest, fullyRepaid");
if (loanDetails != null && loanDetails.getRepaymentSchedule() != null && loanDetails.getRepaymentSchedule().getPeriods() != null) {
loanDetails.getRepaymentSchedule().getPeriods()
.forEach(period -> log.info("{}, \"{}\", {}, {}, {}, {}, {}", period.getPeriod(),
DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH)
.format(Objects.requireNonNull(period.getDueDate())),
period.getPrincipalDue(), period.getFeeChargesDue(), period.getPenaltyChargesDue(), period.getInterestDue(),
period.getTotalOutstandingForPeriod() == null || period.getTotalOutstandingForPeriod().doubleValue() == 0.0));
}
}

private void logInstallmentsOfLoanDetails(GetLoansLoanIdResponse loanDetails) {
log.info("index, dueDate, principal, fee, penalty, interest");
if (loanDetails != null && loanDetails.getRepaymentSchedule() != null && loanDetails.getRepaymentSchedule().getPeriods() != null) {
Expand Down

0 comments on commit 87886c1

Please sign in to comment.