diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java index f49003e41..663f70bd0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/api/BatchApiResource.java @@ -29,8 +29,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; +import javax.ws.rs.HttpMethod; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -43,7 +45,9 @@ import org.apache.fineract.batch.domain.BatchRequest; import org.apache.fineract.batch.domain.BatchResponse; import org.apache.fineract.batch.serialization.BatchRequestJsonHelper; import org.apache.fineract.batch.service.BatchApiService; +import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer; +import org.apache.fineract.infrastructure.security.exception.InvalidInstanceTypeMethodException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -78,6 +82,7 @@ public class BatchApiResource { private final ToApiJsonSerializer toApiJsonSerializer; private final BatchApiService service; private final BatchRequestJsonHelper batchRequestJsonHelper; + private final FineractProperties fineractProperties; /** * Rest assured POST method to get {@link BatchRequest} and returns back the consolidated {@link BatchResponse} @@ -106,6 +111,8 @@ public class BatchApiResource { // Converts request array into BatchRequest List final List requestList = this.batchRequestJsonHelper.extractList(jsonRequestString); + validateRequestMethodsAllowedOnInstanceType(requestList); + // Gets back the consolidated BatchResponse from BatchApiservice List result = new ArrayList<>(); @@ -120,4 +127,22 @@ public class BatchApiResource { return this.toApiJsonSerializer.serialize(result); } + + /** + * Validates to make sure the request methods are allowed on currently running instance mode (type). + * + * @param requestList + * the list of {@link BatchRequest}s + */ + private void validateRequestMethodsAllowedOnInstanceType(final List requestList) { + // Throw exception if instance is read only and any of the batch requests are trying to write/update data. + if (fineractProperties.getMode().isReadOnlyMode()) { + final Optional nonGetRequest = requestList.stream() + .filter(batchRequest -> !HttpMethod.GET.equals(batchRequest.getMethod())).findFirst(); + if (nonGetRequest.isPresent()) { + throw new InvalidInstanceTypeMethodException(nonGetRequest.get().getMethod()); + } + } + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/service/CodeValueReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/service/CodeValueReadPlatformServiceImpl.java index 4decf34b7..7821aa062 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/service/CodeValueReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/service/CodeValueReadPlatformServiceImpl.java @@ -65,6 +65,7 @@ public class CodeValueReadPlatformServiceImpl implements CodeValueReadPlatformSe } @Override + @Cacheable(value = "code_values", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#code+'cv')") public Collection retrieveCodeValuesByCode(final String code) { this.context.authenticatedUser(); @@ -88,6 +89,7 @@ public class CodeValueReadPlatformServiceImpl implements CodeValueReadPlatformSe } @Override + @Cacheable(value = "code_values", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#codeValueId+'cv_by_id')") public CodeValueData retrieveCodeValue(final Long codeValueId) { try { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapper.java new file mode 100644 index 000000000..77831d231 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapper.java @@ -0,0 +1,46 @@ +/** + * 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 javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse; +import org.apache.fineract.infrastructure.security.exception.InvalidInstanceTypeMethodException; +import org.springframework.stereotype.Component; + +/** + * An {@link ExceptionMapper} to map {@link InvalidInstanceTypeMethodException} thrown by platform into an HTTP API + * friendly format. + */ +@Provider +@Component +@Slf4j +public class InvalidInstanceTypeMethodExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(final InvalidInstanceTypeMethodException exception) { + log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage()); + ApiGlobalErrorResponse errorResponse = ApiGlobalErrorResponse.invalidInstanceTypeMethod(exception.getMethod()); + return Response.status(Status.METHOD_NOT_ALLOWED).entity(errorResponse).type(MediaType.APPLICATION_JSON).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilter.java index 4266de6b5..6b366d3f6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilter.java @@ -43,7 +43,10 @@ public class FineractInstanceModeApiFilter extends OncePerRequestFilter { private static final List EXCEPTION_LIST = List.of( item(FineractProperties.FineractModeProperties::isBatchManagerEnabled, pi -> pi.startsWith("/jobs")), - item(p -> true, pi -> pi.startsWith("/instance-mode"))); + item(p -> true, pi -> pi.startsWith("/instance-mode")), + // Batches with all GET requests need to be allowed in read-only instances, hence this check will be moved + // under the Api Resource. + item(p -> true, pi -> pi.startsWith("/batches"))); private final FineractProperties fineractProperties; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/InvalidInstanceTypeMethodException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/InvalidInstanceTypeMethodException.java index a69a0813b..85d44f8df 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/InvalidInstanceTypeMethodException.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/exception/InvalidInstanceTypeMethodException.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.infrastructure.security.exception; +import lombok.Getter; import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; /** @@ -25,9 +26,13 @@ import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainR * * */ +@Getter public class InvalidInstanceTypeMethodException extends AbstractPlatformDomainRuleException { + private final String method; + public InvalidInstanceTypeMethodException(final String method) { super("error.msg.invalid.method.for.instance.type", "Method Not Allowed " + method + " for the instance type"); + this.method = method; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index dfd42a124..3acf93cc1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -5913,6 +5913,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.accountNumber; } + public String getExternalId() { + return this.externalId; + } + public Client getClient() { return this.client; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java index 0de3e8719..b03f44735 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePeriodData.java @@ -423,4 +423,8 @@ public final class LoanSchedulePeriodData { public BigDecimal getTotalOverdue() { return defaultToZeroIfNull(this.totalOverdue); } + + public BigDecimal totalOutstandingForPeriod() { + return defaultToZeroIfNull(this.totalOutstandingForPeriod); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java index 50ee62e35..65fd525b2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java @@ -24,6 +24,7 @@ import java.util.Collection; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; @@ -41,6 +42,7 @@ public class PaymentTypeReadPlatformServiceImpl implements PaymentTypeReadPlatfo } @Override + @Cacheable(value = "payment_types", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('payment_types')") public Collection retrieveAllPaymentTypes() { // TODO Auto-generated method stub this.context.authenticatedUser(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java index 03955093d..0fcf4f26c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java @@ -29,6 +29,7 @@ import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepositoryWrapper; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Service; @@ -50,6 +51,7 @@ public class PaymentTypeWriteServiceImpl implements PaymentTypeWriteService { } @Override + @CacheEvict(value = "payment_types", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('payment_types')") public CommandProcessingResult createPaymentType(JsonCommand command) { this.fromApiJsonDeserializer.validateForCreate(command.json()); String name = command.stringValueOfParameterNamed(PaymentTypeApiResourceConstants.NAME); @@ -63,6 +65,7 @@ public class PaymentTypeWriteServiceImpl implements PaymentTypeWriteService { } @Override + @CacheEvict(value = "payment_types", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('payment_types')") public CommandProcessingResult updatePaymentType(Long paymentTypeId, JsonCommand command) { this.fromApiJsonDeserializer.validateForUpdate(command.json()); @@ -77,6 +80,7 @@ public class PaymentTypeWriteServiceImpl implements PaymentTypeWriteService { } @Override + @CacheEvict(value = "payment_types", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('payment_types')") public CommandProcessingResult deletePaymentType(Long paymentTypeId) { final PaymentType paymentType = this.repositoryWrapper.findOneWithNotFoundDetection(paymentTypeId); try { diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapperTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapperTest.java new file mode 100644 index 000000000..f52ee35d4 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapperTest.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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.Response; +import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse; +import org.apache.fineract.infrastructure.security.exception.InvalidInstanceTypeMethodException; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link InvalidInstanceTypeMethodExceptionMapper}. + */ +public class InvalidInstanceTypeMethodExceptionMapperTest { + + /** + * Tests the {@link InvalidInstanceTypeMethodExceptionMapper#toResponse(InvalidInstanceTypeMethodException)} method + * for successful scenario. + */ + @Test + public void testToResponse() { + final InvalidInstanceTypeMethodExceptionMapper exceptionMapper = new InvalidInstanceTypeMethodExceptionMapper(); + final InvalidInstanceTypeMethodException exception = new InvalidInstanceTypeMethodException(HttpMethod.POST); + + final Response response = exceptionMapper.toResponse(exception); + + assertNotNull(response); + assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), response.getStatus()); + assertNotNull(response.getEntity()); + final ApiGlobalErrorResponse errorResponse = (ApiGlobalErrorResponse) response.getEntity(); + assertEquals(String.valueOf(Response.Status.METHOD_NOT_ALLOWED.getReasonPhrase()), errorResponse.getHttpStatusCode()); + assertEquals("Invalid instance type called in api request for the method POST", errorResponse.getDeveloperMessage()); + assertEquals("error.msg.invalid.instance.type", errorResponse.getUserMessageGlobalisationCode()); + assertEquals("Invalid method POST used with request to this instance type.", errorResponse.getDefaultUserMessage()); + } + +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilterTest.java index 86d14d8f7..a071dc5c8 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilterTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/instancemode/filter/FineractInstanceModeApiFilterTest.java @@ -244,4 +244,44 @@ class FineractInstanceModeApiFilterTest { // then verify(filterChain).doFilter(request, response); } + + @Test + void testDoFilterInternal_ShouldLetBatchesApisThrough_WhenFineractIsInReadMode() throws ServletException, IOException { + // given + FineractProperties.FineractModeProperties modeProperties = InstanceModeMock.createModeProps(true, false, false, false); + given(fineractProperties.getMode()).willReturn(modeProperties); + given(request.getMethod()).willReturn(HttpMethod.POST.name()); + given(request.getPathInfo()).willReturn("/batches"); + // when + underTest.doFilterInternal(request, response, filterChain); + // then + verify(filterChain).doFilter(request, response); + } + + @Test + void testDoFilterInternal_ShouldLetBatchesApisThrough_WhenFineractIsInWriteMode() throws ServletException, IOException { + // given + FineractProperties.FineractModeProperties modeProperties = InstanceModeMock.createModeProps(false, true, false, false); + given(fineractProperties.getMode()).willReturn(modeProperties); + given(request.getMethod()).willReturn(HttpMethod.POST.name()); + given(request.getPathInfo()).willReturn("/batches"); + // when + underTest.doFilterInternal(request, response, filterChain); + // then + verify(filterChain).doFilter(request, response); + } + + @Test + void testDoFilterInternal_ShouldLetBatchesApisThrough_WhenFineractIsInBatchMode() throws ServletException, IOException { + // given + FineractProperties.FineractModeProperties modeProperties = InstanceModeMock.createModeProps(false, false, true, true); + given(fineractProperties.getMode()).willReturn(modeProperties); + given(request.getMethod()).willReturn(HttpMethod.POST.name()); + given(request.getPathInfo()).willReturn("/batches"); + // when + underTest.doFilterInternal(request, response, filterChain); + // then + verify(filterChain).doFilter(request, response); + } + }