This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push: new 35c04c014 FINERACT-1981: Interest recalculation - Recalculate Interest For Loan - Business step for COB - progressive loan 35c04c014 is described below commit 35c04c0141ee19deb2b8fdb70df909d9b04a8ef9 Author: Soma Sörös <soma.so...@dpc.hu> AuthorDate: Thu Sep 12 17:40:03 2024 +0200 FINERACT-1981: Interest recalculation - Recalculate Interest For Loan - Business step for COB - progressive loan --- ...dvancedPaymentScheduleTransactionProcessor.java | 117 ++- .../impl/ProgressiveTransactionCtx.java | 9 +- .../integrationtests/BaseLoanIntegrationTest.java | 165 ++++ .../LoanInterestRecalculationCOBTest.java | 847 +++++++++++++++++++-- 4 files changed, 1073 insertions(+), 65 deletions(-) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index b08b055d2..16a854be9 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -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(); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java index c1348f004..a3d7daa08 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java @@ -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; - } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 15d65da86..ae7912760 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -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; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java index dd526c11e..5722592cc 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java @@ -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); + + 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", 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); - 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", 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); - 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 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); + + 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); - 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", 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,67 +716,104 @@ 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("2 March 2023", () -> { + + 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); - 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", 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(); - 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); + 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(); + 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", 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()); } }