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 java.time.LocalDate;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; 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. * Immutable data object represent the important time-line events of a savings account application.
*/ */
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
@JsonLocalDateArrayFormat
public class SavingsAccountApplicationTimelineData implements Serializable { public class SavingsAccountApplicationTimelineData implements Serializable {
private final LocalDate submittedOnDate; private final LocalDate submittedOnDate;
@@ -25,6 +25,7 @@ import java.time.MonthDay;
import java.util.Collection; import java.util.Collection;
import lombok.Getter; import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; 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.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.data.ChargeData;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; 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. * Immutable data object for Savings Account charge data.
*/ */
@Getter @Getter
@JsonLocalDateArrayFormat
public class SavingsAccountChargeData implements Serializable { public class SavingsAccountChargeData implements Serializable {
private final Long id; private final Long id;
@@ -32,6 +32,7 @@ import lombok.Getter;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; 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.infrastructure.dataqueries.data.DatatableData;
import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.staff.data.StaffData; 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. * Immutable data object representing a savings account.
*/ */
@Getter @Getter
@JsonLocalDateArrayFormat
public final class SavingsAccountData implements Serializable { public final class SavingsAccountData implements Serializable {
private final Long id; private final Long id;
@@ -302,23 +304,23 @@ public final class SavingsAccountData implements Serializable {
} }
public Integer getInterestPostingPeriodTypeId() { public Integer getInterestPostingPeriodTypeId() {
return this.interestPostingPeriodType.getId().intValue(); return this.interestPostingPeriodType != null ? this.interestPostingPeriodType.getId().intValue() : null;
} }
public Integer getDepositTypeId() { public Integer getDepositTypeId() {
return this.depositType.getId().intValue(); return this.depositType != null ? this.depositType.getId().intValue() : null;
} }
public Integer getInterestCompoundingPeriodTypeId() { public Integer getInterestCompoundingPeriodTypeId() {
return this.interestCompoundingPeriodType.getId().intValue(); return this.interestCompoundingPeriodType != null ? this.interestCompoundingPeriodType.getId().intValue() : null;
} }
public Integer getInterestCalculationTypeId() { public Integer getInterestCalculationTypeId() {
return this.interestCalculationType.getId().intValue(); return this.interestCalculationType != null ? this.interestCalculationType.getId().intValue() : null;
} }
public Integer getInterestCalculationDaysInYearTypeId() { public Integer getInterestCalculationDaysInYearTypeId() {
return this.interestCalculationDaysInYearType.getId().intValue(); return this.interestCalculationDaysInYearType != null ? this.interestCalculationDaysInYearType.getId().intValue() : null;
} }
public SavingsAccountTransactionData findLastTransaction(final LocalDate date) { public SavingsAccountTransactionData findLastTransaction(final LocalDate date) {
@@ -347,14 +349,14 @@ public final class SavingsAccountData implements Serializable {
public LocalDate getActivationLocalDate() { public LocalDate getActivationLocalDate() {
LocalDate activationLocalDate = null; LocalDate activationLocalDate = null;
if (this.timeline.getActivatedOnDate() != null) { if (this.timeline != null && this.timeline.getActivatedOnDate() != null) {
activationLocalDate = this.timeline.getActivatedOnDate(); activationLocalDate = this.timeline.getActivatedOnDate();
} }
return activationLocalDate; return activationLocalDate;
} }
public Integer getLockinPeriodFrequencyTypeId() { public Integer getLockinPeriodFrequencyTypeId() {
return this.lockinPeriodFrequencyType.getId().intValue(); return this.lockinPeriodFrequencyType != null ? this.lockinPeriodFrequencyType.getId().intValue() : null;
} }
public Collection<Long> findCurrentTransactionIdsWithPivotDateConfig() { public Collection<Long> findCurrentTransactionIdsWithPivotDateConfig() {
@@ -390,15 +392,15 @@ public final class SavingsAccountData implements Serializable {
} }
public List<SavingsAccountTransactionData> getSavingsAccountTransactionsWithPivotConfig() { public List<SavingsAccountTransactionData> getSavingsAccountTransactionsWithPivotConfig() {
return this.transactions.stream().toList(); return this.transactions != null ? this.transactions.stream().toList() : null;
} }
public Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() { public Boolean isAccrualBasedAccountingEnabledOnSavingsProduct() {
return this.savingsProductData.isAccrualBasedAccountingEnabled(); return this.savingsProductData != null && this.savingsProductData.isAccrualBasedAccountingEnabled();
} }
public Boolean isCashBasedAccountingEnabledOnSavingsProduct() { 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, 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() { 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) { public void setDatatables(final List<DatatableData> datatables) {
@@ -24,6 +24,7 @@ import java.time.LocalDate;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import lombok.Getter; 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.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; 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. * Immutable data object representing Savings Account summary information.
*/ */
@Getter @Getter
@JsonLocalDateArrayFormat
public class SavingsAccountSummaryData implements Serializable { public class SavingsAccountSummaryData implements Serializable {
private final CurrencyData currency; private final CurrencyData currency;
@@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.Getter; import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; 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.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; 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. * Immutable data object representing a savings account transaction.
*/ */
@Getter @Getter
@JsonLocalDateArrayFormat
public final class SavingsAccountTransactionData implements Serializable { public final class SavingsAccountTransactionData implements Serializable {
private Long id; 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.command.CommandStrategyUtils;
import org.apache.fineract.batch.domain.BatchRequest; import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse; 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.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.SavingsAccountsApiResource;
import org.apache.fineract.portfolio.savings.api.SavingsApiSetConstants;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -39,6 +44,8 @@ import org.springframework.stereotype.Component;
public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy { public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
private final SavingsAccountsApiResource savingsAccountsApiResource; private final SavingsAccountsApiResource savingsAccountsApiResource;
private final DefaultToApiJsonSerializer<SavingsAccountData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
@Override @Override
public BatchResponse execute(BatchRequest batchRequest, UriInfo uriInfo) { public BatchResponse execute(BatchRequest batchRequest, UriInfo uriInfo) {
@@ -57,6 +64,7 @@ public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
String staffInSelectedOfficeOnly = null; String staffInSelectedOfficeOnly = null;
String chargeStatus = null; String chargeStatus = null;
String associations = null;
if (!queryParameters.isEmpty()) { if (!queryParameters.isEmpty()) {
if (queryParameters.containsKey("staffInSelectedOfficeOnly")) { if (queryParameters.containsKey("staffInSelectedOfficeOnly")) {
staffInSelectedOfficeOnly = queryParameters.get("staffInSelectedOfficeOnly"); staffInSelectedOfficeOnly = queryParameters.get("staffInSelectedOfficeOnly");
@@ -64,10 +72,17 @@ public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
if (queryParameters.containsKey("chargeStatus")) { if (queryParameters.containsKey("chargeStatus")) {
chargeStatus = queryParameters.get("chargeStatus"); chargeStatus = queryParameters.get("chargeStatus");
} }
if (queryParameters.containsKey("associations")) {
associations = queryParameters.get("associations");
}
} }
final String responseBody = savingsAccountsApiResource.retrieveOne(savingsAccountId, SavingsAccountData savingsAccountData = savingsAccountsApiResource.retrieveOne(savingsAccountId,
Boolean.parseBoolean(staffInSelectedOfficeOnly), chargeStatus, uriInfo); 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) return new BatchResponse().setRequestId(batchRequest.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody)
.setHeaders(batchRequest.getHeaders()); .setHeaders(batchRequest.getHeaders());
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.core.jersey; package org.apache.fineract.infrastructure.core.jersey;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature; 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.converter.JsonConverter;
import org.apache.fineract.infrastructure.core.jersey.serializer.JacksonDeserializerAdapter; 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.JacksonSerializerAdapter;
import org.apache.fineract.infrastructure.core.jersey.serializer.legacy.JacksonLocalDateArrayModule;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@@ -43,9 +45,14 @@ public class JerseyJacksonConverterConfig {
mergedSerializers.addAll(jsonConverters.stream().map(JacksonSerializerAdapter::new).toList()); mergedSerializers.addAll(jsonConverters.stream().map(JacksonSerializerAdapter::new).toList());
List<JsonDeserializer<?>> mergedDeserializers = new ArrayList<>(deserializers); List<JsonDeserializer<?>> mergedDeserializers = new ArrayList<>(deserializers);
mergedDeserializers.addAll(jsonConverters.stream().map(JacksonDeserializerAdapter::new).toList()); mergedDeserializers.addAll(jsonConverters.stream().map(JacksonDeserializerAdapter::new).toList());
return new Jackson2ObjectMapperBuilder().serializers(mergedSerializers.toArray(new JsonSerializer[0])) ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder() //
.serializationInclusion(JsonInclude.Include.NON_NULL).deserializers(mergedDeserializers.toArray(new JsonDeserializer[0])) .serializers(mergedSerializers.toArray(new JsonSerializer[0])) //
.featuresToDisable(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .serializationInclusion(JsonInclude.Include.NON_NULL) //
.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS).build(); .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.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import org.springframework.stereotype.Component;
@Component // This is registered in the JacksonLocalDateBeanSerializerModifier
public class LocalDateJsonConverter implements JsonConverter<LocalDate> { public class LocalDateJsonConverter implements JsonConverter<LocalDate> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; 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.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -187,14 +186,10 @@ public class SavingsAccountsApiResource {
@Path("{accountId}") @Path("{accountId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a savings application/account", description = "Retrieves a savings application/account\n\n" public SavingsAccountData retrieveOne(@PathParam("accountId") final Long accountId,
+ "Example Requests :\n" + "\n" + "savingsaccounts/1\n" + "\n" + "\n" + "savingsaccounts/1?associations=all") @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
@ApiResponses({ @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus,
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountsApiResourceSwagger.GetSavingsAccountsAccountIdResponse.class))) }) @QueryParam("associations") final String associations, @Context final UriInfo uriInfo) {
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) {
return retrieveSavingAccount(accountId, null, staffInSelectedOfficeOnly, chargeStatus, uriInfo); return retrieveSavingAccount(accountId, null, staffInSelectedOfficeOnly, chargeStatus, uriInfo);
} }
@@ -203,15 +198,10 @@ public class SavingsAccountsApiResource {
@Path("/external-id/{externalId}") @Path("/external-id/{externalId}")
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@Produces({ 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" public SavingsAccountData retrieveOne(@PathParam("externalId") final String externalId,
+ "Example Requests :\n" + "\n" + "savingsaccounts/external-id/ExternalId1\n" + "\n" + "\n" @DefaultValue("false") @QueryParam("staffInSelectedOfficeOnly") final boolean staffInSelectedOfficeOnly,
+ "savingsaccounts/external-id/ExternalId1?associations=all") @DefaultValue("all") @QueryParam("chargeStatus") final String chargeStatus,
@ApiResponses({ @QueryParam("associations") final String associations, @Context final UriInfo uriInfo) {
@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) {
return retrieveSavingAccount(null, externalId, staffInSelectedOfficeOnly, chargeStatus, uriInfo); return retrieveSavingAccount(null, externalId, staffInSelectedOfficeOnly, chargeStatus, uriInfo);
} }
@@ -484,8 +474,8 @@ public class SavingsAccountsApiResource {
return toApiJsonSerializer.serialize(importDocumentId); return toApiJsonSerializer.serialize(importDocumentId);
} }
private String retrieveSavingAccount(Long accountId, String externalId, boolean staffInSelectedOfficeOnly, String chargeStatus, private SavingsAccountData retrieveSavingAccount(Long accountId, String externalId, boolean staffInSelectedOfficeOnly,
UriInfo uriInfo) { String chargeStatus, UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) {
@@ -496,14 +486,7 @@ public class SavingsAccountsApiResource {
accountId = getResolvedAccountId(accountId, accountExternalId); accountId = getResolvedAccountId(accountId, accountExternalId);
final SavingsAccountData savingsAccount = savingsAccountReadPlatformService.retrieveOne(accountId); final SavingsAccountData savingsAccount = savingsAccountReadPlatformService.retrieveOne(accountId);
final Set<String> mandatoryResponseParameters = new HashSet<>(); return populateTemplateAndAssociations(accountId, savingsAccount, staffInSelectedOfficeOnly, chargeStatus, uriInfo);
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);
} }
private String updateSavingAccount(Long accountId, String externalId, String apiRequestBodyAsJson, String commandParam) { 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, private SavingsAccountData populateTemplateAndAssociations(final Long accountId, final SavingsAccountData savingsAccount,
final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo, final boolean staffInSelectedOfficeOnly, final String chargeStatus, final UriInfo uriInfo) {
final Set<String> mandatoryResponseParameters) {
Collection<SavingsAccountTransactionData> transactions = null; Collection<SavingsAccountTransactionData> transactions = null;
Collection<SavingsAccountChargeData> charges = null; Collection<SavingsAccountChargeData> charges = null;
@@ -643,7 +625,6 @@ public class SavingsAccountsApiResource {
} }
if (associationParameters.contains(SavingsApiConstants.transactions)) { if (associationParameters.contains(SavingsApiConstants.transactions)) {
mandatoryResponseParameters.add(SavingsApiConstants.transactions);
final Collection<SavingsAccountTransactionData> currentTransactions = savingsAccountReadPlatformService final Collection<SavingsAccountTransactionData> currentTransactions = savingsAccountReadPlatformService
.retrieveAllTransactions(accountId, DepositAccountType.SAVINGS_DEPOSIT); .retrieveAllTransactions(accountId, DepositAccountType.SAVINGS_DEPOSIT);
if (!CollectionUtils.isEmpty(currentTransactions)) { if (!CollectionUtils.isEmpty(currentTransactions)) {
@@ -652,7 +633,6 @@ public class SavingsAccountsApiResource {
} }
if (associationParameters.contains(SavingsApiConstants.charges)) { if (associationParameters.contains(SavingsApiConstants.charges)) {
mandatoryResponseParameters.add(SavingsApiConstants.charges);
final Collection<SavingsAccountChargeData> currentCharges = savingsAccountChargeReadPlatformService final Collection<SavingsAccountChargeData> currentCharges = savingsAccountChargeReadPlatformService
.retrieveSavingsAccountCharges(accountId, chargeStatus); .retrieveSavingsAccountCharges(accountId, chargeStatus);
if (!CollectionUtils.isEmpty(currentCharges)) { if (!CollectionUtils.isEmpty(currentCharges)) {
@@ -56,13 +56,13 @@ public class PostInterestForSavingTasklet implements Tasklet {
private final SavingsAccountReadPlatformService savingAccountReadPlatformService; private final SavingsAccountReadPlatformService savingAccountReadPlatformService;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final Queue<List<SavingsAccountData>> queue = new ArrayDeque<>();
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
@Qualifier(TaskExecutorConstant.CONFIGURABLE_TASK_EXECUTOR_BEAN_NAME) @Qualifier(TaskExecutorConstant.CONFIGURABLE_TASK_EXECUTOR_BEAN_NAME)
private final ThreadPoolTaskExecutor taskExecutor; private final ThreadPoolTaskExecutor taskExecutor;
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { 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")); final int threadPoolSize = Integer.parseInt((String) chunkContext.getStepContext().getJobParameters().get("thread-pool-size"));
taskExecutor.setCorePoolSize(threadPoolSize); taskExecutor.setCorePoolSize(threadPoolSize);
taskExecutor.setMaxPoolSize(threadPoolSize); taskExecutor.setMaxPoolSize(threadPoolSize);
@@ -89,7 +89,7 @@ public class PostInterestForSavingTasklet implements Tasklet {
log.debug("Starting Interest posting - total records - {}", totalFilteredRecords); log.debug("Starting Interest posting - total records - {}", totalFilteredRecords);
List<SavingsAccountData> queueElement = queue.element(); List<SavingsAccountData> queueElement = queue.element();
maxSavingsIdInList = queueElement.get(queueElement.size() - 1).getId(); 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)); } while (!CollectionUtils.isEmpty(queue));
} }
} }
@@ -97,7 +97,7 @@ public class PostInterestForSavingTasklet implements Tasklet {
} }
private void postInterest(List<SavingsAccountData> savingsAccounts, int threadPoolSize, final boolean backdatedTxnsAllowedTill, 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<>(); List<Callable<Void>> posters = new ArrayList<>();
int fromIndex = 0; int fromIndex = 0;
int size = savingsAccounts.size(); int size = savingsAccounts.size();
@@ -40,11 +40,16 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.util.HashMap; import java.util.HashMap;
import lombok.RequiredArgsConstructor; 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.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException; import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
import org.apache.fineract.portfolio.savings.api.SavingsAccountChargesApiResource; import org.apache.fineract.portfolio.savings.api.SavingsAccountChargesApiResource;
import org.apache.fineract.portfolio.savings.api.SavingsAccountTransactionsApiResource; import org.apache.fineract.portfolio.savings.api.SavingsAccountTransactionsApiResource;
import org.apache.fineract.portfolio.savings.api.SavingsAccountsApiResource; 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.savings.exception.SavingsAccountNotFoundException;
import org.apache.fineract.portfolio.self.client.service.AppuserClientMapperReadService; import org.apache.fineract.portfolio.self.client.service.AppuserClientMapperReadService;
import org.apache.fineract.portfolio.self.savings.data.SelfSavingsAccountConstants; import org.apache.fineract.portfolio.self.savings.data.SelfSavingsAccountConstants;
@@ -66,6 +71,8 @@ public class SelfSavingsApiResource {
private final AppuserSavingsMapperReadService appuserSavingsMapperReadService; private final AppuserSavingsMapperReadService appuserSavingsMapperReadService;
private final SelfSavingsDataValidator dataValidator; private final SelfSavingsDataValidator dataValidator;
private final AppuserClientMapperReadService appUserClientMapperReadService; private final AppuserClientMapperReadService appUserClientMapperReadService;
private final DefaultToApiJsonSerializer<SavingsAccountData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
@GET @GET
@Path("{accountId}") @Path("{accountId}")
@@ -77,6 +84,7 @@ public class SelfSavingsApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SelfSavingsApiResourceSwagger.GetSelfSavingsAccountsResponse.class))) }) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SelfSavingsApiResourceSwagger.GetSelfSavingsAccountsResponse.class))) })
public String retrieveSavings(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId, public String retrieveSavings(@PathParam("accountId") @Parameter(description = "accountId") final Long accountId,
@DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus, @DefaultValue("all") @QueryParam("chargeStatus") @Parameter(description = "chargeStatus") final String chargeStatus,
@QueryParam("associations") @Parameter(description = "associations") final String associations,
@Context final UriInfo uriInfo) { @Context final UriInfo uriInfo) {
this.dataValidator.validateRetrieveSavings(uriInfo); this.dataValidator.validateRetrieveSavings(uriInfo);
@@ -84,7 +92,10 @@ public class SelfSavingsApiResource {
validateAppuserSavingsAccountMapping(accountId); validateAppuserSavingsAccountMapping(accountId);
final boolean staffInSelectedOfficeOnly = false; 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 @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 * 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. * get response parameters to match those of request parameters.
*/ */
protected static final Set<String> SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(idParamName, public static final Set<String> SAVINGS_ACCOUNT_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(idParamName, accountNoParamName,
accountNoParamName, externalIdParamName, statusParamName, activatedOnDateParamName, staffIdParamName, clientIdParamName, externalIdParamName, statusParamName, activatedOnDateParamName, staffIdParamName, clientIdParamName, "clientName",
"clientName", groupIdParamName, "groupName", "savingsProductId", "savingsProductName", "currency", groupIdParamName, "groupName", "savingsProductId", "savingsProductName", "currency", nominalAnnualInterestRateParamName,
nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestCalculationTypeParamName, interestCompoundingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName,
interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName,
lockinPeriodFrequencyTypeParamName, withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, withdrawalFeeForTransfersParamName, feeAmountParamName,
withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, "summary", "transactions", "productOptions", feeOnMonthDayParamName, "summary", "transactions", "productOptions", "interestCompoundingPeriodTypeOptions",
"interestCompoundingPeriodTypeOptions", "interestPostingPeriodTypeOptions", "interestCalculationTypeOptions", "interestPostingPeriodTypeOptions", "interestCalculationTypeOptions", "interestCalculationDaysInYearTypeOptions",
"interestCalculationDaysInYearTypeOptions", "lockinPeriodFrequencyTypeOptions", "withdrawalFeeTypeOptions", "withdrawalFee", "lockinPeriodFrequencyTypeOptions", "withdrawalFeeTypeOptions", "withdrawalFee", "annualFee", onHoldFundsParamName,
"annualFee", onHoldFundsParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, datatables, savingsAmountOnHold,
datatables, savingsAmountOnHold, accountMappingForPaymentParamName, interestPostedTillDate)); accountMappingForPaymentParamName, interestPostedTillDate));
protected static final Set<String> SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( protected static final Set<String> SAVINGS_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(idParamName, "accountId", accountNoParamName, "currency", "amount", dateParamName, paymentDetailDataParamName, 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.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@@ -61,7 +62,7 @@ public class SavingsSchedularInterestPoster {
private Collection<SavingsAccountData> savingAccounts; private Collection<SavingsAccountData> savingAccounts;
private boolean backdatedTxnsAllowedTill; private boolean backdatedTxnsAllowedTill;
@Transactional(rollbackFor = Exception.class) @Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
public void postInterest() throws JobExecutionException { public void postInterest() throws JobExecutionException {
if (!savingAccounts.isEmpty()) { if (!savingAccounts.isEmpty()) {
List<Throwable> errors = new ArrayList<>(); List<Throwable> errors = new ArrayList<>();
@@ -18,18 +18,19 @@
*/ */
package org.apache.fineract.integrationtests; package org.apache.fineract.integrationtests;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.UUID; import java.util.UUID;
import org.apache.fineract.client.models.DeleteSavingsAccountsAccountIdResponse; 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.PostSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse; import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse;
import org.apache.fineract.client.models.PostSavingsAccountsRequest; import org.apache.fineract.client.models.PostSavingsAccountsRequest;
import org.apache.fineract.client.models.PostSavingsAccountsResponse; import org.apache.fineract.client.models.PostSavingsAccountsResponse;
import org.apache.fineract.client.models.PutSavingsAccountsAccountIdRequest; import org.apache.fineract.client.models.PutSavingsAccountsAccountIdRequest;
import org.apache.fineract.client.models.PutSavingsAccountsAccountIdResponse; 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.client.util.Calls;
import org.apache.fineract.integrationtests.client.IntegrationTest; import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Order;
@@ -100,13 +101,12 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat); request.dateFormat(dateFormat);
request.setLocale(locale); request.setLocale(locale);
request.setActivatedOnDate(formattedDate); request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = okR( Response<SavingsAccountData> response = okR(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
assertThat(response.isSuccessful()).isTrue(); assertThat(response.isSuccessful()).isTrue();
assertThat(response.body()).isNotNull(); assertThat(response.body()).isNotNull();
assertThat(response.body().getStatus().getCode()).isEqualTo("savingsAccountStatusType.approved"); 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 @Test
@@ -129,8 +129,7 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat); request.dateFormat(dateFormat);
request.setLocale(locale); request.setLocale(locale);
request.setActivatedOnDate(formattedDate); request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = okR( Response<SavingsAccountData> response = okR(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all"));
assertThat(response.isSuccessful()).isTrue(); assertThat(response.isSuccessful()).isTrue();
assertThat(response.body()).isNotNull(); assertThat(response.body()).isNotNull();
@@ -159,8 +158,8 @@ public class SavingsAccountsExternalIdTest extends IntegrationTest {
request.dateFormat(dateFormat); request.dateFormat(dateFormat);
request.setLocale(locale); request.setLocale(locale);
request.setActivatedOnDate(formattedDate); request.setActivatedOnDate(formattedDate);
Response<GetSavingsAccountsAccountIdResponse> response = Calls Response<SavingsAccountData> response = Calls
.executeU(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, "all")); .executeU(fineractClient().savingsAccounts.retrieveOne26(EXTERNAL_ID, false, null, "all"));
assertThat(response.raw().code()).isEqualTo(404); 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;
}
}