From f55b63432e1677bc1b3250861f602c69294db92a Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Tue, 2 Dec 2025 16:51:42 +0100 Subject: [PATCH] FINERACT-2326: Not implemented exception handling --- .../core/data/ApiGlobalErrorResponse.java | 10 ++++ .../UnsupporterOperationExceptionMapper.java | 56 +++++++++++++++++++ .../test/stepdef/loan/LoanReAgingStepDef.java | 17 ++++++ .../resources/features/LoanReAging.feature | 31 +++++++++- ...edPaymentScheduleTransactionProcessor.java | 15 +++-- 5 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupporterOperationExceptionMapper.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java index b71d49826..22cb944df 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java @@ -25,6 +25,7 @@ import static org.apache.http.HttpStatus.SC_FORBIDDEN; import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED; import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.http.HttpStatus.SC_NOT_IMPLEMENTED; import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; @@ -139,6 +140,15 @@ public class ApiGlobalErrorResponse { "Errors contain reason for domain rule violation.", errors); } + public static ApiGlobalErrorResponse notImplemented(final String globalisationMessageCode, final String defaultUserMessage, + final Object... defaultUserMessageArgs) { + final List errors = new ArrayList<>(); + errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs)); + + return create(SC_NOT_IMPLEMENTED, "validation.msg.not.implemented", "Request was understood but not implemented.", + "Errors contain reason for not implemented violation.", errors); + } + public static ApiGlobalErrorResponse dataIntegrityError(final String globalisationMessageCode, final String defaultUserMessage, final String parameterName, final Object... defaultUserMessageArgs) { final List errors = new ArrayList<>(); diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupporterOperationExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupporterOperationExceptionMapper.java new file mode 100644 index 000000000..ce79b61cb --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupporterOperationExceptionMapper.java @@ -0,0 +1,56 @@ +/** + * 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.infrastructure.core.exceptionmapper; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse; +import org.apache.fineract.infrastructure.core.exception.ErrorHandler; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +/** + * An {@link ExceptionMapper} to map {@link UnsupportedOperationException} thrown by platform into a HTTP API friendly + * format. + * + * The {@link UnsupportedOperationException} is thrown when an operation was not yet implemented. + */ +@Provider +@Component +@Scope("singleton") +@Slf4j +public class UnsupporterOperationExceptionMapper implements FineractExceptionMapper, ExceptionMapper { + + @Override + public Response toResponse(final UnsupportedOperationException exception) { + log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception)); + final ApiGlobalErrorResponse notFoundErrorResponse = ApiGlobalErrorResponse.notImplemented("not.implemented", + exception.getMessage()); + return Response.status(Status.NOT_IMPLEMENTED).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build(); + } + + @Override + public int errorCode() { + return 5010; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java index 52283d322..abc8621a3 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java @@ -538,4 +538,21 @@ public class LoanReAgingStepDef extends AbstractStepDef { } return request; } + + @When("Admin creates a Loan re-aging transaction by Loan external ID with the following data, but fails with {int} error code:") + public void adminCreatesALoanReAgingTransactionByLoanExternalIDWithTheFollowingDataButFailsWithErrorCode(int errorCode, + DataTable table) { + PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + String loanExternalId = loanResponse.getResourceExternalId(); + + PostLoansLoanIdTransactionsRequest reAgingRequest = setReAgeingRequestProperties(// + LoanRequestFactory.defaultReAgingRequest(), // + table.row(0), // + table.row(1) // + ); + + CallFailedRuntimeException response = fail(() -> fineractClient.loanTransactions().executeLoanTransaction1(loanExternalId, + reAgingRequest, Map.of("command", "reAge"))); + assertThat(response.getStatus()).isEqualTo(errorCode); + } } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature index 1e2358677..687b3a58b 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature @@ -12320,7 +12320,6 @@ Feature: LoanReAging | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | true | false | | 15 March 2024 | Re-age | 85.08 | 83.57 | 1.51 | 0.0 | 0.0 | 0.0 | false | true | - When Loan Pay-off is made on "01 April 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met @@ -14537,4 +14536,34 @@ Feature: LoanReAging When Loan Pay-off is made on "01 April 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met + @TestRailId:C4272 @AdvancedPaymentAllocation + Scenario: Verify Re-aging on interest bearing, no interest recalculation loan is not allowed (not implemented) + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule + 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_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024| | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + When Admin creates a Loan re-aging transaction by Loan external ID with the following data, but fails with 501 error code: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 16 January 2025 | 5 | + When Loan Pay-off is made on "01 January 2024" + Then Loan is closed with zero outstanding balance and it's all installments have obligations met 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 b0965cf55..556495036 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 @@ -62,7 +62,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; @@ -165,34 +164,34 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep protected Money handleTransactionThatIsALateRepaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, List installments, LoanTransaction loanTransaction, Money transactionAmountUnprocessed, List transactionMappings, Set charges) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } @Override protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, List installments, LoanTransaction loanTransaction, Money paymentInAdvance, List transactionMappings, Set charges) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } @Override protected Money handleTransactionThatIsOnTimePaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, LoanTransaction loanTransaction, Money transactionAmountUnprocessed, List transactionMappings, Set charges) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } @Override protected Money handleRefundTransactionPaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment, LoanTransaction loanTransaction, Money transactionAmountUnprocessed, List transactionMappings) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } @Override public Money handleRepaymentSchedule(List transactionsPostDisbursement, MonetaryCurrency currency, List installments, Set loanCharges) { - throw new NotImplementedException(); + throw new UnsupportedOperationException(); } // only for progressive loans @@ -2957,12 +2956,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep handleReAgeWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx); } else if (loanTransaction.getLoan().isInterestBearing() && !loanTransaction.getLoan().isInterestRecalculationEnabled()) { // TODO: implement interestRecalculation = false logic - throw new NotImplementedException( + throw new UnsupportedOperationException( "Logic for re-aging when interest bearing loan has interestRecalculation disabled is not implemented"); } } else if (LoanReAgeInterestHandlingType.WAIVE_INTEREST.equals(loanReAgeParameter.getInterestHandlingType())) { - throw new NotImplementedException("WAIVE_INTEREST interest handling strategy for re-aging is not implemented"); + throw new UnsupportedOperationException("WAIVE_INTEREST interest handling strategy for re-aging is not implemented"); } else { if (LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.equals(loanReAgeParameter.getInterestHandlingType())) { CommonReAgeSettings settings = new CommonReAgeSettings(false, true, true, true);