FINERACT-1981: Interest recalculation - Recalculate Interest For Loan - Business step for COB - progressive loan

This commit is contained in:
Soma Sörös
2024-09-18 15:04:18 +02:00
committed by Adam Saghy
parent 7b058d6772
commit 35c04c0141
4 changed files with 1074 additions and 66 deletions
@@ -60,6 +60,7 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
@@ -180,9 +181,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
installmentAmountInMultiplesOf, installments, mc);
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
installments, charges, changedTransactionDetail, overpaymentHolder, scheduleModel));
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, ctx));
chargeOrTransaction.getLoanCharge()
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
}
@@ -190,6 +193,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.map(ChargeOrTransaction::getLoanTransaction) //
.filter(Optional::isPresent) //
.map(Optional::get).toList();
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx, true);
reprocessInstallments(disbursementDate, txs, installments, currency);
return Pair.of(changedTransactionDetail, scheduleModel);
}
@@ -562,11 +566,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
}
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) {
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail,
scheduleModel);
private void processSingleTransaction(LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) {
final MonetaryCurrency currency = ctx.getCurrency();
final ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, ctx);
if (loanTransaction.isInterestWaiver()) {
@@ -765,7 +768,107 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate currentDate,
ProgressiveTransactionCtx transactionCtx) {
final LocalDate fromDate = transactionCtx.getInstallments().get(0).getLoan().getApprovedOnDate();
return transactionCtx.getInstallments().stream().filter(installment -> !installment.getFromDate().isBefore(fromDate))
.filter(installment -> installment.getDueDate().isBefore(currentDate))
.filter(installment -> installment.isOverdueOn(currentDate))
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList();
}
private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransactionCtx ctx, boolean isLastRecalculation) {
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()
&& ctx.getInstallments().get(0).getLoan().isInterestRecalculationEnabledForProduct()
&& !ctx.getInstallments().get(0).getLoan().isNpa() && !ctx.getInstallments().get(0).getLoan().isChargedOff()) {
List<LoanRepaymentScheduleInstallment> overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
currentDate, ctx);
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
List<LoanRepaymentScheduleInstallment> possibleCurrentInstallment = ctx.getInstallments().stream().filter(
installment -> installment.getFromDate().isBefore(currentDate) && !installment.getDueDate().isBefore(currentDate))
.toList();
// get DUE installment or last installment
LoanRepaymentScheduleInstallment currentInstallment = !possibleCurrentInstallment.isEmpty()
? possibleCurrentInstallment.get(0)
: ctx.getInstallments().stream().max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber))
.orElseThrow();
Money overDuePrincipal = Money.zero(ctx.getCurrency());
for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (overDuePrincipal.compareTo(Money.zero(ctx.getCurrency())) != 0) {
adjustOverduePrincipalForInstallment(currentDate, isLastRecalculation, processingInstallment, overDuePrincipal,
ctx);
}
overDuePrincipal = overDuePrincipal.add(processingInstallment.getPrincipalOutstanding(ctx.getCurrency()).getAmount());
}
adjustOverduePrincipalForInstallment(currentDate, isLastRecalculation, currentInstallment, overDuePrincipal, ctx);
}
}
}
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean isLastRecalculation,
LoanRepaymentScheduleInstallment currentInstallment, Money overduePrincipal, ProgressiveTransactionCtx ctx) {
LocalDate fromDate = currentInstallment.getFromDate();
boolean hasUpdate = false;
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()) {
// if we have same date for fromDate & last overdue balance change then it meas we have the up-to-date
// model.
if (ctx.getLastOverdueBalanceChange() == null || fromDate.isAfter(ctx.getLastOverdueBalanceChange())) {
emiCalculator.addBalanceCorrection(ctx.getModel(), fromDate, overduePrincipal);
ctx.setLastOverdueBalanceChange(fromDate);
hasUpdate = true;
}
}
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().getRestFrequencyType().isDaily()
// if we have same date for currentDate & last overdue balance change then it meas we have the
// up-to-date model.
&& !currentDate.equals(ctx.getLastOverdueBalanceChange())) {
if (ctx.getLastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(ctx.getLastOverdueBalanceChange())) {
// first overdue hit for installment. setting overdue balance correction from instalment from date.
emiCalculator.addBalanceCorrection(ctx.getModel(), fromDate, overduePrincipal);
} else {
// not the first balance correction on installment period, then setting overdue balance correction from
// last balance change's current date. previous interest period already has the correct balanec
// correction
emiCalculator.addBalanceCorrection(ctx.getModel(), ctx.getLastOverdueBalanceChange(), overduePrincipal);
}
// setting negative correction for the period from current date, expecting the overdue balance's full
// repayment on that day.
if (currentDate.isAfter(currentInstallment.getFromDate()) && currentDate.isBefore(currentInstallment.getDueDate())) {
emiCalculator.addBalanceCorrection(ctx.getModel(), currentDate, overduePrincipal.negated());
ctx.setLastOverdueBalanceChange(currentDate);
}
hasUpdate = true;
}
if (hasUpdate) {
updateInstallmentsPrincipalAndInterestByModel(ctx);
}
}
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
ctx.getModel().repayments().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream()
.filter(ri -> !ri.isDownPayment() && Objects.equals(ri.getFromDate(), rm.getFromDate())).findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(rm.getPrincipalDue().getAmount());
installment.updateInterestCharged(rm.getInterestDue().getAmount());
installment.setRecalculatedInterestComponent(true);
}
});
}
private void handleRepayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
if (transactionCtx instanceof ProgressiveTransactionCtx) {
recalculateInterestForDate(loanTransaction.getTransactionDate(), (ProgressiveTransactionCtx) transactionCtx, false);
}
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents();
}
@@ -18,8 +18,11 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -28,9 +31,12 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Mon
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
@Getter
public class ProgressiveTransactionCtx extends TransactionCtx {
private final ProgressiveLoanInterestScheduleModel model;
@Setter
private LocalDate lastOverdueBalanceChange = null;
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
@@ -39,7 +45,4 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
this.model = model;
}
public ProgressiveLoanInterestScheduleModel getModel() {
return model;
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.integrationtests;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -41,6 +42,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@@ -174,6 +176,24 @@ public abstract class BaseLoanIntegrationTest {
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
}
protected static void validateFullyUnpaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
double principalDue, double feeDue, double penaltyDue, double interestDue) {
validateRepaymentPeriod(loanDetails, index, LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH)),
principalDue, 0, principalDue, feeDue, 0, feeDue, penaltyDue, 0, penaltyDue, interestDue, 0, interestDue, 0, 0);
}
protected static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
double principalDue, double feeDue, double penaltyDue, double interestDue) {
validateRepaymentPeriod(loanDetails, index, LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH)),
principalDue, principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, 0, 0);
}
protected static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
double principalDue, double feeDue, double penaltyDue, double interestDue, double paidLate) {
validateRepaymentPeriod(loanDetails, index, LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH)),
principalDue, principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, 0, paidLate);
}
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
double feeDue, double penaltyDue, double interestDue) {
validateRepaymentPeriod(loanDetails, index, dueDate, principalDue, 0, principalDue, feeDue, 0, feeDue, penaltyDue, 0, penaltyDue,
@@ -217,6 +237,106 @@ public abstract class BaseLoanIntegrationTest {
return createOnePeriod30DaysPeriodicAccrualProduct((double) 0);
}
protected PostLoanProductsRequest create4IProgressive() {
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("4I_PROGRESSIVE_", 6))//
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
.description("4 installment product - progressive")//
.includeInBorrowerCycle(false)//
.useBorrowerCycle(false)//
.currencyCode("EUR")//
.digitsAfterDecimal(2)//
.principal(1000.0)//
.minPrincipal(100.0)//
.maxPrincipal(10000.0)//
.numberOfRepayments(4)//
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L)//
.interestRatePerPeriod(10D)//
.minInterestRatePerPeriod(0D)//
.maxInterestRatePerPeriod(120D)//
.interestRateFrequencyType(InterestRateFrequencyType.YEARS)//
.isLinkedToFloatingInterestRates(false)//
.isLinkedToFloatingInterestRates(false)//
.allowVariableInstallments(false)//
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
.interestType(InterestType.DECLINING_BALANCE)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
.allowPartialPeriodInterestCalcualtion(false)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
.paymentAllocation(List.of(createDefaultPaymentAllocation("NEXT_INSTALLMENT")))//
.creditAllocation(List.of())//
.overdueDaysForNPA(179)//
.daysInMonthType(30)//
.daysInYearType(360)//
.isInterestRecalculationEnabled(true)//
.interestRecalculationCompoundingMethod(0)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
.recalculationRestFrequencyType(RecalculationRestFrequencyType.SAME_AS_REPAYMENT_PERIOD)//
.recalculationRestFrequencyInterval(0)//
.isArrearsBasedOnOriginalSchedule(false)//
.isCompoundingToBePostedAsTransaction(false)//
.preClosureInterestCalculationStrategy(1)//
.allowCompoundingOnEod(false)//
.canDefineInstallmentAmount(true)//
.repaymentStartDateType(1)//
.supportedInterestRefundTypes(List.of())//
.charges(List.of())//
.principalVariationsForBorrowerCycle(List.of())//
.interestRateVariationsForBorrowerCycle(List.of())//
.numberOfRepaymentVariationsForBorrowerCycle(List.of())//
.accountingRule(3)//
.canUseForTopup(false)//
.fundSourceAccountId(fundSource.getAccountID().longValue())//
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
.incomeFromPenaltyAccountId(penaltyIncomeAccount.getAccountID().longValue())//
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
.receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())//
.receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())//
.receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())//
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
.incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
.incomeFromGoodwillCreditFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
.dateFormat(DATETIME_PATTERN)//
.locale("en")//
.enableAccrualActivityPosting(false)//
.multiDisburseLoan(true)//
.maxTrancheCount(10)//
.outstandingLoanBalance(10000.0)//
.disallowExpectedDisbursements(true)//
.allowApprovedDisbursedAmountsOverApplied(true)//
.overAppliedCalculationType("percentage")//
.overAppliedNumber(50)//
.principalThresholdForLastInstallment(50)//
.holdGuaranteeFunds(false)//
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
.allowAttributeOverrides(new AllowAttributeOverrides()//
.amortizationType(true)//
.interestType(true)//
.transactionProcessingStrategyCode(true)//
.interestCalculationPeriodType(true)//
.inArrearsTolerance(true)//
.repaymentEvery(true)//
.graceOnPrincipalAndInterestPayment(true)//
.graceOnArrearsAgeing(true)//
).isEqualAmortization(false)//
.delinquencyBucketId(1L)//
.enableDownPayment(false)//
.enableInstallmentLevelDelinquency(false)//
.loanScheduleType("PROGRESSIVE")//
.loanScheduleProcessingType("HORIZONTAL");//
}
// Loan product with proper accounting setup
protected PostLoanProductsRequest createOnePeriod30DaysPeriodicAccrualProduct(double interestRatePerPeriod) {
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
@@ -700,6 +820,23 @@ public abstract class BaseLoanIntegrationTest {
return postLoansRequest;
}
protected PostLoansRequest applyPin4ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate,
Double amount, Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId)
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY).productId(loanProductId)
.expectedDisbursementDate(loanDisbursementDate).dateFormat(DATETIME_PATTERN).locale("en")
.submittedOnDate(loanDisbursementDate).amortizationType(1).interestRatePerPeriod(BigDecimal.valueOf(interestRate))
.numberOfRepayments(numberOfRepayments).principal(BigDecimal.valueOf(amount)).loanTermFrequency(numberOfRepayments)
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS)
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS).interestType(InterestType.DECLINING_BALANCE)
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).loanType("individual");
if (customizer != null) {
customizer.accept(postLoansRequest);
}
return postLoansRequest;
}
protected PostLoansLoanIdRequest approveLoanRequest(Double amount, String approvalDate) {
return new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(amount)).dateFormat(DATETIME_PATTERN)
.approvedOnDate(approvalDate).locale("en");
@@ -726,6 +863,17 @@ public abstract class BaseLoanIntegrationTest {
return approvedLoanResult.getLoanId();
}
protected Long applyAndApproveProgressiveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyPin4ProgressiveLoanRequest(clientId, loanProductId,
loanDisbursementDate, amount, interestRate, numberOfRepayments, customizer));
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(amount, loanDisbursementDate));
return approvedLoanResult.getLoanId();
}
protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount) {
return applyAndApproveLoan(clientId, loanProductId, loanDisbursementDate, amount, 1);
}
@@ -1039,11 +1187,18 @@ public abstract class BaseLoanIntegrationTest {
public static class RepaymentFrequencyType {
public static final Integer MONTHS = 2;
public static final Long MONTHS_L = 2L;
public static final String MONTHS_STRING = "MONTHS";
public static final Integer DAYS = 0;
public static final String DAYS_STRING = "DAYS";
}
public static class RecalculationRestFrequencyType {
public static final Integer SAME_AS_REPAYMENT_PERIOD = 1;
public static final Integer DAILY = 2;
}
public static class InterestCalculationPeriodType {
public static final Integer DAILY = 0;
@@ -1056,6 +1211,16 @@ public abstract class BaseLoanIntegrationTest {
public static final Integer YEARS = 3;
}
public static class TransactionProcessingStrategyCode {
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
}
public static class RescheduleStrategyMethod {
public static final Integer ADJUST_LAST_UNPAID_PERIOD = 4;
}
public static class DaysInYearType {
public static final Integer INVALID = 0;
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.integrationtests;
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD;
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.RepaymentFrequencyType.DAYS;
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.RepaymentFrequencyType.MONTHS;
@@ -28,19 +29,25 @@ import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -74,35 +81,373 @@ public class LoanInterestRecalculationCOBTest extends BaseLoanIntegrationTest {
"EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS", "ACCRUAL_ACTIVITY_POSTING", "LOAN_INTEREST_RECALCULATION");
}
private void logLoanDetails(GetLoansLoanIdResponse loanDetails) {
log.info("index, dueDate, principal, fee, penalty, interest");
Assertions.assertNotNull(loanDetails.getRepaymentSchedule());
Assertions.assertNotNull(loanDetails.getRepaymentSchedule().getPeriods());
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()));
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOnProgressiveLoanCOB() {
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOn4IProgressiveLoanCOBStepDaily() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(
createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation(16.0, 4)
.maxInterestRatePerPeriod(120.0).maxPrincipal(10000.0));
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 4,
postLoansRequest -> postLoansRequest.loanTermFrequency(4)//
.loanTermFrequencyType(MONTHS)//
.interestRatePerPeriod(BigDecimal.valueOf(120.0)).interestCalculationPeriodType(DAYS)//
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
.repaymentEvery(1)//
.repaymentFrequencyType(MONTHS)//
.principal(BigDecimal.valueOf(8000.0)).maxOutstandingLoanBalance(BigDecimal.valueOf(100000.0)));
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
loanDetails.getRepaymentSchedule().getPeriods().forEach(p -> log.info("validateRepaymentPeriod before: {} {} {}", p.getPeriod(),
p.getPrincipalOriginalDue(), p.getInterestOriginalDue()));//
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1700.66, 0.0, 0.0, 815.34);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 1936.12, 0.0, 0.0, 579.88);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 2071.31, 0.0, 0.0, 444.69);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 2291.91, 0.0, 0.0, 226.05);
});
runAt("1 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("2 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.05, 0.0, 0.0, 50.79);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2025.55, 0.0, 0.0, 16.88);
});
runAt("20 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1980.46, 0.0, 0.0, 61.38);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.14, 0.0, 0.0, 33.7);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2036.23, 0.0, 0.0, 16.97);
});
runAt("1 March 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
payoffOnDateAndVerifyStatus("1 February 2023", loanIdRef.get());
}
private void payoffOnDateAndVerifyStatus(final String date, final Long loanId) {
runAt(date, () -> {
HashMap prepayAmount = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, loanId.intValue());
Assertions.assertNotNull(prepayAmount);
Float amount = (Float) prepayAmount.get("amount");
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanRepayment(date, amount, loanId.intValue());
Assertions.assertNotNull(response);
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails);
Assertions.assertNotNull(loanDetails.getStatus());
log.info("Loan status {}", loanDetails.getStatus().getId());
Assertions.assertTrue(Stream.of(600).anyMatch(v -> loanDetails.getStatus().getId().intValue() == v));
});
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOn4IProgressiveLoanCOBStepLatePayPayOnDuePayLatePayOnDateDaily() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("20 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1980.46, 0.0, 0.0, 61.38);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.14, 0.0, 0.0, 33.7);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2036.23, 0.0, 0.0, 16.97);
});
runAt("1 March 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
loanTransactionHelper.makeLoanRepayment("20 February 2023", 2041.84f, loanId.intValue());
loanTransactionHelper.makeLoanRepayment("01 March 2023", 2041.84f, loanId.intValue());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1980.46, 0.0, 0.0, 61.38);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.14, 0.0, 0.0, 33.7);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2036.23, 0.0, 0.0, 16.97);
});
runAt("10 April 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1980.46, 0.0, 0.0, 61.38);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.14, 0.0, 0.0, 33.7);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2036.23, 0.0, 0.0, 21.99);
loanTransactionHelper.makeLoanRepayment("10 April 2023", 2041.84f, loanId.intValue());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1980.46, 0.0, 0.0, 61.38);
validateFullyPaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.14, 0.0, 0.0, 33.7, 2041.84);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2036.23, 0.0, 0.0, 21.99);
});
runAt("20 April 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1980.46, 0.0, 0.0, 61.38);
validateFullyPaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.14, 0.0, 0.0, 33.7, 2041.84);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2036.23, 0.0, 0.0, 21.99);
});
payoffOnDateAndVerifyStatus("20 April 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOn4IProgressiveLoanCOBStepLatePartialRepaymentDailyInterestCalculation() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("1 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
loanTransactionHelper.makeLoanRepayment("01 February 2023", 2041.84f, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("10 March 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2003.41, 0.0, 0.0, 38.43);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2029.79, 0.0, 0.0, 16.91);
loanTransactionHelper.makeLoanRepayment("10 March 2023", 500.00f, loanId.intValue());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 1991.63, 500.0, 1491.63, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.21,
0, 50.21, 0, 500.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2003.41, 0.0, 0.0, 38.43);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2029.79, 0.0, 0.0, 16.91);
loanTransactionHelper.makeLoanRepayment("10 March 2023", 541.84f, loanId.intValue());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 1991.63, 1041.84, 949.79, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.21,
0, 50.21, 0, 1041.84);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2003.41, 0.0, 0.0, 38.43);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2029.79, 0.0, 0.0, 16.91);
});
runAt("20 March 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 1991.63, 1041.84, 949.79, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.21,
0, 50.21, 0, 1041.84);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2000.85, 0.0, 0.0, 40.99);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2032.35, 0.0, 0.0, 16.94);
loanTransactionHelper.makeLoanRepayment("20 March 2023", 1000f, loanId.intValue());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21, 2041.84);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2000.85, 0.0, 0.0, 40.99);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2032.35, 0.0, 0.0, 16.94);
});
payoffOnDateAndVerifyStatus("1 April 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOn4IProgressiveLoanCOBStep() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive());
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("1 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("2 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("20 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("2 March 2023", () -> {
Long loanId = loanIdRef.get();
@@ -110,16 +455,225 @@ public class LoanInterestRecalculationCOBTest extends BaseLoanIntegrationTest {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
loanDetails.getRepaymentSchedule().getPeriods().forEach(p -> log.info("validateRepaymentPeriod after: {} {} {}", p.getPeriod(),
p.getPrincipalOriginalDue(), p.getInterestOriginalDue()));//
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2074.49, 0.0, 0.0, 17.29);
});
payoffOnDateAndVerifyStatus("1 February 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOn4IProgressiveLoanCOBStepLatePaidPaidOnTimeLatePaidPayoffOnTime() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive());
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1700.66, 0.0, 0.0, 815.34);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 1936.12, 0.0, 0.0, 579.88);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 2071.31, 0.0, 0.0, 444.69);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 2291.91, 0.0, 0.0, 226.05);
});
runAt("2 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("15 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
loanTransactionHelper.makeLoanRepayment("15 February 2023", 500.0F, loanId.intValue());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1975.17, 500, 1475.17, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 66.67, 0,
66.67, 0, 500);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
loanTransactionHelper.makeLoanRepayment("15 February 2023", 500f, loanId.intValue());
loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1975.17, 1000, 975.17, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 66.67, 0,
66.67, 0, 1000);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("20 February 2023", () -> {
Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("20 February 2023", 1041.84f, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67, 2041.84);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("1 March 2023", () -> {
Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("01 March 2023", 2041.84f, loanId.intValue());
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("2 April 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1975.17, 1975.17, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 66.67,
66.67, 0, 0, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 33.75);
});
runAt("1 May 2023", () -> {
Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("15 April 2023", 2041.84f, loanId.intValue());
loanTransactionHelper.makeLoanRepayment("01 May 2023", 2075.32f, loanId.intValue());
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1975.17, 1975.17, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 66.67,
66.67, 0, 0, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 2008.09, 2008.09, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 33.75,
33.75, 0, 0, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 33.75);
});
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOn4IProgressiveLoanCOBStepOnePaid() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive());
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("1 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
loanTransactionHelper.makeLoanRepayment("01 February 2023", 2041.84f, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("1 March 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("2 March 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("20 March 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
payoffOnDateAndVerifyStatus("1 March 2023", loanIdRef.get());
}
@Test
@@ -162,38 +716,72 @@ public class LoanInterestRecalculationCOBTest extends BaseLoanIntegrationTest {
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 4216.94, 0.0, 4216.94, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 403.23,
0.0, 403.23, 0.0, 0.0);
});
payoffOnDateAndVerifyStatus("1 March 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOnProgressiveLoanJob() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(
createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation(16.0, 4)
.maxInterestRatePerPeriod(120.0).maxPrincipal(10000.0));
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive());
Long loanId = applyAndApproveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 4,
postLoansRequest -> postLoansRequest.loanTermFrequency(4)//
.loanTermFrequencyType(MONTHS)//
.interestRatePerPeriod(BigDecimal.valueOf(120.0)).interestCalculationPeriodType(DAYS)//
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
.repaymentEvery(1)//
.repaymentFrequencyType(MONTHS)//
.principal(BigDecimal.valueOf(8000.0)).maxOutstandingLoanBalance(BigDecimal.valueOf(100000.0)));
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
loanDetails.getRepaymentSchedule().getPeriods().forEach(p -> log.info("validateRepaymentPeriod before: {} {} {}", p.getPeriod(),
p.getPrincipalOriginalDue(), p.getInterestOriginalDue()));//
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1700.66, 0.0, 0.0, 815.34);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 1936.12, 0.0, 0.0, 579.88);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 2071.31, 0.0, 0.0, 444.69);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 2291.91, 0.0, 0.0, 226.05);
});
runAt("1 February 2023", () -> {
Long loanId = loanIdRef.get();
schedulerJobHelper.executeAndAwaitJob("Update Loan Arrears Ageing");
schedulerJobHelper.executeAndAwaitJob("Recalculate Interest For Loans");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("2 February 2023", () -> {
Long loanId = loanIdRef.get();
schedulerJobHelper.executeAndAwaitJob("Update Loan Arrears Ageing");
schedulerJobHelper.executeAndAwaitJob("Recalculate Interest For Loans");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("20 February 2023", () -> {
Long loanId = loanIdRef.get();
schedulerJobHelper.executeAndAwaitJob("Update Loan Arrears Ageing");
schedulerJobHelper.executeAndAwaitJob("Recalculate Interest For Loans");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
runAt("2 March 2023", () -> {
Long loanId = loanIdRef.get();
@@ -202,27 +790,30 @@ public class LoanInterestRecalculationCOBTest extends BaseLoanIntegrationTest {
schedulerJobHelper.executeAndAwaitJob("Recalculate Interest For Loans");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
loanDetails.getRepaymentSchedule().getPeriods().forEach(p -> log.info("validateRepaymentPeriod after: {} {} {}", p.getPeriod(),
p.getPrincipalOriginalDue(), p.getInterestOriginalDue()));//
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 1700.66, 0.0, 0.0, 815.34);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 1936.12, 0.0, 0.0, 579.88);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 2071.31, 0.0, 0.0, 444.69);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 2291.91, 0.0, 0.0, 226.05);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2074.49, 0.0, 0.0, 17.29);
});
payoffOnDateAndVerifyStatus("1 February 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOnCumulativeLoanJob() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(createOnePeriod30DaysPeriodicAccrualProduct(10.0)
.isInterestRecalculationEnabled(true)//
.maxPrincipal(10000.0).minNumberOfRepayments(1).rescheduleStrategyMethod(1).recalculationRestFrequencyType(MONTHS)
.recalculationRestFrequencyInterval(1).recalculationCompoundingFrequencyType(MONTHS)
.recalculationCompoundingFrequencyInterval(30).interestRecalculationCompoundingMethod(1));
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(createOnePeriod30DaysPeriodicAccrualProduct(10.0).isInterestRecalculationEnabled(true)//
.maxPrincipal(10000.0) //
.minNumberOfRepayments(1) //
.rescheduleStrategyMethod(1) //
.recalculationRestFrequencyType(MONTHS) //
.recalculationRestFrequencyInterval(1) //
.recalculationCompoundingFrequencyType(MONTHS) //
.recalculationCompoundingFrequencyInterval(30) //
.interestRecalculationCompoundingMethod(1)); //
Long loanId = applyAndApproveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 2,
postLoansRequest -> postLoansRequest.loanTermFrequency(2)//
@@ -255,7 +846,153 @@ public class LoanInterestRecalculationCOBTest extends BaseLoanIntegrationTest {
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 4216.94, 0.0, 4216.94, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 403.23,
0.0, 403.23, 0.0, 0.0);
});
payoffOnDateAndVerifyStatus("1 March 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOn4IProgressiveLoanMultiOverdueSingleRepayment() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive());
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 10.0,
4, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1991.63, 0.0, 0.0, 50.21);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.23, 0.0, 0.0, 33.61);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2024.97, 0.0, 0.0, 16.87);
});
runAt("1 March 2023", () -> {
Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("01 March 2023", 4083.68f, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1975.17, 0.0, 0.0, 66.67, 2041.84);
validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1975.17, 0.0, 0.0, 66.67);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2008.09, 0.0, 0.0, 33.75);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2041.57, 0.0, 0.0, 17.01);
});
payoffOnDateAndVerifyStatus("1 April 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOnCumulativeLoanJobSameAsRepaymentPeriod() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(createOnePeriod30DaysPeriodicAccrualProduct(10.0).isInterestRecalculationEnabled(true)//
.maxPrincipal(10000.0) //
.minNumberOfRepayments(1) //
.maxNumberOfRepayments(10) //
.rescheduleStrategyMethod(1) //
.daysInYearType(360) //
.daysInMonthType(30) //
.recalculationRestFrequencyType(SAME_AS_REPAYMENT_PERIOD) //
.recalculationRestFrequencyInterval(1) //
.recalculationCompoundingFrequencyType(1) //
.recalculationCompoundingFrequencyInterval(1) //
.interestRecalculationCompoundingMethod(0)); //
Long loanId = applyAndApproveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 2,
postLoansRequest -> postLoansRequest.loanTermFrequency(4)//
.loanTermFrequencyType(MONTHS)//
.numberOfRepayments(4).interestRatePerPeriod(BigDecimal.valueOf(10.0)).interestCalculationPeriodType(DAYS)//
.repaymentEvery(1)//
.repaymentFrequencyType(MONTHS)//
.principal(BigDecimal.valueOf(8000.0)).maxOutstandingLoanBalance(BigDecimal.valueOf(100000.0)));
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1724.0, 0.0, 0.0, 800.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1896.4, 0.0, 0.0, 627.6);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2086.04, 0.0, 0.0, 437.96);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2293.56, 0.0, 0.0, 229.36);
});
runAt("8 February 2023", () -> {
Long loanId = loanIdRef.get();
schedulerJobHelper.executeAndAwaitJob("Update Loan Arrears Ageing");
schedulerJobHelper.executeAndAwaitJob("Recalculate Interest For Loans");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1724.0, 0.0, 0.0, 800.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1724.0, 0.0, 0.0, 800.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2068.8, 0.0, 0.0, 455.2);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2483.2, 0.0, 0.0, 248.32);
});
payoffOnDateAndVerifyStatus("1 May 2023", loanIdRef.get());
}
@Test
public void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOnCumulativeLoanCOBSameAsRepaymentPeriod() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2023", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(createOnePeriod30DaysPeriodicAccrualProduct(10.0).isInterestRecalculationEnabled(true)//
.maxPrincipal(10000.0) //
.minNumberOfRepayments(1) //
.maxNumberOfRepayments(10) //
.rescheduleStrategyMethod(1) //
.daysInYearType(360) //
.daysInMonthType(30) //
.recalculationRestFrequencyType(SAME_AS_REPAYMENT_PERIOD) //
.recalculationRestFrequencyInterval(1) //
.recalculationCompoundingFrequencyType(1) //
.recalculationCompoundingFrequencyInterval(1) //
.interestRecalculationCompoundingMethod(0)); //
Long loanId = applyAndApproveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2023", 8000.0, 2,
postLoansRequest -> postLoansRequest.loanTermFrequency(4)//
.loanTermFrequencyType(MONTHS)//
.numberOfRepayments(4).interestRatePerPeriod(BigDecimal.valueOf(10.0)).interestCalculationPeriodType(DAYS)//
.repaymentEvery(1)//
.repaymentFrequencyType(MONTHS)//
.principal(BigDecimal.valueOf(8000.0)).maxOutstandingLoanBalance(BigDecimal.valueOf(100000.0)));
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(8000), "1 January 2023");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1724.0, 0.0, 0.0, 800.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1896.4, 0.0, 0.0, 627.6);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2086.04, 0.0, 0.0, 437.96);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2293.56, 0.0, 0.0, 229.36);
});
runAt("8 February 2023", () -> {
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2023", 1724.0, 0.0, 0.0, 800.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2023", 1724.0, 0.0, 0.0, 800.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2023", 2068.8, 0.0, 0.0, 455.2);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2023", 2483.2, 0.0, 0.0, 248.32);
});
payoffOnDateAndVerifyStatus("1 May 2023", loanIdRef.get());
}
}