diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DueDateRespectiveLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DueDateRespectiveLoanRepaymentScheduleTransactionProcessor.java index 7d5b9ec02b7..4221c2202d9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DueDateRespectiveLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DueDateRespectiveLoanRepaymentScheduleTransactionProcessor.java @@ -162,7 +162,6 @@ protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepa penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion); Money subFeePortion; - if (!ignoreDueDateCheck) { if (calculatedFeeCharge.isGreaterThan(transactionAmountRemaining)) { calculatedFeeCharge = transactionAmountRemaining; @@ -174,14 +173,16 @@ protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepa transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); feeChargesPortion = feeChargesPortion.add(subFeePortion); + Money subInterestPortion; if (ignoreDueDateCheck || !transactionDate.isBefore(currentInstallment.getDueDate())) { - interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion); + interestPortion = interestPortion.add(subInterestPortion); } - principalPortion = principalPortion - .add(currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining)); - transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion); + principalPortion = principalPortion.add(subPrincipalPortion); // If the transactionAmountRemaining is greater than zero, rerun the allocation without due date check // to distribute the in advance portions if (transactionAmountRemaining.isGreaterThanZero()) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java index 1bb1aca81b0..7cb44518358 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java @@ -292,7 +292,7 @@ public void scenario2() { // 3. Adding a charge // 3.1 No reverse-replay // 4. Adding a full repayment - // 4.1 Paying first the in advanced principal portion, and after the in advanced charges + // 4.1 Paying first the in advance principal portion, and after the in advance charges @Test public void scenario3() { try { @@ -401,6 +401,229 @@ public void scenario3() { } } + // Scenario4: + // 1. Disburse the loan with 3 installments + // 2. Adding a charge but not due + // 2. Adding a repayment which fully pays 1st installment and partially the next + // 4.1 Paying first the in advance principal portion of #1 installment, and after the in advance charges of #1 + // installment and in advance principal in #2 installment + @Test + public void scenario4() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.01").dateFormat("yyyy.MM.dd").locale("en")); + + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "3", "0", + LoanProductTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "3", "1", "3", "0", + LoanApplicationTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, "01 January 2023", "01 January 2023"); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 January 2023", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2023", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + Integer firstChargeId = loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), "20 January 2023", "50")); + + Integer firstRepaymentId = (Integer) loanTransactionHelper.makeRepayment("10 January 2023", Float.parseFloat("500.00"), loanID) + .get("resourceId"); + + GetLoansLoanIdResponse response = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(550.0, response.getSummary().getTotalOutstanding()); + assertEquals(550.0, response.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesOutstanding()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalDue()); + assertEquals(116.67, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalPaid()); + assertEquals(216.66, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalOutstanding()); + assertTrue(response.getStatus().getActive()); + + assertEquals(firstRepaymentId, response.getTransactions().get(1).getId().intValue()); + assertNull(response.getTransactions().get(1).getReversedOnDate()); + assertTrue(response.getTransactions().get(1).getTransactionRelations().isEmpty()); + assertTrue(response.getTransactions().get(1).getType().getRepayment()); + assertEquals(500.0, response.getTransactions().get(1).getAmount()); + assertEquals(450.0, response.getTransactions().get(1).getPrincipalPortion()); + assertEquals(0.0, response.getTransactions().get(1).getPenaltyChargesPortion()); + assertEquals(0.0, response.getTransactions().get(1).getOverpaymentPortion()); + assertEquals(0.0, response.getTransactions().get(1).getInterestPortion()); + assertEquals(50.0, response.getTransactions().get(1).getFeeChargesPortion()); + assertEquals(550.0, response.getTransactions().get(1).getOutstandingLoanBalance()); + assertEquals(firstChargeId, response.getTransactions().get(1).getLoanChargePaidByList().get(0).getChargeId().intValue()); + assertEquals(1, response.getTransactions().get(1).getLoanChargePaidByList().size()); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // Scenario5: + // 1. Disburse the loan with 3 installments + // 2. Adding a charge but not due + // 2. Adding a repayment which fully pays 1st installment and partially the next + // 4.1 Paying first the in advance principal portion of #1 installment, and after the in advance charges of #1 + // installment and in advance principal in #2 installment + // 5 Overpay the loan + @Test + public void scenario5() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.01").dateFormat("yyyy.MM.dd").locale("en")); + + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "3", "0", + LoanProductTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "3", "1", "3", "0", + LoanApplicationTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, "01 January 2023", "01 January 2023"); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 January 2023", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2023", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + Integer firstChargeId = loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), "20 January 2023", "50")); + + Integer firstRepaymentId = (Integer) loanTransactionHelper.makeRepayment("10 January 2023", Float.parseFloat("500.00"), loanID) + .get("resourceId"); + + GetLoansLoanIdResponse response = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(550.0, response.getSummary().getTotalOutstanding()); + assertEquals(550.0, response.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesOutstanding()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalDue()); + assertEquals(116.67, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalPaid()); + assertEquals(216.66, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalOutstanding()); + assertTrue(response.getStatus().getActive()); + + assertEquals(firstRepaymentId, response.getTransactions().get(1).getId().intValue()); + assertNull(response.getTransactions().get(1).getReversedOnDate()); + assertTrue(response.getTransactions().get(1).getTransactionRelations().isEmpty()); + assertTrue(response.getTransactions().get(1).getType().getRepayment()); + assertEquals(500.0, response.getTransactions().get(1).getAmount()); + assertEquals(450.0, response.getTransactions().get(1).getPrincipalPortion()); + assertEquals(0.0, response.getTransactions().get(1).getPenaltyChargesPortion()); + assertEquals(0.0, response.getTransactions().get(1).getOverpaymentPortion()); + assertEquals(0.0, response.getTransactions().get(1).getInterestPortion()); + assertEquals(50.0, response.getTransactions().get(1).getFeeChargesPortion()); + assertEquals(550.0, response.getTransactions().get(1).getOutstandingLoanBalance()); + assertEquals(firstChargeId, response.getTransactions().get(1).getLoanChargePaidByList().get(0).getChargeId().intValue()); + assertEquals(1, response.getTransactions().get(1).getLoanChargePaidByList().size()); + + Integer secondRepaymentId = (Integer) loanTransactionHelper.makeRepayment("17 January 2023", Float.parseFloat("650.00"), loanID) + .get("resourceId"); + + response = loanTransactionHelper.getLoanDetails((long) loanID); + + int repaymentOrderNo; + int accrualOrderNo; + + assertEquals(0.0, response.getSummary().getTotalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getFeeChargesOutstanding()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalDue()); + assertEquals(333.33, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(2).getPrincipalOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(3).getPenaltyChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(3).getPenaltyChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(3).getPenaltyChargesOutstanding()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(3).getFeeChargesDue()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(3).getFeeChargesPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(3).getFeeChargesOutstanding()); + assertEquals(333.34, response.getRepaymentSchedule().getPeriods().get(3).getPrincipalDue()); + assertEquals(333.34, response.getRepaymentSchedule().getPeriods().get(3).getPrincipalPaid()); + assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(3).getPrincipalOutstanding()); + assertEquals(100.0, response.getTotalOverpaid()); + assertTrue(response.getStatus().getOverpaid()); + + assertEquals(secondRepaymentId, response.getTransactions().get(2).getId().intValue()); + assertNull(response.getTransactions().get(2).getReversedOnDate()); + assertTrue(response.getTransactions().get(2).getTransactionRelations().isEmpty()); + assertTrue(response.getTransactions().get(2).getType().getRepayment()); + assertEquals(650.0, response.getTransactions().get(2).getAmount()); + assertEquals(550.0, response.getTransactions().get(2).getPrincipalPortion()); + assertEquals(0.0, response.getTransactions().get(2).getPenaltyChargesPortion()); + assertEquals(100.0, response.getTransactions().get(2).getOverpaymentPortion()); + assertEquals(0.0, response.getTransactions().get(2).getInterestPortion()); + assertEquals(0.0, response.getTransactions().get(2).getFeeChargesPortion()); + assertEquals(0.0, response.getTransactions().get(2).getOutstandingLoanBalance()); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, final String principal, final String loanTermFrequency, final String repaymentAfterEvery, final String numberOfRepayments, final String interestRate, final String repaymentStrategy, final String expectedDisbursementDate, final String submittedOnDate) {