This is an automated email from the ASF dual-hosted git repository. arnold pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 5d62cfef04dd800ae9ad2305c349765a3d94fcfd Author: Adam Saghy <[email protected]> AuthorDate: Mon Oct 30 20:28:09 2023 +0100 FINERACT-1968: Adv.paym.disbursement --- ...dvancedPaymentScheduleTransactionProcessor.java | 46 +++++++++- .../domain/LoanTermVariationParams.java | 18 ++++ .../domain/ScheduleCurrentPeriodParams.java | 18 ++++ .../starter/LoanAccountAutoStarter.java | 5 +- ...PaymentAllocationLoanRepaymentScheduleTest.java | 99 +++++++++++++++++++--- 5 files changed, 168 insertions(+), 18 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index feaac762c..57fc21f19 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -49,10 +49,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.springframework.context.annotation.Profile; -//TODO: remove `test` profile when it is finished -@Profile("test") @Slf4j public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { @@ -182,6 +179,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) { switch (loanTransaction.getTypeOf()) { + case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments); case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments); @@ -197,6 +195,48 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } + private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency, + List<LoanRepaymentScheduleInstallment> installments) { + updateLoanSchedule(loanTransaction, currency, installments); + } + + private void updateLoanSchedule(LoanTransaction disbursementTransaction, MonetaryCurrency currency, + List<LoanRepaymentScheduleInstallment> installments) { + final MathContext mc = MoneyHelper.getMathContext(); + List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream() + .filter(i -> !i.getDueDate().isBefore(disbursementTransaction.getTransactionDate()) && !i.isDownPayment()).toList(); + int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size(); + LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail(); + Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); + Money downPaymentAmount = Money.zero(currency); + if (loanProductRelatedDetail.isEnableDownPayment()) { + LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream() + .filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow(); + BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(), + loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc); + if (installmentAmountInMultiplesOf != null) { + downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf); + } + downPaymentAmount = Money.of(currency, downPaymentAmt); + downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); + + } + Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); + Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode()); + if (installmentAmountInMultiplesOf != null) { + increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf); + } + Money remainingAmount = increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode()) + .minus(amortizableAmount); + + Money finalIncreasePrincipalBy = increasePrincipalBy; + candidateRepaymentInstallments + .forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy)); + // Hence the rounding, we might need to amend the last installment amount + candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1) + .addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount); + } + @Override public Money handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> loanCharges) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanTermVariationParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanTermVariationParams.java index 951ba0d36..1651c04b3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanTermVariationParams.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanTermVariationParams.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; import java.time.LocalDate; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduleCurrentPeriodParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduleCurrentPeriodParams.java index da427a4d5..58ac9a9b1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduleCurrentPeriodParams.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduleCurrentPeriodParams.java @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; import java.math.BigDecimal; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java index bef337cb6..68b39629b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java @@ -31,7 +31,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; -import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -104,8 +103,8 @@ public class LoanAccountAutoStarter { @Bean @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) - public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(LoanUtilService loanUtilService) { - return new AdvancedPaymentScheduleTransactionProcessor(loanUtilService); + public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor() { + return new AdvancedPaymentScheduleTransactionProcessor(); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index e8e496f08..a5deb6d74 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -29,9 +29,11 @@ import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.BusinessDateRequest; +import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostClientsResponse; @@ -51,6 +53,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; @@ -89,7 +92,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { final Account expenseAccount = accountHelper.createExpenseAccount(); final Account overpaymentAccount = accountHelper.createLiabilityAccount(); - commonLoanProductId = createLoanProduct("500", "15", "4", assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + commonLoanProductId = createLoanProduct("500", "15", "4", true, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); } @@ -1710,6 +1713,72 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { } } + // UC101: Multiple disbursement test + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 3. Pay over the down payment + // 3. Disburse again + @Test + public void uc101() { + try { + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.20").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 localLoanProductId = createLoanProduct("500", "15", "4", false, assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).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(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("04 January 2023").locale("en").transactionAmount(175.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 325.0, 175.0, 325.0, 175.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 50.0, 75.0, 50.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 175.0, 175.0, 0.0, 325.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("05 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 825.0, 175.0, 825.0, 175.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 250.0, 50.0, 200.0, 50.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment, Double principalOutstanding, Double principalPaid, Double totalOverpaid) { assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding()); @@ -1760,7 +1829,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { } private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, - final Account... accounts) { + boolean autoPayForDownPayment, final Account... accounts) { AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); AdvancedPaymentData goodwillCreditAllocation = createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"); AdvancedPaymentData merchantIssuedRefundAllocation = createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"); @@ -1768,22 +1837,27 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { 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() + .withEnableDownPayment(true, "25", autoPayForDownPayment).withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths() .withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) .addAdvancedPaymentAllocation(defaultAllocation, goodwillCreditAllocation, merchantIssuedRefundAllocation, payoutRefundAllocation) - .withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0").build(null); + .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withInterestTypeAsDecliningBalance().withMultiDisburse() + .withDisallowExpectedDisbursements(true).withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withDaysInMonth("30") + .withDaysInYear("365").withMoratorium("0", "0").build(null); return loanTransactionHelper.getLoanProductId(loanProductJSON); } - private static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, int index, double principalDue, double principalPaid, - double principalOutstanding, double paidInAdvance, double paidLate) { - assertEquals(principalDue, loanDetails.getRepaymentSchedule().getPeriods().get(index).getPrincipalDue()); - assertEquals(principalPaid, loanDetails.getRepaymentSchedule().getPeriods().get(index).getPrincipalPaid()); - assertEquals(principalOutstanding, loanDetails.getRepaymentSchedule().getPeriods().get(index).getPrincipalOutstanding()); - assertEquals(paidInAdvance, loanDetails.getRepaymentSchedule().getPeriods().get(index).getTotalPaidInAdvanceForPeriod()); - assertEquals(paidLate, loanDetails.getRepaymentSchedule().getPeriods().get(index).getTotalPaidLateForPeriod()); + private static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, double principalDue, + double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) { + GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream() + .filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow(); + assertEquals(principalDue, period.getPrincipalDue()); + assertEquals(principalPaid, period.getPrincipalPaid()); + assertEquals(principalOutstanding, period.getPrincipalOutstanding()); + assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod()); + assertEquals(paidLate, period.getTotalPaidLateForPeriod()); } private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final Long principal, @@ -1796,7 +1870,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { .locale("en").submittedOnDate(submittedOnDate).amortizationType(1).interestRatePerPeriod(interestRate) .interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery) .repaymentFrequencyType(0).numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency) - .loanTermFrequencyType(0).principal(BigDecimal.valueOf(principal)).loanType("individual")); + .loanTermFrequencyType(0).principal(BigDecimal.valueOf(principal)).loanType("individual") + .maxOutstandingLoanBalance(BigDecimal.valueOf(35000))); } private static void validateLoanTransaction(GetLoansLoanIdResponse loanDetails, int index, double transactionAmount,
