diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 6e6aa29ad..91bb6094b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @Slf4j public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { @@ -188,12 +189,23 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { - // TODO: implementation + LinkedHashMap> paymentAllocationsMap = paymentAllocationTypes.stream().collect( + Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); + + for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { + transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments, + transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, + transactionMappings, balances); + if (!transactionAmountUnprocessed.isGreaterThanZero()) { + break; + } + } } else if (LoanScheduleProcessingType.VERTICAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { - transactionAmountUnprocessed = refundTransaction(loanTransaction, currency, installments, zero, transactionMappings, - transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType); + transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero, + transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, + paymentAllocationType); if (!transactionAmountUnprocessed.isGreaterThanZero()) { break; } @@ -471,7 +483,74 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } - private Money refundTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, + private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, Money transactionAmountUnprocessed, + List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, + List transactionMappings, Balances balances) { + Money zero = Money.zero(currency); + Money refundedPortion; + outerLoop: do { + LoanRepaymentScheduleInstallment latestPastDueInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, + installments, zero); + LoanRepaymentScheduleInstallment dueInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero); + + List inAdvanceInstallments = getFutureInstallmentsForRefund(loanTransaction, currency, + installments, futureInstallmentAllocationRule, zero); + + for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { + switch (paymentAllocationType.getDueType()) { + case PAST_DUE -> { + if (latestPastDueInstallment != null) { + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, latestPastDueInstallment, currency); + refundedPortion = processPaymentAllocation(paymentAllocationType, latestPastDueInstallment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, + LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); + } else { + break outerLoop; + } + } + case DUE -> { + if (dueInstallment != null) { + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, dueInstallment, currency); + refundedPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances, + LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); + } else { + break outerLoop; + } + } + case IN_ADVANCE -> { + int numberOfInstallments = inAdvanceInstallments.size(); + if (numberOfInstallments > 0) { + Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode()); + Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); + for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { + if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { + evenPortion = evenPortion.add(balanceAdjustment); + } + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, inAdvanceInstallment, currency); + refundedPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, + evenPortion, loanTransactionToRepaymentScheduleMapping, balances, + LoanRepaymentScheduleInstallment.PaymentAction.UNPAY); + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); + } + } else { + break outerLoop; + } + } + } + } + } while (installments.stream().anyMatch(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) + && transactionAmountUnprocessed.isGreaterThanZero()); + return transactionAmountUnprocessed; + } + + private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money zero, List transactionMappings, Money transactionAmountUnprocessed, FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances, @@ -481,9 +560,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep do { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { - currentInstallment = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) - .filter(e -> loanTransaction.isAfter(e.getDueDate())) - .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); + currentInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, zero); if (currentInstallment != null) { LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); @@ -494,9 +571,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } case DUE -> { - currentInstallment = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) - .filter(installment -> loanTransaction.isOn(installment.getDueDate())) - .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); + currentInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero); if (currentInstallment != null) { LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, currentInstallment, currency); @@ -507,22 +582,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } case IN_ADVANCE -> { - List currentInstallments = new ArrayList<>(); - if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { - currentInstallments = installments.stream() - .filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) - .filter(e -> loanTransaction.isBefore(e.getDueDate())).toList(); - } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - currentInstallments = installments.stream() - .filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) - .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); - } else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - currentInstallments = installments.stream() - .filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) - .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); - } + List currentInstallments = getFutureInstallmentsForRefund(loanTransaction, currency, + installments, futureInstallmentAllocationRule, zero); int numberOfInstallments = currentInstallments.size(); refundedPortion = zero; if (numberOfInstallments > 0) { @@ -552,6 +613,42 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return transactionAmountUnprocessed; } + @Nullable + private static LoanRepaymentScheduleInstallment getDueInstallmentForRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, Money zero) { + return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) + .filter(installment -> loanTransaction.isOn(installment.getDueDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); + } + + @Nullable + private static LoanRepaymentScheduleInstallment getLatestPastDueInstallmentForRefund(LoanTransaction loanTransaction, + MonetaryCurrency currency, List installments, Money zero) { + return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) + .filter(e -> loanTransaction.isAfter(e.getDueDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); + } + + @NotNull + private static List getFutureInstallmentsForRefund(LoanTransaction loanTransaction, + MonetaryCurrency currency, List installments, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, Money zero) { + List inAdvanceInstallments = new ArrayList<>(); + if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { + inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) + .filter(e -> loanTransaction.isBefore(e.getDueDate())).toList(); + } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { + inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) + .filter(e -> loanTransaction.isBefore(e.getDueDate())) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + } else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) { + inAdvanceInstallments = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) + .filter(e -> loanTransaction.isBefore(e.getDueDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + } + return inAdvanceInstallments; + } + private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Money transactionAmountUnprocessed, Set charges) { Money zero = Money.zero(currency); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java index e88d237b5..58c884679 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java @@ -90,9 +90,8 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest { } @Test - public void refundForActiveLoanWithDefaultPaymentAllocation() { + public void refundForActiveLoanWithDefaultPaymentAllocationProcessingVertically() { try { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); @@ -102,7 +101,8 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest { final Account expenseAccount = accountHelper.createExpenseAccount(); final Account overpaymentAccount = accountHelper.createLiabilityAccount(); - Integer loanProductId = createLoanProduct("1000", "30", "4", assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + Integer loanProductId = createLoanProduct("1000", "30", "4", LoanScheduleProcessingType.VERTICAL, assetAccount, incomeAccount, + expenseAccount, overpaymentAccount); final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), loanProductId, 1000L, 90, 30, 3, 0, "01 January 2023", "01 January 2023"); @@ -288,15 +288,215 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest { } } + @Test + public void refundForActiveLoanWithDefaultPaymentAllocationProcessingHorizontally() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final Account assetAccount = accountHelper.createAssetAccount(); + final Account incomeAccount = accountHelper.createIncomeAccount(); + final Account expenseAccount = accountHelper.createExpenseAccount(); + final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + + Integer loanProductId = createLoanProduct("1000", "30", "4", LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, + expenseAccount, overpaymentAccount); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), loanProductId, 1000L, 90, 30, 3, 0, + "01 January 2023", "01 January 2023"); + + int loanId = loanResponse.getLoanId().intValue(); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(1000.00)).locale("en")); + + final float feePortion = 50.0f; + final float penaltyPortion = 100.0f; + + Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(feePortion), false)); + + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(penaltyPortion), true)); + + final String firstInstallmentChargeAddedDate = DATE_FORMATTER.format(LocalDate.of(2023, 1, 3)); + loanTransactionHelper.addChargesForLoan(loanId, LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON( + String.valueOf(fee), firstInstallmentChargeAddedDate, String.valueOf(feePortion))); + + loanTransactionHelper.addChargesForLoan(loanId, LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON( + String.valueOf(penalty), firstInstallmentChargeAddedDate, String.valueOf(penaltyPortion))); + + final String secondInstallmentChargeAddedDate = DATE_FORMATTER.format(LocalDate.of(2023, 2, 3)); + loanTransactionHelper.addChargesForLoan(loanId, LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON( + String.valueOf(fee), secondInstallmentChargeAddedDate, String.valueOf(feePortion))); + + loanTransactionHelper.addChargesForLoan(loanId, LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON( + String.valueOf(penalty), secondInstallmentChargeAddedDate, String.valueOf(penaltyPortion))); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + + GetLoansLoanIdRepaymentPeriod firstRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(2); + GetLoansLoanIdRepaymentPeriod secondRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(3); + GetLoansLoanIdRepaymentPeriod thirdRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(4); + + assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size()); + assertEquals(feePortion, firstRepaymentInstallment.getFeeChargesDue()); + assertEquals(feePortion, firstRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, firstRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(penaltyPortion, firstRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue()); + assertEquals(250.0f, firstRepaymentInstallment.getPrincipalOutstanding()); + assertEquals(400.0f, firstRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(400.0f, firstRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 1, 31), firstRepaymentInstallment.getDueDate()); + + assertEquals(feePortion, secondRepaymentInstallment.getFeeChargesDue()); + assertEquals(feePortion, secondRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, secondRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(penaltyPortion, secondRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(400.0f, secondRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(400.0f, secondRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 3, 2), secondRepaymentInstallment.getDueDate()); + + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, thirdRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(250.0f, thirdRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 4, 1), thirdRepaymentInstallment.getDueDate()); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.03.01").dateFormat("yyyy.MM.dd").locale("en")); + loanTransactionHelper.makeRepayment("28 January 2023", 810.0f, loanId); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + + firstRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(2); + secondRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(3); + thirdRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(4); + + assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size()); + assertEquals(feePortion, firstRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, firstRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, firstRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, firstRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue()); + assertEquals(0.0f, firstRepaymentInstallment.getPrincipalOutstanding()); + assertEquals(400.0f, firstRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(0.0f, firstRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 1, 31), firstRepaymentInstallment.getDueDate()); + + assertEquals(feePortion, secondRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, secondRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, secondRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, secondRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(400.0f, secondRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(0.0f, secondRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 3, 2), secondRepaymentInstallment.getDueDate()); + + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, thirdRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(240.0f, thirdRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 4, 1), thirdRepaymentInstallment.getDueDate()); + + loanTransactionHelper.makeRefundByCash("28 January 2023", 15.0f, loanId); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + + firstRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(2); + secondRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(3); + thirdRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(4); + + assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size()); + assertEquals(feePortion, firstRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, firstRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, firstRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, firstRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue()); + assertEquals(0.0f, firstRepaymentInstallment.getPrincipalOutstanding()); + assertEquals(400.0f, firstRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(0.0f, firstRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 1, 31), firstRepaymentInstallment.getDueDate()); + + assertEquals(feePortion, secondRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, secondRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, secondRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, secondRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, secondRepaymentInstallment.getPrincipalDue()); + assertEquals(5.0f, secondRepaymentInstallment.getPrincipalOutstanding()); + assertEquals(400.0f, secondRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(5.0f, secondRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 3, 2), secondRepaymentInstallment.getDueDate()); + + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, thirdRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(250.0f, thirdRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 4, 1), thirdRepaymentInstallment.getDueDate()); + + // fully unpaying the second installment + loanTransactionHelper.makeRefundByCash("28 January 2023", 395.0f, loanId); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); + + firstRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(2); + secondRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(3); + thirdRepaymentInstallment = loanDetails.getRepaymentSchedule().getPeriods().get(4); + + assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size()); + assertEquals(feePortion, firstRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, firstRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, firstRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, firstRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue()); + assertEquals(0.0f, firstRepaymentInstallment.getPrincipalOutstanding()); + assertEquals(400.0f, firstRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(0.0f, firstRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 1, 31), firstRepaymentInstallment.getDueDate()); + + assertEquals(feePortion, secondRepaymentInstallment.getFeeChargesDue()); + assertEquals(feePortion, secondRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(penaltyPortion, secondRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(penaltyPortion, secondRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, secondRepaymentInstallment.getPrincipalDue()); + assertEquals(250.0f, secondRepaymentInstallment.getPrincipalOutstanding()); + assertEquals(400.0f, secondRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(400.0f, secondRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 3, 2), secondRepaymentInstallment.getDueDate()); + + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesOutstanding()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesDue()); + assertEquals(0.0f, thirdRepaymentInstallment.getPenaltyChargesOutstanding()); + assertEquals(250.0f, thirdRepaymentInstallment.getTotalDueForPeriod()); + assertEquals(250.0f, thirdRepaymentInstallment.getTotalOutstandingForPeriod()); + assertEquals(LocalDate.of(2023, 4, 1), thirdRepaymentInstallment.getDueDate()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + private Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, - final Account... accounts) { + LoanScheduleProcessingType loanScheduleProcessingType, final Account... accounts) { AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); log.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal) .withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments) .withEnableDownPayment(true, "25", true).withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths() .withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) - .withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withLoanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL) + .withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withLoanScheduleProcessingType(loanScheduleProcessingType) .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) .addAdvancedPaymentAllocation(defaultAllocation).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0") .build(null);