FINERACT-2258: Fix for making the Post Interest for Savings job properly work in a multitenant environment
Fineract Docker build - MariaDB / build (push) Canceled after 0s
Fineract Docker build - PostgreSQL / build (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract ActiveMQ Smoke / build (push) Canceled after 0s
Fineract Build Progressive Loan Embeddable Jar & Test with a Sample Application / build (push) Canceled after 0s
Fineract Build & Test - MariaDB / build (push) Canceled after 0s
Fineract Build & Test - MySQL / build (push) Canceled after 0s
Fineract Build & Test - PostgreSQL / build (push) Canceled after 0s
Fineract Tests / build (push) Canceled after 0s
Fineract Kafka Smoke / build (push) Canceled after 0s

This commit is contained in:
Arnold Galovics
2025-04-10 11:01:48 +02:00
committed by Arnold Gálovics
parent 09879800e0
commit 872f97b6b6
20 changed files with 611 additions and 75 deletions
@@ -29,8 +29,13 @@ import org.apache.fineract.batch.command.CommandStrategy;
import org.apache.fineract.batch.command.CommandStrategyUtils;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.api.MutableUriInfo;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.portfolio.savings.api.SavingsAccountsApiResource;
import org.apache.fineract.portfolio.savings.api.SavingsApiSetConstants;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
@@ -39,6 +44,8 @@ import org.springframework.stereotype.Component;
public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
private final SavingsAccountsApiResource savingsAccountsApiResource;
private final DefaultToApiJsonSerializer<SavingsAccountData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
@Override
public BatchResponse execute(BatchRequest batchRequest, UriInfo uriInfo) {
@@ -57,6 +64,7 @@ public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
String staffInSelectedOfficeOnly = null;
String chargeStatus = null;
String associations = null;
if (!queryParameters.isEmpty()) {
if (queryParameters.containsKey("staffInSelectedOfficeOnly")) {
staffInSelectedOfficeOnly = queryParameters.get("staffInSelectedOfficeOnly");
@@ -64,10 +72,17 @@ public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
if (queryParameters.containsKey("chargeStatus")) {
chargeStatus = queryParameters.get("chargeStatus");
}
if (queryParameters.containsKey("associations")) {
associations = queryParameters.get("associations");
}
}
final String responseBody = savingsAccountsApiResource.retrieveOne(savingsAccountId,
Boolean.parseBoolean(staffInSelectedOfficeOnly), chargeStatus, uriInfo);
SavingsAccountData savingsAccountData = savingsAccountsApiResource.retrieveOne(savingsAccountId,
Boolean.parseBoolean(staffInSelectedOfficeOnly), chargeStatus, associations, uriInfo);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
final String responseBody = toApiJsonSerializer.serialize(settings, savingsAccountData,
SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS);
return new BatchResponse().setRequestId(batchRequest.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody)
.setHeaders(batchRequest.getHeaders());
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.core.jersey;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
@@ -28,6 +29,7 @@ import java.util.List;
import org.apache.fineract.infrastructure.core.jersey.converter.JsonConverter;
import org.apache.fineract.infrastructure.core.jersey.serializer.JacksonDeserializerAdapter;
import org.apache.fineract.infrastructure.core.jersey.serializer.JacksonSerializerAdapter;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JacksonLocalDateArrayModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@@ -43,9 +45,14 @@ public class JerseyJacksonConverterConfig {
mergedSerializers.addAll(jsonConverters.stream().map(JacksonSerializerAdapter::new).toList());
List<JsonDeserializer<?>> mergedDeserializers = new ArrayList<>(deserializers);
mergedDeserializers.addAll(jsonConverters.stream().map(JacksonDeserializerAdapter::new).toList());
return new Jackson2ObjectMapperBuilder().serializers(mergedSerializers.toArray(new JsonSerializer[0]))
.serializationInclusion(JsonInclude.Include.NON_NULL).deserializers(mergedDeserializers.toArray(new JsonDeserializer[0]))
.featuresToDisable(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS).build();
ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder() //
.serializers(mergedSerializers.toArray(new JsonSerializer[0])) //
.serializationInclusion(JsonInclude.Include.NON_NULL) //
.deserializers(mergedDeserializers.toArray(new JsonDeserializer[0])) //
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) //
.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) //
.build(); //
objectMapper.registerModule(new JacksonLocalDateArrayModule());
return objectMapper;
}
}
@@ -24,9 +24,8 @@ import com.fasterxml.jackson.core.JsonToken;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import org.springframework.stereotype.Component;
@Component
// This is registered in the JacksonLocalDateBeanSerializerModifier
public class LocalDateJsonConverter implements JsonConverter<LocalDate> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
@@ -0,0 +1,59 @@
/**
* 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.jersey.serializer;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.infrastructure.core.jersey.converter.LocalDateJsonConverter;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JacksonLocalDateArraySerializer;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JsonLocalDateArrayFormat;
@SuppressFBWarnings({ "SE_BAD_FIELD", "SE_BAD_FIELD_STORE" })
public class JacksonLocalDateBeanSerializerModifier extends BeanSerializerModifier {
private final JsonSerializer<?> localDateSerializer = new JacksonSerializerAdapter<>(new LocalDateJsonConverter());
private final JsonSerializer<?> localDateArraySerializer = new JacksonLocalDateArraySerializer();
@Override
@SuppressWarnings("unchecked")
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
if (beanDesc.getBeanClass().isAnnotationPresent(JsonLocalDateArrayFormat.class)) {
assignLocalDateSerializer(beanProperties, (JsonSerializer<Object>) localDateArraySerializer);
} else {
assignLocalDateSerializer(beanProperties, (JsonSerializer<Object>) localDateSerializer);
}
return beanProperties;
}
private void assignLocalDateSerializer(List<BeanPropertyWriter> beanProperties, JsonSerializer<Object> serializer) {
for (BeanPropertyWriter writer : beanProperties) {
if (LocalDate.class.equals(writer.getPropertyType())) {
writer.assignSerializer(serializer);
}
}
}
}
@@ -0,0 +1,29 @@
/**
* 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.jersey.serializer.legacy;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.apache.fineract.infrastructure.core.jersey.serializer.JacksonLocalDateBeanSerializerModifier;
public class JacksonLocalDateArrayModule extends SimpleModule {
public JacksonLocalDateArrayModule() {
setSerializerModifier(new JacksonLocalDateBeanSerializerModifier());
}
}
@@ -0,0 +1,41 @@
/**
* 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.jersey.serializer.legacy;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDate;
public class JacksonLocalDateArraySerializer extends JsonSerializer<LocalDate> {
@Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
if (value != null) {
gen.writeStartArray();
gen.writeNumber(value.getYear());
gen.writeNumber(value.getMonthValue());
gen.writeNumber(value.getDayOfMonth());
gen.writeEndArray();
} else {
gen.writeNull();
}
}
}
@@ -43,7 +43,6 @@ import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
@@ -187,14 +186,10 @@ public class SavingsAccountsApiResource {
@Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a savings application/account", description = "Retrieves a savings application/account\n\n"
+ "Example Requests :\n" + "\n" + "savingsaccounts/1\n" + "\n" + "\n" + "savingsaccounts/1?associations=all")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsAccountIdResponse.class))) })
public String retrieveOne(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus,
@Context final UriInfo uriInfo) {
public SavingsAccountData retrieveOne(@PathParam("accountId") final Long accountId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus,
@QueryParam("associations") final String associations, @Context final UriInfo uriInfo) {
return retrieveSavingAccount(accountId, null, staffInSelectedOfficeOnly, chargeStatus, uriInfo);
}
@@ -203,15 +198,10 @@ public class SavingsAccountsApiResource {
@Path("/external-id/{externalId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a savings application/account by external id", description = "Retrieves a savings application/account by external id\n\n"
+ "Example Requests :\n" + "\n" + "savingsaccounts/external-id/ExternalId1\n" + "\n" + "\n"
+ "savingsaccounts/external-id/ExternalId1?associations=all")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsAccountIdResponse.class))) })
public String retrieveOne(@PathParam("externalId") @Parameter(description = "externalId") final String externalId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") @Parameter(description = "staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus,
@Context final UriInfo uriInfo) {
public SavingsAccountData retrieveOne(@PathParam("externalId") final String externalId,
@DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus,
@QueryParam("associations") final String associations, @Context final UriInfo uriInfo) {
return retrieveSavingAccount(null, externalId, staffInSelectedOfficeOnly, chargeStatus, uriInfo);
}
@@ -484,8 +474,8 @@ public class SavingsAccountsApiResource {
return toApiJsonSerializer.serialize(importDocumentId);
}
private String retrieveSavingAccount(Long accountId, String externalId, boolean staffInSelectedOfficeOnly, String chargeStatus,
UriInfo uriInfo) {
private SavingsAccountData retrieveSavingAccount(Long accountId, String externalId, boolean staffInSelectedOfficeOnly,
String chargeStatus, UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) {
@@ -496,14 +486,7 @@ public class SavingsAccountsApiResource {
accountId = getResolvedAccountId(accountId, accountExternalId);
final SavingsAccountData savingsAccount = savingsAccountReadPlatformService.retrieveOne(accountId);
final Set<String> mandatoryResponseParameters = new HashSet<>();
final SavingsAccountData savingsAccountTemplate = populateTemplateAndAssociations(accountId, savingsAccount,
staffInSelectedOfficeOnly, chargeStatus, uriInfo, mandatoryResponseParameters);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
mandatoryResponseParameters);
return toApiJsonSerializer.serialize(settings, savingsAccountTemplate,
SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS);
return populateTemplateAndAssociations(accountId, savingsAccount, staffInSelectedOfficeOnly, chargeStatus, uriInfo);
}
private String updateSavingAccount(Long accountId, String externalId, String apiRequestBodyAsJson, String commandParam) {
@@ -629,8 +612,7 @@ public class SavingsAccountsApiResource {
}
private SavingsAccountData populateTemplateAndAssociations(final Long accountId, final SavingsAccountData savingsAccount,
final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo,
final Set<String> mandatoryResponseParameters) {
final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo) {
Collection<SavingsAccountTransactionData> transactions = null;
Collection<SavingsAccountChargeData> charges = null;
@@ -643,7 +625,6 @@ public class SavingsAccountsApiResource {
}
if (associationParameters.contains(SavingsApiConstants.transactions)) {
mandatoryResponseParameters.add(SavingsApiConstants.transactions);
final Collection<SavingsAccountTransactionData> currentTransactions = savingsAccountReadPlatformService
.retrieveAllTransactions(accountId, DepositAccountType.SAVINGS_DEPOSIT);
if (!CollectionUtils.isEmpty(currentTransactions)) {
@@ -652,7 +633,6 @@ public class SavingsAccountsApiResource {
}
if (associationParameters.contains(SavingsApiConstants.charges)) {
mandatoryResponseParameters.add(SavingsApiConstants.charges);
final Collection<SavingsAccountChargeData> currentCharges = savingsAccountChargeReadPlatformService
.retrieveSavingsAccountCharges(accountId, chargeStatus);
if (!CollectionUtils.isEmpty(currentCharges)) {
@@ -56,13 +56,13 @@ public class PostInterestForSavingTasklet implements Tasklet {
private final SavingsAccountReadPlatformService savingAccountReadPlatformService;
private final ConfigurationDomainService configurationDomainService;
private final Queue<List<SavingsAccountData>> queue = new ArrayDeque<>();
private final ApplicationContext applicationContext;
@Qualifier(TaskExecutorConstant.CONFIGURABLE_TASK_EXECUTOR_BEAN_NAME)
private final ThreadPoolTaskExecutor taskExecutor;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Queue<List<SavingsAccountData>> queue = new ArrayDeque<>();
final int threadPoolSize = Integer.parseInt((String) chunkContext.getStepContext().getJobParameters().get("thread-pool-size"));
taskExecutor.setCorePoolSize(threadPoolSize);
taskExecutor.setMaxPoolSize(threadPoolSize);
@@ -89,7 +89,7 @@ public class PostInterestForSavingTasklet implements Tasklet {
log.debug("Starting Interest posting - total records - {}", totalFilteredRecords);
List<SavingsAccountData> queueElement = queue.element();
maxSavingsIdInList = queueElement.get(queueElement.size() - 1).getId();
postInterest(queue.remove(), threadPoolSize, backdatedTxnsAllowedTill, pageSize, maxSavingsIdInList);
postInterest(queue.remove(), threadPoolSize, backdatedTxnsAllowedTill, pageSize, maxSavingsIdInList, queue);
} while (!CollectionUtils.isEmpty(queue));
}
}
@@ -97,7 +97,7 @@ public class PostInterestForSavingTasklet implements Tasklet {
}
private void postInterest(List<SavingsAccountData> savingsAccounts, int threadPoolSize, final boolean backdatedTxnsAllowedTill,
final int pageSize, Long maxSavingsIdInList) {
final int pageSize, Long maxSavingsIdInList, Queue<List<SavingsAccountData>> queue) {
List<Callable<Void>> posters = new ArrayList<>();
int fromIndex = 0;
int size = savingsAccounts.size();
@@ -40,11 +40,16 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;
import java.util.HashMap;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
import org.apache.fineract.portfolio.savings.api.SavingsAccountChargesApiResource;
import org.apache.fineract.portfolio.savings.api.SavingsAccountTransactionsApiResource;
import org.apache.fineract.portfolio.savings.api.SavingsAccountsApiResource;
import org.apache.fineract.portfolio.savings.api.SavingsApiSetConstants;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
import org.apache.fineract.portfolio.self.client.service.AppuserClientMapperReadService;
import org.apache.fineract.portfolio.self.savings.data.SelfSavingsAccountConstants;
@@ -66,6 +71,8 @@ public class SelfSavingsApiResource {
private final AppuserSavingsMapperReadService appuserSavingsMapperReadService;
private final SelfSavingsDataValidator dataValidator;
private final AppuserClientMapperReadService appUserClientMapperReadService;
private final DefaultToApiJsonSerializer<SavingsAccountData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
@GET
@Path("{accountId}")
@@ -77,6 +84,7 @@ public class SelfSavingsApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SelfSavingsApiResourceSwagger.GetSelfSavingsAccountsResponse.class))) })
public String retrieveSavings(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus,
@QueryParam("associations") @Parameter(description = "associations") final String associations,
@Context final UriInfo uriInfo) {
this.dataValidator.validateRetrieveSavings(uriInfo);
@@ -84,7 +92,10 @@ public class SelfSavingsApiResource {
validateAppuserSavingsAccountMapping(accountId);
final boolean staffInSelectedOfficeOnly = false;
return this.savingsAccountsApiResource.retrieveOne(accountId, staffInSelectedOfficeOnly, chargeStatus, uriInfo);
SavingsAccountData result = this.savingsAccountsApiResource.retrieveOne(accountId, staffInSelectedOfficeOnly, chargeStatus,
associations, uriInfo);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, result, SavingsApiSetConstants.SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET