From ec1c55d022c0020d5810848238bfc6c75e43f9f8 Mon Sep 17 00:00:00 2001 From: Kristof Jozsa Date: Thu, 1 Aug 2024 17:02:32 +0200 Subject: [PATCH] FINERACT-2081: fix disburse error scenario --- .../test/helper/ErrorMessageHelper.java | 4 ++++ .../test/stepdef/loan/LoanStepDef.java | 17 +++++++++++++++++ .../src/test/resources/features/Loan.feature | 14 ++++++++++++++ .../portfolio/loanaccount/domain/Loan.java | 18 ++---------------- .../LoanTransactionValidator.java | 17 +++++++++++++++++ ...nWritePlatformServiceJpaRepositoryImpl.java | 4 ++-- .../LoanAccountBackdatedDisbursementTest.java | 2 -- 7 files changed, 56 insertions(+), 20 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java index 9b64527ecb2..357db71a431 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java @@ -66,6 +66,10 @@ public static String disburseMaxAmountFailure() { return "Loan disbursal amount can't be greater than maximum applied loan amount calculation. Total disbursed amount: [0-9]* Maximum disbursal amount: [0-9]*"; } + public static String disbursePastDateFailure(Integer loanId, String actualDisbursementDate) { + return String.format("The date on which a loan is disbursed cannot be before its approval date: %s", actualDisbursementDate); + } + public static String loanSubmitDateInFutureFailureMsg() { return "The date on which a loan is submitted cannot be in the future."; } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 81b48e52320..2ab4058a170 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -1892,6 +1892,23 @@ public void checkLoanDetailsFieldAndValueInt(int fieldValue) throws IOException, assertThat(fixedLengthactual).as(ErrorMessageHelper.wrongfixedLength(fixedLengthactual, fieldValue)).isEqualTo(fieldValue); } + @Then("Admin fails to disburse the loan on {string} with {string} EUR transaction amount because disbursement date is earlier than {string}") + public void disburseLoanFailureWithPastDate(String actualDisbursementDate, String transactionAmount, String futureApproveDate) + throws IOException { + Response loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId = loanResponse.body().getLoanId(); + PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest() + .actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount)); + + String futureApproveDateISO = FORMATTER_EVENTS.format(FORMATTER.parse(futureApproveDate)); + Response loanDisburseResponse = loansApi.stateTransitions(loanId, disburseRequest, "disburse").execute(); + testContext().set(TestContextKey.LOAN_DISBURSE_RESPONSE, loanDisburseResponse); + ErrorResponse errorDetails = ErrorResponse.from(loanDisburseResponse); + assertThat(errorDetails.getHttpStatusCode()).as(ErrorMessageHelper.dateFailureErrorCodeMsg()).isEqualTo(403); + assertThat(errorDetails.getSingleError().getDeveloperMessage()) + .isEqualTo(ErrorMessageHelper.disbursePastDateFailure((int) loanId, futureApproveDateISO)); + } + private LoanStatusEnumDataV1 getExpectedStatus(String loanStatus) { LoanStatusEnumDataV1 result = new LoanStatusEnumDataV1(); switch (loanStatus) { diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index 48d32ce89d9..60555c9b0ae 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -5508,3 +5508,17 @@ Feature: Loan And Admin successfully approves the loan on "01 February 2024" with "1000" amount and expected disbursement date on "01 February 2024" When Admin successfully disburse the loan on "01 February 2024" with "1000" EUR transaction amount Then LoanDetails has fixedLength field with int value: 60 + + + Scenario: Actual disbursement date is in the past with advanced payment allocation product + submitted on date repaymentStartDateType + When Admin sets repaymentStartDateType for "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product to "SUBMITTED_ON_DATE" + When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule + When Admin sets the business date to "01 January 2023" + And Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2023 | 500 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2023" with "500" amount and expected disbursement date on "01 January 2023" + Then Loan status has changed to "Approved" + Then Admin fails to disburse the loan on "31 December 2022" with "500" EUR transaction amount because disbursement date is earlier than "01 January 2023" + When Admin sets repaymentStartDateType for "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product to "DISBURSEMENT_DATE" diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index aff66589ff3..e6c59a5d75f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1727,19 +1727,13 @@ public List getDisbursedLoanDisbursementDetails() { .collect(Collectors.toList()); } - public boolean canDisburse(final LocalDate actualDisbursementDate) { - LocalDate loanSubmittedOnDate = this.submittedOnDate; + public boolean canDisburse() { final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSED, this); boolean isMultiTrancheDisburse = false; LoanStatus actualLoanStatus = LoanStatus.fromInt(this.loanStatus); if ((actualLoanStatus.isActive() || actualLoanStatus.isClosedObligationsMet() || actualLoanStatus.isOverpaid()) && isAllTranchesNotDisbursed()) { - if (DateUtils.isBefore(actualDisbursementDate, loanSubmittedOnDate)) { - final String errorMsg = "Loan can't be disbursed before " + loanSubmittedOnDate; - throw new LoanDisbursalException(errorMsg, "actualdisbursementdate.before.submittedDate", loanSubmittedOnDate, - actualDisbursementDate); - } isMultiTrancheDisburse = true; } return !statusEnum.hasStateOf(actualLoanStatus) || isMultiTrancheDisburse; @@ -2012,18 +2006,10 @@ public void handleDisbursementTransaction(final LocalDate disbursedOn, final Pay updateLoanOutstandingBalances(); } - if (getApprovedOnDate() != null && DateUtils.isBefore(disbursedOn, getApprovedOnDate())) { - final String errorMessage = "The date on which a loan is disbursed cannot be before its approval date: " - + getApprovedOnDate().toString(); - throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.before.approval.date", errorMessage, disbursedOn, - getApprovedOnDate()); - } - LocalDate expectedDate = getExpectedFirstRepaymentOnDate(); if (expectedDate != null && (DateUtils.isAfter(disbursedOn, this.fetchRepaymentScheduleInstallment(1).getDueDate()) || DateUtils.isAfter(disbursedOn, expectedDate)) && DateUtils.isEqual(disbursedOn, this.actualDisbursementDate)) { - final String errorMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate: " - + expectedDate.toString(); + final String errorMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate: " + expectedDate; throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.expected.first.repayment.date", errorMessage, disbursedOn, expectedDate); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java index feb50685519..de1f979221d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java @@ -80,6 +80,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanRepaymentScheduleNotFoundException; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; @@ -213,6 +214,22 @@ public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, loan.getExpectedDisbursedOnLocalDate()); } + if ((loan.getStatus().isActive() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) + && loan.isAllTranchesNotDisbursed()) { + LocalDate submittedOnDate = loan.getSubmittedOnDate(); + if (DateUtils.isBefore(actualDisbursementDate, submittedOnDate)) { + final String errorMsg = "Loan can't be disbursed before " + submittedOnDate; + throw new LoanDisbursalException(errorMsg, "actualdisbursementdate.before.submittedDate", submittedOnDate, + actualDisbursementDate); + } + } + + LocalDate approvedOnDate = loan.getApprovedOnDate(); + if (DateUtils.isBefore(actualDisbursementDate, approvedOnDate)) { + final String errorMessage = "The date on which a loan is disbursed cannot be before its approval date: " + approvedOnDate; + throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.before.approval.date", errorMessage, + actualDisbursementDate, approvedOnDate); + } }); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 51f1d04f286..823a992010a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -349,7 +349,7 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - if (loan.canDisburse(actualDisbursementDate)) { + if (loan.canDisburse()) { // Get netDisbursalAmount from disbursal screen field. final BigDecimal netDisbursalAmount = command .bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName); @@ -774,7 +774,7 @@ public Map bulkLoanDisbursal(final JsonCommand command, final Co // disbursement and actual disbursement happens on same date loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); updateLoanCounters(loan, actualDisbursementDate); - boolean canDisburse = loan.canDisburse(actualDisbursementDate); + boolean canDisburse = loan.canDisburse(); ChangedTransactionDetail changedTransactionDetail = null; if (canDisburse) { Money amountBeforeAdjust = loan.getPrincipal(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountBackdatedDisbursementTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountBackdatedDisbursementTest.java index 1c5da42c252..e367b68c121 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountBackdatedDisbursementTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountBackdatedDisbursementTest.java @@ -765,7 +765,6 @@ public void loanAccountBackDatedDisbursementAfterTwoRepaymentsForLoanProductWith @Test public void loanAccountBackDatedDisbursementWithDisbursementDateBeforeLoanSubmittedOnDateValidationTest() { try { - final ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(403).build(); final LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.requestSpec, errorResponse); @@ -867,7 +866,6 @@ public void loanAccountBackDatedDisbursementWithDisbursementDateBeforeLoanSubmit } finally { GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); } - } @Test