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
@@ -0,0 +1,28 @@
/**
* 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 java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonLocalDateArrayFormat {}
@@ -22,12 +22,14 @@ import java.io.Serializable;
import java.time.LocalDate;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JsonLocalDateArrayFormat;
/**
* Immutable data object represent the important time-line events of a savings account application.
*/
@Getter
@RequiredArgsConstructor
@JsonLocalDateArrayFormat
public class SavingsAccountApplicationTimelineData implements Serializable {
private final LocalDate submittedOnDate;
@@ -25,6 +25,7 @@ import java.time.MonthDay;
import java.util.Collection;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JsonLocalDateArrayFormat;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.charge.data.ChargeData;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
@@ -33,6 +34,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
* Immutable data object for Savings Account charge data.
*/
@Getter
@JsonLocalDateArrayFormat
public class SavingsAccountChargeData implements Serializable {
private final Long id;
@@ -32,6 +32,7 @@ import lombok.Getter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JsonLocalDateArrayFormat;
import org.apache.fineract.infrastructure.dataqueries.data.DatatableData;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.staff.data.StaffData;
@@ -47,6 +48,7 @@ import org.apache.fineract.portfolio.tax.data.TaxGroupData;
* Immutable data object representing a savings account.
*/
@Getter
@JsonLocalDateArrayFormat
public final class SavingsAccountData implements Serializable {
private final Long id;
@@ -302,23 +304,23 @@ public final class SavingsAccountData implements Serializable {
}
public Integer getInterestPostingPeriodTypeId() {
return this.interestPostingPeriodType.getId().intValue();
return this.interestPostingPeriodType != null ? this.interestPostingPeriodType.getId().intValue() : null;
}
public Integer getDepositTypeId() {
return this.depositType.getId().intValue();
return this.depositType != null ? this.depositType.getId().intValue() : null;
}
public Integer getInterestCompoundingPeriodTypeId() {
return this.interestCompoundingPeriodType.getId().intValue();
return this.interestCompoundingPeriodType != null ? this.interestCompoundingPeriodType.getId().intValue() : null;
}
public Integer getInterestCalculationTypeId() {
return this.interestCalculationType.getId().intValue();
return this.interestCalculationType != null ? this.interestCalculationType.getId().intValue() : null;
}
public Integer getInterestCalculationDaysInYearTypeId() {
return this.interestCalculationDaysInYearType.getId().intValue();
return this.interestCalculationDaysInYearType != null ? this.interestCalculationDaysInYearType.getId().intValue() : null;
}
public SavingsAccountTransactionData findLastTransaction(final LocalDate date) {
@@ -347,14 +349,14 @@ public final class SavingsAccountData implements Serializable {
public LocalDate getActivationLocalDate() {
LocalDate activationLocalDate = null;
if (this.timeline.getActivatedOnDate() != null) {
if (this.timeline != null && this.timeline.getActivatedOnDate() != null) {
activationLocalDate = this.timeline.getActivatedOnDate();
}
return activationLocalDate;
}
public Integer getLockinPeriodFrequencyTypeId() {
return this.lockinPeriodFrequencyType.getId().intValue();
return this.lockinPeriodFrequencyType != null ? this.lockinPeriodFrequencyType.getId().intValue() : null;
}
public Collection<Long> findCurrentTransactionIdsWithPivotDateConfig() {
@@ -390,15 +392,15 @@ public final class SavingsAccountData implements Serializable {
}
public List<SavingsAccountTransactionData> getSavingsAccountTransactionsWithPivotConfig() {
return this.transactions.stream().toList();
return this.transactions != null ? this.transactions.stream().toList() : null;
}
public Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() {
return this.savingsProductData.isAccrualBasedAccountingEnabled();
return this.savingsProductData != null && this.savingsProductData.isAccrualBasedAccountingEnabled();
}
public Boolean isCashBasedAccountingEnabledOnSavingsProduct() {
return this.savingsProductData.isCashBasedAccountingEnabled();
return this.savingsProductData != null && this.savingsProductData.isCashBasedAccountingEnabled();
}
public static SavingsAccountData importInstanceGroup(Long groupId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
@@ -948,7 +950,7 @@ public final class SavingsAccountData implements Serializable {
}
public Collection<SavingsAccountChargeData> charges() {
return (this.charges == null) ? new HashSet<SavingsAccountChargeData>() : this.charges;
return this.charges == null ? new HashSet<>() : this.charges;
}
public void setDatatables(final List<DatatableData> datatables) {
@@ -24,6 +24,7 @@ import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JsonLocalDateArrayFormat;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -36,6 +37,7 @@ import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod;
* Immutable data object representing Savings Account summary information.
*/
@Getter
@JsonLocalDateArrayFormat
public class SavingsAccountSummaryData implements Serializable {
private final CurrencyData currency;
@@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Set;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JsonLocalDateArrayFormat;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -51,6 +52,7 @@ import org.springframework.util.CollectionUtils;
* Immutable data object representing a savings account transaction.
*/
@Getter
@JsonLocalDateArrayFormat
public final class SavingsAccountTransactionData implements Serializable {
private Long id;
@@ -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
@@ -47,17 +47,17 @@ public class SavingsApiSetConstants extends SavingsApiConstants {
* These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to
* get response parameters to match those of request parameters.
*/
protected static final Set<String> SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(idParamName,
accountNoParamName, externalIdParamName, statusParamName, activatedOnDateParamName, staffIdParamName, clientIdParamName,
"clientName", groupIdParamName, "groupName", "savingsProductId", "savingsProductName", "currency",
nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestCalculationTypeParamName,
interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName,
lockinPeriodFrequencyTypeParamName, withdrawalFeeAmountParamName, withdrawalFeeTypeParamName,
withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, "summary", "transactions", "productOptions",
"interestCompoundingPeriodTypeOptions", "interestPostingPeriodTypeOptions", "interestCalculationTypeOptions",
"interestCalculationDaysInYearTypeOptions", "lockinPeriodFrequencyTypeOptions", "withdrawalFeeTypeOptions", "withdrawalFee",
"annualFee", onHoldFundsParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName,
datatables, savingsAmountOnHold, accountMappingForPaymentParamName, interestPostedTillDate));
public static final Set<String> SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(idParamName, accountNoParamName,
externalIdParamName, statusParamName, activatedOnDateParamName, staffIdParamName, clientIdParamName, "clientName",
groupIdParamName, "groupName", "savingsProductId", "savingsProductName", "currency", nominalAnnualInterestRateParamName,
interestCompoundingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName,
minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName,
withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, withdrawalFeeForTransfersParamName, feeAmountParamName,
feeOnMonthDayParamName, "summary", "transactions", "productOptions", "interestCompoundingPeriodTypeOptions",
"interestPostingPeriodTypeOptions", "interestCalculationTypeOptions", "interestCalculationDaysInYearTypeOptions",
"lockinPeriodFrequencyTypeOptions", "withdrawalFeeTypeOptions", "withdrawalFee", "annualFee", onHoldFundsParamName,
nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, datatables, savingsAmountOnHold,
accountMappingForPaymentParamName, interestPostedTillDate));
protected static final Set<String> SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(idParamName, "accountId", accountNoParamName, "currency", "amount", dateParamName, paymentDetailDataParamName,
@@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@@ -61,7 +62,7 @@ public class SavingsSchedularInterestPoster {
private Collection<SavingsAccountData> savingAccounts;
private boolean backdatedTxnsAllowedTill;
@Transactional(rollbackFor = Exception.class)
@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
public void postInterest() throws JobExecutionException {
if (!savingAccounts.isEmpty()) {
List<Throwable> errors = new ArrayList<>();
@@ -18,18 +18,19 @@
*/
package org.apache.fineract.integrationtests;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import org.apache.fineract.client.models.DeleteSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.GetSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsRequest;
import org.apache.fineract.client.models.PostSavingsAccountsResponse;
import org.apache.fineract.client.models.PutSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PutSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.util.Calls;
import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.junit.jupiter.api.Order;
@@ -100,13 +101,12 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat);
request.setLocale(locale);
request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = okR(
fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
Response<SavingsAccountData> response = okR(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
assertThat(response.isSuccessful()).isTrue();
assertThat(response.body()).isNotNull();
assertThat(response.body().getStatus().getCode()).isEqualTo("savingsAccountStatusType.approved");
assertThat(response.body().getNominalAnnualInterestRate()).isEqualTo(5.999);
assertThat(response.body().getNominalAnnualInterestRate()).isEqualByComparingTo(BigDecimal.valueOf(5.999));
}
@Test
@@ -129,8 +129,7 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat);
request.setLocale(locale);
request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = okR(
fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
Response<SavingsAccountData> response = okR(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
assertThat(response.isSuccessful()).isTrue();
assertThat(response.body()).isNotNull();
@@ -159,8 +158,8 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat);
request.setLocale(locale);
request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = Calls
.executeU(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
Response<SavingsAccountData> response = Calls
.executeU(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
assertThat(response.raw().code()).isEqualTo(404);
}
@@ -0,0 +1,108 @@
/**
* 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.integrationtests.savings;
import java.math.BigDecimal;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import org.apache.fineract.client.models.PostSavingsProductsRequest;
import org.apache.fineract.client.models.PostSavingsProductsResponse;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.models.SavingsAccountTransactionData;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.savings.base.BaseSavingsIntegrationTest;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@Order(2)
public class SavingsInterestPostingTest extends BaseSavingsIntegrationTest {
@Test
public void testSavingsInterestPosting_Works_ForMultipleAccounts() {
List<AccountWithDepositAmount> accounts = new ArrayList<>();
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create product
PostSavingsProductsRequest productsRequest = dailyInterestPostingProduct();
PostSavingsProductsResponse product = createProduct(productsRequest);
Integer productId = product.getResourceId();
// Create accounts
IntStream.range(0, 200)//
.parallel().mapToObj(i -> applySavingsRequest(clientId, productId.longValue(), "01 January 2023"))//
.map(this::applySavingsAccount) //
.mapToLong(savingsAccount -> savingsAccount.getResourceId().longValue()) //
.forEach(savingsId -> {
approveSavingsAccount(savingsId, "01 January 2023");
activateSavingsAccount(savingsId, "01 January 2023");
accounts.add(new AccountWithDepositAmount(savingsId));
});
});
runFromToInclusive("02 January 2023", "10 January 2023", (date) -> {
SecureRandom rnd = new SecureRandom();
accounts.parallelStream().forEach(account -> {
long lAmount = rnd.nextLong(10_000_000L);
BigDecimal amount = BigDecimal.valueOf(lAmount);
deposit(account.savingsId, date, amount);
account.addDeposit(amount);
});
});
runAt("11 January 2023", () -> {
String jobName = "Post Interest For Savings";
schedulerJobHelper.executeAndAwaitJob(jobName);
accounts.parallelStream().forEach(account -> {
SavingsAccountData accountData = getSavingsAccount(account.savingsId);
BigDecimal accountBalance = accountData.getSummary().getAccountBalance();
BigDecimal accountBalanceByTransactions = accountData.getTransactions().stream()
.map(SavingsAccountTransactionData::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
assertThat(accountBalance)
.withFailMessage("Account balance (%s) and calculated balance (%s) from transactions do not match", accountBalance,
accountBalanceByTransactions)
.isEqualByComparingTo(accountBalanceByTransactions);
BigDecimal summedDepositAmount = accountData.getTransactions().stream().filter(tx -> tx.getTransactionType().getDeposit())
.map(SavingsAccountTransactionData::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
assertThat(summedDepositAmount).withFailMessage(
"Calculated balance (%s) from deposit transactions and submitted deposit balance (%s) do not match", accountBalance,
summedDepositAmount).isEqualByComparingTo(account.depositedAmount);
});
});
}
static class AccountWithDepositAmount {
public Long savingsId;
public BigDecimal depositedAmount = BigDecimal.ZERO;
AccountWithDepositAmount(Long savingsId) {
this.savingsId = savingsId;
}
public void addDeposit(BigDecimal amount) {
depositedAmount = depositedAmount.add(amount);
}
}
}
@@ -0,0 +1,249 @@
/**
* 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.integrationtests.savings.base;
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import lombok.AllArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.BusinessDateRequest;
import org.apache.fineract.client.models.PostSavingsAccountTransactionsRequest;
import org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsRequest;
import org.apache.fineract.client.models.PostSavingsAccountsResponse;
import org.apache.fineract.client.models.PostSavingsProductsRequest;
import org.apache.fineract.client.models.PostSavingsProductsResponse;
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
import org.apache.fineract.client.models.SavingsAccountData;
import org.apache.fineract.client.models.SavingsAccountTransactionData;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
import org.apache.fineract.integrationtests.common.system.CodeHelper;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
@Slf4j
public class BaseSavingsIntegrationTest extends IntegrationTest {
protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
static {
Utils.initializeRESTAssured();
}
protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200));
private final String fullAdminAuthKey = getFullAdminAuthKey();
protected final RequestSpecification requestSpec = createRequestSpecification(fullAdminAuthKey);
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
protected GlobalConfigurationHelper globalConfigurationHelper = new GlobalConfigurationHelper();
protected final CodeHelper codeHelper = new CodeHelper();
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
protected SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(requestSpec);
protected void runFromToInclusive(String fromDate, String toDate, Consumer<String> runnable) {
DateTimeFormatter format = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
LocalDate startDate = LocalDate.parse(fromDate, format);
LocalDate endDate = LocalDate.parse(toDate, format);
LocalDate currentDate = startDate;
while (currentDate.isBefore(endDate) || currentDate.isEqual(endDate)) {
runAt(format.format(currentDate), runnable);
currentDate = currentDate.plusDays(1);
}
}
protected void runAt(String date, Runnable runnable) {
runAt(date, (d) -> runnable.run());
}
protected void runAt(String date, Consumer<String> runnable) {
try {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(true));
businessDateHelper.updateBusinessDate(
new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
runnable.accept(date);
} finally {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
}
}
protected PostSavingsProductsRequest dailyInterestPostingProduct() {
return new PostSavingsProductsRequest().locale("en").name(Utils.uniqueRandomStringGenerator("DAILY_INTEREST", 6))//
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
.description("Daily interest posting product") //
.nominalAnnualInterestRate(10.0) //
.digitsAfterDecimal(0) //
.currencyCode("EUR") //
.accountingRule(1) // none
.interestCalculationDaysInYearType(DaysInYearType.DAYS_365).interestCompoundingPeriodType(InterestPeriodType.DAILY)
.interestCalculationType(InterestCalculationType.AVERAGE_DAILY_BALANCE) //
.interestPostingPeriodType(InterestPeriodType.DAILY);//
}
protected PostSavingsProductsResponse createProduct(PostSavingsProductsRequest productsRequest) {
return ok(fineractClient().savingsProducts.create13(productsRequest));
}
protected PostSavingsAccountsRequest applySavingsRequest(Long clientId, Long productId, String submittedDate) {
return new PostSavingsAccountsRequest().clientId(clientId.intValue()).productId(productId.intValue()).dateFormat(DATETIME_PATTERN)
.locale("en").submittedOnDate(submittedDate);
}
protected PostSavingsAccountsResponse applySavingsAccount(PostSavingsAccountsRequest request) {
return ok(fineractClient().savingsAccounts.submitApplication2(request));
}
protected PostSavingsAccountsAccountIdResponse approveSavingsAccount(Long savingsId, String date) {
PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest().dateFormat(DATETIME_PATTERN).locale("en")
.approvedOnDate(date);
return ok(fineractClient().savingsAccounts.handleCommands6(savingsId, request, "approve"));
}
protected PostSavingsAccountsAccountIdResponse activateSavingsAccount(Long savingsId, String date) {
PostSavingsAccountsAccountIdRequest request = new PostSavingsAccountsAccountIdRequest().dateFormat(DATETIME_PATTERN).locale("en")
.activatedOnDate(date);
return ok(fineractClient().savingsAccounts.handleCommands6(savingsId, request, "activate"));
}
protected PostSavingsAccountTransactionsResponse deposit(Long savingsId, String date, BigDecimal amount) {
PostSavingsAccountTransactionsRequest request = new PostSavingsAccountTransactionsRequest() //
.dateFormat(DATETIME_PATTERN) //
.locale("en") //
.paymentTypeId(1).transactionAmount(amount) //
.transactionDate(date); //
return ok(fineractClient().savingsTransactions.transaction2(savingsId, request, "deposit"));
}
protected SavingsAccountData getSavingsAccount(Long savingsId) {
return ok(fineractClient().savingsAccounts.retrieveOne25(savingsId, false, null, "transactions"));
}
protected List<SavingsAccountTransactionData> getTransactions(Long savingsId) {
return ok(fineractClient().savingsAccounts.retrieveOne25(savingsId, false, null, "transactions")).getTransactions();
}
protected void verifyNoTransactions(Long savingsId) {
verifyTransactions(savingsId, (Transaction[]) null);
}
protected void verifyTransactions(Long savingsId, Transaction... transactions) {
SavingsAccountData savingsDetails = ok(fineractClient().savingsAccounts.retrieveOne25(savingsId, false, null, "transactions"));
if (transactions == null || transactions.length == 0) {
Assertions.assertTrue(savingsDetails.getTransactions().isEmpty(), "No transaction is expected on savings account " + savingsId);
} else {
Assertions.assertEquals(transactions.length, savingsDetails.getTransactions().size());
Arrays.stream(transactions).forEach(tr -> {
Optional<SavingsAccountTransactionData> optTx = savingsDetails.getTransactions().stream()
.filter(item -> Objects.requireNonNull(item.getAmount()).compareTo(BigDecimal.valueOf(tr.amount)) == 0 //
&& Objects.equals(item.getTransactionType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)))
.findFirst();
Assertions.assertTrue(optTx.isPresent(), "Required transaction not found: " + tr + " on savings account " + savingsId);
SavingsAccountTransactionData tx = optTx.get();
if (tr.reversed != null) {
Assertions.assertEquals(tr.reversed, tx.getReversed(),
"Transaction is not reversed: " + tr + " on savings account " + savingsId);
}
});
}
}
private RequestSpecification createRequestSpecification(String authKey) {
RequestSpecification requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
requestSpec.header("Authorization", "Basic " + authKey);
requestSpec.header("Fineract-Platform-TenantId", "default");
return requestSpec;
}
protected ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) {
return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build();
}
private String getFullAdminAuthKey() {
return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey();
}
@ToString
@AllArgsConstructor
public static class Transaction {
Double amount;
String type;
String date;
Boolean reversed;
}
public static class InterestPeriodType {
public static final int DAILY = 1;
public static final int MONTHLY = 4;
public static final int QUATERLY = 5;
public static final int BIANNUAL = 6;
}
public static class InterestCalculationType {
public static final int DAILY_BALANCE = 1;
public static final int AVERAGE_DAILY_BALANCE = 2;
}
public static class InterestRecalculationCompoundingMethod {
public static final Integer NONE = 0;
}
public static class DaysInYearType {
public static final Integer INVALID = 0;
public static final Integer ACTUAL = 1;
public static final Integer DAYS_360 = 360;
public static final Integer DAYS_364 = 364;
public static final Integer DAYS_365 = 365;
}
}