diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 2ef93add68e..c2d34e0c840 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -365,15 +365,6 @@ public CommandWrapperBuilder updateCreditBureauConfiguration(final long configur return this; } - public CommandWrapperBuilder addClientAddress(final long clientId, final long addressTypeId) { - this.actionName = ACTION_CREATE; - this.entityName = ENTITY_ADDRESS; - this.entityId = addressTypeId; - this.href = "/clients/" + clientId + "/addresses"; - this.clientId = clientId; - return this; - } - public CommandWrapperBuilder updateClientAddress(final long clientId) { this.actionName = ACTION_UPDATE; this.entityName = ENTITY_ADDRESS; diff --git a/fineract-provider/dependencies.gradle b/fineract-provider/dependencies.gradle index 669f7b94e19..49c8f97bb14 100644 --- a/fineract-provider/dependencies.gradle +++ b/fineract-provider/dependencies.gradle @@ -45,6 +45,8 @@ dependencies { implementation(project(path: ':fineract-security')) implementation(project(path: ':fineract-working-capital-loan')) implementation(project(path: ':fineract-mix')) + implementation 'org.mapstruct:mapstruct' + annotationProcessor 'org.mapstruct:mapstruct-processor' providedRuntime("org.springframework.boot:spring-boot-starter-tomcat") diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 27a328d7b8c..b093ba79767 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -397,6 +397,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_HOOK") .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/hooks/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_HOOK") + // template .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/templates/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_TEMPLATE") @@ -413,6 +414,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/standinginstructionrunhistory")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_STANDINGINSTRUCTION") + // client address + .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/client/*/addresses")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_ADDRESS") + .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() .requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated() .requestMatchers(API_MATCHER.matcher("/api/**")).access(allOfRequestManagers(authorizationManagers)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/command/AddressCreateCommand.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/command/AddressCreateCommand.java new file mode 100644 index 00000000000..702fd2885f4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/command/AddressCreateCommand.java @@ -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.portfolio.address.command; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AddressCreateCommand extends Command {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/data/AddressCreateRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/data/AddressCreateRequest.java new file mode 100644 index 00000000000..c5741b9b52b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/data/AddressCreateRequest.java @@ -0,0 +1,84 @@ +/** + * 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.portfolio.address.data; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldNameConstants; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldNameConstants +public class AddressCreateRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JsonIgnore + @NotNull(message = "{org.apache.fineract.portfolio.address.create.assertion.client-id-required}") + @Positive(message = "{org.apache.fineract.portfolio.address.create.assertion.client-id-positive}") + private Long clientId; + + @JsonIgnore + @NotNull(message = "{org.apache.fineract.portfolio.address.create.assertion.address-type-id-required}") + @Positive(message = "{org.apache.fineract.portfolio.address.create.assertion.address-type-id-positive}") + private Long addressTypeId; + + @Size(max = 100) + private String city; + + private Long countryId; + + private Boolean isActive; + + @Size(max = 20) + private String postalCode; + + @Size(max = 100) + private String addressLine1; + + @Size(max = 100) + private String addressLine2; + + @Size(max = 100) + private String addressLine3; + + @Size(max = 100) + private String townVillage; + + @Size(max = 100) + private String countyDistrict; + + private Long stateProvinceId; + + private BigDecimal latitude; + + private BigDecimal longitude; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/data/AddressCreateResponse.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/data/AddressCreateResponse.java new file mode 100644 index 00000000000..1cd4a0bc5d5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/data/AddressCreateResponse.java @@ -0,0 +1,39 @@ +/** + * 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.portfolio.address.data; + +import java.io.Serial; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AddressCreateResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long resourceId; + private Long clientId; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/handler/AddressCreateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/handler/AddressCreateCommandHandler.java new file mode 100644 index 00000000000..4e0202918d1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/handler/AddressCreateCommandHandler.java @@ -0,0 +1,50 @@ +/** + * 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.portfolio.address.handler; + +import io.github.resilience4j.retry.annotation.Retry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.command.core.Command; +import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; +import org.apache.fineract.portfolio.address.data.AddressCreateResponse; +import org.apache.fineract.portfolio.address.service.AddressDomainService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AddressCreateCommandHandler implements CommandHandler { + + private final AddressDomainService domainService; + + @Retry(name = "commandAddressCreate", fallbackMethod = "fallback") + @Override + @Transactional + public AddressCreateResponse handle(Command command) { + return domainService.create(command.getPayload()); + } + + @Override + public AddressCreateResponse fallback(Command command, Throwable t) { + return CommandHandler.super.fallback(command, t); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/mapper/AddressMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/mapper/AddressMapper.java new file mode 100644 index 00000000000..eadcea6b467 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/mapper/AddressMapper.java @@ -0,0 +1,39 @@ +/** + * 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.portfolio.address.mapper; + +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; +import org.apache.fineract.portfolio.address.domain.Address; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface AddressMapper { + + @Mapping(target = "id", ignore = true) + @Mapping(target = "clientaddress", ignore = true) + @Mapping(target = "stateProvince", ignore = true) + @Mapping(target = "country", ignore = true) + @Mapping(target = "createdBy", ignore = true) + @Mapping(target = "createdOn", ignore = true) + @Mapping(target = "updatedBy", ignore = true) + @Mapping(target = "updatedOn", ignore = true) + @Mapping(target = "street", ignore = true) + Address toAddress(AddressCreateRequest request); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressDomainService.java new file mode 100644 index 00000000000..e6ac325be61 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressDomainService.java @@ -0,0 +1,27 @@ +/** + * 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.portfolio.address.service; + +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; +import org.apache.fineract.portfolio.address.data.AddressCreateResponse; + +public interface AddressDomainService { + + AddressCreateResponse create(AddressCreateRequest request); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressDomainServiceImpl.java new file mode 100644 index 00000000000..035dacf920f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressDomainServiceImpl.java @@ -0,0 +1,81 @@ +/** + * 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.portfolio.address.service; + +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; +import org.apache.fineract.portfolio.address.data.AddressCreateResponse; +import org.apache.fineract.portfolio.address.domain.Address; +import org.apache.fineract.portfolio.address.domain.AddressRepository; +import org.apache.fineract.portfolio.address.mapper.AddressMapper; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientAddress; +import org.apache.fineract.portfolio.client.domain.ClientAddressRepository; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnMissingBean(value = AddressDomainService.class, ignored = AddressDomainServiceImpl.class) +public class AddressDomainServiceImpl implements AddressDomainService { + + private final AddressRepository addressRepository; + private final ClientAddressRepository clientAddressRepository; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final CodeValueRepository codeValueRepository; + private final AddressMapper addressMapper; + + @Override + @Transactional + public AddressCreateResponse create(final AddressCreateRequest request) { + final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(request.getClientId()); + final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(request.getAddressTypeId()); + + CodeValue stateIdCodeValue = null; + if (request.getStateProvinceId() != null) { + stateIdCodeValue = codeValueRepository.getReferenceById(request.getStateProvinceId()); + } + + CodeValue countryIdCodeValue = null; + if (request.getCountryId() != null) { + countryIdCodeValue = codeValueRepository.getReferenceById(request.getCountryId()); + } + + final Address address = addressMapper.toAddress(request); + address.setStateProvince(stateIdCodeValue); + address.setCountry(countryIdCodeValue); + address.setCreatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); + address.setUpdatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); + addressRepository.save(address); + + final boolean isActive = Boolean.TRUE.equals(request.getIsActive()); + final ClientAddress clientAddress = ClientAddress.fromJson(isActive, client, address, addressTypeIdCodeValue); + clientAddressRepository.saveAndFlush(clientAddress); + + return AddressCreateResponse.builder().resourceId(clientAddress.getId()).clientId(client.getId()).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformService.java index 9c5f4d4a197..3fb06b18e74 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformService.java @@ -20,13 +20,8 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.client.domain.Client; public interface AddressWritePlatformService { - CommandProcessingResult addClientAddress(Long clientId, Long addressTypeId, JsonCommand command); - - CommandProcessingResult addNewClientAddress(Client client, JsonCommand command); - CommandProcessingResult updateClientAddress(Long clientId, JsonCommand command); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java index 420691f2024..b3695d936f0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java @@ -18,8 +18,6 @@ */ package org.apache.fineract.portfolio.address.service; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import java.math.BigDecimal; import java.time.LocalDate; import lombok.RequiredArgsConstructor; @@ -34,11 +32,8 @@ import org.apache.fineract.portfolio.address.domain.AddressRepository; import org.apache.fineract.portfolio.address.exception.AddressNotFoundException; import org.apache.fineract.portfolio.address.serialization.AddressCommandFromApiJsonDeserializer; -import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.ClientAddress; -import org.apache.fineract.portfolio.client.domain.ClientAddressRepository; import org.apache.fineract.portfolio.client.domain.ClientAddressRepositoryWrapper; -import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; import org.springframework.stereotype.Service; @Service @@ -47,109 +42,23 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ private final PlatformSecurityContext context; private final CodeValueRepository codeValueRepository; - private final ClientAddressRepository clientAddressRepository; - private final ClientRepositoryWrapper clientRepositoryWrapper; private final AddressRepository addressRepository; private final ClientAddressRepositoryWrapper clientAddressRepositoryWrapper; private final AddressCommandFromApiJsonDeserializer fromApiJsonDeserializer; - @Override - public CommandProcessingResult addClientAddress(final Long clientId, final Long addressTypeId, final JsonCommand command) { - JsonObject jsonObject = command.parsedJson().getAsJsonObject(); - context.authenticatedUser(); - fromApiJsonDeserializer.validateForCreate(jsonObject.toString(), false); - - final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(addressTypeId); - final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); - - final Address address = createAddress(jsonObject); - addressRepository.save(address); - - final ClientAddress clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address); - clientAddressRepository.saveAndFlush(clientAddress); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(clientAddress.getId()) // - .build(); - } - - @Override - public CommandProcessingResult addNewClientAddress(final Client client, final JsonCommand command) { - ClientAddress clientAddress = new ClientAddress(); - final JsonArray addressArray = command.arrayOfParameterNamed("address"); - - if (addressArray != null) { - for (int i = 0; i < addressArray.size(); i++) { - final JsonObject jsonObject = addressArray.get(i).getAsJsonObject(); - - fromApiJsonDeserializer.validateForCreate(jsonObject.toString(), true); - - final long addressTypeId = jsonObject.get("addressTypeId").getAsLong(); - final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(addressTypeId); - - final Address address = createAddress(jsonObject); - addressRepository.save(address); - - clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address); - clientAddressRepository.saveAndFlush(clientAddress); - - } - } - - // This is confusing because only the last client address id is returned - // TODO: clean this up - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(clientAddress.getId()) // - .build(); - } - - private ClientAddress createClientAddress(Client client, JsonObject jsonObject, CodeValue addressTypeIdCodeValue, Address address) { - boolean clientAddressIsActive = false; - if (jsonObject.get("isActive") != null) { - clientAddressIsActive = jsonObject.get("isActive").getAsBoolean(); - } - return ClientAddress.fromJson(clientAddressIsActive, client, address, addressTypeIdCodeValue); - } - - private Address createAddress(JsonObject jsonObject) { - CodeValue stateIdCodeValue = null; - if (jsonObject.get("stateProvinceId") != null) { - long stateId = jsonObject.get("stateProvinceId").getAsLong(); - stateIdCodeValue = codeValueRepository.getReferenceById(stateId); - } - - CodeValue countryIdCodeValue = null; - if (jsonObject.get("countryId") != null) { - long countryId = jsonObject.get("countryId").getAsLong(); - countryIdCodeValue = codeValueRepository.getReferenceById(countryId); - } - - final Address address = Address.fromJsonObject(jsonObject, stateIdCodeValue, countryIdCodeValue); - address.setCreatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); - address.setUpdatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); - return address; - } - @Override public CommandProcessingResult updateClientAddress(final Long clientId, final JsonCommand command) { this.context.authenticatedUser(); long stateId; - long countryId; - CodeValue stateIdobj; - CodeValue countryIdObj; - boolean is_address_update = false; this.fromApiJsonDeserializer.validateForUpdate(command.json()); final long addressId = command.longValueOfParameterNamed("addressId"); - final ClientAddress clientAddressObj = this.clientAddressRepositoryWrapper.findOneByClientIdAndAddressId(clientId, addressId); if (clientAddressObj == null) { @@ -159,30 +68,24 @@ public CommandProcessingResult updateClientAddress(final Long clientId, final Js final Address addobj = this.addressRepository.getReferenceById(addressId); if (!command.stringValueOfParameterNamed("addressLine1").isEmpty()) { - is_address_update = true; final String addressLine1 = command.stringValueOfParameterNamed("addressLine1"); addobj.setAddressLine1(addressLine1); - } if (!command.stringValueOfParameterNamed("addressLine2").isEmpty()) { - is_address_update = true; final String addressLine2 = command.stringValueOfParameterNamed("addressLine2"); addobj.setAddressLine2(addressLine2); - } if (!command.stringValueOfParameterNamed("addressLine3").isEmpty()) { is_address_update = true; final String addressLine3 = command.stringValueOfParameterNamed("addressLine3"); addobj.setAddressLine3(addressLine3); - } if (!command.stringValueOfParameterNamed("townVillage").isEmpty()) { - is_address_update = true; final String townVillage = command.stringValueOfParameterNamed("townVillage"); addobj.setTownVillage(townVillage); @@ -207,8 +110,8 @@ public CommandProcessingResult updateClientAddress(final Long clientId, final Js stateIdobj = this.codeValueRepository.getReferenceById(stateId); addobj.setStateProvince(stateIdobj); } - } + if (command.longValueOfParameterNamed("countryId") != null) { if (command.longValueOfParameterNamed("countryId") != 0) { is_address_update = true; @@ -216,7 +119,6 @@ public CommandProcessingResult updateClientAddress(final Long clientId, final Js countryIdObj = this.codeValueRepository.getReferenceById(countryId); addobj.setCountry(countryIdObj); } - } if (!command.stringValueOfParameterNamed("postalCode").isEmpty()) { @@ -226,23 +128,20 @@ public CommandProcessingResult updateClientAddress(final Long clientId, final Js } if (command.bigDecimalValueOfParameterNamed("latitude") != null) { - is_address_update = true; final BigDecimal latitude = command.bigDecimalValueOfParameterNamed("latitude"); - addobj.setLatitude(latitude); } + if (command.bigDecimalValueOfParameterNamed("longitude") != null) { is_address_update = true; final BigDecimal longitude = command.bigDecimalValueOfParameterNamed("longitude"); addobj.setLongitude(longitude); - } if (is_address_update) { addobj.setUpdatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); this.addressRepository.save(addobj); - } final Boolean testActive = command.booleanPrimitiveValueOfParameterNamed("isActive"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAddressApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAddressApiResource.java index 1288debbe62..e7b49b4c29b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAddressApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientAddressApiResource.java @@ -36,6 +36,7 @@ import jakarta.ws.rs.core.MediaType; import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.fineract.command.core.CommandDispatcher; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; @@ -43,6 +44,9 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.address.command.AddressCreateCommand; +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; +import org.apache.fineract.portfolio.address.data.AddressCreateResponse; import org.apache.fineract.portfolio.address.data.AddressData; import org.apache.fineract.portfolio.address.filter.ClientAddressSearchParam; import org.apache.fineract.portfolio.address.service.AddressReadPlatformServiceImpl; @@ -60,6 +64,7 @@ public class ClientAddressApiResource { private final AddressReadPlatformServiceImpl readPlatformService; private final DefaultToApiJsonSerializer toApiJsonSerializer; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final CommandDispatcher dispatcher; @GET @Path("addresses/template") @@ -79,15 +84,15 @@ public AddressData getAddressesTemplate() { @Operation(summary = "Create an address for a Client", operationId = "createClientAddress", description = "Mandatory Fields : \n" + "type and clientId") @AlternativeOperationId("addClientAddress") - @RequestBody(required = true, content = @Content(schema = @Schema(implementation = ClientAddressRequest.class))) - @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientAddressApiResourcesSwagger.PostClientClientIdAddressesResponse.class))) - public CommandProcessingResult addClientAddress(@QueryParam("type") @Parameter(description = "type") final long addressTypeId, - @PathParam("clientid") @Parameter(description = "clientId") final long clientid, - @Parameter(hidden = true) ClientAddressRequest clientAddressRequest) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().addClientAddress(clientid, addressTypeId) - .withJson(toApiJsonSerializer.serialize(clientAddressRequest)).build(); - - return commandsSourceWritePlatformService.logCommandSource(commandRequest); + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = AddressCreateRequest.class))) + public AddressCreateResponse addClientAddress(@QueryParam("type") @Parameter(description = "type") final Long addressTypeId, + @PathParam("clientid") @Parameter(description = "clientId") final Long clientId, + @Parameter(hidden = true) AddressCreateRequest request) { + request.setClientId(clientId); + request.setAddressTypeId(addressTypeId); + final var command = new AddressCreateCommand(); + command.setPayload(request); + return dispatcher.dispatch(command).get(); } @GET diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/handler/AddClientAddressCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/handler/AddClientAddressCommandHandler.java deleted file mode 100644 index e534a80dbf3..00000000000 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/handler/AddClientAddressCommandHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * 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.portfolio.client.handler; - -import lombok.RequiredArgsConstructor; -import org.apache.fineract.commands.annotation.CommandType; -import org.apache.fineract.commands.handler.NewCommandSourceHandler; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.portfolio.address.service.AddressWritePlatformService; -import org.springframework.stereotype.Service; - -@Service -@CommandType(entity = "ADDRESS", action = "CREATE") -@RequiredArgsConstructor -public class AddClientAddressCommandHandler implements NewCommandSourceHandler { - - private final AddressWritePlatformService writePlatformService; - - @Override - public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.addClientAddress(command.getClientId(), command.entityId(), command); - - } - -} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index 59ea3118e64..4e5de8934d9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -18,7 +18,9 @@ */ package org.apache.fineract.portfolio.client.service; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import jakarta.persistence.PersistenceException; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -61,7 +63,8 @@ import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; -import org.apache.fineract.portfolio.address.service.AddressWritePlatformService; +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; +import org.apache.fineract.portfolio.address.service.AddressDomainService; import org.apache.fineract.portfolio.client.api.ClientApiConstants; import org.apache.fineract.portfolio.client.data.ClientDataValidator; import org.apache.fineract.portfolio.client.domain.Client; @@ -119,7 +122,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP private final ConfigurationDomainService configurationDomainService; private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository; private final FromJsonHelper fromApiJsonHelper; - private final AddressWritePlatformService addressWritePlatformService; + private final AddressDomainService addressDomainService; private final ClientFamilyMembersWritePlatformService clientFamilyMembersWritePlatformService; private final BusinessEventNotifierService businessEventNotifierService; private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; @@ -317,7 +320,7 @@ public CommandProcessingResult createClient(final JsonCommand command) { } if (isAddressEnabled) { - this.addressWritePlatformService.addNewClientAddress(newClient, command); + createClientAddressesFromCommand(newClient, command); } if (command.arrayOfParameterNamed("familyMembers") != null) { @@ -1093,4 +1096,31 @@ public CommandProcessingResult undoWithdrawal(Long entityId, JsonCommand command .build(); } + private void createClientAddressesFromCommand(final Client client, final JsonCommand command) { + if (!command.parameterExists("address")) { + return; + } + final JsonArray addressArray = command.arrayOfParameterNamed("address"); + if (addressArray == null || addressArray.size() == 0) { + return; + } + for (int i = 0; i < addressArray.size(); i++) { + final JsonObject addressJson = addressArray.get(i).getAsJsonObject(); + final AddressCreateRequest request = AddressCreateRequest.builder().clientId(client.getId()) + .addressTypeId(this.fromApiJsonHelper.extractLongNamed("addressTypeId", addressJson)) + .addressLine1(this.fromApiJsonHelper.extractStringNamed("addressLine1", addressJson)) + .addressLine2(this.fromApiJsonHelper.extractStringNamed("addressLine2", addressJson)) + .addressLine3(this.fromApiJsonHelper.extractStringNamed("addressLine3", addressJson)) + .townVillage(this.fromApiJsonHelper.extractStringNamed("townVillage", addressJson)) + .city(this.fromApiJsonHelper.extractStringNamed("city", addressJson)) + .countyDistrict(this.fromApiJsonHelper.extractStringNamed("countyDistrict", addressJson)) + .stateProvinceId(this.fromApiJsonHelper.extractLongNamed("stateProvinceId", addressJson)) + .countryId(this.fromApiJsonHelper.extractLongNamed("countryId", addressJson)) + .postalCode(this.fromApiJsonHelper.extractStringNamed("postalCode", addressJson)) + .latitude(this.fromApiJsonHelper.extractBigDecimalNamed("latitude", addressJson, command.extractLocale())) + .longitude(this.fromApiJsonHelper.extractBigDecimalNamed("longitude", addressJson, command.extractLocale())) + .isActive(this.fromApiJsonHelper.extractBooleanNamed("isActive", addressJson)).build(); + this.addressDomainService.create(request); + } + } } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index d8eed37f191..783e6a30d32 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -864,6 +864,13 @@ resilience4j.retry.instances.commandStore.enable-exponential-backoff=${FINERACT_ resilience4j.retry.instances.commandStore.exponential-backoff-multiplier=${FINERACT_COMMAND_STORE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2} resilience4j.retry.instances.commandStore.retryExceptions=${FINERACT_COMMAND_STORE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException} +# client address create +resilience4j.retry.instances.commandAddressCreate.max-attempts=${FINERACT_COMMAND_ADDRESS_CREATE_RETRY_MAX_ATTEMPTS:3} +resilience4j.retry.instances.commandAddressCreate.wait-duration=${FINERACT_COMMAND_ADDRESS_CREATE_RETRY_WAIT_DURATION:1s} +resilience4j.retry.instances.commandAddressCreate.enable-exponential-backoff=${FINERACT_COMMAND_ADDRESS_CREATE_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true} +resilience4j.retry.instances.commandAddressCreate.exponential-backoff-multiplier=${FINERACT_COMMAND_ADDRESS_CREATE_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2} +resilience4j.retry.instances.commandAddressCreate.retryExceptions=${FINERACT_COMMAND_ADDRESS_CREATE_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException} + # command async (WIP) # fineract.command.async.enabled=${FINERACT_COMMAND_ASYNC_ENABLED:false} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/address/handler/AddressCreateCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/address/handler/AddressCreateCommandHandlerTest.java new file mode 100644 index 00000000000..438447bad9d --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/address/handler/AddressCreateCommandHandlerTest.java @@ -0,0 +1,61 @@ +/** + * 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.portfolio.address.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.fineract.portfolio.address.command.AddressCreateCommand; +import org.apache.fineract.portfolio.address.data.AddressCreateRequest; +import org.apache.fineract.portfolio.address.data.AddressCreateResponse; +import org.apache.fineract.portfolio.address.service.AddressDomainService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AddressCreateCommandHandlerTest { + + @Mock + private AddressDomainService domainService; + + @InjectMocks + private AddressCreateCommandHandler underTest; + + @Test + void handle_delegatesToServiceAndReturnsResponse() { + AddressCreateRequest request = AddressCreateRequest.builder().clientId(1L).addressTypeId(2L).city("Pune").isActive(true).build(); + AddressCreateResponse expected = AddressCreateResponse.builder().resourceId(42L).clientId(1L).build(); + + when(domainService.create(any(AddressCreateRequest.class))).thenReturn(expected); + + AddressCreateCommand command = new AddressCreateCommand(); + command.setPayload(request); + + AddressCreateResponse response = underTest.handle(command); + + verify(domainService).create(request); + assertThat(response.getResourceId()).isEqualTo(42L); + assertThat(response.getClientId()).isEqualTo(1L); + } +} diff --git a/fineract-validation/src/main/resources/ValidationMessages.properties b/fineract-validation/src/main/resources/ValidationMessages.properties index c5fd63504a6..dfafcb26c7f 100644 --- a/fineract-validation/src/main/resources/ValidationMessages.properties +++ b/fineract-validation/src/main/resources/ValidationMessages.properties @@ -135,3 +135,9 @@ org.apache.fineract.portfolio.meeting.date-format.not-null=The parameter 'dateFo org.apache.fineract.portfolio.meeting.locale.not-null=The parameter 'locale' is mandatory org.apache.fineract.portfolio.meeting.attendance.client-id.not-null=The parameter 'clientId' is mandatory org.apache.fineract.portfolio.meeting.attendance.attendance-type.not-null=The parameter 'attendanceType' is mandatory + +# address +org.apache.fineract.portfolio.address.create.assertion.client-id-required=Client ID is required. +org.apache.fineract.portfolio.address.create.assertion.client-id-positive=Client ID must be greater than zero. +org.apache.fineract.portfolio.address.create.assertion.address-type-id-required=Address type ID is required. +org.apache.fineract.portfolio.address.create.assertion.address-type-id-positive=Address type ID must be greater than zero. diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientTest.java index 4a966d81027..d69d65b6b89 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientTest.java @@ -35,11 +35,11 @@ import java.util.HashMap; import java.util.List; import java.util.UUID; +import org.apache.fineract.client.models.AddressCreateResponse; import org.apache.fineract.client.models.AddressData; import org.apache.fineract.client.models.ClientAddressRequest; import org.apache.fineract.client.models.GetClientsClientIdResponse; import org.apache.fineract.client.models.GlobalConfigurationPropertyData; -import org.apache.fineract.client.models.PostClientClientIdAddressesResponse; import org.apache.fineract.client.models.PostClientsRequest; import org.apache.fineract.client.models.PutGlobalConfigurationsRequest; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; @@ -254,8 +254,8 @@ public void testClientAddressCreationWorksAfterClientIsCreated() { // when ClientAddressRequest request = new ClientAddressRequest().postalCode(postalCode).city(city).countryId(Long.valueOf(countryId)) .stateProvinceId(Long.valueOf(stateId)).isActive(addressIsActive); - PostClientClientIdAddressesResponse response = ClientHelper.createClientAddress(requestSpec, responseSpec, clientId.longValue(), - addressTypeId, request); + AddressCreateResponse response = ClientHelper.createClientAddress(requestSpec, responseSpec, clientId.longValue(), addressTypeId, + request); // then assertThat(response.getResourceId()).isNotNull(); List clientAddresses = ClientHelper.getClientAddresses(requestSpec, responseSpec, clientId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java index 9ff646269f3..9632cd8ef94 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java @@ -42,6 +42,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.feign.util.FeignCalls; +import org.apache.fineract.client.models.AddressCreateResponse; import org.apache.fineract.client.models.AddressData; import org.apache.fineract.client.models.ClientAddressRequest; import org.apache.fineract.client.models.ClientTextSearch; @@ -55,7 +56,6 @@ import org.apache.fineract.client.models.LoanAccountLockResponseDTO; import org.apache.fineract.client.models.PageClientSearchData; import org.apache.fineract.client.models.PagedRequestClientTextSearch; -import org.apache.fineract.client.models.PostClientClientIdAddressesResponse; import org.apache.fineract.client.models.PostClientsClientIdIdentifiersRequest; import org.apache.fineract.client.models.PostClientsClientIdIdentifiersResponse; import org.apache.fineract.client.models.PostClientsClientIdRequest; @@ -265,14 +265,14 @@ public static PostClientsResponse createClient(final RequestSpecification reques // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest) @Deprecated(forRemoval = true) - public static PostClientClientIdAddressesResponse createClientAddress(final RequestSpecification requestSpec, + public static AddressCreateResponse createClientAddress(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, long clientId, long addressTypeId, ClientAddressRequest request) { final String CREATE_CLIENT_ADDRESS_URL = "/fineract-provider/api/v1/client/" + clientId + "/addresses?type=" + addressTypeId + "&" + Utils.TENANT_IDENTIFIER; log.info("---------------------------------CREATING A CLIENT ADDRESS ---------------------------------------------"); String requestBody = GSON.toJson(request); String response = Utils.performServerPost(requestSpec, responseSpec, CREATE_CLIENT_ADDRESS_URL, requestBody); - return GSON.fromJson(response, PostClientClientIdAddressesResponse.class); + return GSON.fromJson(response, AddressCreateResponse.class); } // TODO: Rewrite to use fineract-client instead!