This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 68c6053bd FINERACT-1724: Client search V2 API
68c6053bd is described below

commit 68c6053bd7c89d9c1f9bd084c828c0b7824b8a7c
Author: Arnold Galovics <[email protected]>
AuthorDate: Mon May 22 18:48:20 2023 +0200

    FINERACT-1724: Client search V2 API
---
 .../fineract/client/util/FineractClient.java       |   8 +-
 .../java/org/apache/fineract/client/util/JSON.java |   6 +-
 .../client/util/adapter/ExternalIdAdapter.java     |  50 ++++++
 .../core/jpa/CriteriaQueryFactory.java             |  57 ++++++
 .../infrastructure/core/service/PagedRequest.java  |  72 ++++++++
 .../core/{config => jersey}/JerseyConfig.java      |   2 +-
 .../core/jersey/JerseyJacksonConverterConfig.java  |  52 ++++++
 .../jersey/JerseyJacksonObjectArgumentHandler.java | 116 ++++++++++++
 .../core/jersey/converter/DateJsonConverter.java   |  56 ++++++
 .../jersey/converter/ExternalIdJsonConverter.java  |  52 ++++++
 .../core/jersey/converter/JsonConverter.java       |  32 ++++
 .../jersey/converter/LocalDateJsonConverter.java   |  55 ++++++
 .../converter/LocalDateTimeJsonConverter.java      |  56 ++++++
 .../jersey/converter/LocalTimeJsonConverter.java   |  55 ++++++
 .../converter/OffsetDateTimeJsonConverter.java     |  55 ++++++
 .../serializer/JacksonDeserializerAdapter.java     |  43 +++++
 .../serializer/JacksonSerializerAdapter.java       |  42 +++++
 .../client/api/v2/search/ClientSearchV2Api.java    |  29 +++
 .../api/v2/search/ClientSearchV2ApiDelegate.java   |  39 ++++
 .../api/v2/search/ClientSearchV2ApiResource.java   |  53 ++++++
 .../portfolio/client/domain/ClientRepository.java  |   3 +-
 .../client/domain/search/SearchedClient.java       |  41 +++++
 .../domain/search/SearchingClientRepository.java   |  27 +++
 .../search/SearchingClientRepositoryImpl.java      |  82 +++++++++
 .../client/service/search/ClientSearchService.java |  67 +++++++
 .../service/search/domain/ClientSearchData.java    |  40 +++++
 .../service/search/domain/ClientTextSearch.java    |  27 +++
 .../search/mapper/ClientSearchDataMapper.java      |  40 +++++
 .../core/config/ApiVerificationTest.java           |   1 +
 .../integrationtests/client/ClientSearchTest.java  | 196 +++++++++++++++++++++
 .../integrationtests/common/ClientHelper.java      |  25 +++
 31 files changed, 1473 insertions(+), 6 deletions(-)

diff --git 
a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java
 
b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java
index dc38f50be..51590b519 100644
--- 
a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java
+++ 
b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java
@@ -52,6 +52,7 @@ import org.apache.fineract.client.services.ChargesApi;
 import org.apache.fineract.client.services.ClientApi;
 import org.apache.fineract.client.services.ClientChargesApi;
 import org.apache.fineract.client.services.ClientIdentifierApi;
+import org.apache.fineract.client.services.ClientSearchV2Api;
 import org.apache.fineract.client.services.ClientTransactionApi;
 import org.apache.fineract.client.services.ClientsAddressApi;
 import org.apache.fineract.client.services.CodeValuesApi;
@@ -153,7 +154,7 @@ import retrofit2.Retrofit;
 import retrofit2.converter.scalars.ScalarsConverterFactory;
 
 /**
- * Fineract Client Java SDK API entry point. Use this instead of the {@link 
ApiClient}.
+ * Fineract Client Java SDK API entry point.
  *
  * @author Michael Vorburger.ch
  */
@@ -187,6 +188,8 @@ public final class FineractClient {
     public final CentersApi centers;
     public final ChargesApi charges;
     public final ClientApi clients;
+
+    public final ClientSearchV2Api clientSearchV2;
     public final ClientChargesApi clientCharges;
     public final ClientIdentifierApi clientIdentifiers;
     public final ClientsAddressApi clientAddresses;
@@ -305,6 +308,7 @@ public final class FineractClient {
         centers = retrofit.create(CentersApi.class);
         charges = retrofit.create(ChargesApi.class);
         clients = retrofit.create(ClientApi.class);
+        clientSearchV2 = retrofit.create(ClientSearchV2Api.class);
         clientCharges = retrofit.create(ClientChargesApi.class);
         clientIdentifiers = retrofit.create(ClientIdentifierApi.class);
         clientAddresses = retrofit.create(ClientsAddressApi.class);
@@ -537,7 +541,6 @@ public final class FineractClient {
          * Obtain the internal Retrofit Builder. This method is typically not 
required to be invoked for simple API
          * usages, but can be a handy back door for non-trivial advanced 
customizations of the API client.
          *
-         * @return the {@link ApiClient} which {@link #build()} will use.
          */
         public retrofit2.Retrofit.Builder getRetrofitBuilder() {
             return retrofitBuilder;
@@ -547,7 +550,6 @@ public final class FineractClient {
          * Obtain the internal OkHttp Builder. This method is typically not 
required to be invoked for simple API
          * usages, but can be a handy back door for non-trivial advanced 
customizations of the API client.
          *
-         * @return the {@link ApiClient} which {@link #build()} will use.
          */
         public okhttp3.OkHttpClient.Builder getOkBuilder() {
             return okBuilder;
diff --git 
a/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java 
b/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java
index c478a7bb5..cfbc15cbf 100644
--- a/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java
+++ b/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java
@@ -37,6 +37,8 @@ import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import okhttp3.RequestBody;
 import okhttp3.ResponseBody;
+import org.apache.fineract.client.models.ExternalId;
+import org.apache.fineract.client.util.adapter.ExternalIdAdapter;
 import retrofit2.Converter;
 import retrofit2.Retrofit;
 import retrofit2.converter.gson.GsonConverterFactory;
@@ -51,12 +53,14 @@ public class JSON {
     private final SqlDateTypeAdapter sqlDateTypeAdapter = new 
SqlDateTypeAdapter();
     private final OffsetDateTimeTypeAdapter offsetDateTimeTypeAdapter = new 
OffsetDateTimeTypeAdapter();
     private final LocalDateTypeAdapter localDateTypeAdapter = new 
LocalDateTypeAdapter();
+    private final ExternalIdAdapter externalIdAdapter = new 
ExternalIdAdapter();
 
     public JSON() {
         gson = new 
GsonFireBuilder().createGsonBuilder().registerTypeAdapter(Date.class, 
dateTypeAdapter)
                 .registerTypeAdapter(java.sql.Date.class, sqlDateTypeAdapter)
                 .registerTypeAdapter(OffsetDateTime.class, 
offsetDateTimeTypeAdapter)
-                .registerTypeAdapter(LocalDate.class, 
localDateTypeAdapter).create();
+                .registerTypeAdapter(LocalDate.class, 
localDateTypeAdapter).registerTypeAdapter(ExternalId.class, externalIdAdapter)
+                .create();
     }
 
     public Gson getGson() {
diff --git 
a/fineract-client/src/main/java/org/apache/fineract/client/util/adapter/ExternalIdAdapter.java
 
b/fineract-client/src/main/java/org/apache/fineract/client/util/adapter/ExternalIdAdapter.java
new file mode 100644
index 000000000..284c7f69c
--- /dev/null
+++ 
b/fineract-client/src/main/java/org/apache/fineract/client/util/adapter/ExternalIdAdapter.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.client.util.adapter;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import org.apache.fineract.client.models.ExternalId;
+
+public class ExternalIdAdapter extends TypeAdapter<ExternalId> {
+
+    @Override
+    public void write(JsonWriter out, ExternalId value) throws IOException {
+        if (value != null && Boolean.FALSE.equals(value.getEmpty())) {
+            out.value(value.getValue());
+        } else {
+            out.nullValue();
+        }
+    }
+
+    @Override
+    public ExternalId read(JsonReader in) throws IOException {
+        ExternalId result = new ExternalId().empty(true);
+        switch (in.peek()) {
+            case NULL:
+                in.nextNull();
+                return result;
+            default:
+                String value = in.nextString();
+                return new ExternalId().empty(false).value(value);
+        }
+    }
+}
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java
new file mode 100644
index 000000000..c617b7364
--- /dev/null
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java
@@ -0,0 +1,57 @@
+/**
+ * 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.jpa;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Order;
+import javax.persistence.criteria.Root;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CriteriaQueryFactory {
+
+    public List<Order> fromPageable(Pageable pageable, CriteriaBuilder cb, 
Root<?> root) {
+        return fromPageable(pageable, cb, root, () -> null);
+    }
+
+    public List<Order> fromPageable(Pageable pageable, CriteriaBuilder cb, 
Root<?> root, Supplier<Order> defaultOrderSupplier) {
+        List<Order> orders = new ArrayList<>();
+        Sort sort = pageable.getSort();
+        if (sort.isSorted()) {
+            for (Sort.Order order : sort) {
+                if (order.isAscending()) {
+                    orders.add(cb.asc(root.get(order.getProperty())));
+                } else {
+                    orders.add(cb.desc(root.get(order.getProperty())));
+                }
+            }
+        } else {
+            Order defaultOrder = defaultOrderSupplier.get();
+            if (defaultOrder != null) {
+                orders.add(defaultOrder);
+            }
+        }
+        return orders;
+    }
+}
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java
new file mode 100644
index 000000000..fd9f7b56f
--- /dev/null
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java
@@ -0,0 +1,72 @@
+/**
+ * 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.service;
+
+import static org.apache.commons.collections4.CollectionUtils.isEmpty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import lombok.Data;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+@Data
+public class PagedRequest<T> {
+
+    public static final int DEFAULT_PAGE_SIZE = 50;
+
+    private T request;
+
+    private int page;
+    private int size = DEFAULT_PAGE_SIZE;
+
+    private List<SortOrder> sorts = new ArrayList<>();
+
+    public Optional<T> getRequest() {
+        return Optional.ofNullable(request);
+    }
+
+    public Pageable toPageable() {
+        if (isEmpty(sorts)) {
+            return PageRequest.of(page, size);
+        } else {
+            List<Sort.Order> orders = 
sorts.stream().map(SortOrder::toOrder).toList();
+            return PageRequest.of(page, size, Sort.by(orders));
+        }
+    }
+
+    @Data
+    @SuppressWarnings({ "unused" })
+    private static class SortOrder {
+
+        private Direction direction;
+        private String property;
+
+        private enum Direction {
+            ASC, DESC;
+        }
+
+        private Sort.Order toOrder() {
+            Sort.Direction d = Sort.Direction.fromString(direction.name());
+            return new Sort.Order(d, property);
+        }
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/JerseyConfig.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java
similarity index 96%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/JerseyConfig.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java
index bf4504788..90641f22d 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/JerseyConfig.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.fineract.infrastructure.core.config;
+package org.apache.fineract.infrastructure.core.jersey;
 
 import javax.annotation.PostConstruct;
 import javax.ws.rs.ApplicationPath;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonConverterConfig.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonConverterConfig.java
new file mode 100644
index 000000000..35683df5a
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonConverterConfig.java
@@ -0,0 +1,52 @@
+/**
+ * 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;
+
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.fineract.infrastructure.core.jersey.converter.JsonConverter;
+import 
org.apache.fineract.infrastructure.core.jersey.serializer.JacksonDeserializerAdapter;
+import 
org.apache.fineract.infrastructure.core.jersey.serializer.JacksonSerializerAdapter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import 
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+
+@Configuration
+public class JerseyJacksonConverterConfig {
+
+    @Bean
+    public MappingJackson2HttpMessageConverter 
jacksonHttpConverter(List<JsonSerializer<?>> serializers,
+            List<JsonDeserializer<?>> deserializers, List<JsonConverter<?>> 
jsonConverters) {
+        List<JsonSerializer<?>> mergedSerializers = new 
ArrayList<>(serializers);
+        
mergedSerializers.addAll(jsonConverters.stream().map(JacksonSerializerAdapter::new).toList());
+
+        List<JsonDeserializer<?>> mergedDeserializers = new 
ArrayList<>(deserializers);
+        
mergedDeserializers.addAll(jsonConverters.stream().map(JacksonDeserializerAdapter::new).toList());
+
+        return new MappingJackson2HttpMessageConverter(new 
Jackson2ObjectMapperBuilder().indentOutput(true)
+                .serializers(mergedSerializers.toArray(new JsonSerializer[0]))
+                .deserializers(mergedDeserializers.toArray(new 
JsonDeserializer[0]))
+                
.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS).modulesToInstall(new
 ParameterNamesModule()).build());
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonObjectArgumentHandler.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonObjectArgumentHandler.java
new file mode 100644
index 000000000..6d57d23d1
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonObjectArgumentHandler.java
@@ -0,0 +1,116 @@
+/**
+ * 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;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.List;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.io.IOUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import 
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJacksonInputMessage;
+import org.springframework.stereotype.Component;
+
+@Provider
+@Produces(MediaType.APPLICATION_JSON_VALUE)
+@Consumes(MediaType.APPLICATION_JSON_VALUE)
+@Component
+@RequiredArgsConstructor
+public class JerseyJacksonObjectArgumentHandler<T> implements 
MessageBodyReader<T>, MessageBodyWriter<T> {
+
+    private final MappingJackson2HttpMessageConverter converter;
+
+    @Override
+    public boolean isReadable(Class<?> type, Type genericType, Annotation[] 
annotations, javax.ws.rs.core.MediaType mediaType) {
+        return true;
+    }
+
+    @Override
+    @SuppressWarnings({ "unchecked" })
+    public T readFrom(Class<T> type, Type genericType, Annotation[] 
annotations, javax.ws.rs.core.MediaType mediaType,
+            MultivaluedMap<String, String> httpHeaders, InputStream 
entityStream) throws IOException, WebApplicationException {
+        if (String.class == genericType) {
+            // If the request type is String, keep it that way.
+            StringWriter writer = new StringWriter();
+            IOUtils.copy(entityStream, writer, UTF_8);
+            String json = writer.toString();
+            return type.cast(json);
+        } else {
+            // Create the proper type from the JSON
+            HttpHeaders headers = new HttpHeaders();
+            headers.putAll(httpHeaders);
+            return (T) converter.read(genericType, type, new 
MappingJacksonInputMessage(entityStream, headers));
+        }
+    }
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] 
annotations, javax.ws.rs.core.MediaType mediaType) {
+        return true;
+    }
+
+    @Override
+    public void writeTo(T t, Class<?> type, Type genericType, Annotation[] 
annotations, javax.ws.rs.core.MediaType mediaType,
+            MultivaluedMap<String, Object> httpHeaders, OutputStream 
entityStream) throws IOException, WebApplicationException {
+        if (String.class == genericType) {
+            // If the response type is String, keep it that way.
+            IOUtils.write((String) t, entityStream, UTF_8);
+        } else {
+            // Create the proper JSON string from the object
+            HttpHeaders headers = new HttpHeaders();
+            httpHeaders.forEach((header, rawValues) -> {
+                List<String> values = 
rawValues.stream().map(Object::toString).toList();
+                headers.put(header, values);
+            });
+            converter.write(t, genericType, MediaType.APPLICATION_JSON, new 
SimpleHttpOutputMessage(entityStream, headers));
+        }
+    }
+
+    @RequiredArgsConstructor
+    private static class SimpleHttpOutputMessage implements HttpOutputMessage {
+
+        private final OutputStream outputStream;
+        private final HttpHeaders headers;
+
+        @Override
+        public OutputStream getBody() throws IOException {
+            return outputStream;
+        }
+
+        @Override
+        public HttpHeaders getHeaders() {
+            return headers;
+        }
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/DateJsonConverter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/DateJsonConverter.java
new file mode 100644
index 000000000..080905ea2
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/DateJsonConverter.java
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.jersey.converter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DateJsonConverter implements JsonConverter<Date> {
+
+    private static final DateTimeFormatter FORMATTER = 
DateTimeFormatter.ISO_INSTANT;
+
+    @Override
+    public Date convertToObject(JsonParser parser) throws IOException {
+        Date result = null;
+        if (parser.hasToken(JsonToken.VALUE_STRING)) {
+            String formattedDate = parser.getText();
+            result = Date.from(Instant.from(FORMATTER.parse(formattedDate)));
+        }
+        return result;
+    }
+
+    @Override
+    public void convertToJson(Date value, JsonGenerator generator) throws 
IOException {
+        if (value != null) {
+            generator.writeString(FORMATTER.format(value.toInstant()));
+        }
+    }
+
+    @Override
+    public Class<Date> convertedType() {
+        return Date.class;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/ExternalIdJsonConverter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/ExternalIdJsonConverter.java
new file mode 100644
index 000000000..b8e089d65
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/ExternalIdJsonConverter.java
@@ -0,0 +1,52 @@
+/**
+ * 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.converter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ExternalIdJsonConverter implements JsonConverter<ExternalId> {
+
+    @Override
+    public ExternalId convertToObject(JsonParser parser) throws IOException {
+        ExternalId result = ExternalId.empty();
+        if (parser.hasToken(JsonToken.VALUE_STRING)) {
+            String externalId = parser.getText();
+            result = new ExternalId(externalId);
+        }
+        return result;
+    }
+
+    @Override
+    public void convertToJson(ExternalId value, JsonGenerator generator) 
throws IOException {
+        if (value != null && !value.isEmpty()) {
+            generator.writeString(value.getValue());
+        }
+    }
+
+    @Override
+    public Class<ExternalId> convertedType() {
+        return ExternalId.class;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/JsonConverter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/JsonConverter.java
new file mode 100644
index 000000000..daad86f00
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/JsonConverter.java
@@ -0,0 +1,32 @@
+/**
+ * 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.converter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import java.io.IOException;
+
+public interface JsonConverter<T> {
+
+    T convertToObject(JsonParser parser) throws IOException;
+
+    void convertToJson(T value, JsonGenerator generator) throws IOException;
+
+    Class<T> convertedType();
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateJsonConverter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateJsonConverter.java
new file mode 100644
index 000000000..afb264d54
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateJsonConverter.java
@@ -0,0 +1,55 @@
+/**
+ * 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.converter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LocalDateJsonConverter implements JsonConverter<LocalDate> {
+
+    private static final DateTimeFormatter FORMATTER = 
DateTimeFormatter.ISO_LOCAL_DATE;
+
+    @Override
+    public LocalDate convertToObject(JsonParser parser) throws IOException {
+        LocalDate result = null;
+        if (parser.hasToken(JsonToken.VALUE_STRING)) {
+            String formattedDate = parser.getText();
+            result = LocalDate.parse(formattedDate, FORMATTER);
+        }
+        return result;
+    }
+
+    @Override
+    public void convertToJson(LocalDate value, JsonGenerator generator) throws 
IOException {
+        if (value != null) {
+            generator.writeString(FORMATTER.format(value));
+        }
+    }
+
+    @Override
+    public Class<LocalDate> convertedType() {
+        return LocalDate.class;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateTimeJsonConverter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateTimeJsonConverter.java
new file mode 100644
index 000000000..6c9f2a284
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateTimeJsonConverter.java
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.jersey.converter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LocalDateTimeJsonConverter implements 
JsonConverter<LocalDateTime> {
+
+    private static final DateTimeFormatter FORMATTER = 
DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+    @Override
+    public LocalDateTime convertToObject(JsonParser parser) throws IOException 
{
+        LocalDateTime result = null;
+        if (parser.hasToken(JsonToken.VALUE_STRING)) {
+            String formattedDate = parser.getText();
+            result = LocalDateTime.parse(formattedDate, FORMATTER);
+        }
+        return result;
+    }
+
+    @Override
+    public void convertToJson(LocalDateTime value, JsonGenerator generator) 
throws IOException {
+        if (value != null) {
+            generator.writeString(FORMATTER.format(value));
+        }
+
+    }
+
+    @Override
+    public Class<LocalDateTime> convertedType() {
+        return LocalDateTime.class;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalTimeJsonConverter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalTimeJsonConverter.java
new file mode 100644
index 000000000..20b574800
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalTimeJsonConverter.java
@@ -0,0 +1,55 @@
+/**
+ * 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.converter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LocalTimeJsonConverter implements JsonConverter<LocalTime> {
+
+    private static final DateTimeFormatter FORMATTER = 
DateTimeFormatter.ISO_LOCAL_TIME;
+
+    @Override
+    public LocalTime convertToObject(JsonParser parser) throws IOException {
+        LocalTime result = null;
+        if (parser.hasToken(JsonToken.VALUE_STRING)) {
+            String formattedDate = parser.getText();
+            result = LocalTime.parse(formattedDate, FORMATTER);
+        }
+        return result;
+    }
+
+    @Override
+    public void convertToJson(LocalTime value, JsonGenerator generator) throws 
IOException {
+        if (value != null) {
+            generator.writeString(FORMATTER.format(value));
+        }
+    }
+
+    @Override
+    public Class<LocalTime> convertedType() {
+        return LocalTime.class;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/OffsetDateTimeJsonConverter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/OffsetDateTimeJsonConverter.java
new file mode 100644
index 000000000..19b0a9059
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/OffsetDateTimeJsonConverter.java
@@ -0,0 +1,55 @@
+/**
+ * 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.converter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import org.springframework.stereotype.Component;
+
+@Component
+public class OffsetDateTimeJsonConverter implements 
JsonConverter<OffsetDateTime> {
+
+    private static final DateTimeFormatter FORMATTER = 
DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+
+    @Override
+    public OffsetDateTime convertToObject(JsonParser parser) throws 
IOException {
+        OffsetDateTime result = null;
+        if (parser.hasToken(JsonToken.VALUE_STRING)) {
+            String formattedDate = parser.getText();
+            result = OffsetDateTime.parse(formattedDate, FORMATTER);
+        }
+        return result;
+    }
+
+    @Override
+    public void convertToJson(OffsetDateTime value, JsonGenerator generator) 
throws IOException {
+        if (value != null) {
+            generator.writeString(FORMATTER.format(value));
+        }
+    }
+
+    @Override
+    public Class<OffsetDateTime> convertedType() {
+        return OffsetDateTime.class;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonDeserializerAdapter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonDeserializerAdapter.java
new file mode 100644
index 000000000..18ef095ae
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonDeserializerAdapter.java
@@ -0,0 +1,43 @@
+/**
+ * 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.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import java.io.IOException;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.jersey.converter.JsonConverter;
+
+@RequiredArgsConstructor
+public class JacksonDeserializerAdapter<T> extends JsonDeserializer<T> {
+
+    private final JsonConverter<T> converter;
+
+    @Override
+    public T deserialize(JsonParser p, DeserializationContext ctxt) throws 
IOException, JacksonException {
+        return converter.convertToObject(p);
+    }
+
+    @Override
+    public Class<?> handledType() {
+        return converter.convertedType();
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonSerializerAdapter.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonSerializerAdapter.java
new file mode 100644
index 000000000..951a0872e
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonSerializerAdapter.java
@@ -0,0 +1,42 @@
+/**
+ * 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.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import java.io.IOException;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.jersey.converter.JsonConverter;
+
+@RequiredArgsConstructor
+public class JacksonSerializerAdapter<T> extends JsonSerializer<T> {
+
+    private final JsonConverter<T> converter;
+
+    @Override
+    public void serialize(T value, JsonGenerator gen, SerializerProvider 
serializers) throws IOException {
+        converter.convertToJson(value, gen);
+    }
+
+    @Override
+    public Class<T> handledType() {
+        return converter.convertedType();
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2Api.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2Api.java
new file mode 100644
index 000000000..b45527c53
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2Api.java
@@ -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.portfolio.client.api.v2.search;
+
+import org.apache.fineract.infrastructure.core.service.PagedRequest;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch;
+import org.springframework.data.domain.Page;
+
+public interface ClientSearchV2Api {
+
+    Page<ClientSearchData> searchByText(PagedRequest<ClientTextSearch> 
request);
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiDelegate.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiDelegate.java
new file mode 100644
index 000000000..780c1eb35
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiDelegate.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.client.api.v2.search;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.service.PagedRequest;
+import org.apache.fineract.portfolio.client.service.search.ClientSearchService;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch;
+import org.springframework.data.domain.Page;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ClientSearchV2ApiDelegate implements ClientSearchV2Api {
+
+    private final ClientSearchService searchService;
+
+    @Override
+    public Page<ClientSearchData> searchByText(PagedRequest<ClientTextSearch> 
request) {
+        return searchService.searchByText(request);
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiResource.java
new file mode 100644
index 000000000..19ff06925
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiResource.java
@@ -0,0 +1,53 @@
+/**
+ * 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.api.v2.search;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.service.PagedRequest;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch;
+import org.springframework.data.domain.Page;
+import org.springframework.stereotype.Component;
+
+@Path("/v2/clients")
+@Component
+@Tag(name = "ClientSearchV2")
+@RequiredArgsConstructor
+public class ClientSearchV2ApiResource implements ClientSearchV2Api {
+
+    private final ClientSearchV2ApiDelegate delegate;
+
+    @Override
+    @POST
+    @Path("search")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Operation(summary = "Search Clients by text")
+    public Page<ClientSearchData> searchByText(@Parameter 
PagedRequest<ClientTextSearch> request) {
+        return delegate.searchByText(request);
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java
index 7d09ec67c..e6db66606 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java
@@ -19,12 +19,13 @@
 package org.apache.fineract.portfolio.client.domain;
 
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import 
org.apache.fineract.portfolio.client.domain.search.SearchingClientRepository;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
 
-interface ClientRepository extends JpaRepository<Client, Long>, 
JpaSpecificationExecutor<Client> {
+public interface ClientRepository extends JpaRepository<Client, Long>, 
JpaSpecificationExecutor<Client>, SearchingClientRepository {
 
     String FIND_CLIENT_BY_ACCOUNT_NUMBER = "select client from Client client 
where client.accountNumber = :accountNumber";
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java
new file mode 100644
index 000000000..93da80ce9
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java
@@ -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.portfolio.client.domain.search;
+
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+
+@Getter
+@RequiredArgsConstructor
+public class SearchedClient {
+
+    private final Long id;
+    private final String displayName;
+    private final ExternalId externalId;
+    private final String accountNo;
+    private final Long officeId;
+    private final String officeName;
+    private final String mobileNo;
+    private final Integer status;
+    private final LocalDate activationDate;
+    private final OffsetDateTime createdDate;
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepository.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepository.java
new file mode 100644
index 000000000..e1827da1b
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepository.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.client.domain.search;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface SearchingClientRepository {
+
+    Page<SearchedClient> searchByText(String searchText, Pageable pageable, 
String officeHierarchy);
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java
new file mode 100644
index 000000000..a6d6d3a42
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java
@@ -0,0 +1,82 @@
+/**
+ * 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.domain.search;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.persistence.EntityManager;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Order;
+import javax.persistence.criteria.Path;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.jpa.CriteriaQueryFactory;
+import org.apache.fineract.organisation.office.domain.Office;
+import org.apache.fineract.portfolio.client.domain.Client;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class SearchingClientRepositoryImpl implements 
SearchingClientRepository {
+
+    private final EntityManager entityManager;
+    private final CriteriaQueryFactory criteriaQueryFactory;
+
+    @Override
+    public Page<SearchedClient> searchByText(String searchText, Pageable 
pageable, String officeHierarchy) {
+        /*
+         * this whole thing can be replaced with Spring Data JPA 3+ with a 
findBy(Specification, Pageable) call but at
+         * this point the upgrade is too costly
+         *
+         * https://github.com/spring-projects/spring-data-jpa/issues/2499
+         */
+        String hierarchyLikeValue = officeHierarchy + "%";
+
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<SearchedClient> query = 
cb.createQuery(SearchedClient.class);
+        Root<Client> root = query.from(Client.class);
+        Path<Office> office = root.get("office");
+
+        query.select(cb.construct(SearchedClient.class, root.get("id"), 
root.get("displayName"), root.get("externalId"),
+                root.get("accountNumber"), office.get("id"), 
office.get("name"), root.get("mobileNo"), root.get("status"),
+                root.get("activationDate"), root.get("createdDate")));
+
+        List<Predicate> predicates = new ArrayList<>();
+        predicates.add(cb.like(office.get("hierarchy"), hierarchyLikeValue));
+
+        String searchLikeValue = "%" + searchText + "%";
+        predicates.add(cb.or(cb.like(root.get("accountNumber"), 
searchLikeValue), cb.like(root.get("displayName"), searchLikeValue),
+                cb.like(root.get("externalId"), searchLikeValue), 
cb.like(root.get("mobileNo"), searchLikeValue)));
+
+        query.where(cb.and(predicates.toArray(new Predicate[0])));
+
+        List<Order> orders = criteriaQueryFactory.fromPageable(pageable, cb, 
root, () -> cb.desc(root.get("id")));
+        query.orderBy(orders);
+
+        List<SearchedClient> result = 
entityManager.createQuery(query).setFirstResult(pageable.getPageNumber())
+                .setMaxResults(pageable.getPageSize()).getResultList();
+
+        return new PageImpl<>(result, pageable, result.size());
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/ClientSearchService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/ClientSearchService.java
new file mode 100644
index 000000000..751e78c65
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/ClientSearchService.java
@@ -0,0 +1,67 @@
+/**
+ * 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.service.search;
+
+import java.util.Objects;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.service.PagedRequest;
+import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.portfolio.client.domain.ClientRepository;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch;
+import 
org.apache.fineract.portfolio.client.service.search.mapper.ClientSearchDataMapper;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+public class ClientSearchService {
+
+    private final PlatformSecurityContext context;
+    private final ClientRepository clientRepository;
+    private final ClientSearchDataMapper clientSearchDataMapper;
+
+    public Page<ClientSearchData> searchByText(PagedRequest<ClientTextSearch> 
searchRequest) {
+        validateTextSearchRequest(searchRequest);
+        return executeTextSearch(searchRequest);
+    }
+
+    private void validateTextSearchRequest(PagedRequest<ClientTextSearch> 
searchRequest) {
+        Objects.requireNonNull(searchRequest, "searchRequest must not be 
null");
+
+        context.isAuthenticated();
+    }
+
+    private Page<ClientSearchData> 
executeTextSearch(PagedRequest<ClientTextSearch> searchRequest) {
+        final String hierarchy = 
context.authenticatedUser().getOffice().getHierarchy();
+
+        Optional<ClientTextSearch> request = searchRequest.getRequest();
+        String requestSearchText = 
request.map(ClientTextSearch::getText).orElse(null);
+        String searchText = StringUtils.defaultString(requestSearchText, "");
+
+        Pageable pageable = searchRequest.toPageable();
+
+        return clientRepository.searchByText(searchText, pageable, 
hierarchy).map(clientSearchDataMapper::map);
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java
new file mode 100644
index 000000000..dac1637bc
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java
@@ -0,0 +1,40 @@
+/**
+ * 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.service.search.domain;
+
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import lombok.Data;
+import org.apache.fineract.infrastructure.core.data.EnumOptionData;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+
+@Data
+public class ClientSearchData {
+
+    private Long id;
+    private String displayName;
+    private ExternalId externalId;
+    private String accountNo;
+    private Long officeId;
+    private String officeName;
+    private String mobileNo;
+    private EnumOptionData status;
+    private LocalDate activationDate;
+    private OffsetDateTime createdDate;
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientTextSearch.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientTextSearch.java
new file mode 100644
index 000000000..f6523aaff
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientTextSearch.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.client.service.search.domain;
+
+import lombok.Data;
+
+@Data
+public class ClientTextSearch {
+
+    private String text;
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/mapper/ClientSearchDataMapper.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/mapper/ClientSearchDataMapper.java
new file mode 100644
index 000000000..2085db861
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/mapper/ClientSearchDataMapper.java
@@ -0,0 +1,40 @@
+/**
+ * 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.service.search.mapper;
+
+import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
+import org.apache.fineract.infrastructure.core.data.EnumOptionData;
+import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
+import org.apache.fineract.portfolio.client.domain.search.SearchedClient;
+import 
org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+
+@Mapper(config = MapstructMapperConfig.class)
+public interface ClientSearchDataMapper {
+
+    @Mapping(target = "status", source = "source", qualifiedByName = 
"toStatus")
+    ClientSearchData map(SearchedClient source);
+
+    @Named("toStatus")
+    default EnumOptionData toStatus(SearchedClient client) {
+        return ClientEnumerations.status(client.getStatus());
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java
index d8ea651b2..b6d5e7c7b 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java
@@ -22,6 +22,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import javax.ws.rs.Path;
 import org.apache.fineract.AbstractSpringTest;
+import org.apache.fineract.infrastructure.core.jersey.JerseyConfig;
 import org.assertj.core.api.SoftAssertions;
 import org.glassfish.jersey.server.model.Resource;
 import org.junit.jupiter.api.Test;
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java
new file mode 100644
index 000000000..8a9c982c0
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java
@@ -0,0 +1,196 @@
+/**
+ * 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.client;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+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 org.apache.fineract.client.models.GetClientsClientIdResponse;
+import org.apache.fineract.client.models.PageClientSearchData;
+import org.apache.fineract.client.models.PostClientsRequest;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.client.models.SortOrder;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class ClientSearchTest {
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private ClientHelper clientHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        requestSpec = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        requestSpec.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        clientHelper = new ClientHelper(requestSpec, responseSpec);
+    }
+
+    @Test
+    public void testClientSearchWorks_WithLastnameTextOnDefaultOrdering() {
+        // given
+        String lastname = Utils.randomStringGenerator("Client_LastName_", 5);
+        PostClientsRequest request1 = 
ClientHelper.defaultClientCreationRequest();
+        request1.setLastname(lastname);
+        clientHelper.createClient(request1);
+
+        PostClientsRequest request2 = 
ClientHelper.defaultClientCreationRequest();
+        request2.setLastname(lastname);
+        clientHelper.createClient(request2);
+
+        PostClientsRequest request3 = 
ClientHelper.defaultClientCreationRequest();
+        request3.setLastname(lastname);
+        clientHelper.createClient(request3);
+        // when
+        PageClientSearchData result = clientHelper.searchClients(lastname);
+        // then
+        assertThat(result.getTotalElements()).isEqualTo(3);
+        
assertThat(result.getContent().get(0).getExternalId().getValue()).isEqualTo(request3.getExternalId());
+        
assertThat(result.getContent().get(1).getExternalId().getValue()).isEqualTo(request2.getExternalId());
+        
assertThat(result.getContent().get(2).getExternalId().getValue()).isEqualTo(request1.getExternalId());
+    }
+
+    @Test
+    public void testClientSearchWorks_WithLastnameText_OrderedByIdAsc() {
+        // given
+        String lastname = Utils.randomStringGenerator("Client_LastName_", 5);
+        PostClientsRequest request1 = 
ClientHelper.defaultClientCreationRequest();
+        request1.setLastname(lastname);
+        clientHelper.createClient(request1);
+
+        PostClientsRequest request2 = 
ClientHelper.defaultClientCreationRequest();
+        request2.setLastname(lastname);
+        clientHelper.createClient(request2);
+
+        PostClientsRequest request3 = 
ClientHelper.defaultClientCreationRequest();
+        request3.setLastname(lastname);
+        clientHelper.createClient(request3);
+
+        SortOrder sortOrder = new 
SortOrder().property("id").direction(SortOrder.DirectionEnum.ASC);
+        // when
+        PageClientSearchData result = clientHelper.searchClients(lastname, 
sortOrder);
+        // then
+        assertThat(result.getTotalElements()).isEqualTo(3);
+        
assertThat(result.getContent().get(0).getExternalId().getValue()).isEqualTo(request1.getExternalId());
+        
assertThat(result.getContent().get(1).getExternalId().getValue()).isEqualTo(request2.getExternalId());
+        
assertThat(result.getContent().get(2).getExternalId().getValue()).isEqualTo(request3.getExternalId());
+    }
+
+    @Test
+    public void testClientSearchWorks_ByExternalId() {
+        // given
+        PostClientsRequest request1 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request1);
+
+        PostClientsRequest request2 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request2);
+
+        PostClientsRequest request3 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request3);
+        // when
+        PageClientSearchData result = 
clientHelper.searchClients(request2.getExternalId());
+        // then
+        assertThat(result.getTotalElements()).isEqualTo(1);
+        
assertThat(result.getContent().get(0).getExternalId().getValue()).isEqualTo(request2.getExternalId());
+    }
+
+    @Test
+    public void testClientSearchWorks_ByAccountNumber() {
+        // given
+        PostClientsRequest request1 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request1);
+
+        PostClientsRequest request2 = 
ClientHelper.defaultClientCreationRequest();
+        PostClientsResponse response2 = clientHelper.createClient(request2);
+        GetClientsClientIdResponse client2Data = 
ClientHelper.getClient(requestSpec, responseSpec,
+                Math.toIntExact(response2.getClientId()));
+
+        PostClientsRequest request3 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request3);
+        // when
+        PageClientSearchData result = 
clientHelper.searchClients(client2Data.getAccountNo());
+        // then
+        assertThat(result.getTotalElements()).isEqualTo(1);
+        
assertThat(result.getContent().get(0).getAccountNo()).isEqualTo(client2Data.getAccountNo());
+    }
+
+    @Test
+    public void testClientSearchWorks_ByDisplayName() {
+        // given
+        PostClientsRequest request1 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request1);
+
+        PostClientsRequest request2 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request2);
+        String client2DisplayName = "%s %s".formatted(request2.getFirstname(), 
request2.getLastname());
+
+        PostClientsRequest request3 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request3);
+        // when
+        PageClientSearchData result = 
clientHelper.searchClients(client2DisplayName);
+        // then
+        assertThat(result.getTotalElements()).isEqualTo(1);
+        
assertThat(result.getContent().get(0).getDisplayName()).isEqualTo(client2DisplayName);
+    }
+
+    @Test
+    public void testClientSearchWorks_ByMobileNo() {
+        // given
+        PostClientsRequest request1 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request1);
+
+        PostClientsRequest request2 = 
ClientHelper.defaultClientCreationRequest();
+        request2.setMobileNo(Utils.randomNumberGenerator(8).toString());
+        clientHelper.createClient(request2);
+
+        PostClientsRequest request3 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request3);
+        // when
+        PageClientSearchData result = 
clientHelper.searchClients(request2.getMobileNo());
+        // then
+        assertThat(result.getTotalElements()).isEqualTo(1);
+        
assertThat(result.getContent().get(0).getMobileNo()).isEqualTo(request2.getMobileNo());
+    }
+
+    @Test
+    public void testClientSearchDoesntReturnAnything_ByMobileNo() {
+        // given
+        PostClientsRequest request1 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request1);
+
+        PostClientsRequest request2 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request2);
+
+        PostClientsRequest request3 = 
ClientHelper.defaultClientCreationRequest();
+        clientHelper.createClient(request3);
+        // when
+        PageClientSearchData result = 
clientHelper.searchClients(Utils.randomNumberGenerator(8).toString());
+        // then
+        assertThat(result.getTotalElements()).isEqualTo(0);
+        assertThat(result.getContent()).isEmpty();
+    }
+}
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 2659bc33d..3ed19480c 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
@@ -38,18 +38,22 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.client.models.ClientTextSearch;
 import org.apache.fineract.client.models.DeleteClientsClientIdResponse;
 import org.apache.fineract.client.models.GetClientClientIdAddressesResponse;
 import org.apache.fineract.client.models.GetClientTransferProposalDateResponse;
 import org.apache.fineract.client.models.GetClientsClientIdAccountsResponse;
 import org.apache.fineract.client.models.GetClientsClientIdResponse;
 import org.apache.fineract.client.models.GetObligeeData;
+import org.apache.fineract.client.models.PageClientSearchData;
+import org.apache.fineract.client.models.PagedRequestClientTextSearch;
 import org.apache.fineract.client.models.PostClientClientIdAddressesRequest;
 import org.apache.fineract.client.models.PostClientClientIdAddressesResponse;
 import org.apache.fineract.client.models.PostClientsClientIdResponse;
 import org.apache.fineract.client.models.PostClientsRequest;
 import org.apache.fineract.client.models.PostClientsResponse;
 import org.apache.fineract.client.models.PutClientsClientIdResponse;
+import org.apache.fineract.client.models.SortOrder;
 import org.apache.fineract.client.util.JSON;
 import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
 import org.apache.fineract.integrationtests.client.IntegrationTest;
@@ -97,6 +101,27 @@ public class ClientHelper extends IntegrationTest {
         return ok(fineract().clients.create6(request));
     }
 
+    public PageClientSearchData searchClients(String text) {
+        ClientTextSearch clientTextSearch = new ClientTextSearch();
+        clientTextSearch.setText(text);
+        PagedRequestClientTextSearch request = new 
PagedRequestClientTextSearch();
+        request.setRequest(clientTextSearch);
+        return searchClients(request);
+    }
+
+    public PageClientSearchData searchClients(String text, SortOrder 
sortOrder) {
+        ClientTextSearch clientTextSearch = new ClientTextSearch();
+        clientTextSearch.setText(text);
+        PagedRequestClientTextSearch request = new 
PagedRequestClientTextSearch();
+        request.setRequest(clientTextSearch);
+        request.setSorts(List.of(sortOrder));
+        return searchClients(request);
+    }
+
+    public PageClientSearchData searchClients(PagedRequestClientTextSearch 
request) {
+        return ok(fineract().clientSearchV2.searchByText(request));
+    }
+
     public static PostClientsResponse addClientAsPerson(final 
RequestSpecification requestSpec, final ResponseSpecification responseSpec,
             final String jsonPayload) {
         final String response = Utils.performServerPost(requestSpec, 
responseSpec, CREATE_CLIENT_URL, jsonPayload);

Reply via email to