You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2025/10/17 12:25:45 UTC

(fineract) branch develop updated: FINERACT-2380: create feign client

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

adamsaghy 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 553347b320 FINERACT-2380: create feign client
553347b320 is described below

commit 553347b32057325faee69efba6ebc3bc114cadf4
Author: Attila Budai <bu...@gmail.com>
AuthorDate: Fri Oct 3 08:12:14 2025 +0200

    FINERACT-2380: create feign client
---
 build.gradle                                       |    5 +
 fineract-client-feign/.openapi-generator-ignore    |   32 +
 fineract-client-feign/build.gradle                 |  194 ++++
 fineract-client-feign/dependencies.gradle          |   49 +
 .../fineract/client/adapter/ExternalIdAdapter.java |   84 ++
 .../client/feign/BasicAuthRequestInterceptor.java  |   56 ++
 .../fineract/client/feign/FeignException.java      |  115 +++
 .../client/feign/FineractErrorDecoder.java         |  107 +++
 .../fineract/client/feign/FineractFeignClient.java | 1002 ++++++++++++++++++++
 .../client/feign/FineractFeignClientConfig.java    |  293 ++++++
 .../client/feign/FineractMultipartEncoder.java     |  198 ++++
 .../fineract/client/feign/ObjectMapperFactory.java |   73 ++
 .../client/feign/TenantIdRequestInterceptor.java   |   42 +
 .../client/feign/services/DocumentsApiFixed.java   |  140 +++
 .../fineract/client/feign/services/ImagesApi.java  |   97 ++
 .../client/feign/services/RunReportsApi.java       |   66 ++
 .../feign/util/CallFailedRuntimeException.java     |   91 ++
 .../fineract/client/feign/util/FeignCalls.java     |   80 ++
 .../apache/fineract/client/util/FeignParts.java    |  136 +++
 .../src/main/resources/templates/java/api.mustache |   47 +
 .../client/adapter/ExternalIdAdapterTest.java      |  192 ++++
 .../fineract/client/feign/FeignExceptionTest.java  |  140 +++
 .../client/feign/FineractErrorDecoderTest.java     |  180 ++++
 .../feign/FineractFeignClientConfigTest.java       |   93 ++
 .../client/feign/ObjectMapperFactoryTest.java      |  120 +++
 .../integration/EncoderDecoderIntegrationTest.java |  421 ++++++++
 .../FineractFeignClientIntegrationTest.java        |  252 +++++
 .../performance/ConnectionPoolPerformanceTest.java |  269 ++++++
 .../services/DocumentsApiFixedIntegrationTest.java |  208 ++++
 .../client/services/ImagesApiIntegrationTest.java  |  180 ++++
 .../services/RunReportsApiIntegrationTest.java     |  166 ++++
 .../fineract/client/util/FeignPartsTest.java       |  296 ++++++
 .../domain/BasicPasswordEncodablePlatformUser.java |   12 +-
 integration-tests/build.gradle                     |   62 +-
 integration-tests/dependencies.gradle              |   13 +-
 .../client/FeignClientSmokeTest.java               |   65 ++
 .../integrationtests/client/FeignDocumentTest.java |  150 +++
 .../integrationtests/client/FeignImageTest.java    |  111 +++
 .../client/FeignIntegrationTest.java               |  103 ++
 .../client/feign/FeignLoanTestBase.java            |  219 +++++
 .../client/feign/helpers/FeignAccountHelper.java   |   95 ++
 .../feign/helpers/FeignBusinessDateHelper.java     |   62 ++
 .../client/feign/helpers/FeignClientHelper.java    |   65 ++
 .../feign/helpers/FeignJournalEntryHelper.java     |   69 ++
 .../client/feign/helpers/FeignLoanHelper.java      |  109 +++
 .../feign/helpers/FeignTransactionHelper.java      |   75 ++
 .../client/feign/modules/LoanProductTemplates.java |  240 +++++
 .../client/feign/modules/LoanRequestBuilders.java  |  135 +++
 .../client/feign/modules/LoanTestAccounts.java     |  204 ++++
 .../client/feign/modules/LoanTestData.java         |  231 +++++
 .../client/feign/modules/LoanTestValidators.java   |  126 +++
 .../client/feign/tests/FeignLoanCreationTest.java  |  159 ++++
 .../feign/tests/FeignLoanTestBaseSmokeTest.java    |   45 +
 .../integrationtests/cob/CobPartitioningTest.java  |   98 +-
 .../common/FineractFeignClientHelper.java          |   58 ++
 settings.gradle                                    |    1 +
 56 files changed, 7908 insertions(+), 23 deletions(-)

diff --git a/build.gradle b/build.gradle
index 0d7d4af5d3..1d25ed0514 100644
--- a/build.gradle
+++ b/build.gradle
@@ -44,6 +44,7 @@ buildscript {
                 'twofactor-tests',
                 'oauth2-tests',
                 'fineract-client',
+                'fineract-client-feign',
                 'fineract-avro-schemas',
                 'fineract-e2e-tests-core',
                 'fineract-e2e-tests-runner',
@@ -55,6 +56,7 @@ buildscript {
             [
                 'fineract-avro-schemas',
                 'fineract-client',
+                'fineract-client-feign',
                 'fineract-core',
                 'fineract-cob',
                 'fineract-validation',
@@ -558,6 +560,9 @@ configure(project.fineractJavaProjects) {
             if (project.path == ':fineract-client') {
                 excludedPaths = '.*/build/generated/java/src/main/java/.*'
             }
+            if (project.path == ':fineract-client-feign') {
+                excludedPaths = '.*/build/generated/java/src/main/java/.*'
+            }
             disable(
                     // TODO Remove disabled checks from this list, by fixing remaining usages
                     "UnusedVariable",
diff --git a/fineract-client-feign/.openapi-generator-ignore b/fineract-client-feign/.openapi-generator-ignore
new file mode 100644
index 0000000000..67b0c43e13
--- /dev/null
+++ b/fineract-client-feign/.openapi-generator-ignore
@@ -0,0 +1,32 @@
+**/README.md
+**/pom.xml
+**/build.sbt
+**/*.gradle
+**/.gitignore
+**/git_push.sh
+**/api/*
+**/gradle*
+**/gradle
+**/src/main/AndroidManifest.xml
+
+# https://issues.apache.org/jira/browse/FINERACT-1231
+**/feign/*.java
+!**/feign/CollectionFormats.java
+!**/feign/StringUtil.java
+
+# Manual API overrides - do not regenerate
+# https://issues.apache.org/jira/browse/FINERACT-1263
+**/services/RunReportsApi.java
+**/services/ImagesApi.java
+**/services/DocumentsApiFixed.java
+
+# Utility classes - do not regenerate
+**/util/*.java
+**/adapter/*.java
+
+# Feign configuration - do not regenerate
+**/feign/FineractFeignClient.java
+**/feign/FineractFeignClientConfig.java
+**/feign/BasicAuthRequestInterceptor.java
+**/feign/ObjectMapperFactory.java
+**/feign/FeignException.java
diff --git a/fineract-client-feign/build.gradle b/fineract-client-feign/build.gradle
new file mode 100644
index 0000000000..c158b75aa6
--- /dev/null
+++ b/fineract-client-feign/build.gradle
@@ -0,0 +1,194 @@
+/**
+ * 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.
+ */
+apply plugin: 'org.openapi.generator'
+apply plugin: 'jacoco'
+description = 'Fineract Client with Feign'
+
+apply from: 'dependencies.gradle'
+
+openApiMeta {
+    generatorName = 'Fineract-Feign'
+    packageName = 'org.apache.fineract.client.feign'
+    outputFolder = "$buildDir/meta".toString()
+}
+
+openApiValidate {
+    inputSpec = "file:///$swaggerFile"
+    recommend = true
+}
+
+tasks.register('buildJavaSdk', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
+    generatorName = 'java'
+    library = 'feign'
+    verbose = false
+    validateSpec = false
+    skipValidateSpec = true
+    inputSpec = "file:///$swaggerFile"
+    outputDir = "$buildDir/generated/temp-java".toString()
+    templateDir = "$projectDir/src/main/resources/templates/java"
+    groupId = 'org.apache.fineract'
+    apiPackage = 'org.apache.fineract.client.feign.services'
+    invokerPackage = 'org.apache.fineract.client.feign'
+    modelPackage = 'org.apache.fineract.client.models'
+    generateModelTests = false
+    generateApiTests = false
+    ignoreFileOverride = "$projectDir/.openapi-generator-ignore"
+    configOptions = [
+            dateLibrary                : 'java8',
+            library                    : 'feign',
+            feignVersion               : '13.6',
+            feignApacheHttpClient      : 'true',
+            useFeign13                 : 'true',
+            useFeignApacheHttpClient   : 'true',
+            hideGenerationTimestamp    : 'true',
+            containerDefaultToNull     : 'true',
+            oauth2Implementation       : 'none',
+            useJakartaEe               : 'true'
+
+    ]
+    dependsOn(':fineract-provider:resolve')
+}
+
+sourceSets {
+    main {
+        java {
+            srcDirs = [
+                new File(buildDir, "generated/java/src/main/java"),
+                "$projectDir/src/main/java"
+            ]
+            destinationDirectory = layout.buildDirectory.dir('classes/java/main').get().asFile
+        }
+        output.resourcesDir = layout.buildDirectory.dir('resources/main').get().asFile
+    }
+    test {
+        java {
+            destinationDirectory = layout.buildDirectory.dir('classes/java/test').get().asFile
+        }
+        output.resourcesDir = layout.buildDirectory.dir('resources/test').get().asFile
+    }
+}
+
+tasks.withType(Jar).configureEach {
+    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+}
+
+task cleanupGeneratedJavaFiles() {
+    def tempDir = file("$buildDir/generated/temp-java")
+    def targetDir = file("$buildDir/generated/java")
+
+    inputs.dir(tempDir)
+    outputs.dir(targetDir)
+
+    doLast {
+        copy {
+            from tempDir
+            into targetDir
+            filter { line ->
+                line
+                    .replaceAll(", \\)", ")")
+                    .replaceAll(", , @HeaderMap", ", @HeaderMap")
+                    .replaceAll("\\(, ", "(")
+            }
+            duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+        }
+    }
+    dependsOn("buildJavaSdk")
+}
+
+tasks.named('compileJava') {
+    outputs.cacheIf { true }
+    dependsOn(buildJavaSdk, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply)
+    mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles)
+}
+
+tasks.named('sourcesJar') {
+    dependsOn(cleanupGeneratedJavaFiles)
+    mustRunAfter(cleanupGeneratedJavaFiles)
+
+    from(sourceSets.main.java.srcDirs) {
+        include "**/*.java"
+    }
+}
+
+tasks.named('licenseFormatMain') {
+    dependsOn(cleanupGeneratedJavaFiles)
+    mustRunAfter(cleanupGeneratedJavaFiles)
+    source = sourceSets.main.java.srcDirs
+}
+
+tasks.named('licenseMain') {
+    dependsOn(licenseFormatMain)
+    mustRunAfter(licenseFormatMain)
+}
+
+tasks.withType(JavaCompile).configureEach {
+    options.encoding = 'UTF-8'
+    options.compilerArgs << '-parameters'
+    options.errorprone {
+        excludedPaths = '.*/build/generated/java/src/main/java/.*'
+    }
+}
+
+test {
+    useJUnitPlatform()
+}
+
+configurations {
+    generatedCompileClasspath.extendsFrom implementation
+    generatedRuntimeClasspath.extendsFrom runtimeClasspath
+}
+
+javadoc {
+    options.encoding = 'UTF-8'
+}
+
+spotbugsMain {
+    enabled = false
+}
+
+spotbugsTest {
+    enabled = false
+}
+
+jacoco {
+    toolVersion = "0.8.11"
+}
+
+jacocoTestReport {
+    dependsOn test
+    reports {
+        xml.required = true
+        html.required = true
+        csv.required = false
+    }
+    afterEvaluate {
+        classDirectories.setFrom(files(classDirectories.files.collect {
+            fileTree(dir: it, exclude: [
+                '**/build/generated/**',
+                '**/org/apache/fineract/client/models/**',
+                '**/org/apache/fineract/client/services/**Api.class',
+                '**/org/apache/fineract/client/auth/**'
+            ])
+        }))
+    }
+}
+
+test {
+    finalizedBy jacocoTestReport
+}
diff --git a/fineract-client-feign/dependencies.gradle b/fineract-client-feign/dependencies.gradle
new file mode 100644
index 0000000000..59cb8278b0
--- /dev/null
+++ b/fineract-client-feign/dependencies.gradle
@@ -0,0 +1,49 @@
+/**
+ * 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.
+ */
+dependencies {
+    // Feign dependencies
+    implementation(
+            'io.github.openfeign:feign-core:13.6',
+            'io.github.openfeign:feign-jackson:13.6',
+            'io.github.openfeign:feign-slf4j:13.6',
+            'io.github.openfeign:feign-hc5:13.6',
+            'io.github.openfeign:feign-okhttp:13.6',
+            'io.github.openfeign.form:feign-form:3.8.0',
+            'org.apache.httpcomponents.client5:httpclient5:5.2.1',
+            'com.squareup.okhttp3:okhttp:4.12.0',
+            'com.fasterxml.jackson.core:jackson-databind',
+            'com.fasterxml.jackson.datatype:jackson-datatype-jsr310',
+            'com.fasterxml.jackson.datatype:jackson-datatype-jdk8',
+            'jakarta.annotation:jakarta.annotation-api:3.0.0',
+            'io.swagger.core.v3:swagger-annotations-jakarta:2.2.15',
+            'org.apache.commons:commons-lang3:3.12.0',
+            'org.slf4j:slf4j-api:1.7.36',
+            'org.projectlombok:lombok'
+            )
+
+    // Test dependencies
+    testImplementation(
+            'org.junit.jupiter:junit-jupiter-api:5.11.3',
+            'org.junit.jupiter:junit-jupiter-engine:5.11.3',
+            'org.mockito:mockito-core:5.14.2',
+            'org.assertj:assertj-core:3.26.3',
+            'org.slf4j:slf4j-simple:1.7.36',
+            'org.wiremock:wiremock-standalone'
+            )
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/adapter/ExternalIdAdapter.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/adapter/ExternalIdAdapter.java
new file mode 100644
index 0000000000..00bd2f0ee0
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/adapter/ExternalIdAdapter.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.client.adapter;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import java.io.IOException;
+import org.apache.fineract.client.models.ExternalId;
+
+/**
+ * Custom Jackson adapter for ExternalId type serialization and deserialization. This adapter ensures that ExternalId
+ * objects are properly serialized to their string value and deserialized from string values.
+ */
+public final class ExternalIdAdapter {
+
+    private ExternalIdAdapter() {}
+
+    /**
+     * Jackson Serializer for ExternalId. Serializes an ExternalId object to its string value, or null if the ExternalId
+     * or its value is null.
+     */
+    public static class Serializer extends JsonSerializer<ExternalId> {
+
+        @Override
+        public void serialize(ExternalId value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+            if (value == null || value.getValue() == null) {
+                gen.writeNull();
+            } else {
+                gen.writeString(value.getValue());
+            }
+        }
+    }
+
+    /**
+     * Jackson Deserializer for ExternalId. Deserializes a string value to an ExternalId object, or null if the input is
+     * null.
+     */
+    public static class Deserializer extends JsonDeserializer<ExternalId> {
+
+        @Override
+        public ExternalId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+            String value = p.getValueAsString();
+            if (value == null) {
+                return null;
+            }
+            ExternalId externalId = new ExternalId();
+            externalId.setValue(value);
+            return externalId;
+        }
+    }
+
+    /**
+     * Creates a Jackson SimpleModule configured with the ExternalId serializer and deserializer.
+     *
+     * @return A configured SimpleModule ready to be registered with an ObjectMapper
+     */
+    public static SimpleModule createModule() {
+        SimpleModule module = new SimpleModule("ExternalIdModule");
+        module.addSerializer(ExternalId.class, new Serializer());
+        module.addDeserializer(ExternalId.class, new Deserializer());
+        return module;
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/BasicAuthRequestInterceptor.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/BasicAuthRequestInterceptor.java
new file mode 100644
index 0000000000..dad772522d
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/BasicAuthRequestInterceptor.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.client.feign;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Request interceptor that adds Basic Authentication header to requests.
+ */
+public class BasicAuthRequestInterceptor implements RequestInterceptor {
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String BASIC_AUTH_PREFIX = "Basic ";
+
+    private final String credentials;
+
+    /**
+     * Creates a new BasicAuthRequestInterceptor with the specified credentials.
+     *
+     * @param username
+     *            the username for authentication
+     * @param password
+     *            the password for authentication
+     */
+    public BasicAuthRequestInterceptor(String username, String password) {
+        if (username == null || password == null) {
+            throw new IllegalArgumentException("Username and password cannot be null");
+        }
+        String auth = username + ":" + password;
+        this.credentials = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public void apply(RequestTemplate template) {
+        template.header(AUTHORIZATION_HEADER, BASIC_AUTH_PREFIX + credentials);
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FeignException.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FeignException.java
new file mode 100644
index 0000000000..a80430c00b
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FeignException.java
@@ -0,0 +1,115 @@
+/**
+ * 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.feign;
+
+import feign.Request;
+import java.nio.charset.Charset;
+
+/**
+ * Base exception class for Feign client exceptions.
+ */
+public class FeignException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    private final int status;
+    private final Request request;
+    private final byte[] responseBody;
+    private final String developerMessage;
+    private final String userMessage;
+
+    protected FeignException(int status, String message, Request request) {
+        this(status, message, request, (byte[]) null);
+    }
+
+    protected FeignException(int status, String message, Request request, Throwable cause) {
+        this(status, message, request, null, cause);
+    }
+
+    protected FeignException(int status, String message, Request request, byte[] responseBody) {
+        super(message);
+        this.status = status;
+        this.request = request;
+        this.responseBody = responseBody;
+        this.developerMessage = null;
+        this.userMessage = null;
+    }
+
+    protected FeignException(int status, String message, Request request, byte[] responseBody, Throwable cause) {
+        super(message, cause);
+        this.status = status;
+        this.request = request;
+        this.responseBody = responseBody;
+        this.developerMessage = null;
+        this.userMessage = null;
+    }
+
+    public FeignException(int status, String message, Request request, byte[] responseBody, String developerMessage, String userMessage) {
+        super(message);
+        this.status = status;
+        this.request = request;
+        this.responseBody = responseBody;
+        this.developerMessage = developerMessage;
+        this.userMessage = userMessage;
+    }
+
+    public int status() {
+        return status;
+    }
+
+    public Request request() {
+        return request;
+    }
+
+    public byte[] responseBody() {
+        return responseBody;
+    }
+
+    public String responseBodyAsString() {
+        return responseBody != null ? new String(responseBody, Charset.defaultCharset()) : null;
+    }
+
+    public String getDeveloperMessage() {
+        return developerMessage;
+    }
+
+    public String getUserMessage() {
+        return userMessage;
+    }
+
+    @Override
+    public String getMessage() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("status ").append(status);
+
+        if (userMessage != null) {
+            sb.append(": ").append(userMessage);
+        }
+
+        if (developerMessage != null) {
+            sb.append(" (").append(developerMessage).append(")");
+        }
+
+        if (super.getMessage() != null && userMessage == null && developerMessage == null) {
+            sb.append(": ").append(super.getMessage());
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractErrorDecoder.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractErrorDecoder.java
new file mode 100644
index 0000000000..6c5525983f
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractErrorDecoder.java
@@ -0,0 +1,107 @@
+/**
+ * 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.feign;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import feign.Response;
+import feign.codec.ErrorDecoder;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class FineractErrorDecoder implements ErrorDecoder {
+
+    private final ErrorDecoder defaultDecoder = new Default();
+    private final ObjectMapper objectMapper = ObjectMapperFactory.getShared();
+
+    @Override
+    public Exception decode(String methodKey, Response response) {
+        try {
+            if (response.body() != null) {
+                byte[] bodyData = readResponseBody(response);
+
+                try {
+                    JsonNode rootNode = objectMapper.readTree(bodyData);
+
+                    String developerMessage = extractField(rootNode, "developerMessage");
+                    String userMessage = extractField(rootNode, "userMessage");
+                    String validationErrors = extractValidationErrors(rootNode);
+
+                    if (developerMessage != null || userMessage != null || validationErrors != null) {
+                        String enhancedDeveloperMessage = developerMessage;
+                        if (validationErrors != null) {
+                            enhancedDeveloperMessage = validationErrors;
+                        }
+                        return new FeignException(response.status(), userMessage != null ? userMessage : enhancedDeveloperMessage,
+                                response.request(), bodyData, enhancedDeveloperMessage, userMessage);
+                    }
+                } catch (IOException e) {
+                    return defaultDecoder.decode(methodKey, response);
+                }
+            }
+        } catch (IOException e) {
+            return defaultDecoder.decode(methodKey, response);
+        }
+
+        return defaultDecoder.decode(methodKey, response);
+    }
+
+    private byte[] readResponseBody(Response response) throws IOException {
+        if (response.body() == null) {
+            return new byte[0];
+        }
+
+        try (InputStream inputStream = response.body().asInputStream()) {
+            return inputStream.readAllBytes();
+        }
+    }
+
+    private String extractField(JsonNode node, String fieldName) {
+        JsonNode fieldNode = node.get(fieldName);
+        return fieldNode != null && !fieldNode.isNull() ? fieldNode.asText() : null;
+    }
+
+    private String extractValidationErrors(JsonNode rootNode) {
+        JsonNode errorsNode = rootNode.get("errors");
+        if (errorsNode != null && errorsNode.isArray() && errorsNode.size() > 0) {
+            StringBuilder errors = new StringBuilder("Validation errors: ");
+            for (JsonNode error : errorsNode) {
+                String parameterName = extractField(error, "parameterName");
+                String defaultUserMessage = extractField(error, "defaultUserMessage");
+                String developerMessage = extractField(error, "developerMessage");
+
+                if (errors.length() > "Validation errors: ".length()) {
+                    errors.append("; ");
+                }
+
+                if (parameterName != null) {
+                    errors.append("[").append(parameterName).append("] ");
+                }
+
+                if (defaultUserMessage != null) {
+                    errors.append(defaultUserMessage);
+                } else if (developerMessage != null) {
+                    errors.append(developerMessage);
+                }
+            }
+            return errors.length() > "Validation errors: ".length() ? errors.toString() : null;
+        }
+        return null;
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java
new file mode 100644
index 0000000000..8c049039f8
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java
@@ -0,0 +1,1002 @@
+/**
+ * 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.feign;
+
+import org.apache.fineract.client.feign.services.AccountNumberFormatApi;
+import org.apache.fineract.client.feign.services.AccountTransfersApi;
+import org.apache.fineract.client.feign.services.AccountingClosureApi;
+import org.apache.fineract.client.feign.services.AccountingRulesApi;
+import org.apache.fineract.client.feign.services.AdhocQueryApiApi;
+import org.apache.fineract.client.feign.services.AuditsApi;
+import org.apache.fineract.client.feign.services.AuthenticationHttpBasicApi;
+import org.apache.fineract.client.feign.services.BatchApiApi;
+import org.apache.fineract.client.feign.services.BulkImportApi;
+import org.apache.fineract.client.feign.services.BulkLoansApi;
+import org.apache.fineract.client.feign.services.BusinessDateManagementApi;
+import org.apache.fineract.client.feign.services.BusinessStepConfigurationApi;
+import org.apache.fineract.client.feign.services.CacheApi;
+import org.apache.fineract.client.feign.services.CalendarApi;
+import org.apache.fineract.client.feign.services.CashierJournalsApi;
+import org.apache.fineract.client.feign.services.CashiersApi;
+import org.apache.fineract.client.feign.services.CentersApi;
+import org.apache.fineract.client.feign.services.ChargesApi;
+import org.apache.fineract.client.feign.services.ClientApi;
+import org.apache.fineract.client.feign.services.ClientChargesApi;
+import org.apache.fineract.client.feign.services.ClientCollateralManagementApi;
+import org.apache.fineract.client.feign.services.ClientFamilyMemberApi;
+import org.apache.fineract.client.feign.services.ClientIdentifierApi;
+import org.apache.fineract.client.feign.services.ClientSearchV2Api;
+import org.apache.fineract.client.feign.services.ClientTransactionApi;
+import org.apache.fineract.client.feign.services.ClientsAddressApi;
+import org.apache.fineract.client.feign.services.CodeValuesApi;
+import org.apache.fineract.client.feign.services.CodesApi;
+import org.apache.fineract.client.feign.services.CollateralManagementApi;
+import org.apache.fineract.client.feign.services.CollectionSheetApi;
+import org.apache.fineract.client.feign.services.CreditBureauConfigurationApi;
+import org.apache.fineract.client.feign.services.CurrencyApi;
+import org.apache.fineract.client.feign.services.DataTablesApi;
+import org.apache.fineract.client.feign.services.DefaultApi;
+import org.apache.fineract.client.feign.services.DelinquencyRangeAndBucketsManagementApi;
+import org.apache.fineract.client.feign.services.DepositAccountOnHoldFundTransactionsApi;
+import org.apache.fineract.client.feign.services.DeviceRegistrationApi;
+import org.apache.fineract.client.feign.services.DocumentsApi;
+import org.apache.fineract.client.feign.services.DocumentsApiFixed;
+import org.apache.fineract.client.feign.services.EntityDataTableApi;
+import org.apache.fineract.client.feign.services.EntityFieldConfigurationApi;
+import org.apache.fineract.client.feign.services.ExternalAssetOwnerLoanProductAttributesApi;
+import org.apache.fineract.client.feign.services.ExternalAssetOwnersApi;
+import org.apache.fineract.client.feign.services.ExternalEventConfigurationApi;
+import org.apache.fineract.client.feign.services.ExternalServicesApi;
+import org.apache.fineract.client.feign.services.FetchAuthenticatedUserDetailsApi;
+import org.apache.fineract.client.feign.services.FineractEntityApi;
+import org.apache.fineract.client.feign.services.FixedDepositAccountApi;
+import org.apache.fineract.client.feign.services.FixedDepositAccountTransactionsApi;
+import org.apache.fineract.client.feign.services.FixedDepositProductApi;
+import org.apache.fineract.client.feign.services.FloatingRatesApi;
+import org.apache.fineract.client.feign.services.FundsApi;
+import org.apache.fineract.client.feign.services.GeneralLedgerAccountApi;
+import org.apache.fineract.client.feign.services.GlobalConfigurationApi;
+import org.apache.fineract.client.feign.services.GroupsApi;
+import org.apache.fineract.client.feign.services.GroupsLevelApi;
+import org.apache.fineract.client.feign.services.GuarantorsApi;
+import org.apache.fineract.client.feign.services.HolidaysApi;
+import org.apache.fineract.client.feign.services.HooksApi;
+import org.apache.fineract.client.feign.services.ImagesApi;
+import org.apache.fineract.client.feign.services.InlineJobApi;
+import org.apache.fineract.client.feign.services.InstanceModeApi;
+import org.apache.fineract.client.feign.services.InterOperationApi;
+import org.apache.fineract.client.feign.services.InterestRateChartApi;
+import org.apache.fineract.client.feign.services.InterestRateSlabAKAInterestBandsApi;
+import org.apache.fineract.client.feign.services.InternalCobApi;
+import org.apache.fineract.client.feign.services.JournalEntriesApi;
+import org.apache.fineract.client.feign.services.LikelihoodApi;
+import org.apache.fineract.client.feign.services.ListReportMailingJobHistoryApi;
+import org.apache.fineract.client.feign.services.LoanAccountLockApi;
+import org.apache.fineract.client.feign.services.LoanBuyDownFeesApi;
+import org.apache.fineract.client.feign.services.LoanCapitalizedIncomeApi;
+import org.apache.fineract.client.feign.services.LoanChargesApi;
+import org.apache.fineract.client.feign.services.LoanCobCatchUpApi;
+import org.apache.fineract.client.feign.services.LoanCollateralApi;
+import org.apache.fineract.client.feign.services.LoanCollateralManagementApi;
+import org.apache.fineract.client.feign.services.LoanDisbursementDetailsApi;
+import org.apache.fineract.client.feign.services.LoanInterestPauseApi;
+import org.apache.fineract.client.feign.services.LoanProductsApi;
+import org.apache.fineract.client.feign.services.LoanReschedulingApi;
+import org.apache.fineract.client.feign.services.LoanTransactionsApi;
+import org.apache.fineract.client.feign.services.LoansApi;
+import org.apache.fineract.client.feign.services.LoansPointInTimeApi;
+import org.apache.fineract.client.feign.services.MakerCheckerOr4EyeFunctionalityApi;
+import org.apache.fineract.client.feign.services.MappingFinancialActivitiesToAccountsApi;
+import org.apache.fineract.client.feign.services.MeetingsApi;
+import org.apache.fineract.client.feign.services.MixMappingApi;
+import org.apache.fineract.client.feign.services.MixReportApi;
+import org.apache.fineract.client.feign.services.MixTaxonomyApi;
+import org.apache.fineract.client.feign.services.NotesApi;
+import org.apache.fineract.client.feign.services.NotificationApi;
+import org.apache.fineract.client.feign.services.OfficesApi;
+import org.apache.fineract.client.feign.services.PasswordPreferencesApi;
+import org.apache.fineract.client.feign.services.PaymentTypeApi;
+import org.apache.fineract.client.feign.services.PeriodicAccrualAccountingApi;
+import org.apache.fineract.client.feign.services.PermissionsApi;
+import org.apache.fineract.client.feign.services.PocketApi;
+import org.apache.fineract.client.feign.services.PovertyLineApi;
+import org.apache.fineract.client.feign.services.ProductMixApi;
+import org.apache.fineract.client.feign.services.ProductsApi;
+import org.apache.fineract.client.feign.services.ProgressiveLoanApi;
+import org.apache.fineract.client.feign.services.ProvisioningCategoryApi;
+import org.apache.fineract.client.feign.services.ProvisioningCriteriaApi;
+import org.apache.fineract.client.feign.services.ProvisioningEntriesApi;
+import org.apache.fineract.client.feign.services.RateApi;
+import org.apache.fineract.client.feign.services.RecurringDepositAccountApi;
+import org.apache.fineract.client.feign.services.RecurringDepositAccountTransactionsApi;
+import org.apache.fineract.client.feign.services.RecurringDepositProductApi;
+import org.apache.fineract.client.feign.services.RepaymentWithPostDatedChecksApi;
+import org.apache.fineract.client.feign.services.ReportMailingJobsApi;
+import org.apache.fineract.client.feign.services.ReportsApi;
+import org.apache.fineract.client.feign.services.RescheduleLoansApi;
+import org.apache.fineract.client.feign.services.RolesApi;
+import org.apache.fineract.client.feign.services.RunReportsApi;
+import org.apache.fineract.client.feign.services.SavingsAccountApi;
+import org.apache.fineract.client.feign.services.SavingsAccountTransactionsApi;
+import org.apache.fineract.client.feign.services.SavingsChargesApi;
+import org.apache.fineract.client.feign.services.SavingsProductApi;
+import org.apache.fineract.client.feign.services.SchedulerApi;
+import org.apache.fineract.client.feign.services.SchedulerJobApi;
+import org.apache.fineract.client.feign.services.ScoreCardApi;
+import org.apache.fineract.client.feign.services.SearchApiApi;
+import org.apache.fineract.client.feign.services.SelfAccountTransferApi;
+import org.apache.fineract.client.feign.services.SelfAuthenticationApi;
+import org.apache.fineract.client.feign.services.SelfClientApi;
+import org.apache.fineract.client.feign.services.SelfDividendApi;
+import org.apache.fineract.client.feign.services.SelfLoanProductsApi;
+import org.apache.fineract.client.feign.services.SelfLoansApi;
+import org.apache.fineract.client.feign.services.SelfRunReportApi;
+import org.apache.fineract.client.feign.services.SelfSavingsAccountApi;
+import org.apache.fineract.client.feign.services.SelfSavingsProductsApi;
+import org.apache.fineract.client.feign.services.SelfScoreCardApi;
+import org.apache.fineract.client.feign.services.SelfServiceRegistrationApi;
+import org.apache.fineract.client.feign.services.SelfShareAccountsApi;
+import org.apache.fineract.client.feign.services.SelfShareProductsApi;
+import org.apache.fineract.client.feign.services.SelfSpmApi;
+import org.apache.fineract.client.feign.services.SelfThirdPartyTransferApi;
+import org.apache.fineract.client.feign.services.SelfUserApi;
+import org.apache.fineract.client.feign.services.SelfUserDetailsApi;
+import org.apache.fineract.client.feign.services.ShareAccountApi;
+import org.apache.fineract.client.feign.services.SmsApi;
+import org.apache.fineract.client.feign.services.SpmApiLookUpTableApi;
+import org.apache.fineract.client.feign.services.SpmSurveysApi;
+import org.apache.fineract.client.feign.services.StaffApi;
+import org.apache.fineract.client.feign.services.StandingInstructionsApi;
+import org.apache.fineract.client.feign.services.StandingInstructionsHistoryApi;
+import org.apache.fineract.client.feign.services.SurveyApi;
+import org.apache.fineract.client.feign.services.TaxComponentsApi;
+import org.apache.fineract.client.feign.services.TaxGroupApi;
+import org.apache.fineract.client.feign.services.TellerCashManagementApi;
+import org.apache.fineract.client.feign.services.TwoFactorApi;
+import org.apache.fineract.client.feign.services.UserGeneratedDocumentsApi;
+import org.apache.fineract.client.feign.services.UsersApi;
+import org.apache.fineract.client.feign.services.WorkingDaysApi;
+
+/**
+ * Main entry point for creating Feign-based clients for the Fineract API.
+ * <p>
+ * Example usage:
+ *
+ * <pre>
+ * {@code
+ *
+ * FineractFeignClient client = FineractFeignClient.builder().baseUrl("https://localhost:8443/fineract-provider/api/v1")
+ *         .credentials("username", "password").build();
+ *
+ * // Access API clients
+ * ClientApi clientsApi = client.clients();
+ * List<ClientData> clients = clientsApi.retrieveAll();
+ * }
+ * </pre>
+ */
+public final class FineractFeignClient {
+
+    private final FineractFeignClientConfig config;
+
+    private FineractFeignClient(Builder builder) {
+        this.config = builder.configBuilder.build();
+    }
+
+    /**
+     * Creates a new builder for configuring a FineractFeignClient.
+     *
+     * @return A new builder instance
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Creates a new client for the specified API interface.
+     *
+     * @param <T>
+     *            The API interface type
+     * @param apiType
+     *            The API interface class
+     * @return A configured Feign client for the specified API
+     */
+    public <T> T create(Class<T> apiType) {
+        return config.createClient(apiType);
+    }
+
+    public AccountNumberFormatApi accountNumberFormat() {
+        return create(AccountNumberFormatApi.class);
+    }
+
+    public AccountTransfersApi accountTransfers() {
+        return create(AccountTransfersApi.class);
+    }
+
+    public AccountingClosureApi accountingClosure() {
+        return create(AccountingClosureApi.class);
+    }
+
+    public AccountingRulesApi accountingRules() {
+        return create(AccountingRulesApi.class);
+    }
+
+    public AdhocQueryApiApi adhocQuery() {
+        return create(AdhocQueryApiApi.class);
+    }
+
+    public AuditsApi audits() {
+        return create(AuditsApi.class);
+    }
+
+    public AuthenticationHttpBasicApi authenticationHttpBasic() {
+        return create(AuthenticationHttpBasicApi.class);
+    }
+
+    public BatchApiApi batch() {
+        return create(BatchApiApi.class);
+    }
+
+    public BulkImportApi bulkImport() {
+        return create(BulkImportApi.class);
+    }
+
+    public BulkLoansApi bulkLoans() {
+        return create(BulkLoansApi.class);
+    }
+
+    public BusinessDateManagementApi businessDateManagement() {
+        return create(BusinessDateManagementApi.class);
+    }
+
+    public BusinessStepConfigurationApi businessStepConfiguration() {
+        return create(BusinessStepConfigurationApi.class);
+    }
+
+    public CacheApi cache() {
+        return create(CacheApi.class);
+    }
+
+    public CalendarApi calendar() {
+        return create(CalendarApi.class);
+    }
+
+    public CashierJournalsApi cashierJournals() {
+        return create(CashierJournalsApi.class);
+    }
+
+    public CashiersApi cashiers() {
+        return create(CashiersApi.class);
+    }
+
+    public CentersApi centers() {
+        return create(CentersApi.class);
+    }
+
+    public ChargesApi charges() {
+        return create(ChargesApi.class);
+    }
+
+    public ClientApi clients() {
+        return create(ClientApi.class);
+    }
+
+    public ClientChargesApi clientCharges() {
+        return create(ClientChargesApi.class);
+    }
+
+    public ClientCollateralManagementApi clientCollateralManagement() {
+        return create(ClientCollateralManagementApi.class);
+    }
+
+    public ClientFamilyMemberApi clientFamilyMember() {
+        return create(ClientFamilyMemberApi.class);
+    }
+
+    public ClientIdentifierApi clientIdentifier() {
+        return create(ClientIdentifierApi.class);
+    }
+
+    public ClientSearchV2Api clientSearchV2() {
+        return create(ClientSearchV2Api.class);
+    }
+
+    public ClientTransactionApi clientTransaction() {
+        return create(ClientTransactionApi.class);
+    }
+
+    public ClientsAddressApi clientsAddress() {
+        return create(ClientsAddressApi.class);
+    }
+
+    public CodeValuesApi codeValues() {
+        return create(CodeValuesApi.class);
+    }
+
+    public CodesApi codes() {
+        return create(CodesApi.class);
+    }
+
+    public CollateralManagementApi collateralManagement() {
+        return create(CollateralManagementApi.class);
+    }
+
+    public CollectionSheetApi collectionSheet() {
+        return create(CollectionSheetApi.class);
+    }
+
+    public CreditBureauConfigurationApi creditBureauConfiguration() {
+        return create(CreditBureauConfigurationApi.class);
+    }
+
+    public CurrencyApi currency() {
+        return create(CurrencyApi.class);
+    }
+
+    public DataTablesApi dataTables() {
+        return create(DataTablesApi.class);
+    }
+
+    public DefaultApi defaultApi() {
+        return create(DefaultApi.class);
+    }
+
+    public DelinquencyRangeAndBucketsManagementApi delinquencyRangeAndBucketsManagement() {
+        return create(DelinquencyRangeAndBucketsManagementApi.class);
+    }
+
+    public DepositAccountOnHoldFundTransactionsApi depositAccountOnHoldFundTransactions() {
+        return create(DepositAccountOnHoldFundTransactionsApi.class);
+    }
+
+    public DeviceRegistrationApi deviceRegistration() {
+        return create(DeviceRegistrationApi.class);
+    }
+
+    public DocumentsApi documents() {
+        return create(DocumentsApi.class);
+    }
+
+    public DocumentsApiFixed documentsFixed() {
+        return create(DocumentsApiFixed.class);
+    }
+
+    public EntityDataTableApi entityDataTable() {
+        return create(EntityDataTableApi.class);
+    }
+
+    public EntityFieldConfigurationApi entityFieldConfiguration() {
+        return create(EntityFieldConfigurationApi.class);
+    }
+
+    public ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributes() {
+        return create(ExternalAssetOwnerLoanProductAttributesApi.class);
+    }
+
+    public ExternalAssetOwnersApi externalAssetOwners() {
+        return create(ExternalAssetOwnersApi.class);
+    }
+
+    public ExternalEventConfigurationApi externalEventConfiguration() {
+        return create(ExternalEventConfigurationApi.class);
+    }
+
+    public ExternalServicesApi externalServices() {
+        return create(ExternalServicesApi.class);
+    }
+
+    public FetchAuthenticatedUserDetailsApi fetchAuthenticatedUserDetails() {
+        return create(FetchAuthenticatedUserDetailsApi.class);
+    }
+
+    public FineractEntityApi fineractEntity() {
+        return create(FineractEntityApi.class);
+    }
+
+    public FixedDepositAccountApi fixedDepositAccount() {
+        return create(FixedDepositAccountApi.class);
+    }
+
+    public FixedDepositAccountTransactionsApi fixedDepositAccountTransactions() {
+        return create(FixedDepositAccountTransactionsApi.class);
+    }
+
+    public FixedDepositProductApi fixedDepositProduct() {
+        return create(FixedDepositProductApi.class);
+    }
+
+    public FloatingRatesApi floatingRates() {
+        return create(FloatingRatesApi.class);
+    }
+
+    public FundsApi funds() {
+        return create(FundsApi.class);
+    }
+
+    public GeneralLedgerAccountApi generalLedgerAccount() {
+        return create(GeneralLedgerAccountApi.class);
+    }
+
+    public GlobalConfigurationApi globalConfiguration() {
+        return create(GlobalConfigurationApi.class);
+    }
+
+    public GroupsApi groups() {
+        return create(GroupsApi.class);
+    }
+
+    public GroupsLevelApi groupsLevel() {
+        return create(GroupsLevelApi.class);
+    }
+
+    public GuarantorsApi guarantors() {
+        return create(GuarantorsApi.class);
+    }
+
+    public HolidaysApi holidays() {
+        return create(HolidaysApi.class);
+    }
+
+    public HooksApi hooks() {
+        return create(HooksApi.class);
+    }
+
+    public ImagesApi images() {
+        return create(ImagesApi.class);
+    }
+
+    public InlineJobApi inlineJob() {
+        return create(InlineJobApi.class);
+    }
+
+    public InstanceModeApi instanceMode() {
+        return create(InstanceModeApi.class);
+    }
+
+    public InterOperationApi interOperation() {
+        return create(InterOperationApi.class);
+    }
+
+    public InterestRateChartApi interestRateChart() {
+        return create(InterestRateChartApi.class);
+    }
+
+    public InterestRateSlabAKAInterestBandsApi interestRateSlabAKAInterestBands() {
+        return create(InterestRateSlabAKAInterestBandsApi.class);
+    }
+
+    public InternalCobApi internalCob() {
+        return create(InternalCobApi.class);
+    }
+
+    public JournalEntriesApi journalEntries() {
+        return create(JournalEntriesApi.class);
+    }
+
+    public LikelihoodApi likelihood() {
+        return create(LikelihoodApi.class);
+    }
+
+    public ListReportMailingJobHistoryApi listReportMailingJobHistory() {
+        return create(ListReportMailingJobHistoryApi.class);
+    }
+
+    public LoanAccountLockApi loanAccountLock() {
+        return create(LoanAccountLockApi.class);
+    }
+
+    public LoanBuyDownFeesApi loanBuyDownFees() {
+        return create(LoanBuyDownFeesApi.class);
+    }
+
+    public LoanCapitalizedIncomeApi loanCapitalizedIncome() {
+        return create(LoanCapitalizedIncomeApi.class);
+    }
+
+    public LoanChargesApi loanCharges() {
+        return create(LoanChargesApi.class);
+    }
+
+    public LoanCobCatchUpApi loanCobCatchUp() {
+        return create(LoanCobCatchUpApi.class);
+    }
+
+    public LoanCollateralApi loanCollateral() {
+        return create(LoanCollateralApi.class);
+    }
+
+    public LoanCollateralManagementApi loanCollateralManagement() {
+        return create(LoanCollateralManagementApi.class);
+    }
+
+    public LoanDisbursementDetailsApi loanDisbursementDetails() {
+        return create(LoanDisbursementDetailsApi.class);
+    }
+
+    public LoanInterestPauseApi loanInterestPause() {
+        return create(LoanInterestPauseApi.class);
+    }
+
+    public LoanProductsApi loanProducts() {
+        return create(LoanProductsApi.class);
+    }
+
+    public LoanReschedulingApi loanRescheduling() {
+        return create(LoanReschedulingApi.class);
+    }
+
+    public LoanTransactionsApi loanTransactions() {
+        return create(LoanTransactionsApi.class);
+    }
+
+    public LoansApi loans() {
+        return create(LoansApi.class);
+    }
+
+    public LoansPointInTimeApi loansPointInTime() {
+        return create(LoansPointInTimeApi.class);
+    }
+
+    public MakerCheckerOr4EyeFunctionalityApi makerCheckerOr4EyeFunctionality() {
+        return create(MakerCheckerOr4EyeFunctionalityApi.class);
+    }
+
+    public MappingFinancialActivitiesToAccountsApi mappingFinancialActivitiesToAccounts() {
+        return create(MappingFinancialActivitiesToAccountsApi.class);
+    }
+
+    public MeetingsApi meetings() {
+        return create(MeetingsApi.class);
+    }
+
+    public MixMappingApi mixMapping() {
+        return create(MixMappingApi.class);
+    }
+
+    public MixReportApi mixReport() {
+        return create(MixReportApi.class);
+    }
+
+    public MixTaxonomyApi mixTaxonomy() {
+        return create(MixTaxonomyApi.class);
+    }
+
+    public NotesApi notes() {
+        return create(NotesApi.class);
+    }
+
+    public NotificationApi notification() {
+        return create(NotificationApi.class);
+    }
+
+    public OfficesApi offices() {
+        return create(OfficesApi.class);
+    }
+
+    public PasswordPreferencesApi passwordPreferences() {
+        return create(PasswordPreferencesApi.class);
+    }
+
+    public PaymentTypeApi paymentType() {
+        return create(PaymentTypeApi.class);
+    }
+
+    public PeriodicAccrualAccountingApi periodicAccrualAccounting() {
+        return create(PeriodicAccrualAccountingApi.class);
+    }
+
+    public PermissionsApi permissions() {
+        return create(PermissionsApi.class);
+    }
+
+    public PocketApi pocket() {
+        return create(PocketApi.class);
+    }
+
+    public PovertyLineApi povertyLine() {
+        return create(PovertyLineApi.class);
+    }
+
+    public ProductMixApi productMix() {
+        return create(ProductMixApi.class);
+    }
+
+    public ProductsApi products() {
+        return create(ProductsApi.class);
+    }
+
+    public ProgressiveLoanApi progressiveLoan() {
+        return create(ProgressiveLoanApi.class);
+    }
+
+    public ProvisioningCategoryApi provisioningCategory() {
+        return create(ProvisioningCategoryApi.class);
+    }
+
+    public ProvisioningCriteriaApi provisioningCriteria() {
+        return create(ProvisioningCriteriaApi.class);
+    }
+
+    public ProvisioningEntriesApi provisioningEntries() {
+        return create(ProvisioningEntriesApi.class);
+    }
+
+    public RateApi rate() {
+        return create(RateApi.class);
+    }
+
+    public RecurringDepositAccountApi recurringDepositAccount() {
+        return create(RecurringDepositAccountApi.class);
+    }
+
+    public RecurringDepositAccountTransactionsApi recurringDepositAccountTransactions() {
+        return create(RecurringDepositAccountTransactionsApi.class);
+    }
+
+    public RecurringDepositProductApi recurringDepositProduct() {
+        return create(RecurringDepositProductApi.class);
+    }
+
+    public RepaymentWithPostDatedChecksApi repaymentWithPostDatedChecks() {
+        return create(RepaymentWithPostDatedChecksApi.class);
+    }
+
+    public ReportMailingJobsApi reportMailingJobs() {
+        return create(ReportMailingJobsApi.class);
+    }
+
+    public ReportsApi reports() {
+        return create(ReportsApi.class);
+    }
+
+    public RescheduleLoansApi rescheduleLoans() {
+        return create(RescheduleLoansApi.class);
+    }
+
+    public RolesApi roles() {
+        return create(RolesApi.class);
+    }
+
+    public RunReportsApi runReports() {
+        return create(RunReportsApi.class);
+    }
+
+    public SavingsAccountApi savingsAccount() {
+        return create(SavingsAccountApi.class);
+    }
+
+    public SavingsAccountTransactionsApi savingsAccountTransactions() {
+        return create(SavingsAccountTransactionsApi.class);
+    }
+
+    public SavingsChargesApi savingsCharges() {
+        return create(SavingsChargesApi.class);
+    }
+
+    public SavingsProductApi savingsProduct() {
+        return create(SavingsProductApi.class);
+    }
+
+    public SchedulerApi scheduler() {
+        return create(SchedulerApi.class);
+    }
+
+    public SchedulerJobApi schedulerJob() {
+        return create(SchedulerJobApi.class);
+    }
+
+    public ScoreCardApi scoreCard() {
+        return create(ScoreCardApi.class);
+    }
+
+    public SearchApiApi search() {
+        return create(SearchApiApi.class);
+    }
+
+    public SelfAccountTransferApi selfAccountTransfer() {
+        return create(SelfAccountTransferApi.class);
+    }
+
+    public SelfAuthenticationApi selfAuthentication() {
+        return create(SelfAuthenticationApi.class);
+    }
+
+    public SelfClientApi selfClient() {
+        return create(SelfClientApi.class);
+    }
+
+    public SelfDividendApi selfDividend() {
+        return create(SelfDividendApi.class);
+    }
+
+    public SelfLoanProductsApi selfLoanProducts() {
+        return create(SelfLoanProductsApi.class);
+    }
+
+    public SelfLoansApi selfLoans() {
+        return create(SelfLoansApi.class);
+    }
+
+    public SelfRunReportApi selfRunReport() {
+        return create(SelfRunReportApi.class);
+    }
+
+    public SelfSavingsAccountApi selfSavingsAccount() {
+        return create(SelfSavingsAccountApi.class);
+    }
+
+    public SelfSavingsProductsApi selfSavingsProducts() {
+        return create(SelfSavingsProductsApi.class);
+    }
+
+    public SelfScoreCardApi selfScoreCard() {
+        return create(SelfScoreCardApi.class);
+    }
+
+    public SelfServiceRegistrationApi selfServiceRegistration() {
+        return create(SelfServiceRegistrationApi.class);
+    }
+
+    public SelfShareAccountsApi selfShareAccounts() {
+        return create(SelfShareAccountsApi.class);
+    }
+
+    public SelfShareProductsApi selfShareProducts() {
+        return create(SelfShareProductsApi.class);
+    }
+
+    public SelfSpmApi selfSpm() {
+        return create(SelfSpmApi.class);
+    }
+
+    public SelfThirdPartyTransferApi selfThirdPartyTransfer() {
+        return create(SelfThirdPartyTransferApi.class);
+    }
+
+    public SelfUserApi selfUser() {
+        return create(SelfUserApi.class);
+    }
+
+    public SelfUserDetailsApi selfUserDetails() {
+        return create(SelfUserDetailsApi.class);
+    }
+
+    public ShareAccountApi shareAccount() {
+        return create(ShareAccountApi.class);
+    }
+
+    public SmsApi sms() {
+        return create(SmsApi.class);
+    }
+
+    public SpmApiLookUpTableApi spmApiLookUpTable() {
+        return create(SpmApiLookUpTableApi.class);
+    }
+
+    public SpmSurveysApi spmSurveys() {
+        return create(SpmSurveysApi.class);
+    }
+
+    public StaffApi staff() {
+        return create(StaffApi.class);
+    }
+
+    public StandingInstructionsApi standingInstructions() {
+        return create(StandingInstructionsApi.class);
+    }
+
+    public StandingInstructionsHistoryApi standingInstructionsHistory() {
+        return create(StandingInstructionsHistoryApi.class);
+    }
+
+    public SurveyApi survey() {
+        return create(SurveyApi.class);
+    }
+
+    public TaxComponentsApi taxComponents() {
+        return create(TaxComponentsApi.class);
+    }
+
+    public TaxGroupApi taxGroup() {
+        return create(TaxGroupApi.class);
+    }
+
+    public TellerCashManagementApi tellerCashManagement() {
+        return create(TellerCashManagementApi.class);
+    }
+
+    public TwoFactorApi twoFactor() {
+        return create(TwoFactorApi.class);
+    }
+
+    public UserGeneratedDocumentsApi userGeneratedDocuments() {
+        return create(UserGeneratedDocumentsApi.class);
+    }
+
+    public UsersApi users() {
+        return create(UsersApi.class);
+    }
+
+    public WorkingDaysApi workingDays() {
+        return create(WorkingDaysApi.class);
+    }
+
+    /**
+     * Builder for creating and configuring a FineractFeignClient.
+     */
+    public static class Builder {
+
+        private final FineractFeignClientConfig.Builder configBuilder = FineractFeignClientConfig.builder();
+
+        /**
+         * Sets the base URL for the Fineract API.
+         *
+         * @param baseUrl
+         *            The base URL (e.g., "https://localhost:8443/fineract-provider/api/v1")
+         * @return This builder instance
+         */
+        public Builder baseUrl(String baseUrl) {
+            configBuilder.baseUrl(baseUrl);
+            return this;
+        }
+
+        /**
+         * Sets the credentials for Basic Authentication.
+         *
+         * @param username
+         *            The username
+         * @param password
+         *            The password
+         * @return This builder instance
+         */
+        public Builder credentials(String username, String password) {
+            configBuilder.credentials(username, password);
+            return this;
+        }
+
+        /**
+         * Sets the connection timeout.
+         *
+         * @param timeout
+         *            The timeout value
+         * @param unit
+         *            The time unit
+         * @return This builder instance
+         */
+        public Builder connectTimeout(int timeout, java.util.concurrent.TimeUnit unit) {
+            configBuilder.connectTimeout(timeout, unit);
+            return this;
+        }
+
+        /**
+         * Sets the read timeout.
+         *
+         * @param timeout
+         *            The timeout value
+         * @param unit
+         *            The time unit
+         * @return This builder instance
+         */
+        public Builder readTimeout(int timeout, java.util.concurrent.TimeUnit unit) {
+            configBuilder.readTimeout(timeout, unit);
+            return this;
+        }
+
+        /**
+         * Enables or disables debug logging.
+         *
+         * @param enabled
+         *            true to enable debug logging, false to disable
+         * @return This builder instance
+         */
+        public Builder debug(boolean enabled) {
+            configBuilder.debugEnabled(enabled);
+            return this;
+        }
+
+        /**
+         * Disables SSL certificate verification. Use only for testing with self-signed certificates.
+         *
+         * @param disable
+         *            true to disable SSL verification, false to enable
+         * @return This builder instance
+         */
+        public Builder disableSslVerification(boolean disable) {
+            configBuilder.disableSslVerification(disable);
+            return this;
+        }
+
+        public Builder tenantId(String tenantId) {
+            configBuilder.tenantId(tenantId);
+            return this;
+        }
+
+        /**
+         * Sets the connection time-to-live (TTL) for connection pool recycling.
+         *
+         * @param ttl
+         *            The time-to-live value
+         * @param unit
+         *            The time unit
+         * @return This builder instance
+         */
+        public Builder connectionTimeToLive(long ttl, java.util.concurrent.TimeUnit unit) {
+            configBuilder.connectionTimeToLive(ttl, unit);
+            return this;
+        }
+
+        /**
+         * Sets the idle connection eviction time.
+         *
+         * @param time
+         *            The eviction time value
+         * @param unit
+         *            The time unit
+         * @return This builder instance
+         */
+        public Builder idleConnectionEvictionTime(long time, java.util.concurrent.TimeUnit unit) {
+            configBuilder.idleConnectionEvictionTime(time, unit);
+            return this;
+        }
+
+        /**
+         * Sets the maximum total connections in the pool.
+         *
+         * @param max
+         *            Maximum total connections
+         * @return This builder instance
+         */
+        public Builder maxConnections(int max) {
+            configBuilder.maxConnTotal(max);
+            return this;
+        }
+
+        /**
+         * Sets the maximum connections per route.
+         *
+         * @param max
+         *            Maximum connections per route
+         * @return This builder instance
+         */
+        public Builder maxConnectionsPerRoute(int max) {
+            configBuilder.maxConnPerRoute(max);
+            return this;
+        }
+
+        /**
+         * Sets the HTTP client type.
+         *
+         * @param clientType
+         *            The HTTP client type (APACHE or OKHTTP)
+         * @return This builder instance
+         */
+        public Builder httpClientType(FineractFeignClientConfig.HttpClientType clientType) {
+            configBuilder.httpClientType(clientType);
+            return this;
+        }
+
+        /**
+         * Builds a new FineractFeignClient with the current configuration.
+         *
+         * @return A new FineractFeignClient instance
+         */
+        public FineractFeignClient build() {
+            return new FineractFeignClient(this);
+        }
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClientConfig.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClientConfig.java
new file mode 100644
index 0000000000..04fe8e1ef8
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClientConfig.java
@@ -0,0 +1,293 @@
+/**
+ * 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.feign;
+
+import feign.Client;
+import feign.Feign;
+import feign.Request;
+import feign.Retryer;
+import feign.codec.Encoder;
+import feign.hc5.ApacheHttp5Client;
+import feign.jackson.JacksonDecoder;
+import feign.jackson.JacksonEncoder;
+import feign.slf4j.Slf4jLogger;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+
+/**
+ * Configuration class for Feign client.
+ */
+public final class FineractFeignClientConfig {
+
+    public enum HttpClientType {
+        APACHE, OKHTTP
+    }
+
+    private final String baseUrl;
+    private final String username;
+    private final String password;
+    private final String tenantId;
+    private final int connectTimeout;
+    private final int readTimeout;
+    private final boolean debugEnabled;
+    private final long connectionTimeToLive;
+    private final TimeUnit connectionTimeToLiveUnit;
+    private final boolean disableSslVerification;
+    private final int maxConnTotal;
+    private final int maxConnPerRoute;
+    private final long idleConnectionEvictionTime;
+    private final TimeUnit idleConnectionEvictionTimeUnit;
+    private final HttpClientType clientType;
+    private volatile Client cachedHttpClient;
+
+    private FineractFeignClientConfig(Builder builder) {
+        this.baseUrl = builder.baseUrl;
+        this.username = builder.username;
+        this.password = builder.password;
+        this.tenantId = builder.tenantId;
+        this.connectTimeout = builder.connectTimeout;
+        this.readTimeout = builder.readTimeout;
+        this.debugEnabled = builder.debugEnabled;
+        this.connectionTimeToLive = builder.connectionTimeToLive;
+        this.connectionTimeToLiveUnit = builder.connectionTimeToLiveUnit;
+        this.disableSslVerification = builder.disableSslVerification;
+        this.maxConnTotal = builder.maxConnTotal;
+        this.maxConnPerRoute = builder.maxConnPerRoute;
+        this.idleConnectionEvictionTime = builder.idleConnectionEvictionTime;
+        this.idleConnectionEvictionTimeUnit = builder.idleConnectionEvictionTimeUnit;
+        this.clientType = builder.clientType;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public <T> T createClient(Class<T> apiType) {
+        JacksonEncoder jacksonEncoder = new JacksonEncoder(ObjectMapperFactory.getShared());
+        Encoder multipartEncoder = new FineractMultipartEncoder(jacksonEncoder);
+
+        return Feign.builder().client(getOrCreateHttpClient()).encoder(multipartEncoder)
+                .decoder(new JacksonDecoder(ObjectMapperFactory.getShared())).errorDecoder(new FineractErrorDecoder())
+                .options(new Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, true))
+                .retryer(Retryer.NEVER_RETRY).requestInterceptor(new BasicAuthRequestInterceptor(username, password))
+                .requestInterceptor(new TenantIdRequestInterceptor(tenantId)).logger(new Slf4jLogger(apiType))
+                .logLevel(debugEnabled ? feign.Logger.Level.FULL : feign.Logger.Level.BASIC).target(apiType, baseUrl);
+    }
+
+    private Client getOrCreateHttpClient() {
+        if (cachedHttpClient == null) {
+            synchronized (this) {
+                if (cachedHttpClient == null) {
+                    cachedHttpClient = createHttpClient();
+                }
+            }
+        }
+        return cachedHttpClient;
+    }
+
+    private Client createHttpClient() {
+        switch (clientType) {
+            case APACHE:
+                return createApacheHttpClient();
+            case OKHTTP:
+                return createOkHttpClient();
+            default:
+                throw new IllegalStateException("Unsupported HTTP client type: " + clientType);
+        }
+    }
+
+    private Client createApacheHttpClient() {
+        try {
+            PoolingHttpClientConnectionManagerBuilder connManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create()
+                    .setMaxConnTotal(maxConnTotal).setMaxConnPerRoute(maxConnPerRoute);
+
+            if (disableSslVerification) {
+                SSLContext sslContext = createTrustAllSslContext();
+                SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create().setSslContext(sslContext).build();
+                connManagerBuilder.setSSLSocketFactory(sslSocketFactory);
+            }
+
+            if (connectionTimeToLive > 0) {
+                connManagerBuilder.setConnectionTimeToLive(TimeValue.of(connectionTimeToLive, connectionTimeToLiveUnit));
+            }
+
+            PoolingHttpClientConnectionManager connectionManager = connManagerBuilder.build();
+
+            CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager)
+                    .setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(Timeout.ofMilliseconds(connectTimeout))
+                            .setResponseTimeout(Timeout.ofMilliseconds(readTimeout)).build())
+                    .evictIdleConnections(TimeValue.of(idleConnectionEvictionTime, idleConnectionEvictionTimeUnit))
+                    .evictExpiredConnections().build();
+
+            return new ApacheHttp5Client(httpClient);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create Apache HTTP client", e);
+        }
+    }
+
+    private Client createOkHttpClient() {
+        try {
+            okhttp3.OkHttpClient.Builder builder = new okhttp3.OkHttpClient.Builder().connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
+                    .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
+                    .connectionPool(new okhttp3.ConnectionPool(maxConnTotal, connectionTimeToLive > 0 ? connectionTimeToLive : 5,
+                            connectionTimeToLive > 0 ? connectionTimeToLiveUnit : TimeUnit.MINUTES));
+
+            if (disableSslVerification) {
+                SSLContext sslContext = createTrustAllSslContext();
+                builder.sslSocketFactory(sslContext.getSocketFactory(), createTrustAllManager());
+                builder.hostnameVerifier((hostname, session) -> true);
+            }
+
+            return new feign.okhttp.OkHttpClient(builder.build());
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create OkHttp client", e);
+        }
+    }
+
+    private X509TrustManager createTrustAllManager() {
+        return new X509TrustManager() {
+
+            @Override
+            public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[0];
+            }
+
+            @Override
+            public void checkClientTrusted(X509Certificate[] certs, String authType) {}
+
+            @Override
+            public void checkServerTrusted(X509Certificate[] certs, String authType) {}
+        };
+    }
+
+    private SSLContext createTrustAllSslContext() throws Exception {
+        TrustManager[] trustAllCerts = new TrustManager[] { createTrustAllManager() };
+
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
+        return sslContext;
+    }
+
+    public static class Builder {
+
+        private String baseUrl;
+        private String username;
+        private String password;
+        private String tenantId = "default";
+        private int connectTimeout = 30000; // 30 seconds
+        private int readTimeout = 60000; // 60 seconds
+        private boolean debugEnabled = false;
+        private long connectionTimeToLive = -1;
+        private TimeUnit connectionTimeToLiveUnit = TimeUnit.MILLISECONDS;
+        private boolean disableSslVerification = false;
+        private int maxConnTotal = 200;
+        private int maxConnPerRoute = 20;
+        private long idleConnectionEvictionTime = 30;
+        private TimeUnit idleConnectionEvictionTimeUnit = TimeUnit.SECONDS;
+        private HttpClientType clientType = HttpClientType.APACHE;
+
+        public Builder baseUrl(String baseUrl) {
+            this.baseUrl = baseUrl;
+            return this;
+        }
+
+        public Builder credentials(String username, String password) {
+            this.username = username;
+            this.password = password;
+            return this;
+        }
+
+        public Builder tenantId(String tenantId) {
+            this.tenantId = tenantId;
+            return this;
+        }
+
+        public Builder connectTimeout(int timeout, TimeUnit unit) {
+            this.connectTimeout = (int) unit.toMillis(timeout);
+            return this;
+        }
+
+        public Builder readTimeout(int timeout, TimeUnit unit) {
+            this.readTimeout = (int) unit.toMillis(timeout);
+            return this;
+        }
+
+        public Builder connectionTimeToLive(long ttl, TimeUnit unit) {
+            this.connectionTimeToLive = ttl;
+            this.connectionTimeToLiveUnit = unit;
+            return this;
+        }
+
+        public Builder debugEnabled(boolean debugEnabled) {
+            this.debugEnabled = debugEnabled;
+            return this;
+        }
+
+        public Builder disableSslVerification(boolean disableSslVerification) {
+            this.disableSslVerification = disableSslVerification;
+            return this;
+        }
+
+        public Builder maxConnTotal(int maxConnTotal) {
+            this.maxConnTotal = maxConnTotal;
+            return this;
+        }
+
+        public Builder maxConnPerRoute(int maxConnPerRoute) {
+            this.maxConnPerRoute = maxConnPerRoute;
+            return this;
+        }
+
+        public Builder idleConnectionEvictionTime(long time, TimeUnit unit) {
+            this.idleConnectionEvictionTime = time;
+            this.idleConnectionEvictionTimeUnit = unit;
+            return this;
+        }
+
+        public Builder httpClientType(HttpClientType clientType) {
+            this.clientType = clientType;
+            return this;
+        }
+
+        public FineractFeignClientConfig build() {
+            if (baseUrl == null || baseUrl.trim().isEmpty()) {
+                throw new IllegalStateException("baseUrl is required");
+            }
+            if (username == null || username.trim().isEmpty()) {
+                throw new IllegalStateException("username is required");
+            }
+            if (password == null) {
+                throw new IllegalStateException("password is required");
+            }
+            return new FineractFeignClientConfig(this);
+        }
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractMultipartEncoder.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractMultipartEncoder.java
new file mode 100644
index 0000000000..7f81679002
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractMultipartEncoder.java
@@ -0,0 +1,198 @@
+/**
+ * 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.feign;
+
+import feign.RequestTemplate;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
+import org.apache.hc.core5.http.HttpEntity;
+
+/**
+ * Custom multipart encoder for Fineract Feign clients. Uses Apache HttpClient's MultipartEntityBuilder to properly
+ * construct multipart/form-data requests.
+ */
+public class FineractMultipartEncoder implements Encoder {
+
+    private final Encoder delegate;
+
+    public FineractMultipartEncoder(Encoder delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
+        if (object instanceof MultipartData) {
+            encodeMultipart((MultipartData) object, template);
+        } else if (object instanceof File) {
+            encodeFileAsMultipart((File) object, template);
+        } else if (object instanceof String && template.headers().containsKey("Content-Type")) {
+            String contentType = template.headers().get("Content-Type").iterator().next();
+            if (contentType.startsWith("text/html") || contentType.startsWith("text/plain")) {
+                byte[] bodyBytes = ((String) object).getBytes(StandardCharsets.UTF_8);
+                template.body(bodyBytes, StandardCharsets.UTF_8);
+            } else {
+                delegate.encode(object, bodyType, template);
+            }
+        } else {
+            delegate.encode(object, bodyType, template);
+        }
+    }
+
+    private void encodeFileAsMultipart(File file, RequestTemplate template) throws EncodeException {
+        try {
+            byte[] fileData = Files.readAllBytes(file.toPath());
+            String contentType = detectContentType(file);
+            MultipartData multipartData = new MultipartData().addFile("file", file.getName(), fileData, contentType);
+            encodeMultipart(multipartData, template);
+        } catch (IOException e) {
+            throw new EncodeException("Failed to encode File as multipart: " + file, e);
+        }
+    }
+
+    private String detectContentType(File file) {
+        try {
+            String contentType = Files.probeContentType(file.toPath());
+            return contentType != null ? contentType : "application/octet-stream";
+        } catch (IOException e) {
+            return "application/octet-stream";
+        }
+    }
+
+    private void encodeMultipart(MultipartData data, RequestTemplate template) throws EncodeException {
+        try {
+            MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+            builder.setMode(org.apache.hc.client5.http.entity.mime.HttpMultipartMode.STRICT);
+
+            for (MultipartData.Part part : data.getParts()) {
+                if (part.getFileData() != null) {
+                    org.apache.hc.core5.http.ContentType ct = org.apache.hc.core5.http.ContentType.create(part.getContentType());
+                    builder.addBinaryBody(part.getName(), part.getFileData(), ct, part.getFileName());
+                } else if (part.getTextValue() != null) {
+                    builder.addTextBody(part.getName(), part.getTextValue(), org.apache.hc.core5.http.ContentType.TEXT_PLAIN);
+                }
+            }
+
+            HttpEntity entity = builder.build();
+
+            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            entity.writeTo(outputStream);
+            byte[] body = outputStream.toByteArray();
+
+            template.body(body, null);
+            template.removeHeader("Content-Type");
+            String contentTypeValue = entity.getContentType();
+            String cleanContentType = contentTypeValue;
+            if (contentTypeValue.contains(";")) {
+                int firstSemicolon = contentTypeValue.indexOf(';');
+                String mainType = contentTypeValue.substring(0, firstSemicolon).trim();
+                String paramsSection = contentTypeValue.substring(firstSemicolon + 1);
+
+                String boundary = null;
+                int boundaryIndex = paramsSection.indexOf("boundary=");
+                if (boundaryIndex != -1) {
+                    int boundaryStart = boundaryIndex;
+                    int boundaryEnd = paramsSection.indexOf(';', boundaryStart);
+                    if (boundaryEnd == -1) {
+                        boundary = paramsSection.substring(boundaryStart).trim();
+                    } else {
+                        boundary = paramsSection.substring(boundaryStart, boundaryEnd).trim();
+                    }
+                }
+
+                if (boundary != null) {
+                    cleanContentType = mainType + "; " + boundary;
+                }
+            }
+            template.header("Content-Type", cleanContentType);
+        } catch (IOException e) {
+            throw new EncodeException("Failed to encode multipart request", e);
+        }
+    }
+
+    public static class MultipartData {
+
+        private final java.util.List<Part> parts = new java.util.ArrayList<>();
+
+        public MultipartData addFile(String name, String fileName, byte[] data, String contentType) {
+            parts.add(new Part(name, fileName, data, contentType));
+            return this;
+        }
+
+        public MultipartData addText(String name, String value) {
+            parts.add(new Part(name, value));
+            return this;
+        }
+
+        public java.util.List<Part> getParts() {
+            return parts;
+        }
+
+        public static class Part {
+
+            private final String name;
+            private final String fileName;
+            private final byte[] fileData;
+            private final String contentType;
+            private final String textValue;
+
+            public Part(String name, String fileName, byte[] fileData, String contentType) {
+                this.name = name;
+                this.fileName = fileName;
+                this.fileData = fileData;
+                this.contentType = contentType;
+                this.textValue = null;
+            }
+
+            public Part(String name, String textValue) {
+                this.name = name;
+                this.textValue = textValue;
+                this.fileName = null;
+                this.fileData = null;
+                this.contentType = null;
+            }
+
+            public String getName() {
+                return name;
+            }
+
+            public String getFileName() {
+                return fileName;
+            }
+
+            public byte[] getFileData() {
+                return fileData;
+            }
+
+            public String getContentType() {
+                return contentType;
+            }
+
+            public String getTextValue() {
+                return textValue;
+            }
+        }
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/ObjectMapperFactory.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/ObjectMapperFactory.java
new file mode 100644
index 0000000000..18e163041e
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/ObjectMapperFactory.java
@@ -0,0 +1,73 @@
+/**
+ * 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.feign;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.apache.fineract.client.adapter.ExternalIdAdapter;
+
+/**
+ * Factory for creating and configuring Jackson ObjectMapper instances.
+ */
+public final class ObjectMapperFactory {
+
+    private static final ObjectMapper INSTANCE = createObjectMapper();
+
+    private ObjectMapperFactory() {
+        // Private constructor to prevent instantiation
+    }
+
+    /**
+     * Creates and configures a new ObjectMapper instance.
+     *
+     * @return A new configured ObjectMapper instance
+     */
+    public static ObjectMapper createObjectMapper() {
+        ObjectMapper mapper = new ObjectMapper();
+
+        // Configure the ObjectMapper
+        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
+        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+
+        // Register Java 8 date/time support
+        mapper.registerModule(new JavaTimeModule());
+
+        // Register ExternalId adapter
+        mapper.registerModule(ExternalIdAdapter.createModule());
+
+        // Disable FAIL_ON_EMPTY_BEANS for empty responses
+        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+
+        return mapper;
+    }
+
+    /**
+     * Returns a shared, pre-configured ObjectMapper instance.
+     *
+     * @return A shared ObjectMapper instance
+     */
+    public static ObjectMapper getShared() {
+        return INSTANCE;
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/TenantIdRequestInterceptor.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/TenantIdRequestInterceptor.java
new file mode 100644
index 0000000000..15578cd3ec
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/TenantIdRequestInterceptor.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.client.feign;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+
+public class TenantIdRequestInterceptor implements RequestInterceptor {
+
+    private final String tenantId;
+
+    public TenantIdRequestInterceptor(String tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public void apply(RequestTemplate template) {
+        template.header("Fineract-Platform-TenantId", tenantId);
+        if (!template.headers().containsKey("Content-Type")) {
+            template.header("Content-Type", "application/json");
+        }
+        if (!template.headers().containsKey("Accept")) {
+            template.header("Accept", "application/json");
+        }
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/DocumentsApiFixed.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/DocumentsApiFixed.java
new file mode 100644
index 0000000000..de3f7934a0
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/DocumentsApiFixed.java
@@ -0,0 +1,140 @@
+/**
+ * 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.feign.services;
+
+import feign.Param;
+import feign.RequestLine;
+import feign.Response;
+import java.util.List;
+import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.client.models.DocumentData;
+import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.client.models.PutEntityTypeEntityIdDocumentsResponse;
+
+/**
+ * This class was originally generated by OpenAPI Generator (https://openapi-generator.tech), but then had to be
+ * manually edited to manually fix https://issues.apache.org/jira/browse/FINERACT-1227. If we could fix our OpenAPI /
+ * Swagger YAML generation from the JAX RS and OpenAPI annotation to have the correct notation for binary document files
+ * and images, then this could be removed again.
+ */
+public interface DocumentsApiFixed {
+
+    /**
+     * Create a Document Note: A document is created using a Multi-part form upload Body Parts name : Name or summary of
+     * the document description : Description of the document file : The file to be uploaded Mandatory Fields : file and
+     * description
+     *
+     * @param entityType
+     *            entityType (required)
+     * @param entityId
+     *            entityId (required)
+     * @param file
+     *            (required)
+     * @param name
+     *            name (optional)
+     * @param description
+     *            description (optional)
+     * @return PostEntityTypeEntityIdDocumentsResponse
+     */
+    @RequestLine("POST /v1/{entityType}/{entityId}/documents")
+    PostEntityTypeEntityIdDocumentsResponse createDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
+            org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
+
+    /**
+     * Remove a Document
+     *
+     * @param entityType
+     *            entityType (required)
+     * @param entityId
+     *            entityId (required)
+     * @param documentId
+     *            documentId (required)
+     * @return DeleteEntityTypeEntityIdDocumentsResponse
+     */
+    @RequestLine("DELETE /v1/{entityType}/{entityId}/documents/{documentId}")
+    DeleteEntityTypeEntityIdDocumentsResponse deleteDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
+            @Param("documentId") Long documentId);
+
+    /**
+     * Retrieve Binary File associated with Document Request used to download the file associated with the document
+     * Example Requests: clients/1/documents/1/attachment loans/1/documents/1/attachment
+     *
+     * @param entityType
+     *            entityType (required)
+     * @param entityId
+     *            entityId (required)
+     * @param documentId
+     *            documentId (required)
+     * @return Response
+     */
+    @RequestLine("GET /v1/{entityType}/{entityId}/documents/{documentId}/attachment")
+    @feign.Headers("Accept: */*")
+    Response downloadFile(@Param("entityType") String entityType, @Param("entityId") Long entityId, @Param("documentId") Long documentId);
+
+    /**
+     * Retrieve a Document Example Requests: clients/1/documents/1 loans/1/documents/1
+     * client_identifiers/1/documents/1?fields=name,description
+     *
+     * @param entityType
+     *            entityType (required)
+     * @param entityId
+     *            entityId (required)
+     * @param documentId
+     *            documentId (required)
+     * @return DocumentData
+     */
+    @RequestLine("GET /v1/{entityType}/{entityId}/documents/{documentId}")
+    DocumentData getDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
+            @Param("documentId") Long documentId);
+
+    /**
+     * List documents Example Requests: clients/1/documents client_identifiers/1/documents
+     * loans/1/documents?fields=name,description
+     *
+     * @param entityType
+     *            entityType (required)
+     * @param entityId
+     *            entityId (required)
+     * @return List&lt;DocumentData&gt;
+     */
+    @RequestLine("GET /v1/{entityType}/{entityId}/documents")
+    List<DocumentData> retrieveAllDocuments(@Param("entityType") String entityType, @Param("entityId") Long entityId);
+
+    /**
+     * Update a Document Note: A document is updated using a Multi-part form upload Body Parts name Name or summary of
+     * the document description Description of the document file The file to be uploaded
+     *
+     * @param entityType
+     *            entityType (required)
+     * @param entityId
+     *            entityId (required)
+     * @param documentId
+     *            documentId (required)
+     * @param file
+     *            (optional)
+     * @param name
+     *            name (optional)
+     * @param description
+     *            description (optional)
+     * @return PutEntityTypeEntityIdDocumentsResponse
+     */
+    @RequestLine("PUT /v1/{entityType}/{entityId}/documents/{documentId}")
+    PutEntityTypeEntityIdDocumentsResponse updateDocument(@Param("entityType") String entityType, @Param("entityId") Long entityId,
+            @Param("documentId") Long documentId, org.apache.fineract.client.feign.FineractMultipartEncoder.MultipartData multipartData);
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/ImagesApi.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/ImagesApi.java
new file mode 100644
index 0000000000..6fa311e8da
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/ImagesApi.java
@@ -0,0 +1,97 @@
+/**
+ * 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.feign.services;
+
+import feign.Headers;
+import feign.Param;
+import feign.QueryMap;
+import feign.RequestLine;
+import feign.Response;
+import java.io.File;
+import java.util.Map;
+
+/**
+ * Client API (Feign) for /images.
+ *
+ * This class is entirely hand-written, inspired by DocumentsApiFixed, and from /images methods which currently end up
+ * in DefaultApi (see <a href="https://issues.apache.org/jira/browse/FINERACT-1222">FINERACT-1222</a>), but fixed for
+ * bugs in the code generation (see <a href="https://issues.apache.org/jira/browse/FINERACT-1227">FINERACT-1227</a>).
+ *
+ * Note: For image uploads, use {@link #prepareFileUpload(File)} to prepare file data as a Data URL.
+ */
+public interface ImagesApi {
+
+    @RequestLine("POST /v1/{entityType}/{entityId}/images")
+    @Headers("Content-Type: text/html")
+    Response create(@Param("entityType") String entityType, @Param("entityId") Long entityId, String dataUrl);
+
+    @RequestLine("GET /v1/{entityType}/{entityId}/images")
+    Response get(@Param("entityType") String entityType, @Param("entityId") Long entityId, @QueryMap Map<String, Object> queryParams);
+
+    @RequestLine("PUT /v1/{entityType}/{entityId}/images")
+    @Headers("Content-Type: text/html")
+    Response update(@Param("entityType") String entityType, @Param("entityId") Long entityId, String dataUrl);
+
+    @RequestLine("DELETE /v1/{entityType}/{entityId}/images")
+    Response delete(@Param("entityType") String entityType, @Param("entityId") Long entityId);
+
+    static String prepareFileUpload(File file) {
+        try {
+            byte[] fileData = java.nio.file.Files.readAllBytes(file.toPath());
+            String contentType = detectMediaType(file.getName());
+            String dataUrlPrefix = getDataUrlPrefix(contentType);
+            String base64Data = java.util.Base64.getEncoder().encodeToString(fileData);
+            return dataUrlPrefix + base64Data;
+        } catch (java.io.IOException e) {
+            throw new RuntimeException("Failed to prepare file for upload: " + file, e);
+        }
+    }
+
+    private static String getDataUrlPrefix(String contentType) {
+        return "data:" + contentType + ";base64,";
+    }
+
+    private static String detectMediaType(String fileName) {
+        if (fileName == null) {
+            return "application/octet-stream";
+        }
+        int dotPos = fileName.lastIndexOf('.');
+        if (dotPos == -1) {
+            return "application/octet-stream";
+        }
+        String ext = fileName.substring(dotPos + 1).toLowerCase();
+
+        switch (ext) {
+            case "jpg":
+            case "jpeg":
+                return "image/jpeg";
+            case "png":
+                return "image/png";
+            case "gif":
+                return "image/gif";
+            case "tif":
+            case "tiff":
+                return "image/tiff";
+            case "pdf":
+                return "application/pdf";
+            default:
+                return "application/octet-stream";
+        }
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/RunReportsApi.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/RunReportsApi.java
new file mode 100644
index 0000000000..dfcaa298b6
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/services/RunReportsApi.java
@@ -0,0 +1,66 @@
+/**
+ * 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.feign.services;
+
+import feign.Param;
+import feign.QueryMap;
+import feign.RequestLine;
+import feign.Response;
+import java.util.Map;
+import org.apache.fineract.client.models.RunReportsResponse;
+
+public interface RunReportsApi {
+
+    /**
+     * Running a Report This resource allows you to run and receive output from pre-defined Apache Fineract reports.
+     * Reports can also be used to provide data for searching and workflow functionality. The default output is a JSON
+     * formatted "Generic Resultset". The Generic Resultset contains Column Heading as well as Data information.
+     * However, you can export to CSV format by simply adding "&amp;exportCSV=true" to the end of your URL. If Pentaho
+     * reports have been pre-defined, they can also be run through this resource. Pentaho reports can return HTML, PDF
+     * or CSV formats. The Apache Fineract reference application uses a JQuery plugin called stretchy reporting which,
+     * itself, uses this reports resource to provide a pretty flexible reporting User Interface (UI). Example Requests:
+     * runreports/Client%20Listing?R_officeId=1 runreports/Client%20Listing?R_officeId=1&amp;exportCSV=true
+     * runreports/OfficeIdSelectOne?R_officeId=1&amp;parameterType=true
+     * runreports/OfficeIdSelectOne?R_officeId=1&amp;parameterType=true&amp;exportCSV=true
+     * runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=HTML&amp;R_officeId=1
+     * runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=XLS&amp;R_officeId=1
+     * runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=CSV&amp;R_officeId=1
+     * runreports/Expected%20Payments%20By%20Date%20-%20Formatted?R_endDate=2013-04-30&amp;R_loanOfficerId=-1&amp;R_officeId=1&amp;R_startDate=2013-04-16&amp;output-type=PDF&amp;R_officeId=1
+     *
+     * @param reportName
+     *            reportName (required)
+     * @param parameters
+     *            Dynamic query parameters for the report (required)
+     * @return RunReportsResponse
+     */
+    @RequestLine("GET /v1/runreports/{reportName}")
+    RunReportsResponse runReportGetData(@Param("reportName") String reportName, @QueryMap Map<String, String> parameters);
+
+    /**
+     * Run Report which returns a response such as a PDF, CSV, XLS or XLSX file.
+     *
+     * @param reportName
+     *            reportName (required)
+     * @param parameters
+     *            Dynamic query parameters for the report (required)
+     * @return Response containing the file content
+     */
+    @RequestLine("GET /v1/runreports/{reportName}")
+    Response runReportGetFile(@Param("reportName") String reportName, @QueryMap Map<String, String> parameters);
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/util/CallFailedRuntimeException.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/util/CallFailedRuntimeException.java
new file mode 100644
index 0000000000..c126899bb3
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/util/CallFailedRuntimeException.java
@@ -0,0 +1,91 @@
+/**
+ * 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.feign.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import feign.FeignException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Exception thrown by {@link FeignCalls} utility when Feign calls fail.
+ */
+@Slf4j
+@Getter
+public class CallFailedRuntimeException extends RuntimeException {
+
+    private final int status;
+    private final String developerMessage;
+
+    public CallFailedRuntimeException(FeignException cause) {
+        super(createMessage(cause), cause);
+        this.status = cause.status();
+        this.developerMessage = extractDeveloperMessage(cause);
+    }
+
+    private static String createMessage(FeignException e) {
+        StringBuilder sb = new StringBuilder("HTTP failed: status=").append(e.status());
+
+        if (e.request() != null) {
+            sb.append(", request=").append(e.request().url());
+        }
+
+        String contentString = e.contentUTF8();
+        if (contentString != null && !contentString.isEmpty()) {
+            sb.append(", errorBody=").append(contentString);
+        }
+
+        return sb.toString();
+    }
+
+    private static String extractDeveloperMessage(FeignException e) {
+        try {
+            byte[] content = e.content();
+            if (content == null || content.length == 0) {
+                return e.getMessage();
+            }
+
+            String contentString = new String(content, StandardCharsets.UTF_8);
+            ObjectMapper mapper = new ObjectMapper();
+            JsonNode root = mapper.readTree(contentString);
+
+            if (root.has("developerMessage")) {
+                return root.get("developerMessage").asText();
+            }
+
+            if (root.has("errors")) {
+                JsonNode errors = root.get("errors");
+                if (errors.isArray() && errors.size() > 0) {
+                    JsonNode firstError = errors.get(0);
+                    if (firstError.has("developerMessage")) {
+                        return firstError.get("developerMessage").asText();
+                    }
+                }
+            }
+
+            return contentString;
+        } catch (IOException ex) {
+            log.warn("Failed to extract developer message from error response", ex);
+            return e.getMessage();
+        }
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/util/FeignCalls.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/util/FeignCalls.java
new file mode 100644
index 0000000000..950c459c46
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/util/FeignCalls.java
@@ -0,0 +1,80 @@
+/**
+ * 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.feign.util;
+
+import feign.FeignException;
+import java.util.function.Supplier;
+
+/**
+ * Extension methods for Feign calls. This class is recommended to be statically imported.
+ */
+public final class FeignCalls {
+
+    private FeignCalls() {}
+
+    /**
+     * Execute a Feign call, expecting success, returning strongly typed body. This covers the most typical use and is
+     * thus most used.
+     *
+     * @param feignCall
+     *            the Feign call to execute
+     * @return the result of the successful call (never null)
+     * @throws CallFailedRuntimeException
+     *             thrown if a problem occurred talking to the server, or the HTTP response code was not [200..300)
+     *             successful
+     */
+    public static <T> T ok(Supplier<T> feignCall) throws CallFailedRuntimeException {
+        try {
+            return feignCall.get();
+        } catch (FeignException e) {
+            throw new CallFailedRuntimeException(e);
+        }
+    }
+
+    /**
+     * Execute a Feign call that returns void. This is useful for operations that don't return a response body.
+     *
+     * @param feignCall
+     *            the Feign call to execute
+     * @throws CallFailedRuntimeException
+     *             thrown if a problem occurred talking to the server, or the HTTP response code was not [200..300)
+     *             successful
+     */
+    public static void executeVoid(Runnable feignCall) throws CallFailedRuntimeException {
+        try {
+            feignCall.run();
+        } catch (FeignException e) {
+            throw new CallFailedRuntimeException(e);
+        }
+    }
+
+    /**
+     * Execute a Feign call, returning the result without throwing CallFailedRuntimeException. This is useful when you
+     * want to handle FeignException yourself.
+     *
+     * @param feignCall
+     *            the Feign call to execute
+     * @return the result of the call
+     * @throws FeignException
+     *             thrown if a problem occurred talking to the server
+     */
+    public static <T> T execute(Supplier<T> feignCall) throws FeignException {
+        return feignCall.get();
+    }
+}
diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/util/FeignParts.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/util/FeignParts.java
new file mode 100644
index 0000000000..cd7cd80bf3
--- /dev/null
+++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/util/FeignParts.java
@@ -0,0 +1,136 @@
+/**
+ * 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;
+
+import feign.Response;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Optional;
+import org.apache.fineract.client.feign.services.DocumentsApiFixed;
+import org.apache.fineract.client.feign.services.ImagesApi;
+
+/**
+ * Convenience utilities for file handling in Feign API calls.
+ *
+ * Provides helper methods for: - Media type detection from file extensions - File name extraction from HTTP response
+ * headers - Content type probing
+ *
+ * Used in conjunction with {@link DocumentsApiFixed} and {@link ImagesApi} which use Data URL format for file uploads
+ * (base64-encoded strings with data URI scheme).
+ */
+public final class FeignParts {
+
+    private FeignParts() {}
+
+    /**
+     * Determine the media type based on file extension.
+     *
+     * @param fileName
+     *            the name of the file
+     * @return the media type string, or null if not recognized
+     */
+    public static String mediaType(String fileName) {
+        if (fileName == null) {
+            return null;
+        }
+        int dotPos = fileName.lastIndexOf('.');
+        if (dotPos == -1) {
+            return null;
+        }
+        String ext = fileName.substring(dotPos + 1).toLowerCase();
+
+        switch (ext) {
+            case "jpg":
+            case "jpeg":
+                return "image/jpeg";
+            case "png":
+                return "image/png";
+            case "tif":
+            case "tiff":
+                return "image/tiff";
+            case "gif":
+                return "image/gif";
+            case "pdf":
+                return "application/pdf";
+            case "docx":
+                return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+            case "doc":
+                return "application/msword";
+            case "xlsx":
+                return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+            case "xls":
+                return "application/vnd.ms-excel";
+            case "odt":
+                return "application/vnd.oasis.opendocument.text";
+            case "ods":
+                return "application/vnd.oasis.opendocument.spreadsheet";
+            case "txt":
+                return "text/plain";
+            default:
+                return "application/octet-stream";
+        }
+    }
+
+    /**
+     * Extract the file name from the Content-Disposition header of a response.
+     *
+     * @param response
+     *            the HTTP response
+     * @return Optional containing the file name if present
+     */
+    public static Optional<String> fileName(Response response) {
+        if (response.headers() == null) {
+            return Optional.empty();
+        }
+
+        java.util.Collection<String> contentDispositionHeaders = response.headers().get("Content-Disposition");
+        if (contentDispositionHeaders == null || contentDispositionHeaders.isEmpty()) {
+            return Optional.empty();
+        }
+
+        String contentDisposition = contentDispositionHeaders.iterator().next();
+        if (contentDisposition == null) {
+            return Optional.empty();
+        }
+
+        int i = contentDisposition.indexOf("; filename=\"");
+        if (i == -1) {
+            return Optional.empty();
+        }
+        return Optional.of(contentDisposition.substring(i + "; filename=\"".length(), contentDisposition.length() - 1));
+    }
+
+    /**
+     * Probe the content type of a file using Files.probeContentType.
+     *
+     * @param file
+     *            the file to probe
+     * @return the content type, or application/octet-stream as fallback
+     * @throws IOException
+     *             if an I/O error occurs
+     */
+    public static String probeContentType(File file) throws IOException {
+        String contentType = Files.probeContentType(file.toPath());
+        if (contentType == null) {
+            contentType = mediaType(file.getName());
+        }
+        return contentType != null ? contentType : "application/octet-stream";
+    }
+}
diff --git a/fineract-client-feign/src/main/resources/templates/java/api.mustache b/fineract-client-feign/src/main/resources/templates/java/api.mustache
new file mode 100644
index 0000000000..44edc0a3b7
--- /dev/null
+++ b/fineract-client-feign/src/main/resources/templates/java/api.mustache
@@ -0,0 +1,47 @@
+package {{package}};
+
+import feign.*;
+import feign.Param;
+import feign.RequestLine;
+
+{{#imports}}import {{import}};
+{{/imports}}
+
+import java.util.List;
+import java.util.Map;
+
+public interface {{classname}} {
+
+{{#operations}}
+    {{#operation}}
+        /**
+        * {{summary}}
+        * {{notes}}
+        {{#allParams}}
+            * @param {{paramName}} {{description}}
+        {{/allParams}}
+        * @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}
+        */
+        @RequestLine("{{httpMethod}} {{path}}")
+        {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} {{operationId}}(
+        {{#pathParams}}
+            @Param("{{baseName}}") {{{dataType}}} {{paramName}}{{^-last}},{{/-last}}
+        {{/pathParams}}
+        {{#bodyParam}}
+            {{#hasPathParams}},{{/hasPathParams}}{{{dataType}}} {{paramName}}
+        {{/bodyParam}}
+        {{#hasQueryParams}}
+            {{#hasPathParams}}{{^bodyParam}},{{/bodyParam}}{{/hasPathParams}}{{#bodyParam}},{{/bodyParam}}
+            @QueryMap Map<String, Object> queryParams
+        {{/hasQueryParams}}
+        {{#hasHeaderParams}}
+            {{#hasPathParams}}{{^bodyParam}}{{^hasQueryParams}},{{/hasQueryParams}}{{/bodyParam}}{{/hasPathParams}}
+            {{#bodyParam}}{{^hasQueryParams}},{{/hasQueryParams}}{{/bodyParam}}
+            {{#hasQueryParams}},{{/hasQueryParams}}
+            @HeaderMap Map<String, Object> headerParams
+        {{/hasHeaderParams}}
+        );
+
+    {{/operation}}
+{{/operations}}
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/adapter/ExternalIdAdapterTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/adapter/ExternalIdAdapterTest.java
new file mode 100644
index 0000000000..0840a1aa2c
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/adapter/ExternalIdAdapterTest.java
@@ -0,0 +1,192 @@
+/**
+ * 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.adapter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.fineract.client.feign.ObjectMapperFactory;
+import org.apache.fineract.client.models.ExternalId;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ExternalIdAdapterTest {
+
+    private ObjectMapper mapper;
+
+    @BeforeEach
+    void setUp() {
+        mapper = ObjectMapperFactory.getShared();
+    }
+
+    @Test
+    void testSerializeExternalId() throws Exception {
+        ExternalId externalId = new ExternalId().value("EXT-12345");
+
+        String json = mapper.writeValueAsString(externalId);
+
+        assertThat(json).isEqualTo("\"EXT-12345\"");
+    }
+
+    @Test
+    void testSerializeExternalIdWithSpecialCharacters() throws Exception {
+        ExternalId externalId = new ExternalId().value("EXT-ID-2024/01/15");
+
+        String json = mapper.writeValueAsString(externalId);
+
+        assertThat(json).isEqualTo("\"EXT-ID-2024/01/15\"");
+    }
+
+    @Test
+    void testSerializeNullExternalId() throws Exception {
+        ExternalId externalId = new ExternalId().value(null);
+
+        String json = mapper.writeValueAsString(externalId);
+
+        assertThat(json).isEqualTo("null");
+    }
+
+    @Test
+    void testSerializeExternalIdObjectNull() throws Exception {
+        ExternalId externalId = null;
+
+        String json = mapper.writeValueAsString(externalId);
+
+        assertThat(json).isEqualTo("null");
+    }
+
+    @Test
+    void testDeserializeExternalId() throws Exception {
+        String json = "\"EXT-67890\"";
+
+        ExternalId externalId = mapper.readValue(json, ExternalId.class);
+
+        assertThat(externalId).isNotNull();
+        assertThat(externalId.getValue()).isEqualTo("EXT-67890");
+    }
+
+    @Test
+    void testDeserializeExternalIdWithSpecialCharacters() throws Exception {
+        String json = "\"CLIENT-2024-001/ABC\"";
+
+        ExternalId externalId = mapper.readValue(json, ExternalId.class);
+
+        assertThat(externalId).isNotNull();
+        assertThat(externalId.getValue()).isEqualTo("CLIENT-2024-001/ABC");
+    }
+
+    @Test
+    void testDeserializeNullExternalId() throws Exception {
+        String json = "null";
+
+        ExternalId externalId = mapper.readValue(json, ExternalId.class);
+
+        assertThat(externalId).isNull();
+    }
+
+    @Test
+    void testRoundTripSerialization() throws Exception {
+        ExternalId original = new ExternalId().value("TEST-ID-001");
+
+        String json = mapper.writeValueAsString(original);
+        ExternalId deserialized = mapper.readValue(json, ExternalId.class);
+
+        assertThat(deserialized.getValue()).isEqualTo(original.getValue());
+    }
+
+    @Test
+    void testRoundTripSerializationWithUUID() throws Exception {
+        ExternalId original = new ExternalId().value("550e8400-e29b-41d4-a716-446655440000");
+
+        String json = mapper.writeValueAsString(original);
+        ExternalId deserialized = mapper.readValue(json, ExternalId.class);
+
+        assertThat(deserialized.getValue()).isEqualTo(original.getValue());
+    }
+
+    @Test
+    void testRoundTripSerializationWithNumericString() throws Exception {
+        ExternalId original = new ExternalId().value("123456789");
+
+        String json = mapper.writeValueAsString(original);
+        ExternalId deserialized = mapper.readValue(json, ExternalId.class);
+
+        assertThat(deserialized.getValue()).isEqualTo(original.getValue());
+    }
+
+    @Test
+    void testDeserializeEmptyString() throws Exception {
+        String json = "\"\"";
+
+        ExternalId externalId = mapper.readValue(json, ExternalId.class);
+
+        assertThat(externalId).isNotNull();
+        assertThat(externalId.getValue()).isEmpty();
+    }
+
+    @Test
+    void testSerializeExternalIdWithEmptyValue() throws Exception {
+        ExternalId externalId = new ExternalId().value("");
+
+        String json = mapper.writeValueAsString(externalId);
+
+        assertThat(json).isEqualTo("\"\"");
+    }
+
+    @Test
+    void testModuleCreation() {
+        assertThat(ExternalIdAdapter.createModule()).isNotNull();
+        assertThat(ExternalIdAdapter.createModule().getModuleName()).isEqualTo("ExternalIdModule");
+    }
+
+    @Test
+    void testDeserializeExternalIdWithWhitespace() throws Exception {
+        String json = "\"  EXT-001  \"";
+
+        ExternalId externalId = mapper.readValue(json, ExternalId.class);
+
+        assertThat(externalId).isNotNull();
+        assertThat(externalId.getValue()).isEqualTo("  EXT-001  ");
+    }
+
+    @Test
+    void testSerializeExternalIdInObject() throws Exception {
+        TestObject obj = new TestObject();
+        obj.externalId = new ExternalId().value("TEST-123");
+
+        String json = mapper.writeValueAsString(obj);
+
+        assertThat(json).contains("\"externalId\":\"TEST-123\"");
+    }
+
+    @Test
+    void testDeserializeExternalIdInObject() throws Exception {
+        String json = "{\"externalId\":\"TEST-456\"}";
+
+        TestObject obj = mapper.readValue(json, TestObject.class);
+
+        assertThat(obj.externalId).isNotNull();
+        assertThat(obj.externalId.getValue()).isEqualTo("TEST-456");
+    }
+
+    static class TestObject {
+
+        public ExternalId externalId;
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FeignExceptionTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FeignExceptionTest.java
new file mode 100644
index 0000000000..a61d5e0f1e
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FeignExceptionTest.java
@@ -0,0 +1,140 @@
+/**
+ * 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.feign;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import feign.Request;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class FeignExceptionTest {
+
+    private Request request;
+    private byte[] responseBody;
+
+    @BeforeEach
+    void setUp() {
+        request = Request.create(Request.HttpMethod.GET, "/api/test", Collections.emptyMap(), null, StandardCharsets.UTF_8, null);
+        responseBody = "Error response body".getBytes(StandardCharsets.UTF_8);
+    }
+
+    @Test
+    void testConstructorWithErrorDetails() {
+        String developerMessage = "Technical error details";
+        String userMessage = "User-friendly error message";
+
+        FeignException exception = new FeignException(400, "Error message", request, responseBody, developerMessage, userMessage);
+
+        assertNotNull(exception);
+        assertEquals(400, exception.status());
+        assertEquals(developerMessage, exception.getDeveloperMessage());
+        assertEquals(userMessage, exception.getUserMessage());
+        assertEquals(request, exception.request());
+        assertEquals(responseBody, exception.responseBody());
+    }
+
+    @Test
+    void testGetMessage() {
+        String developerMessage = "Developer error";
+        String userMessage = "User error";
+
+        FeignException exception = new FeignException(500, "Base message", request, responseBody, developerMessage, userMessage);
+
+        String message = exception.getMessage();
+        assertNotNull(message);
+        assertTrue(message.contains("500"));
+        assertTrue(message.contains(userMessage));
+        assertTrue(message.contains(developerMessage));
+    }
+
+    @Test
+    void testGetDeveloperMessage() {
+        String developerMessage = "This is a technical error";
+
+        FeignException exception = new FeignException(400, "Error", request, responseBody, developerMessage, null);
+
+        assertEquals(developerMessage, exception.getDeveloperMessage());
+    }
+
+    @Test
+    void testGetUserMessage() {
+        String userMessage = "This is a user-friendly error";
+
+        FeignException exception = new FeignException(400, "Error", request, responseBody, null, userMessage);
+
+        assertEquals(userMessage, exception.getUserMessage());
+    }
+
+    @Test
+    void testNullErrorMessages() {
+        FeignException exception = new FeignException(404, "Not found", request, responseBody, null, null);
+
+        assertNull(exception.getDeveloperMessage());
+        assertNull(exception.getUserMessage());
+        String message = exception.getMessage();
+        assertNotNull(message);
+        assertTrue(message.contains("404"));
+        assertTrue(message.contains("Not found"));
+    }
+
+    @Test
+    void testGetMessageWithOnlyUserMessage() {
+        String userMessage = "User error only";
+
+        FeignException exception = new FeignException(400, "Base message", request, responseBody, null, userMessage);
+
+        String message = exception.getMessage();
+        assertTrue(message.contains("400"));
+        assertTrue(message.contains(userMessage));
+    }
+
+    @Test
+    void testGetMessageWithOnlyDeveloperMessage() {
+        String developerMessage = "Developer error only";
+
+        FeignException exception = new FeignException(400, "Base message", request, responseBody, developerMessage, null);
+
+        String message = exception.getMessage();
+        assertTrue(message.contains("400"));
+        assertTrue(message.contains(developerMessage));
+    }
+
+    @Test
+    void testResponseBodyAsString() {
+        FeignException exception = new FeignException(400, "Error", request, responseBody, null, null);
+
+        String bodyString = exception.responseBodyAsString();
+        assertNotNull(bodyString);
+        assertEquals("Error response body", bodyString);
+    }
+
+    @Test
+    void testResponseBodyAsStringWithNullBody() {
+        FeignException exception = new FeignException(400, "Error", request, null, null, null);
+
+        String bodyString = exception.responseBodyAsString();
+        assertNull(bodyString);
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FineractErrorDecoderTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FineractErrorDecoderTest.java
new file mode 100644
index 0000000000..59d580fe88
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FineractErrorDecoderTest.java
@@ -0,0 +1,180 @@
+/**
+ * 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.feign;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import feign.Request;
+import feign.Response;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class FineractErrorDecoderTest {
+
+    private FineractErrorDecoder decoder;
+    private Request request;
+
+    @BeforeEach
+    void setUp() {
+        decoder = new FineractErrorDecoder();
+        request = Request.create(Request.HttpMethod.GET, "/api/test", Collections.emptyMap(), null, StandardCharsets.UTF_8, null);
+    }
+
+    @Test
+    void testDecodeValidFineractError() {
+        String jsonError = "{\"developerMessage\":\"Developer error message\",\"userMessage\":\"User error message\"}";
+        Response response = createResponse(400, jsonError);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertInstanceOf(FeignException.class, exception);
+        FeignException feignException = (FeignException) exception;
+        assertEquals(400, feignException.status());
+        assertEquals("User error message", feignException.getUserMessage());
+        assertEquals("Developer error message", feignException.getDeveloperMessage());
+    }
+
+    @Test
+    void testDecodeInvalidJson() {
+        String invalidJson = "This is not valid JSON";
+        Response response = createResponse(400, invalidJson);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertNotNull(exception);
+    }
+
+    @Test
+    void testDecodeNullBody() {
+        Response response = createResponse(404, null);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertNotNull(exception);
+    }
+
+    @Test
+    void testDecode400Error() {
+        String jsonError = "{\"developerMessage\":\"Bad request details\",\"userMessage\":\"Invalid input\"}";
+        Response response = createResponse(400, jsonError);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertInstanceOf(FeignException.class, exception);
+        FeignException feignException = (FeignException) exception;
+        assertEquals(400, feignException.status());
+    }
+
+    @Test
+    void testDecode404Error() {
+        String jsonError = "{\"developerMessage\":\"Resource not found details\",\"userMessage\":\"Not found\"}";
+        Response response = createResponse(404, jsonError);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertInstanceOf(FeignException.class, exception);
+        FeignException feignException = (FeignException) exception;
+        assertEquals(404, feignException.status());
+    }
+
+    @Test
+    void testDecode500Error() {
+        String jsonError = "{\"developerMessage\":\"Internal server error details\",\"userMessage\":\"Server error\"}";
+        Response response = createResponse(500, jsonError);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertInstanceOf(FeignException.class, exception);
+        FeignException feignException = (FeignException) exception;
+        assertEquals(500, feignException.status());
+    }
+
+    @Test
+    void testExtractDeveloperMessage() {
+        String jsonError = "{\"developerMessage\":\"Technical details here\"}";
+        Response response = createResponse(400, jsonError);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertInstanceOf(FeignException.class, exception);
+        FeignException feignException = (FeignException) exception;
+        assertEquals("Technical details here", feignException.getDeveloperMessage());
+    }
+
+    @Test
+    void testExtractUserMessage() {
+        String jsonError = "{\"userMessage\":\"User-friendly message\"}";
+        Response response = createResponse(400, jsonError);
+
+        Exception exception = decoder.decode("Test#method", response);
+
+        assertInstanceOf(FeignException.class, exception);
+        FeignException feignException = (FeignException) exception;
+        assertEquals("User-friendly message", feignException.getUserMessage());
+    }
+
+    private Response createResponse(int status, String body) {
+        Map<String, Collection<String>> headers = new HashMap<>();
+
+        Response.Body responseBody = null;
+        if (body != null) {
+            byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
+            responseBody = new Response.Body() {
+
+                @Override
+                public Integer length() {
+                    return bodyBytes.length;
+                }
+
+                @Override
+                public boolean isRepeatable() {
+                    return true;
+                }
+
+                @Override
+                public ByteArrayInputStream asInputStream() {
+                    return new ByteArrayInputStream(bodyBytes);
+                }
+
+                @Override
+                public java.io.Reader asReader() {
+                    return new java.io.InputStreamReader(asInputStream(), StandardCharsets.UTF_8);
+                }
+
+                @Override
+                public java.io.Reader asReader(java.nio.charset.Charset charset) {
+                    return new java.io.InputStreamReader(asInputStream(), charset);
+                }
+
+                @Override
+                public void close() {}
+            };
+        }
+
+        return Response.builder().status(status).reason("Test").request(request).headers(headers).body(responseBody).build();
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FineractFeignClientConfigTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FineractFeignClientConfigTest.java
new file mode 100644
index 0000000000..d2de297de5
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/FineractFeignClientConfigTest.java
@@ -0,0 +1,93 @@
+/**
+ * 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.feign;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import feign.RequestLine;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+class FineractFeignClientConfigTest {
+
+    @Test
+    void testBuilderDefaults() {
+        FineractFeignClientConfig.Builder builder = FineractFeignClientConfig.builder();
+        FineractFeignClientConfig config = builder.baseUrl("http://localhost:8080").credentials("admin", "password").build();
+
+        assertNotNull(config);
+    }
+
+    @Test
+    void testBuilderConfiguration() {
+        String baseUrl = "http://example.com:8080";
+        String username = "testuser";
+        String password = "testpass";
+        int connectTimeoutSeconds = 10;
+        int readTimeoutSeconds = 30;
+
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials(username, password)
+                .connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS).readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
+                .debugEnabled(true).build();
+
+        assertNotNull(config);
+    }
+
+    @Test
+    void testConnectionTimeToLiveConfiguration() {
+        long ttl = 5;
+        TimeUnit ttlUnit = TimeUnit.MINUTES;
+
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
+                .credentials("admin", "password").connectionTimeToLive(ttl, ttlUnit).build();
+
+        assertNotNull(config);
+    }
+
+    @Test
+    void testEncoderDecoderConfiguration() {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
+                .credentials("admin", "password").build();
+
+        assertNotNull(config);
+    }
+
+    @Test
+    void testErrorDecoderConfiguration() {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
+                .credentials("admin", "password").build();
+
+        assertNotNull(config);
+    }
+
+    @Test
+    void testClientCreation() {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:8080")
+                .credentials("admin", "password").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+
+        TestApi client = config.createClient(TestApi.class);
+        assertNotNull(client);
+    }
+
+    interface TestApi {
+
+        @RequestLine("GET /test")
+        String test();
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/ObjectMapperFactoryTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/ObjectMapperFactoryTest.java
new file mode 100644
index 0000000000..237ee4779f
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/ObjectMapperFactoryTest.java
@@ -0,0 +1,120 @@
+/**
+ * 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.feign;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.time.LocalDate;
+import org.junit.jupiter.api.Test;
+
+class ObjectMapperFactoryTest {
+
+    @Test
+    void testSharedObjectMapperNotNull() {
+        ObjectMapper mapper = ObjectMapperFactory.getShared();
+        assertNotNull(mapper);
+    }
+
+    @Test
+    void testSharedObjectMapperSingleton() {
+        ObjectMapper mapper1 = ObjectMapperFactory.getShared();
+        ObjectMapper mapper2 = ObjectMapperFactory.getShared();
+        assertSame(mapper1, mapper2);
+    }
+
+    @Test
+    void testJava8DateTimeSerialization() throws JsonProcessingException {
+        ObjectMapper mapper = ObjectMapperFactory.getShared();
+        LocalDate date = LocalDate.of(2024, 1, 15);
+
+        String json = mapper.writeValueAsString(new TestObject(date));
+        assertNotNull(json);
+    }
+
+    @Test
+    void testJava8DateTimeDeserialization() throws IOException {
+        ObjectMapper mapper = ObjectMapperFactory.getShared();
+        String json = "{\"date\":\"2024-01-15\"}";
+
+        TestObject obj = mapper.readValue(json, TestObject.class);
+        assertNotNull(obj);
+        assertNotNull(obj.getDate());
+        assertEquals(LocalDate.of(2024, 1, 15), obj.getDate());
+    }
+
+    @Test
+    void testNullHandling() throws JsonProcessingException {
+        ObjectMapper mapper = ObjectMapperFactory.getShared();
+        TestObject obj = new TestObject(null);
+
+        String json = mapper.writeValueAsString(obj);
+        assertNotNull(json);
+    }
+
+    @Test
+    void testDeserializeNullValue() throws IOException {
+        ObjectMapper mapper = ObjectMapperFactory.getShared();
+        String json = "{\"date\":null}";
+
+        TestObject obj = mapper.readValue(json, TestObject.class);
+        assertNotNull(obj);
+        assertNull(obj.getDate());
+    }
+
+    @Test
+    void testUnknownPropertiesIgnored() throws IOException {
+        ObjectMapper mapper = ObjectMapperFactory.getShared();
+        String json = "{\"date\":\"2024-01-15\",\"unknownField\":\"value\"}";
+
+        TestObject obj = mapper.readValue(json, TestObject.class);
+        assertNotNull(obj);
+        assertEquals(LocalDate.of(2024, 1, 15), obj.getDate());
+    }
+
+    @Test
+    void testCreateObjectMapperNotNull() {
+        ObjectMapper mapper = ObjectMapperFactory.createObjectMapper();
+        assertNotNull(mapper);
+    }
+
+    static class TestObject {
+
+        private LocalDate date;
+
+        TestObject() {}
+
+        TestObject(LocalDate date) {
+            this.date = date;
+        }
+
+        public LocalDate getDate() {
+            return date;
+        }
+
+        public void setDate(LocalDate date) {
+            this.date = date;
+        }
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/integration/EncoderDecoderIntegrationTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/integration/EncoderDecoderIntegrationTest.java
new file mode 100644
index 0000000000..e486de21b5
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/integration/EncoderDecoderIntegrationTest.java
@@ -0,0 +1,421 @@
+/**
+ * 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.feign.integration;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import feign.RequestLine;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.fineract.client.feign.FineractFeignClientConfig;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("integration")
+class EncoderDecoderIntegrationTest {
+
+    private WireMockServer wireMockServer;
+    private String baseUrl;
+    private FineractFeignClientConfig config;
+
+    @BeforeEach
+    void setUp() {
+        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
+        wireMockServer.start();
+        baseUrl = "http://localhost:" + wireMockServer.port();
+
+        config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
+                .connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (wireMockServer != null) {
+            wireMockServer.stop();
+        }
+    }
+
+    @Test
+    void testComplexObjectSerialization() {
+        wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(aResponse().withStatus(201)
+                .withHeader("Content-Type", "application/json").withBody("{\"id\":100,\"status\":\"created\"}")));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName("complex-test");
+        request.setAmount(new BigDecimal("1234.56"));
+        request.setCreatedDate(LocalDate.of(2024, 1, 15));
+        request.setItems(Arrays.asList("item1", "item2", "item3"));
+
+        Map<String, String> metadata = new HashMap<>();
+        metadata.put("key1", "value1");
+        metadata.put("key2", "value2");
+        request.setMetadata(metadata);
+
+        NestedObject nested = new NestedObject();
+        nested.setNestedName("nested-value");
+        nested.setNestedDate(LocalDate.of(2024, 2, 20));
+        request.setNested(nested);
+
+        SimpleResponse response = client.createComplex(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getId()).isEqualTo(100L);
+        assertThat(response.getStatus()).isEqualTo("created");
+    }
+
+    @Test
+    void testNullValueHandling() {
+        wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":null}")));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName("null-test");
+        request.setAmount(null);
+        request.setCreatedDate(null);
+        request.setItems(null);
+        request.setMetadata(null);
+        request.setNested(null);
+
+        SimpleResponse response = client.createComplex(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getId()).isEqualTo(1L);
+        assertThat(response.getStatus()).isNull();
+    }
+
+    @Test
+    void testDateFormats() {
+        String responseJson = "{\"id\":1,\"date\":\"2024-03-15\",\"dateTime\":\"2024-03-15T14:30:00\",\"timestamp\":\"2024-03-15T14:30:00.123\"}";
+
+        wireMockServer.stubFor(post(urlEqualTo("/date-test"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseJson)));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName("date-test");
+        request.setCreatedDate(LocalDate.of(2024, 3, 15));
+
+        DateResponse response = client.testDates(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getDate()).isEqualTo(LocalDate.of(2024, 3, 15));
+        assertThat(response.getDateTime()).isEqualTo(LocalDateTime.of(2024, 3, 15, 14, 30, 0));
+        assertThat(response.getTimestamp()).isNotNull();
+    }
+
+    @Test
+    void testLargePayload() {
+        StringBuilder largeString = new StringBuilder();
+        for (int i = 0; i < 1000; i++) {
+            largeString.append("This is line ").append(i).append(". ");
+        }
+
+        wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(aResponse().withStatus(200)
+                .withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"processed\"}")));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName(largeString.toString());
+        request.setAmount(new BigDecimal("999999.99"));
+        request.setCreatedDate(LocalDate.now(ZoneId.systemDefault()));
+
+        SimpleResponse response = client.createComplex(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getStatus()).isEqualTo("processed");
+    }
+
+    @Test
+    void testEmptyCollections() {
+        wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"ok\"}")));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName("empty-test");
+        request.setItems(Arrays.asList());
+        request.setMetadata(new HashMap<>());
+
+        SimpleResponse response = client.createComplex(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getStatus()).isEqualTo("ok");
+    }
+
+    @Test
+    void testBigDecimalPrecision() {
+        wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"precise\"}")));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName("precision-test");
+        request.setAmount(new BigDecimal("123456789.123456789"));
+
+        SimpleResponse response = client.createComplex(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getStatus()).isEqualTo("precise");
+    }
+
+    @Test
+    void testNestedObjectDeserialization() {
+        String responseJson = "{\"id\":1,\"nested\":{\"nestedName\":\"deep-value\",\"nestedDate\":\"2024-04-01\"},\"items\":[\"a\",\"b\",\"c\"]}";
+
+        wireMockServer.stubFor(post(urlEqualTo("/nested"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseJson)));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName("nested-test");
+
+        NestedResponse response = client.testNested(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getId()).isEqualTo(1L);
+        assertThat(response.getNested()).isNotNull();
+        assertThat(response.getNested().getNestedName()).isEqualTo("deep-value");
+        assertThat(response.getNested().getNestedDate()).isEqualTo(LocalDate.of(2024, 4, 1));
+        assertThat(response.getItems()).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    void testSpecialCharacterHandling() {
+        wireMockServer.stubFor(post(urlEqualTo("/complex")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"id\":1,\"status\":\"special\"}")));
+
+        ComplexApi client = config.createClient(ComplexApi.class);
+
+        ComplexRequest request = new ComplexRequest();
+        request.setName("Test with special chars: <>\"'&\n\t");
+
+        SimpleResponse response = client.createComplex(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getStatus()).isEqualTo("special");
+    }
+
+    interface ComplexApi {
+
+        @RequestLine("POST /complex")
+        SimpleResponse createComplex(ComplexRequest request);
+
+        @RequestLine("POST /date-test")
+        DateResponse testDates(ComplexRequest request);
+
+        @RequestLine("POST /nested")
+        NestedResponse testNested(ComplexRequest request);
+    }
+
+    static class ComplexRequest {
+
+        private String name;
+        private BigDecimal amount;
+        private LocalDate createdDate;
+        private List<String> items;
+        private Map<String, String> metadata;
+        private NestedObject nested;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public BigDecimal getAmount() {
+            return amount;
+        }
+
+        public void setAmount(BigDecimal amount) {
+            this.amount = amount;
+        }
+
+        public LocalDate getCreatedDate() {
+            return createdDate;
+        }
+
+        public void setCreatedDate(LocalDate createdDate) {
+            this.createdDate = createdDate;
+        }
+
+        public List<String> getItems() {
+            return items;
+        }
+
+        public void setItems(List<String> items) {
+            this.items = items;
+        }
+
+        public Map<String, String> getMetadata() {
+            return metadata;
+        }
+
+        public void setMetadata(Map<String, String> metadata) {
+            this.metadata = metadata;
+        }
+
+        public NestedObject getNested() {
+            return nested;
+        }
+
+        public void setNested(NestedObject nested) {
+            this.nested = nested;
+        }
+    }
+
+    static class NestedObject {
+
+        private String nestedName;
+        private LocalDate nestedDate;
+
+        public String getNestedName() {
+            return nestedName;
+        }
+
+        public void setNestedName(String nestedName) {
+            this.nestedName = nestedName;
+        }
+
+        public LocalDate getNestedDate() {
+            return nestedDate;
+        }
+
+        public void setNestedDate(LocalDate nestedDate) {
+            this.nestedDate = nestedDate;
+        }
+    }
+
+    static class SimpleResponse {
+
+        private Long id;
+        private String status;
+
+        public Long getId() {
+            return id;
+        }
+
+        public void setId(Long id) {
+            this.id = id;
+        }
+
+        public String getStatus() {
+            return status;
+        }
+
+        public void setStatus(String status) {
+            this.status = status;
+        }
+    }
+
+    static class DateResponse {
+
+        private Long id;
+        private LocalDate date;
+        private LocalDateTime dateTime;
+        private LocalDateTime timestamp;
+
+        public Long getId() {
+            return id;
+        }
+
+        public void setId(Long id) {
+            this.id = id;
+        }
+
+        public LocalDate getDate() {
+            return date;
+        }
+
+        public void setDate(LocalDate date) {
+            this.date = date;
+        }
+
+        public LocalDateTime getDateTime() {
+            return dateTime;
+        }
+
+        public void setDateTime(LocalDateTime dateTime) {
+            this.dateTime = dateTime;
+        }
+
+        public LocalDateTime getTimestamp() {
+            return timestamp;
+        }
+
+        public void setTimestamp(LocalDateTime timestamp) {
+            this.timestamp = timestamp;
+        }
+    }
+
+    static class NestedResponse {
+
+        private Long id;
+        private NestedObject nested;
+        private List<String> items;
+
+        public Long getId() {
+            return id;
+        }
+
+        public void setId(Long id) {
+            this.id = id;
+        }
+
+        public NestedObject getNested() {
+            return nested;
+        }
+
+        public void setNested(NestedObject nested) {
+            this.nested = nested;
+        }
+
+        public List<String> getItems() {
+            return items;
+        }
+
+        public void setItems(List<String> items) {
+            this.items = items;
+        }
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/integration/FineractFeignClientIntegrationTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/integration/FineractFeignClientIntegrationTest.java
new file mode 100644
index 0000000000..290428028b
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/integration/FineractFeignClientIntegrationTest.java
@@ -0,0 +1,252 @@
+/**
+ * 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.feign.integration;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import feign.RequestLine;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.concurrent.TimeUnit;
+import org.apache.fineract.client.feign.FeignException;
+import org.apache.fineract.client.feign.FineractFeignClientConfig;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("integration")
+class FineractFeignClientIntegrationTest {
+
+    private WireMockServer wireMockServer;
+    private String baseUrl;
+    private FineractFeignClientConfig config;
+
+    @BeforeEach
+    void setUp() {
+        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
+        wireMockServer.start();
+        baseUrl = "http://localhost:" + wireMockServer.port();
+
+        config = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
+                .connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (wireMockServer != null) {
+            wireMockServer.stop();
+        }
+    }
+
+    @Test
+    void testJsonEncoding() {
+        wireMockServer
+                .stubFor(post(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
+                        .withBody("{\"id\":1,\"message\":\"success\",\"timestamp\":\"2024-01-15T10:30:00\"}")));
+
+        TestApi client = config.createClient(TestApi.class);
+        TestRequest request = new TestRequest();
+        request.setName("test-name");
+        request.setDate(LocalDate.of(2024, 1, 15));
+
+        TestResponse response = client.createTest(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getId()).isEqualTo(1L);
+        assertThat(response.getMessage()).isEqualTo("success");
+        assertThat(response.getTimestamp()).isEqualTo(LocalDateTime.of(2024, 1, 15, 10, 30, 0));
+    }
+
+    @Test
+    void testJsonDecoding() {
+        wireMockServer
+                .stubFor(get(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
+                        .withBody("{\"id\":42,\"message\":\"decoded-message\",\"timestamp\":\"2024-12-25T18:45:30\"}")));
+
+        TestApi client = config.createClient(TestApi.class);
+        TestResponse response = client.getTest();
+
+        assertThat(response).isNotNull();
+        assertThat(response.getId()).isEqualTo(42L);
+        assertThat(response.getMessage()).isEqualTo("decoded-message");
+        assertThat(response.getTimestamp()).isEqualTo(LocalDateTime.of(2024, 12, 25, 18, 45, 30));
+    }
+
+    @Test
+    void testJava8DateTimeSerialization() {
+        wireMockServer.stubFor(post(urlEqualTo("/test")).withRequestBody(containing("2024-06-30"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
+                        .withBody("{\"id\":99,\"message\":\"date-test\",\"timestamp\":\"2024-06-30T12:00:00\"}")));
+
+        TestApi client = config.createClient(TestApi.class);
+        TestRequest request = new TestRequest();
+        request.setName("date-test");
+        request.setDate(LocalDate.of(2024, 6, 30));
+
+        TestResponse response = client.createTest(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getId()).isEqualTo(99L);
+        assertThat(response.getTimestamp()).isNotNull();
+        assertThat(response.getTimestamp().toLocalDate()).isEqualTo(LocalDate.of(2024, 6, 30));
+    }
+
+    @Test
+    void testErrorDecoder() {
+        wireMockServer.stubFor(
+                get(urlEqualTo("/error")).willReturn(aResponse().withStatus(404).withHeader("Content-Type", "application/json").withBody(
+                        "{\"developerMessage\":\"Resource not found\",\"httpStatusCode\":\"404\",\"defaultUserMessage\":\"Not Found\"}")));
+
+        TestApi client = config.createClient(TestApi.class);
+
+        assertThatThrownBy(client::getError).isInstanceOf(FeignException.class).hasMessageContaining("Resource not found");
+    }
+
+    @Test
+    void testErrorDecoderWithServerError() {
+        wireMockServer.stubFor(
+                get(urlEqualTo("/error")).willReturn(aResponse().withStatus(500).withHeader("Content-Type", "application/json").withBody(
+                        "{\"developerMessage\":\"Internal server error occurred\",\"httpStatusCode\":\"500\",\"defaultUserMessage\":\"Server Error\"}")));
+
+        TestApi client = config.createClient(TestApi.class);
+
+        assertThatThrownBy(client::getError).isInstanceOf(FeignException.class).hasMessageContaining("Internal server error occurred");
+    }
+
+    @Test
+    void testConnectionTimeToLiveConfiguration() {
+        FineractFeignClientConfig ttlConfig = FineractFeignClientConfig.builder().baseUrl(baseUrl).credentials("testuser", "testpass")
+                .connectionTimeToLive(5, TimeUnit.MINUTES).build();
+
+        wireMockServer
+                .stubFor(get(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
+                        .withBody("{\"id\":1,\"message\":\"ttl-test\",\"timestamp\":\"2024-01-01T00:00:00\"}")));
+
+        TestApi client = ttlConfig.createClient(TestApi.class);
+        TestResponse response = client.getTest();
+
+        assertThat(response).isNotNull();
+        assertThat(response.getMessage()).isEqualTo("ttl-test");
+    }
+
+    @Test
+    void testBasicAuthentication() {
+        wireMockServer.stubFor(get(urlEqualTo("/test")).withHeader("Authorization", equalTo("Basic dGVzdHVzZXI6dGVzdHBhc3M="))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
+                        .withBody("{\"id\":1,\"message\":\"authenticated\",\"timestamp\":\"2024-01-01T00:00:00\"}")));
+
+        TestApi client = config.createClient(TestApi.class);
+        TestResponse response = client.getTest();
+
+        assertThat(response).isNotNull();
+        assertThat(response.getMessage()).isEqualTo("authenticated");
+    }
+
+    @Test
+    void testNullValueHandling() {
+        wireMockServer
+                .stubFor(post(urlEqualTo("/test")).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
+                        .withBody("{\"id\":1,\"message\":null,\"timestamp\":\"2024-01-01T00:00:00\"}")));
+
+        TestApi client = config.createClient(TestApi.class);
+        TestRequest request = new TestRequest();
+        request.setName("null-test");
+        request.setDate(null);
+
+        TestResponse response = client.createTest(request);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getMessage()).isNull();
+    }
+
+    interface TestApi {
+
+        @RequestLine("GET /test")
+        TestResponse getTest();
+
+        @RequestLine("POST /test")
+        TestResponse createTest(TestRequest request);
+
+        @RequestLine("GET /error")
+        TestResponse getError();
+    }
+
+    static class TestRequest {
+
+        private String name;
+        private LocalDate date;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public LocalDate getDate() {
+            return date;
+        }
+
+        public void setDate(LocalDate date) {
+            this.date = date;
+        }
+    }
+
+    static class TestResponse {
+
+        private Long id;
+        private String message;
+        private LocalDateTime timestamp;
+
+        public Long getId() {
+            return id;
+        }
+
+        public void setId(Long id) {
+            this.id = id;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public void setMessage(String message) {
+            this.message = message;
+        }
+
+        public LocalDateTime getTimestamp() {
+            return timestamp;
+        }
+
+        public void setTimestamp(LocalDateTime timestamp) {
+            this.timestamp = timestamp;
+        }
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/performance/ConnectionPoolPerformanceTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/performance/ConnectionPoolPerformanceTest.java
new file mode 100644
index 0000000000..74a3121abc
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/feign/performance/ConnectionPoolPerformanceTest.java
@@ -0,0 +1,269 @@
+/**
+ * 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.feign.performance;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import feign.RequestLine;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.apache.fineract.client.feign.FineractFeignClientConfig;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("performance")
+class ConnectionPoolPerformanceTest {
+
+    private WireMockServer wireMockServer;
+
+    @BeforeEach
+    void setUp() {
+        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
+        wireMockServer.start();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (wireMockServer != null) {
+            wireMockServer.stop();
+        }
+    }
+
+    @Test
+    void testConnectionPoolTTL() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").connectionTimeToLive(2, TimeUnit.SECONDS).connectTimeout(5, TimeUnit.SECONDS)
+                .readTimeout(10, TimeUnit.SECONDS).build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        String response1 = api.getTest();
+        assertThat(response1).isEqualTo("OK");
+
+        Thread.sleep(3000);
+
+        String response2 = api.getTest();
+        assertThat(response2).isEqualTo("OK");
+    }
+
+    @Test
+    void testConnectionPoolTTLWithMultipleRequests() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").connectionTimeToLive(1, TimeUnit.SECONDS).connectTimeout(5, TimeUnit.SECONDS)
+                .readTimeout(10, TimeUnit.SECONDS).build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        for (int i = 0; i < 5; i++) {
+            String response = api.getTest();
+            assertThat(response).isEqualTo("OK");
+            Thread.sleep(1500);
+        }
+    }
+
+    @Test
+    void testConcurrentRequests() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"").withFixedDelay(100)));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        int numRequests = 50;
+        ExecutorService executor = Executors.newFixedThreadPool(10);
+        List<Future<String>> futures = new ArrayList<>();
+
+        long startTime = System.currentTimeMillis();
+
+        for (int i = 0; i < numRequests; i++) {
+            futures.add(executor.submit(api::getTest));
+        }
+
+        for (Future<String> future : futures) {
+            assertThat(future.get()).isEqualTo("OK");
+        }
+
+        long duration = System.currentTimeMillis() - startTime;
+
+        executor.shutdown();
+        executor.awaitTermination(30, TimeUnit.SECONDS);
+
+        assertThat(duration).isLessThan(2000);
+    }
+
+    @Test
+    void testConcurrentRequestsWithHighLoad() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"").withFixedDelay(50)));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        int numRequests = 100;
+        ExecutorService executor = Executors.newFixedThreadPool(20);
+        List<Future<String>> futures = new ArrayList<>();
+
+        long startTime = System.currentTimeMillis();
+
+        for (int i = 0; i < numRequests; i++) {
+            futures.add(executor.submit(api::getTest));
+        }
+
+        int successCount = 0;
+        for (Future<String> future : futures) {
+            if ("OK".equals(future.get())) {
+                successCount++;
+            }
+        }
+
+        long duration = System.currentTimeMillis() - startTime;
+
+        executor.shutdown();
+        executor.awaitTermination(30, TimeUnit.SECONDS);
+
+        assertThat(successCount).isEqualTo(numRequests);
+        assertThat(duration).isLessThan(5000);
+    }
+
+    @Test
+    void testConnectionRecycling() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").connectionTimeToLive(500, TimeUnit.MILLISECONDS).connectTimeout(5, TimeUnit.SECONDS)
+                .readTimeout(10, TimeUnit.SECONDS).build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        for (int i = 0; i < 10; i++) {
+            String response = api.getTest();
+            assertThat(response).isEqualTo("OK");
+            Thread.sleep(600);
+        }
+    }
+
+    @Test
+    void testDefaultConnectionTimeToLive() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        String response = api.getTest();
+        assertThat(response).isEqualTo("OK");
+    }
+
+    @Test
+    void testSequentialRequests() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        long startTime = System.currentTimeMillis();
+
+        for (int i = 0; i < 100; i++) {
+            String response = api.getTest();
+            assertThat(response).isEqualTo("OK");
+        }
+
+        long duration = System.currentTimeMillis() - startTime;
+
+        assertThat(duration).isLessThan(5000);
+    }
+
+    @Test
+    void testConnectionPoolWithDifferentEndpoints() throws Exception {
+        FineractFeignClientConfig config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port())
+                .credentials("test", "test").connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+
+        wireMockServer.stubFor(get(urlEqualTo("/test"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK\"")));
+        wireMockServer.stubFor(get(urlEqualTo("/test2"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK2\"")));
+        wireMockServer.stubFor(get(urlEqualTo("/test3"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("\"OK3\"")));
+
+        TestApi api = config.createClient(TestApi.class);
+
+        ExecutorService executor = Executors.newFixedThreadPool(5);
+        List<Future<String>> futures = new ArrayList<>();
+
+        for (int i = 0; i < 30; i++) {
+            int endpoint = i % 3;
+            futures.add(executor.submit(() -> {
+                switch (endpoint) {
+                    case 0:
+                        return api.getTest();
+                    case 1:
+                        return api.getTest2();
+                    default:
+                        return api.getTest3();
+                }
+            }));
+        }
+
+        for (Future<String> future : futures) {
+            assertThat(future.get()).isIn("OK", "OK2", "OK3");
+        }
+
+        executor.shutdown();
+        executor.awaitTermination(30, TimeUnit.SECONDS);
+    }
+
+    interface TestApi {
+
+        @RequestLine("GET /test")
+        String getTest();
+
+        @RequestLine("GET /test2")
+        String getTest2();
+
+        @RequestLine("GET /test3")
+        String getTest3();
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/services/DocumentsApiFixedIntegrationTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/DocumentsApiFixedIntegrationTest.java
new file mode 100644
index 0000000000..d19703607a
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/DocumentsApiFixedIntegrationTest.java
@@ -0,0 +1,208 @@
+/**
+ * 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.services;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import feign.Response;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.fineract.client.feign.FineractFeignClientConfig;
+import org.apache.fineract.client.feign.services.DocumentsApiFixed;
+import org.apache.fineract.client.models.DeleteEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.client.models.DocumentData;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+@Tag("integration")
+class DocumentsApiFixedIntegrationTest {
+
+    @TempDir
+    File tempDir;
+
+    private WireMockServer wireMockServer;
+    private FineractFeignClientConfig config;
+    private File testDocumentFile;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
+        wireMockServer.start();
+        config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
+                .connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+
+        testDocumentFile = new File(tempDir, "test-doc.pdf");
+        Files.write(testDocumentFile.toPath(), "%PDF-1.4 test content".getBytes(StandardCharsets.UTF_8));
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (wireMockServer != null) {
+            wireMockServer.stop();
+        }
+    }
+
+    @Test
+    void testGetDocumentsForClient() {
+        String responseBody = "[{\"id\":1,\"name\":\"Document1\",\"description\":\"Test document\"}]";
+        wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        List<DocumentData> documents = api.retrieveAllDocuments("clients", 123L);
+
+        assertThat(documents).isNotNull();
+        assertThat(documents).hasSize(1);
+        assertThat(documents.get(0).getId()).isEqualTo(1L);
+    }
+
+    @Test
+    void testGetDocumentsForLoan() {
+        String responseBody = "[{\"id\":2,\"name\":\"LoanDocument\",\"description\":\"Loan agreement\"}]";
+        wireMockServer.stubFor(get(urlEqualTo("/v1/loans/456/documents"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        List<DocumentData> documents = api.retrieveAllDocuments("loans", 456L);
+
+        assertThat(documents).isNotNull();
+        assertThat(documents).hasSize(1);
+        assertThat(documents.get(0).getName()).isEqualTo("LoanDocument");
+    }
+
+    @Test
+    void testGetDocumentsEmpty() {
+        wireMockServer.stubFor(get(urlEqualTo("/v1/clients/999/documents"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("[]")));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        List<DocumentData> documents = api.retrieveAllDocuments("clients", 999L);
+
+        assertThat(documents).isNotNull();
+        assertThat(documents).isEmpty();
+    }
+
+    @Test
+    void testGetSingleDocument() {
+        String responseBody = "{\"id\":1,\"name\":\"Document1\",\"description\":\"Test document\",\"fileName\":\"doc.pdf\"}";
+        wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/1"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        DocumentData document = api.getDocument("clients", 123L, 1L);
+
+        assertThat(document).isNotNull();
+        assertThat(document.getId()).isEqualTo(1L);
+        assertThat(document.getName()).isEqualTo("Document1");
+        assertThat(document.getFileName()).isEqualTo("doc.pdf");
+    }
+
+    @Test
+    void testDownloadDocument() throws IOException {
+        byte[] docContent = "PDF document content here".getBytes(StandardCharsets.UTF_8);
+        wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/456/attachment"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/pdf")
+                        .withHeader("Content-Disposition", "attachment; filename=\"document.pdf\"").withBody(docContent)));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        Response response = api.downloadFile("clients", 123L, 456L);
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.headers()).containsKey("Content-Type");
+        assertThat(response.headers().get("Content-Type")).contains("application/pdf");
+        assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(docContent);
+    }
+
+    @Test
+    void testDeleteDocument() {
+        String responseBody = "{\"resourceId\":456}";
+        wireMockServer.stubFor(delete(urlEqualTo("/v1/clients/123/documents/456"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("clients", 123L, 456L);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getResourceId()).isEqualTo(456L);
+    }
+
+    @Test
+    void testDeleteDocumentForLoan() {
+        String responseBody = "{\"resourceId\":999}";
+        wireMockServer.stubFor(delete(urlEqualTo("/v1/loans/789/documents/999"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody(responseBody)));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        DeleteEntityTypeEntityIdDocumentsResponse response = api.deleteDocument("loans", 789L, 999L);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getResourceId()).isEqualTo(999L);
+    }
+
+    @Test
+    void testMultipleEntityTypes() {
+        String[] entityTypes = { "clients", "loans", "savings", "groups" };
+        Long[] entityIds = { 100L, 200L, 300L, 400L };
+
+        for (int i = 0; i < entityTypes.length; i++) {
+            String entityType = entityTypes[i];
+            Long entityId = entityIds[i];
+
+            wireMockServer.stubFor(get(urlEqualTo("/v1/" + entityType + "/" + entityId + "/documents"))
+                    .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("[]")));
+        }
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+
+        for (int i = 0; i < entityTypes.length; i++) {
+            List<DocumentData> documents = api.retrieveAllDocuments(entityTypes[i], entityIds[i]);
+            assertThat(documents).isNotNull();
+            assertThat(documents).isEmpty();
+        }
+    }
+
+    @Test
+    void testDownloadTextDocument() throws IOException {
+        String textContent = "Plain text document content";
+        wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/documents/999/attachment"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/plain")
+                        .withHeader("Content-Disposition", "attachment; filename=\"readme.txt\"").withBody(textContent)));
+
+        DocumentsApiFixed api = config.createClient(DocumentsApiFixed.class);
+        Response response = api.downloadFile("clients", 123L, 999L);
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.headers().get("Content-Type")).contains("text/plain");
+        String content = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
+        assertThat(content).isEqualTo(textContent);
+    }
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/services/ImagesApiIntegrationTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/ImagesApiIntegrationTest.java
new file mode 100644
index 0000000000..94c5de229f
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/ImagesApiIntegrationTest.java
@@ -0,0 +1,180 @@
+/**
+ * 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.services;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import feign.Response;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.fineract.client.feign.FineractFeignClientConfig;
+import org.apache.fineract.client.feign.services.ImagesApi;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+@Tag("integration")
+class ImagesApiIntegrationTest {
+
+    @TempDir
+    File tempDir;
+
+    private WireMockServer wireMockServer;
+    private FineractFeignClientConfig config;
+    private File testImageFile;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
+        wireMockServer.start();
+        config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
+                .connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+
+        testImageFile = new File(tempDir, "test-image.jpg");
+        Files.write(testImageFile.toPath(), new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x01, 0x02, 0x03 });
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (wireMockServer != null) {
+            wireMockServer.stop();
+        }
+    }
+
+    @Test
+    void testRetrieveClientImage() throws IOException {
+        byte[] imageData = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x10, 0x11, 0x12 };
+        wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/images"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(imageData)));
+
+        ImagesApi api = config.createClient(ImagesApi.class);
+        Response response = api.get("clients", 123L, new HashMap<>());
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.headers()).containsKey("Content-Type");
+        assertThat(response.headers().get("Content-Type")).contains("image/jpeg");
+        assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(imageData);
+    }
+
+    @Test
+    void testRetrieveClientImageWithMaxDimensions() throws IOException {
+        byte[] resizedImage = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x20, 0x21 };
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/clients/123/images")).withQueryParam("maxWidth", equalTo("100"))
+                .withQueryParam("maxHeight", equalTo("100"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(resizedImage)));
+
+        ImagesApi api = config.createClient(ImagesApi.class);
+        Map<String, Object> params = new HashMap<>();
+        params.put("maxWidth", 100);
+        params.put("maxHeight", 100);
+
+        Response response = api.get("clients", 123L, params);
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(resizedImage);
+    }
+
+    @Test
+    void testRetrieveClientImageWithMaxWidthOnly() throws IOException {
+        byte[] resizedImage = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 0x30 };
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/clients/123/images")).withQueryParam("maxWidth", equalTo("200"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(resizedImage)));
+
+        ImagesApi api = config.createClient(ImagesApi.class);
+        Map<String, Object> params = new HashMap<>();
+        params.put("maxWidth", 200);
+
+        Response response = api.get("clients", 123L, params);
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(resizedImage);
+    }
+
+    @Test
+    void testDeleteClientImage() {
+        wireMockServer.stubFor(delete(urlEqualTo("/v1/clients/123/images"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"resourceId\":123}")));
+
+        ImagesApi api = config.createClient(ImagesApi.class);
+        Response response = api.delete("clients", 123L);
+
+        assertThat(response.status()).isEqualTo(200);
+    }
+
+    @Test
+    void testDeleteStaffImage() {
+        wireMockServer.stubFor(delete(urlEqualTo("/v1/staff/456/images"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"resourceId\":456}")));
+
+        ImagesApi api = config.createClient(ImagesApi.class);
+        Response response = api.delete("staff", 456L);
+
+        assertThat(response.status()).isEqualTo(200);
+    }
+
+    @Test
+    void testRetrieveImageWithPngFormat() throws IOException {
+        byte[] pngImage = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A };
+        wireMockServer.stubFor(get(urlEqualTo("/v1/clients/123/images"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/png").withBody(pngImage)));
+
+        ImagesApi api = config.createClient(ImagesApi.class);
+        Response response = api.get("clients", 123L, new HashMap<>());
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.headers().get("Content-Type")).contains("image/png");
+        assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(pngImage);
+    }
+
+    @Test
+    void testMultipleEntityTypes() {
+        String[] entityTypes = { "clients", "staff" };
+        Long[] entityIds = { 100L, 200L };
+
+        for (int i = 0; i < entityTypes.length; i++) {
+            String entityType = entityTypes[i];
+            Long entityId = entityIds[i];
+
+            wireMockServer.stubFor(get(urlEqualTo("/v1/" + entityType + "/" + entityId + "/images"))
+                    .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "image/jpeg").withBody(new byte[] { 0x01 })));
+        }
+
+        ImagesApi api = config.createClient(ImagesApi.class);
+
+        for (int i = 0; i < entityTypes.length; i++) {
+            Response response = api.get(entityTypes[i], entityIds[i], new HashMap<>());
+            assertThat(response.status()).isEqualTo(200);
+        }
+    }
+
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/services/RunReportsApiIntegrationTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/RunReportsApiIntegrationTest.java
new file mode 100644
index 0000000000..528cc7600c
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/services/RunReportsApiIntegrationTest.java
@@ -0,0 +1,166 @@
+/**
+ * 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.services;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import feign.Response;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.fineract.client.feign.FineractFeignClientConfig;
+import org.apache.fineract.client.feign.services.RunReportsApi;
+import org.apache.fineract.client.models.RunReportsResponse;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("integration")
+class RunReportsApiIntegrationTest {
+
+    private WireMockServer wireMockServer;
+    private FineractFeignClientConfig config;
+
+    @BeforeEach
+    void setUp() {
+        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
+        wireMockServer.start();
+        config = FineractFeignClientConfig.builder().baseUrl("http://localhost:" + wireMockServer.port()).credentials("test", "test")
+                .connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build();
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (wireMockServer != null) {
+            wireMockServer.stop();
+        }
+    }
+
+    @Test
+    void testRunReportGetDataWithMultipleParameters() {
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/DateRangeReport")).withQueryParam("officeId", equalTo("1"))
+                .withQueryParam("fromDate", equalTo("2024-01-01")).withQueryParam("toDate", equalTo("2024-12-31")).willReturn(aResponse()
+                        .withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
+
+        RunReportsApi api = config.createClient(RunReportsApi.class);
+        Map<String, String> params = new HashMap<>();
+        params.put("officeId", "1");
+        params.put("fromDate", "2024-01-01");
+        params.put("toDate", "2024-12-31");
+
+        RunReportsResponse response = api.runReportGetData("DateRangeReport", params);
+
+        assertThat(response).isNotNull();
+    }
+
+    @Test
+    void testRunReportGetDataWithEmptyResult() {
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/EmptyReport")).withQueryParam("officeId", equalTo("1")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
+
+        RunReportsApi api = config.createClient(RunReportsApi.class);
+        Map<String, String> params = new HashMap<>();
+        params.put("officeId", "1");
+
+        RunReportsResponse response = api.runReportGetData("EmptyReport", params);
+
+        assertThat(response).isNotNull();
+        assertThat(response.getColumnHeaders()).isEmpty();
+        assertThat(response.getData()).isEmpty();
+    }
+
+    @Test
+    void testRunReportGetFileAsPdf() throws IOException {
+        byte[] pdfContent = "%PDF-1.4 test content".getBytes(StandardCharsets.UTF_8);
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/LoanSummary")).withQueryParam("output-type", equalTo("PDF"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/pdf")
+                        .withHeader("Content-Disposition", "attachment; filename=\"report.pdf\"").withBody(pdfContent)));
+
+        RunReportsApi api = config.createClient(RunReportsApi.class);
+        Map<String, String> params = new HashMap<>();
+        params.put("output-type", "PDF");
+
+        Response response = api.runReportGetFile("LoanSummary", params);
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.headers()).containsKey("Content-Type");
+        assertThat(response.headers().get("Content-Type")).contains("application/pdf");
+        assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(pdfContent);
+    }
+
+    @Test
+    void testRunReportGetFileAsCsv() throws IOException {
+        String csvContent = "id,name,balance\n1,Client1,1000.00\n2,Client2,2000.00";
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/ClientBalance")).withQueryParam("exportCSV", equalTo("true"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "text/csv")
+                        .withHeader("Content-Disposition", "attachment; filename=\"report.csv\"").withBody(csvContent)));
+
+        RunReportsApi api = config.createClient(RunReportsApi.class);
+        Map<String, String> params = new HashMap<>();
+        params.put("exportCSV", "true");
+
+        Response response = api.runReportGetFile("ClientBalance", params);
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.headers()).containsKey("Content-Type");
+        assertThat(response.headers().get("Content-Type")).contains("text/csv");
+        String content = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
+        assertThat(content).isEqualTo(csvContent);
+    }
+
+    @Test
+    void testRunReportGetFileAsExcel() throws IOException {
+        byte[] excelContent = "fake excel data".getBytes(StandardCharsets.UTF_8);
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/FinancialReport")).withQueryParam("output-type", equalTo("XLS"))
+                .willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/vnd.ms-excel")
+                        .withHeader("Content-Disposition", "attachment; filename=\"report.xls\"").withBody(excelContent)));
+
+        RunReportsApi api = config.createClient(RunReportsApi.class);
+        Map<String, String> params = new HashMap<>();
+        params.put("output-type", "XLS");
+
+        Response response = api.runReportGetFile("FinancialReport", params);
+
+        assertThat(response.status()).isEqualTo(200);
+        assertThat(response.headers()).containsKey("Content-Type");
+        assertThat(response.body().asInputStream().readAllBytes()).isEqualTo(excelContent);
+    }
+
+    @Test
+    void testRunReportGetDataWithNoParameters() {
+        wireMockServer.stubFor(get(urlPathEqualTo("/v1/runreports/AllClients")).willReturn(
+                aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("{\"columnHeaders\":[],\"data\":[]}")));
+
+        RunReportsApi api = config.createClient(RunReportsApi.class);
+
+        RunReportsResponse response = api.runReportGetData("AllClients", new HashMap<>());
+
+        assertThat(response).isNotNull();
+    }
+
+}
diff --git a/fineract-client-feign/src/test/java/org/apache/fineract/client/util/FeignPartsTest.java b/fineract-client-feign/src/test/java/org/apache/fineract/client/util/FeignPartsTest.java
new file mode 100644
index 0000000000..c095cb9fe5
--- /dev/null
+++ b/fineract-client-feign/src/test/java/org/apache/fineract/client/util/FeignPartsTest.java
@@ -0,0 +1,296 @@
+/**
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import feign.Request;
+import feign.Response;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class FeignPartsTest {
+
+    @TempDir
+    File tempDir;
+
+    @Test
+    void testMediaTypeForJpeg() {
+        String mediaType = FeignParts.mediaType("photo.jpg");
+
+        assertThat(mediaType).isEqualTo("image/jpeg");
+    }
+
+    @Test
+    void testMediaTypeForJpegUppercase() {
+        String mediaType = FeignParts.mediaType("photo.JPEG");
+
+        assertThat(mediaType).isEqualTo("image/jpeg");
+    }
+
+    @Test
+    void testMediaTypeForPng() {
+        String mediaType = FeignParts.mediaType("image.png");
+
+        assertThat(mediaType).isEqualTo("image/png");
+    }
+
+    @Test
+    void testMediaTypeForPdf() {
+        String mediaType = FeignParts.mediaType("document.pdf");
+
+        assertThat(mediaType).isEqualTo("application/pdf");
+    }
+
+    @Test
+    void testMediaTypeForDocx() {
+        String mediaType = FeignParts.mediaType("report.docx");
+
+        assertThat(mediaType).isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+    }
+
+    @Test
+    void testMediaTypeForDoc() {
+        String mediaType = FeignParts.mediaType("report.doc");
+
+        assertThat(mediaType).isEqualTo("application/msword");
+    }
+
+    @Test
+    void testMediaTypeForXlsx() {
+        String mediaType = FeignParts.mediaType("spreadsheet.xlsx");
+
+        assertThat(mediaType).isEqualTo("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+    }
+
+    @Test
+    void testMediaTypeForXls() {
+        String mediaType = FeignParts.mediaType("spreadsheet.xls");
+
+        assertThat(mediaType).isEqualTo("application/vnd.ms-excel");
+    }
+
+    @Test
+    void testMediaTypeForTiff() {
+        String mediaType = FeignParts.mediaType("scan.tiff");
+
+        assertThat(mediaType).isEqualTo("image/tiff");
+    }
+
+    @Test
+    void testMediaTypeForGif() {
+        String mediaType = FeignParts.mediaType("animation.gif");
+
+        assertThat(mediaType).isEqualTo("image/gif");
+    }
+
+    @Test
+    void testMediaTypeForOdt() {
+        String mediaType = FeignParts.mediaType("document.odt");
+
+        assertThat(mediaType).isEqualTo("application/vnd.oasis.opendocument.text");
+    }
+
+    @Test
+    void testMediaTypeForOds() {
+        String mediaType = FeignParts.mediaType("spreadsheet.ods");
+
+        assertThat(mediaType).isEqualTo("application/vnd.oasis.opendocument.spreadsheet");
+    }
+
+    @Test
+    void testMediaTypeForTxt() {
+        String mediaType = FeignParts.mediaType("readme.txt");
+
+        assertThat(mediaType).isEqualTo("text/plain");
+    }
+
+    @Test
+    void testMediaTypeForUnknownExtension() {
+        String mediaType = FeignParts.mediaType("file.xyz");
+
+        assertThat(mediaType).isEqualTo("application/octet-stream");
+    }
+
+    @Test
+    void testMediaTypeCaseInsensitive() {
+        assertThat(FeignParts.mediaType("FILE.JPG")).isEqualTo("image/jpeg");
+        assertThat(FeignParts.mediaType("file.PDF")).isEqualTo("application/pdf");
+        assertThat(FeignParts.mediaType("Document.DOCX"))
+                .isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+    }
+
+    @Test
+    void testMediaTypeForNullFilename() {
+        String mediaType = FeignParts.mediaType(null);
+
+        assertThat(mediaType).isNull();
+    }
+
+    @Test
+    void testMediaTypeForFilenameWithoutExtension() {
+        String mediaType = FeignParts.mediaType("filename");
+
+        assertThat(mediaType).isNull();
+    }
+
+    @Test
+    void testFileNameExtractionFromContentDisposition() {
+        Response response = Response.builder().status(200)
+                .request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
+                .headers(Map.of("Content-Disposition", List.of("attachment; filename=\"test.pdf\""))).body(new byte[0]).build();
+
+        Optional<String> filename = FeignParts.fileName(response);
+
+        assertThat(filename).isPresent();
+        assertThat(filename.get()).isEqualTo("test.pdf");
+    }
+
+    @Test
+    void testFileNameExtractionWithComplexFilename() {
+        Response response = Response.builder().status(200)
+                .request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
+                .headers(Map.of("Content-Disposition", List.of("attachment; filename=\"report-2024-01-15.xlsx\""))).body(new byte[0])
+                .build();
+
+        Optional<String> filename = FeignParts.fileName(response);
+
+        assertThat(filename).isPresent();
+        assertThat(filename.get()).isEqualTo("report-2024-01-15.xlsx");
+    }
+
+    @Test
+    void testFileNameWithoutContentDisposition() {
+        Response response = Response.builder().status(200)
+                .request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null)).headers(Map.of()).body(new byte[0])
+                .build();
+
+        Optional<String> filename = FeignParts.fileName(response);
+
+        assertThat(filename).isEmpty();
+    }
+
+    @Test
+    void testFileNameWithNullHeaders() {
+        Response response = Response.builder().status(200)
+                .request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null)).headers(null).body(new byte[0])
+                .build();
+
+        Optional<String> filename = FeignParts.fileName(response);
+
+        assertThat(filename).isEmpty();
+    }
+
+    @Test
+    void testFileNameWithEmptyContentDisposition() {
+        Response response = Response.builder().status(200)
+                .request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
+                .headers(Map.of("Content-Disposition", Collections.emptyList())).body(new byte[0]).build();
+
+        Optional<String> filename = FeignParts.fileName(response);
+
+        assertThat(filename).isEmpty();
+    }
+
+    @Test
+    void testFileNameWithInvalidContentDisposition() {
+        Response response = Response.builder().status(200)
+                .request(Request.create(Request.HttpMethod.GET, "/test", Map.of(), null, null, null))
+                .headers(Map.of("Content-Disposition", List.of("inline"))).body(new byte[0]).build();
+
+        Optional<String> filename = FeignParts.fileName(response);
+
+        assertThat(filename).isEmpty();
+    }
+
+    @Test
+    void testProbeContentTypeForJpegFile() throws IOException {
+        File jpegFile = new File(tempDir, "test.jpg");
+        Files.write(jpegFile.toPath(), new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF });
+
+        String contentType = FeignParts.probeContentType(jpegFile);
+
+        assertThat(contentType).isIn("image/jpeg", "image/jpg");
+    }
+
+    @Test
+    void testProbeContentTypeForPdfFile() throws IOException {
+        File pdfFile = new File(tempDir, "test.pdf");
+        Files.write(pdfFile.toPath(), "%PDF-1.4".getBytes(StandardCharsets.UTF_8));
+
+        String contentType = FeignParts.probeContentType(pdfFile);
+
+        assertThat(contentType).isEqualTo("application/pdf");
+    }
+
+    @Test
+    void testProbeContentTypeFallsBackToMediaType() throws IOException {
+        File unknownFile = new File(tempDir, "test.docx");
+        Files.write(unknownFile.toPath(), "test content".getBytes(StandardCharsets.UTF_8));
+
+        String contentType = FeignParts.probeContentType(unknownFile);
+
+        assertThat(contentType).isNotNull();
+        assertThat(contentType).isNotEqualTo("application/octet-stream");
+    }
+
+    @Test
+    void testProbeContentTypeForUnknownFile() throws IOException {
+        File unknownFile = new File(tempDir, "test.xyz");
+        Files.write(unknownFile.toPath(), "unknown content".getBytes(StandardCharsets.UTF_8));
+
+        String contentType = FeignParts.probeContentType(unknownFile);
+
+        assertThat(contentType).isNotNull();
+    }
+
+    @Test
+    void testMediaTypeForAllSupportedImageFormats() {
+        assertThat(FeignParts.mediaType("image.jpg")).isEqualTo("image/jpeg");
+        assertThat(FeignParts.mediaType("image.jpeg")).isEqualTo("image/jpeg");
+        assertThat(FeignParts.mediaType("image.png")).isEqualTo("image/png");
+        assertThat(FeignParts.mediaType("image.gif")).isEqualTo("image/gif");
+        assertThat(FeignParts.mediaType("image.tif")).isEqualTo("image/tiff");
+        assertThat(FeignParts.mediaType("image.tiff")).isEqualTo("image/tiff");
+    }
+
+    @Test
+    void testMediaTypeForAllSupportedDocumentFormats() {
+        assertThat(FeignParts.mediaType("doc.pdf")).isEqualTo("application/pdf");
+        assertThat(FeignParts.mediaType("doc.doc")).isEqualTo("application/msword");
+        assertThat(FeignParts.mediaType("doc.docx")).isEqualTo("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+        assertThat(FeignParts.mediaType("doc.odt")).isEqualTo("application/vnd.oasis.opendocument.text");
+        assertThat(FeignParts.mediaType("doc.txt")).isEqualTo("text/plain");
+    }
+
+    @Test
+    void testMediaTypeForAllSupportedSpreadsheetFormats() {
+        assertThat(FeignParts.mediaType("sheet.xls")).isEqualTo("application/vnd.ms-excel");
+        assertThat(FeignParts.mediaType("sheet.xlsx")).isEqualTo("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        assertThat(FeignParts.mediaType("sheet.ods")).isEqualTo("application/vnd.oasis.opendocument.spreadsheet");
+    }
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/security/domain/BasicPasswordEncodablePlatformUser.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/security/domain/BasicPasswordEncodablePlatformUser.java
index 943703ead3..590942545d 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/security/domain/BasicPasswordEncodablePlatformUser.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/security/domain/BasicPasswordEncodablePlatformUser.java
@@ -32,11 +32,19 @@ public class BasicPasswordEncodablePlatformUser implements PlatformUser {
 
     @Getter
     private Long id;
-    @Getter(onMethod_ = @Override)
     private String username;
-    @Getter(onMethod_ = @Override)
     private String password;
 
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public String getPassword() {
+        return password;
+    }
+
     @Override
     public Collection<GrantedAuthority> getAuthorities() {
         return null;
diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle
index 1861b2ed12..3aed9915fa 100644
--- a/integration-tests/build.gradle
+++ b/integration-tests/build.gradle
@@ -88,15 +88,69 @@ cargo {
 
 cargoRunLocal.dependsOn ':fineract-war:war'
 cargoStartLocal.dependsOn ':fineract-war:war'
-cargoStartLocal.mustRunAfter 'testClasses'
 
-if (!project.hasProperty('cargoDisabled')) {
-    test {
-        dependsOn(cargoStartLocal)
+import java.net.HttpURLConnection
+import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+
+tasks.register('waitForFineract') {
+    doLast {
+        int timeoutSeconds = (project.findProperty('waitForFineractTimeoutSeconds') ?: '600') as int
+        int waited = 0
+        int interval = 5
+
+        TrustManager[] trustAllCerts = [
+            new X509TrustManager() {
+                java.security.cert.X509Certificate[] getAcceptedIssuers() { return null }
+                void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
+                void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
+            }
+        ] as TrustManager[]
+
+        SSLContext sc = SSLContext.getInstance("SSL")
+        sc.init(null, trustAllCerts, new java.security.SecureRandom())
+        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory())
+        HttpsURLConnection.setDefaultHostnameVerifier({ hostname, session -> true })
+
+        URL url = new URL("https://localhost:8443/fineract-provider/actuator/health")
+        println "Waiting for Fineract startup (timeout: ${timeoutSeconds}s)..."
+
+        while (waited < timeoutSeconds) {
+            try {
+                HttpURLConnection connection = (HttpURLConnection) url.openConnection()
+                connection.setConnectTimeout(2000)
+                connection.setReadTimeout(2000)
+                connection.setRequestMethod("GET")
+                int responseCode = connection.getResponseCode()
+                if (responseCode == 200) {
+                    println "Fineract is up!"
+                    return
+                }
+            } catch (Exception ignored) { }
+            sleep(interval * 1000)
+            waited += interval
+            println "Still waiting..."
+        }
+
+        throw new GradleException("Fineract did not start within ${timeoutSeconds} seconds")
+    }
+}
+
+tasks.named('test').configure {
+    if (!project.hasProperty('cargoDisabled')) {
+        dependsOn cargoStartLocal, waitForFineract
         finalizedBy cargoStopLocal
     }
 }
 
+if (!project.hasProperty('cargoDisabled')) {
+    cargoStartLocal.mustRunAfter testClasses
+    waitForFineract.mustRunAfter cargoStartLocal
+}
+
 // Configure proper test output directories
 sourceSets {
     test {
diff --git a/integration-tests/dependencies.gradle b/integration-tests/dependencies.gradle
index 66f45bb744..9b0cb8cafe 100644
--- a/integration-tests/dependencies.gradle
+++ b/integration-tests/dependencies.gradle
@@ -21,7 +21,8 @@ dependencies {
     // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly!
     //
     tomcat 'org.apache.tomcat:tomcat:10.1.42@zip'
-    testImplementation( files("$rootDir/fineract-provider/build/classes/java/main/"),
+    def providerMainOutput = project(':fineract-provider').extensions.getByType(SourceSetContainer).named('main').get().output
+    testImplementation( providerMainOutput,
             project(path: ':fineract-core', configuration: 'runtimeElements'),
             project(path: ':fineract-accounting', configuration: 'runtimeElements'),
             project(path: ':fineract-investor', configuration: 'runtimeElements'),
@@ -32,9 +33,17 @@ dependencies {
             project(path: ':fineract-savings', configuration: 'runtimeElements'),
             project(path: ':fineract-provider', configuration: 'runtimeElements'),
             project(path: ':fineract-avro-schemas', configuration: 'runtimeElements'),
-            project(path: ':fineract-client', configuration: 'runtimeElements'),
             project(path: ':fineract-progressive-loan', configuration: 'runtimeElements')
             )
+
+    // Retrofit client for existing tests
+    testImplementation project(path: ':fineract-client', configuration: 'runtimeElements')
+
+    // Feign client - exclude fineract-client module to avoid Retrofit/Feign model conflicts
+    // Both generated and hand-written Feign services are available in feign.services package
+    testImplementation(project(path: ':fineract-client-feign', configuration: 'runtimeElements')) {
+        exclude group: 'org.apache.fineract', module: 'fineract-client'
+    }
     testImplementation ('org.mock-server:mockserver-junit-jupiter') {
         exclude group: 'com.sun.mail', module: 'mailapi'
         exclude group: 'javax.servlet', module: 'javax.servlet-api'
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignClientSmokeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignClientSmokeTest.java
new file mode 100644
index 0000000000..597f05d654
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignClientSmokeTest.java
@@ -0,0 +1,65 @@
+/**
+ * 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 org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.feign.FineractFeignClientConfig;
+import org.apache.fineract.integrationtests.ConfigProperties;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Simple smoke tests for Feign-based Fineract client
+ *
+ */
+public class FeignClientSmokeTest extends FeignIntegrationTest {
+
+    private String getTestUrl() {
+        return System.getProperty("fineract.it.url", ConfigProperties.Backend.PROTOCOL + "://" + ConfigProperties.Backend.HOST + ":"
+                + ConfigProperties.Backend.PORT + "/fineract-provider/api");
+    }
+
+    @Test
+    @Order(1)
+    public void testFeignClientBuilder() {
+        FineractFeignClient.Builder builder = FineractFeignClient.builder().baseUrl(getTestUrl())
+                .credentials(ConfigProperties.Backend.USERNAME, ConfigProperties.Backend.PASSWORD);
+
+        assertThat(builder).isNotNull();
+    }
+
+    @Test
+    @Order(2)
+    public void testFeignClientConfig() {
+        FineractFeignClientConfig.Builder configBuilder = FineractFeignClientConfig.builder().baseUrl(getTestUrl())
+                .credentials(ConfigProperties.Backend.USERNAME, ConfigProperties.Backend.PASSWORD).debugEnabled(true);
+
+        FineractFeignClientConfig config = configBuilder.build();
+
+        assertThat(config).isNotNull();
+    }
+
+    @Test
+    @Order(3)
+    public void testFeignClientCanInstantiateApis() {
+        assertThat(fineractClient()).isNotNull();
+        assertThat(fineractClient().offices()).isNotNull();
+        assertThat(fineractClient().clients()).isNotNull();
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java
new file mode 100644
index 0000000000..77241fd7a1
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignDocumentTest.java
@@ -0,0 +1,150 @@
+/**
+ * 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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import feign.Response;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import org.apache.fineract.client.feign.FeignException;
+import org.apache.fineract.client.feign.FineractMultipartEncoder;
+import org.apache.fineract.client.models.DocumentData;
+import org.apache.fineract.client.models.PostEntityTypeEntityIdDocumentsResponse;
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class FeignDocumentTest extends FeignIntegrationTest {
+
+    final File testFile = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile());
+
+    Long clientId;
+    Long documentId;
+
+    @Test
+    @Order(1)
+    void setupClient() {
+        FeignClientHelper clientHelper = new FeignClientHelper(fineractClient());
+        clientId = clientHelper.createClient("Feign", "Test");
+        assertThat(clientId).isNotNull();
+    }
+
+    @Test
+    @Order(2)
+    void testCreateDocument() throws IOException {
+        String name = "Feign Test Document";
+        String description = "Testing DocumentsApiFixed with Feign client";
+
+        byte[] fileData = Files.readAllBytes(testFile.toPath());
+        FineractMultipartEncoder.MultipartData multipartData = new FineractMultipartEncoder.MultipartData()
+                .addFile("file", testFile.getName(), fileData, "image/jpeg").addText("name", name).addText("description", description);
+
+        PostEntityTypeEntityIdDocumentsResponse response = ok(
+                () -> fineractClient().documentsFixed().createDocument("clients", clientId, multipartData));
+
+        assertThat(response).isNotNull();
+        assertThat(response.getResourceId()).isNotNull();
+        documentId = response.getResourceId();
+    }
+
+    @Test
+    @Order(3)
+    void testRetrieveAllDocuments() {
+        var documents = ok(() -> fineractClient().documentsFixed().retrieveAllDocuments("clients", clientId));
+
+        assertThat(documents).isNotNull();
+        assertThat(documents).isNotEmpty();
+    }
+
+    @Test
+    @Order(4)
+    void testGetDocument() {
+        DocumentData doc = ok(() -> fineractClient().documentsFixed().getDocument("clients", clientId, documentId));
+
+        assertThat(doc).isNotNull();
+        assertThat(doc.getName()).isEqualTo("Feign Test Document");
+        assertThat(doc.getFileName()).isEqualTo(testFile.getName());
+        assertThat(doc.getDescription()).isEqualTo("Testing DocumentsApiFixed with Feign client");
+        assertThat(doc.getId()).isEqualTo(documentId);
+        assertThat(doc.getParentEntityType()).isEqualTo("clients");
+        assertThat(doc.getParentEntityId()).isEqualTo(clientId);
+        assertThat(doc.getType()).isEqualTo("image/jpeg");
+    }
+
+    @Test
+    @Order(5)
+    void testDownloadFile() throws IOException {
+        Response response = fineractClient().documentsFixed().downloadFile("clients", clientId, documentId);
+
+        assertNotNull(response);
+        assertEquals(200, response.status());
+
+        try (InputStream inputStream = response.body().asInputStream()) {
+            byte[] bytes = inputStream.readAllBytes();
+            assertThat(bytes.length).isEqualTo((int) testFile.length());
+        }
+    }
+
+    @Test
+    @Order(6)
+    void testUpdateDocument() {
+        String newName = "Updated Feign Test";
+        String newDescription = "Updated via Feign client";
+
+        FineractMultipartEncoder.MultipartData multipartData = new FineractMultipartEncoder.MultipartData().addText("name", newName)
+                .addText("description", newDescription);
+
+        var updateResponse = ok(() -> fineractClient().documentsFixed().updateDocument("clients", clientId, documentId, multipartData));
+
+        assertThat(updateResponse).isNotNull();
+
+        DocumentData doc = ok(() -> fineractClient().documentsFixed().getDocument("clients", clientId, documentId));
+        assertThat(doc.getName()).isEqualTo(newName);
+        assertThat(doc.getDescription()).isEqualTo(newDescription);
+    }
+
+    @Test
+    @Order(99)
+    void testDeleteDocument() {
+        var deleteResponse = ok(() -> fineractClient().documentsFixed().deleteDocument("clients", clientId, documentId));
+        assertThat(deleteResponse).isNotNull();
+
+        FeignException exception = assertThrows(FeignException.class,
+                () -> fineractClient().documentsFixed().getDocument("clients", clientId, documentId));
+        assertEquals(404, exception.status());
+    }
+
+    @Test
+    @Order(9999)
+    void testCreateDocumentBadArgs() {
+        FineractMultipartEncoder.MultipartData multipartData = new FineractMultipartEncoder.MultipartData().addText("name", "test.pdf");
+
+        FeignException exception = assertThrows(FeignException.class,
+                () -> fineractClient().documentsFixed().createDocument("clients", clientId, multipartData));
+        assertEquals(400, exception.status());
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignImageTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignImageTest.java
new file mode 100644
index 0000000000..22c7c22a0f
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignImageTest.java
@@ -0,0 +1,111 @@
+/**
+ * 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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import feign.Response;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.HashMap;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.CreateStaffResponse;
+import org.apache.fineract.client.models.StaffRequest;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class FeignImageTest extends FeignIntegrationTest {
+
+    final File testImage = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile());
+
+    Long staffId;
+
+    @Override
+    protected FineractFeignClient fineractClient() {
+        return org.apache.fineract.integrationtests.common.FineractFeignClientHelper.createNewFineractFeignClient("mifos", "password",
+                true);
+    }
+
+    @Test
+    @Order(1)
+    void setupStaff() {
+        StaffRequest request = new StaffRequest();
+        request.setOfficeId(1L);
+        request.setFirstname("Feign");
+        request.setLastname("ImageTest" + System.currentTimeMillis());
+        request.setJoiningDate(LocalDate.now(ZoneId.of("UTC")).toString());
+        request.setDateFormat("yyyy-MM-dd");
+        request.setLocale("en_US");
+
+        CreateStaffResponse response = ok(() -> fineractClient().staff().create3(request));
+        assertThat(response).isNotNull();
+        assertThat(response.getResourceId()).isNotNull();
+        staffId = response.getResourceId();
+    }
+
+    @Test
+    @Order(2)
+    void testCreateStaffImage() throws Exception {
+        String dataUrl = org.apache.fineract.client.feign.services.ImagesApi.prepareFileUpload(testImage);
+        Response response = fineractClient().images().create("staff", staffId, dataUrl);
+
+        assertNotNull(response);
+        assertEquals(200, response.status());
+    }
+
+    @Test
+    @Order(3)
+    void testRetrieveStaffImage() throws IOException {
+        Response response = fineractClient().images().get("staff", staffId, new HashMap<>());
+
+        assertNotNull(response);
+        assertEquals(200, response.status());
+
+        try (InputStream inputStream = response.body().asInputStream()) {
+            byte[] bytes = inputStream.readAllBytes();
+            assertThat(bytes.length).isGreaterThan(0);
+        }
+    }
+
+    @Test
+    @Order(4)
+    void testUpdateStaffImage() {
+        Response response = fineractClient().images().update("staff", staffId,
+                org.apache.fineract.client.feign.services.ImagesApi.prepareFileUpload(testImage));
+
+        assertNotNull(response);
+        assertEquals(200, response.status());
+    }
+
+    @Test
+    @Order(99)
+    void testDeleteStaffImage() {
+        Response response = fineractClient().images().delete("staff", staffId);
+
+        assertNotNull(response);
+        assertEquals(200, response.status());
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignIntegrationTest.java
new file mode 100644
index 0000000000..593601a0d2
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/FeignIntegrationTest.java
@@ -0,0 +1,103 @@
+/**
+ * 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 java.math.BigDecimal;
+import java.util.Optional;
+import java.util.function.Supplier;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.feign.util.FeignCalls;
+import org.apache.fineract.integrationtests.common.FineractFeignClientHelper;
+import org.assertj.core.api.AbstractBigDecimalAssert;
+import org.assertj.core.api.AbstractBooleanAssert;
+import org.assertj.core.api.AbstractDoubleAssert;
+import org.assertj.core.api.AbstractFloatAssert;
+import org.assertj.core.api.AbstractIntegerAssert;
+import org.assertj.core.api.AbstractLongAssert;
+import org.assertj.core.api.AbstractStringAssert;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.IterableAssert;
+import org.assertj.core.api.ObjectAssert;
+import org.assertj.core.api.OptionalAssert;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.api.TestMethodOrder;
+
+/**
+ * Base Integration Test class for Feign-based client
+ *
+ * @author Apache Fineract
+ */
+@TestInstance(Lifecycle.PER_CLASS)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public abstract class FeignIntegrationTest {
+
+    protected FineractFeignClient fineractClient() {
+        return FineractFeignClientHelper.getFineractFeignClient();
+    }
+
+    protected <T> T ok(Supplier<T> call) {
+        return FeignCalls.ok(call);
+    }
+
+    protected void executeVoid(Runnable call) {
+        FeignCalls.executeVoid(call);
+    }
+
+    public static IterableAssert<?> assertThat(Iterable<?> actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static AbstractBigDecimalAssert<?> assertThat(BigDecimal actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static <T> ObjectAssert<T> assertThat(T actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static AbstractLongAssert<?> assertThat(Long actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static AbstractDoubleAssert<?> assertThat(Double actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static AbstractFloatAssert<?> assertThat(Float actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static AbstractIntegerAssert<?> assertThat(Integer actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static AbstractBooleanAssert<?> assertThat(Boolean actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static AbstractStringAssert<?> assertThat(String actual) {
+        return Assertions.assertThat(actual);
+    }
+
+    public static <T> OptionalAssert<T> assertThat(Optional<T> actual) {
+        return Assertions.assertThat(actual);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/FeignLoanTestBase.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/FeignLoanTestBase.java
new file mode 100644
index 0000000000..0a4155ca8a
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/FeignLoanTestBase.java
@@ -0,0 +1,219 @@
+/**
+ * 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.feign;
+
+import java.time.LocalDate;
+import java.util.function.Function;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdStatus;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.integrationtests.client.FeignIntegrationTest;
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignAccountHelper;
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignBusinessDateHelper;
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignJournalEntryHelper;
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanHelper;
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignTransactionHelper;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanProductTemplates;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanRequestBuilders;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestAccounts;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestValidators;
+import org.apache.fineract.integrationtests.common.FineractFeignClientHelper;
+import org.junit.jupiter.api.BeforeAll;
+
+public abstract class FeignLoanTestBase extends FeignIntegrationTest implements LoanProductTemplates {
+
+    protected static FeignAccountHelper accountHelper;
+    protected static FeignLoanHelper loanHelper;
+    protected static FeignTransactionHelper transactionHelper;
+    protected static FeignJournalEntryHelper journalHelper;
+    protected static FeignBusinessDateHelper businessDateHelper;
+    protected static FeignClientHelper clientHelper;
+    protected static LoanTestAccounts accounts;
+
+    @BeforeAll
+    public static void setupHelpers() {
+        FineractFeignClient client = FineractFeignClientHelper.getFineractFeignClient();
+        accountHelper = new FeignAccountHelper(client);
+        loanHelper = new FeignLoanHelper(client);
+        transactionHelper = new FeignTransactionHelper(client);
+        journalHelper = new FeignJournalEntryHelper(client);
+        businessDateHelper = new FeignBusinessDateHelper(client);
+        clientHelper = new FeignClientHelper(client);
+    }
+
+    protected LoanTestAccounts getAccounts() {
+        if (accounts == null) {
+            accounts = new LoanTestAccounts(accountHelper);
+        }
+        return accounts;
+    }
+
+    @Override
+    public Long getAssetAccountId(String accountName) {
+        return getAccounts().getAssetAccountId(accountName);
+    }
+
+    @Override
+    public Long getLiabilityAccountId(String accountName) {
+        return getAccounts().getLiabilityAccountId(accountName);
+    }
+
+    @Override
+    public Long getIncomeAccountId(String accountName) {
+        return getAccounts().getIncomeAccountId(accountName);
+    }
+
+    @Override
+    public Long getExpenseAccountId(String accountName) {
+        return getAccounts().getExpenseAccountId(accountName);
+    }
+
+    protected Long createClient(String firstName, String lastName) {
+        return clientHelper.createClient(firstName, lastName);
+    }
+
+    protected Long createLoanProduct(PostLoanProductsRequest request) {
+        return loanHelper.createLoanProduct(request);
+    }
+
+    protected Long applyForLoan(PostLoansRequest request) {
+        return loanHelper.applyForLoan(request);
+    }
+
+    protected Long approveLoan(Long loanId, PostLoansLoanIdRequest request) {
+        return loanHelper.approveLoan(loanId, request);
+    }
+
+    protected Long disburseLoan(Long loanId, PostLoansLoanIdRequest request) {
+        return loanHelper.disburseLoan(loanId, request);
+    }
+
+    protected GetLoansLoanIdResponse getLoanDetails(Long loanId) {
+        return loanHelper.getLoanDetails(loanId);
+    }
+
+    protected void undoApproval(Long loanId) {
+        loanHelper.undoApproval(loanId);
+    }
+
+    protected void undoDisbursement(Long loanId) {
+        loanHelper.undoDisbursement(loanId);
+    }
+
+    protected Long addRepayment(Long loanId, PostLoansLoanIdTransactionsRequest request) {
+        return transactionHelper.addRepayment(loanId, request);
+    }
+
+    protected Long addInterestWaiver(Long loanId, PostLoansLoanIdTransactionsRequest request) {
+        return transactionHelper.addInterestWaiver(loanId, request);
+    }
+
+    protected Long chargeOff(Long loanId, PostLoansLoanIdTransactionsRequest request) {
+        return transactionHelper.chargeOff(loanId, request);
+    }
+
+    protected Long addChargeback(Long loanId, Long transactionId, PostLoansLoanIdTransactionsRequest request) {
+        return transactionHelper.addChargeback(loanId, transactionId, request);
+    }
+
+    protected void undoRepayment(Long loanId, Long transactionId, String transactionDate) {
+        transactionHelper.undoRepayment(loanId, transactionId, transactionDate);
+    }
+
+    protected void verifyJournalEntries(Long loanId, LoanTestData.Journal... expectedEntries) {
+        journalHelper.verifyJournalEntries(loanId, expectedEntries);
+    }
+
+    protected void verifyJournalEntriesSequentially(Long loanId, LoanTestData.Journal... expectedEntries) {
+        journalHelper.verifyJournalEntriesSequentially(loanId, expectedEntries);
+    }
+
+    protected void runAt(String date, Runnable action) {
+        businessDateHelper.runAt(date, action);
+    }
+
+    protected void updateBusinessDate(String type, String date) {
+        businessDateHelper.updateBusinessDate(type, date);
+    }
+
+    protected void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
+            double principalPaid, double principalOutstanding) {
+        LoanTestValidators.validateRepaymentPeriod(loanDetails, index, dueDate, principalDue, principalPaid, principalOutstanding, 0.0,
+                0.0);
+    }
+
+    protected void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
+            double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
+        LoanTestValidators.validateRepaymentPeriod(loanDetails, index, dueDate, principalDue, principalPaid, principalOutstanding,
+                paidInAdvance, paidLate);
+    }
+
+    protected void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, Function<GetLoansLoanIdStatus, Boolean> extractor) {
+        LoanTestValidators.verifyLoanStatus(loanDetails, extractor);
+    }
+
+    protected Long createApproveAndDisburseLoan(Long clientId, Long productId, String date, Double principal, Integer numberOfRepayments) {
+        PostLoansRequest applyRequest = LoanRequestBuilders.applyLoan(clientId, productId, date, principal, numberOfRepayments);
+        Long loanId = applyForLoan(applyRequest);
+
+        PostLoansLoanIdRequest approveRequest = LoanRequestBuilders.approveLoan(principal, date);
+        approveLoan(loanId, approveRequest);
+
+        PostLoansLoanIdRequest disburseRequest = LoanRequestBuilders.disburseLoan(principal, date);
+        disburseLoan(loanId, disburseRequest);
+
+        return loanId;
+    }
+
+    protected Long createApprovedLoan(Long clientId, Long productId, String date, Double principal, Integer numberOfRepayments) {
+        PostLoansRequest applyRequest = LoanRequestBuilders.applyLoan(clientId, productId, date, principal, numberOfRepayments);
+        Long loanId = applyForLoan(applyRequest);
+
+        PostLoansLoanIdRequest approveRequest = LoanRequestBuilders.approveLoan(principal, date);
+        approveLoan(loanId, approveRequest);
+
+        return loanId;
+    }
+
+    protected LoanTestData.Journal debit(Long glAccountId, double amount) {
+        return LoanTestData.Journal.debit(glAccountId, amount);
+    }
+
+    protected LoanTestData.Journal credit(Long glAccountId, double amount) {
+        return LoanTestData.Journal.credit(glAccountId, amount);
+    }
+
+    protected PostLoansLoanIdTransactionsRequest repayment(double amount, String date) {
+        return LoanRequestBuilders.repayLoan(amount, date);
+    }
+
+    protected PostLoansLoanIdTransactionsRequest waiveInterest(double amount, String date) {
+        return LoanRequestBuilders.waiveInterest(amount, date);
+    }
+
+    protected PostLoansLoanIdTransactionsRequest chargeOff(String date) {
+        return LoanRequestBuilders.chargeOff(date);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignAccountHelper.java
new file mode 100644
index 0000000000..4dc91104c1
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignAccountHelper.java
@@ -0,0 +1,95 @@
+/**
+ * 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.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+
+import java.util.Collections;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.GetGLAccountsResponse;
+import org.apache.fineract.client.models.PostGLAccountsRequest;
+import org.apache.fineract.client.models.PostGLAccountsResponse;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+
+public class FeignAccountHelper {
+
+    private final FineractFeignClient fineractClient;
+
+    public FeignAccountHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+    }
+
+    public Account createAssetAccount(String name) {
+        return createAccount(name, "1", "ASSET");
+    }
+
+    public Account createLiabilityAccount(String name) {
+        return createAccount(name, "2", "LIABILITY");
+    }
+
+    public Account createIncomeAccount(String name) {
+        return createAccount(name, "4", "INCOME");
+    }
+
+    public Account createExpenseAccount(String name) {
+        return createAccount(name, "5", "EXPENSE");
+    }
+
+    private Account createAccount(String name, String glCode, String type) {
+        String uniqueName = Utils.uniqueRandomStringGenerator(name + "_", 4);
+        String accountCode = Utils.uniqueRandomStringGenerator("GL_" + glCode, 6);
+
+        PostGLAccountsRequest request = new PostGLAccountsRequest()//
+                .name(uniqueName)//
+                .glCode(accountCode)//
+                .manualEntriesAllowed(true)//
+                .type(getAccountTypeId(type))//
+                .usage(1);
+
+        PostGLAccountsResponse response = ok(() -> fineractClient.generalLedgerAccount().createGLAccount1(request));
+
+        GetGLAccountsResponse account = ok(
+                () -> fineractClient.generalLedgerAccount().retreiveAccount(response.getResourceId(), Collections.emptyMap()));
+
+        return new Account(account.getId().intValue(), getAccountType(type));
+    }
+
+    private Integer getAccountTypeId(String type) {
+        return switch (type) {
+            case "ASSET" -> 1;
+            case "LIABILITY" -> 2;
+            case "EQUITY" -> 3;
+            case "INCOME" -> 4;
+            case "EXPENSE" -> 5;
+            default -> throw new IllegalArgumentException("Unknown account type: " + type);
+        };
+    }
+
+    private Account.AccountType getAccountType(String type) {
+        return switch (type) {
+            case "ASSET" -> Account.AccountType.ASSET;
+            case "LIABILITY" -> Account.AccountType.LIABILITY;
+            case "EQUITY" -> Account.AccountType.EQUITY;
+            case "INCOME" -> Account.AccountType.INCOME;
+            case "EXPENSE" -> Account.AccountType.EXPENSE;
+            default -> throw new IllegalArgumentException("Unknown account type: " + type);
+        };
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignBusinessDateHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignBusinessDateHelper.java
new file mode 100644
index 0000000000..88038af59f
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignBusinessDateHelper.java
@@ -0,0 +1,62 @@
+/**
+ * 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.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+
+import java.util.Collections;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.BusinessDateResponse;
+import org.apache.fineract.client.models.BusinessDateUpdateRequest;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
+
+public class FeignBusinessDateHelper {
+
+    private final FineractFeignClient fineractClient;
+
+    public FeignBusinessDateHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+    }
+
+    public BusinessDateResponse getBusinessDate(String type) {
+        return ok(() -> fineractClient.businessDateManagement().getBusinessDate(type));
+    }
+
+    public void updateBusinessDate(String type, String date) {
+        BusinessDateUpdateRequest request = new BusinessDateUpdateRequest()//
+                .type(BusinessDateUpdateRequest.TypeEnum.fromValue(type))//
+                .date(date)//
+                .dateFormat("yyyy-MM-dd")//
+                .locale(LoanTestData.LOCALE);
+
+        ok(() -> fineractClient.businessDateManagement().updateBusinessDate(request, Collections.emptyMap()));
+    }
+
+    public void runAt(String date, Runnable action) {
+        BusinessDateResponse originalDate = getBusinessDate("BUSINESS_DATE");
+        try {
+            updateBusinessDate("BUSINESS_DATE", date);
+            action.run();
+        } finally {
+            if (originalDate != null && originalDate.getDate() != null) {
+                updateBusinessDate("BUSINESS_DATE", originalDate.getDate().toString());
+            }
+        }
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignClientHelper.java
new file mode 100644
index 0000000000..3c2f806f4b
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignClientHelper.java
@@ -0,0 +1,65 @@
+/**
+ * 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.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+
+import java.util.Collections;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.GetClientsClientIdResponse;
+import org.apache.fineract.client.models.PostClientsRequest;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
+import org.apache.fineract.integrationtests.common.Utils;
+
+public class FeignClientHelper {
+
+    private final FineractFeignClient fineractClient;
+
+    public FeignClientHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+    }
+
+    public Long createClient(String firstName, String lastName) {
+        String externalId = Utils.randomStringGenerator("EXT_", 7);
+        String activationDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+
+        PostClientsRequest request = new PostClientsRequest()//
+                .officeId(1L)//
+                .legalFormId(1L)//
+                .firstname(firstName)//
+                .lastname(lastName)//
+                .externalId(externalId)//
+                .active(true)//
+                .activationDate(activationDate)//
+                .dateFormat(LoanTestData.DATETIME_PATTERN)//
+                .locale(LoanTestData.LOCALE);
+
+        return createClient(request);
+    }
+
+    public Long createClient(PostClientsRequest request) {
+        PostClientsResponse response = ok(() -> fineractClient.clients().create6(request));
+        return response.getClientId();
+    }
+
+    public GetClientsClientIdResponse getClient(Long clientId) {
+        return ok(() -> fineractClient.clients().retrieveOne11(clientId, Collections.emptyMap()));
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignJournalEntryHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignJournalEntryHelper.java
new file mode 100644
index 0000000000..3f1416006b
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignJournalEntryHelper.java
@@ -0,0 +1,69 @@
+/**
+ * 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.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
+import org.apache.fineract.client.models.JournalEntryTransactionItem;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData;
+
+public class FeignJournalEntryHelper {
+
+    private final FineractFeignClient fineractClient;
+
+    public FeignJournalEntryHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+    }
+
+    public GetJournalEntriesTransactionIdResponse getJournalEntriesForLoan(Long loanId) {
+        return ok(() -> fineractClient.journalEntries().retrieveAll1(Map.of("loanId", loanId)));
+    }
+
+    public void verifyJournalEntries(Long loanId, LoanTestData.Journal... expectedEntries) {
+        GetJournalEntriesTransactionIdResponse journalEntries = getJournalEntriesForLoan(loanId);
+        assertNotNull(journalEntries);
+        assertNotNull(journalEntries.getPageItems());
+
+        List<JournalEntryTransactionItem> actualEntries = journalEntries.getPageItems();
+        assertEquals(expectedEntries.length, actualEntries.size(),
+                "Expected " + expectedEntries.length + " journal entries but found " + actualEntries.size());
+
+        for (int i = 0; i < expectedEntries.length; i++) {
+            LoanTestData.Journal expected = expectedEntries[i];
+            JournalEntryTransactionItem actual = actualEntries.get(i);
+
+            Double expectedAmount = expected.amount;
+            Double actualAmount = actual.getAmount();
+            assertEquals(0, Double.compare(expectedAmount, actualAmount),
+                    "Journal entry " + i + " amount mismatch: expected " + expectedAmount + " but got " + actualAmount);
+            assertEquals(expected.account.getAccountID().longValue(), actual.getGlAccountId(), "Journal entry " + i + " account mismatch");
+            assertEquals(expected.type, actual.getEntryType().getValue(), "Journal entry " + i + " type mismatch");
+        }
+    }
+
+    public void verifyJournalEntriesSequentially(Long loanId, LoanTestData.Journal... expectedEntries) {
+        verifyJournalEntries(loanId, expectedEntries);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java
new file mode 100644
index 0000000000..cff401185e
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java
@@ -0,0 +1,109 @@
+/**
+ * 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.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+
+import java.math.BigDecimal;
+import java.util.Map;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+
+public class FeignLoanHelper {
+
+    private final FineractFeignClient fineractClient;
+
+    public FeignLoanHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+    }
+
+    public Long createLoanProduct(PostLoanProductsRequest request) {
+        PostLoanProductsResponse response = ok(() -> fineractClient.loanProducts().createLoanProduct(request));
+        return response.getResourceId();
+    }
+
+    public Long applyForLoan(PostLoansRequest request) {
+        PostLoansResponse response = ok(() -> fineractClient.loans().calculateLoanScheduleOrSubmitLoanApplication(request, null));
+        return response.getLoanId();
+    }
+
+    public Long approveLoan(Long loanId, PostLoansLoanIdRequest request) {
+        PostLoansLoanIdResponse response = ok(() -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "approve")));
+        return response.getLoanId();
+    }
+
+    public Long disburseLoan(Long loanId, PostLoansLoanIdRequest request) {
+        PostLoansLoanIdResponse response = ok(
+                () -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "disburse")));
+        return response.getLoanId();
+    }
+
+    public GetLoansLoanIdResponse getLoanDetails(Long loanId) {
+        return ok(() -> fineractClient.loans().retrieveLoan(loanId, Map.of("associations", "all", "exclude", "guarantors,futureSchedule")));
+    }
+
+    public void undoApproval(Long loanId) {
+        PostLoansLoanIdRequest request = new PostLoansLoanIdRequest();
+        ok(() -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "undoApproval")));
+    }
+
+    public void undoDisbursement(Long loanId) {
+        PostLoansLoanIdRequest request = new PostLoansLoanIdRequest();
+        ok(() -> fineractClient.loans().stateTransitions(loanId, request, Map.of("command", "undoDisbursal")));
+    }
+
+    public Long applyAndApproveLoan(Long clientId, Long productId, String submittedOnDate, Double principal, Integer numberOfRepayments) {
+        PostLoansRequest applyRequest = new PostLoansRequest()//
+                .clientId(clientId)//
+                .productId(productId)//
+                .loanType("individual")//
+                .submittedOnDate(submittedOnDate)//
+                .expectedDisbursementDate(submittedOnDate)//
+                .principal(BigDecimal.valueOf(principal))//
+                .loanTermFrequency(numberOfRepayments)//
+                .loanTermFrequencyType(2)//
+                .numberOfRepayments(numberOfRepayments)//
+                .repaymentEvery(1)//
+                .repaymentFrequencyType(2)//
+                .interestRatePerPeriod(BigDecimal.ZERO)//
+                .amortizationType(1)//
+                .interestType(0)//
+                .interestCalculationPeriodType(1)//
+                .transactionProcessingStrategyCode("mifos-standard-strategy")//
+                .locale("en")//
+                .dateFormat("dd MMMM yyyy");
+
+        Long loanId = applyForLoan(applyRequest);
+
+        PostLoansLoanIdRequest approveRequest = new PostLoansLoanIdRequest()//
+                .approvedLoanAmount(BigDecimal.valueOf(principal))//
+                .approvedOnDate(submittedOnDate)//
+                .locale("en")//
+                .dateFormat("dd MMMM yyyy");
+
+        approveLoan(loanId, approveRequest);
+        return loanId;
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignTransactionHelper.java
new file mode 100644
index 0000000000..7356844e2b
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignTransactionHelper.java
@@ -0,0 +1,75 @@
+/**
+ * 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.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+
+import java.util.Map;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
+
+public class FeignTransactionHelper {
+
+    private final FineractFeignClient fineractClient;
+
+    public FeignTransactionHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+    }
+
+    public Long addRepayment(Long loanId, PostLoansLoanIdTransactionsRequest request) {
+        PostLoansLoanIdTransactionsResponse response = ok(
+                () -> fineractClient.loanTransactions().executeLoanTransaction(loanId, request, Map.of("command", "repayment")));
+        return response.getResourceId();
+    }
+
+    public Long addInterestWaiver(Long loanId, PostLoansLoanIdTransactionsRequest request) {
+        PostLoansLoanIdTransactionsResponse response = ok(
+                () -> fineractClient.loanTransactions().executeLoanTransaction(loanId, request, Map.of("command", "waiveInterest")));
+        return response.getResourceId();
+    }
+
+    public Long chargeOff(Long loanId, PostLoansLoanIdTransactionsRequest request) {
+        PostLoansLoanIdTransactionsResponse response = ok(
+                () -> fineractClient.loanTransactions().executeLoanTransaction(loanId, request, Map.of("command", "chargeOff")));
+        return response.getResourceId();
+    }
+
+    public Long addChargeback(Long loanId, Long transactionId, PostLoansLoanIdTransactionsRequest request) {
+        PostLoansLoanIdTransactionsTransactionIdRequest chargebackRequest = new PostLoansLoanIdTransactionsTransactionIdRequest()//
+                .transactionDate(request.getTransactionDate())//
+                .transactionAmount(request.getTransactionAmount())//
+                .locale(request.getLocale())//
+                .dateFormat(request.getDateFormat());
+
+        PostLoansLoanIdTransactionsResponse response = ok(() -> fineractClient.loanTransactions().adjustLoanTransaction(loanId,
+                transactionId, chargebackRequest, Map.of("command", "chargeback")));
+        return response.getResourceId();
+    }
+
+    public void undoRepayment(Long loanId, Long transactionId, String transactionDate) {
+        PostLoansLoanIdTransactionsTransactionIdRequest request = new PostLoansLoanIdTransactionsTransactionIdRequest();
+        request.setTransactionDate(transactionDate);
+        request.setTransactionAmount(0.0);
+        request.setDateFormat("dd MMMM yyyy");
+        request.setLocale("en");
+        ok(() -> fineractClient.loanTransactions().adjustLoanTransaction(loanId, transactionId, request, Map.of("command", "undo")));
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanProductTemplates.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanProductTemplates.java
new file mode 100644
index 0000000000..95cdedb1f9
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanProductTemplates.java
@@ -0,0 +1,240 @@
+/**
+ * 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.feign.modules;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import org.apache.fineract.client.models.AllowAttributeOverrides;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.AmortizationType;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.DaysInMonthType;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.DaysInYearType;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.InterestCalculationPeriodType;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.InterestRateFrequencyType;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.InterestType;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.RepaymentFrequencyType;
+import org.apache.fineract.integrationtests.client.feign.modules.LoanTestData.RescheduleStrategyMethod;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
+
+public interface LoanProductTemplates {
+
+    Long getAssetAccountId(String accountName);
+
+    Long getLiabilityAccountId(String accountName);
+
+    Long getIncomeAccountId(String accountName);
+
+    Long getExpenseAccountId(String accountName);
+
+    default PostLoanProductsRequest onePeriod30DaysNoInterest() {
+        return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
+                .shortName(Utils.uniqueRandomStringGenerator("", 4))//
+                .description("Loan Product Description")//
+                .includeInBorrowerCycle(false)//
+                .currencyCode("USD")//
+                .digitsAfterDecimal(2)//
+                .inMultiplesOf(0)//
+                .installmentAmountInMultiplesOf(1)//
+                .useBorrowerCycle(false)//
+                .minPrincipal(100.0)//
+                .principal(1000.0)//
+                .maxPrincipal(100000.0)//
+                .minNumberOfRepayments(1)//
+                .numberOfRepayments(1)//
+                .maxNumberOfRepayments(30)//
+                .isLinkedToFloatingInterestRates(false)//
+                .minInterestRatePerPeriod(0.0)//
+                .interestRatePerPeriod(0.0)//
+                .maxInterestRatePerPeriod(100.0)//
+                .interestRateFrequencyType(InterestRateFrequencyType.MONTHS)//
+                .repaymentEvery(30)//
+                .repaymentFrequencyType(RepaymentFrequencyType.DAYS_L)//
+                .amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
+                .interestType(InterestType.DECLINING_BALANCE)//
+                .isEqualAmortization(false)//
+                .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
+                .transactionProcessingStrategyCode(
+                        LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)//
+                .loanScheduleType(LoanScheduleType.CUMULATIVE.toString())//
+                .daysInYearType(DaysInYearType.ACTUAL)//
+                .daysInMonthType(DaysInMonthType.ACTUAL)//
+                .canDefineInstallmentAmount(true)//
+                .graceOnArrearsAgeing(3)//
+                .overdueDaysForNPA(179)//
+                .accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
+                .principalThresholdForLastInstallment(50)//
+                .allowVariableInstallments(false)//
+                .canUseForTopup(false)//
+                .isInterestRecalculationEnabled(false)//
+                .holdGuaranteeFunds(false)//
+                .multiDisburseLoan(true)//
+                .allowAttributeOverrides(new AllowAttributeOverrides()//
+                        .amortizationType(true)//
+                        .interestType(true)//
+                        .transactionProcessingStrategyCode(true)//
+                        .interestCalculationPeriodType(true)//
+                        .inArrearsTolerance(true)//
+                        .repaymentEvery(true)//
+                        .graceOnPrincipalAndInterestPayment(true)//
+                        .graceOnArrearsAgeing(true))//
+                .allowPartialPeriodInterestCalcualtion(true)//
+                .maxTrancheCount(10)//
+                .outstandingLoanBalance(10000.0)//
+                .charges(Collections.emptyList())//
+                .accountingRule(3)//
+                .fundSourceAccountId(getLiabilityAccountId("fundSource"))//
+                .loanPortfolioAccountId(getAssetAccountId("loansReceivable"))//
+                .transfersInSuspenseAccountId(getAssetAccountId("suspense"))//
+                .interestOnLoanAccountId(getIncomeAccountId("interestIncome"))//
+                .incomeFromFeeAccountId(getIncomeAccountId("feeIncome"))//
+                .incomeFromPenaltyAccountId(getIncomeAccountId("penaltyIncome"))//
+                .incomeFromRecoveryAccountId(getIncomeAccountId("recoveries"))//
+                .writeOffAccountId(getExpenseAccountId("writtenOff"))//
+                .overpaymentLiabilityAccountId(getLiabilityAccountId("overpayment"))//
+                .receivableInterestAccountId(getAssetAccountId("interestReceivable"))//
+                .receivableFeeAccountId(getAssetAccountId("feeReceivable"))//
+                .receivablePenaltyAccountId(getAssetAccountId("penaltyReceivable"))//
+                .goodwillCreditAccountId(getExpenseAccountId("goodwillExpense"))//
+                .incomeFromGoodwillCreditInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
+                .incomeFromGoodwillCreditFeesAccountId(getIncomeAccountId("feeChargeOff"))//
+                .incomeFromGoodwillCreditPenaltyAccountId(getIncomeAccountId("feeChargeOff"))//
+                .incomeFromChargeOffInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
+                .incomeFromChargeOffFeesAccountId(getIncomeAccountId("feeChargeOff"))//
+                .incomeFromChargeOffPenaltyAccountId(getIncomeAccountId("penaltyChargeOff"))//
+                .chargeOffExpenseAccountId(getExpenseAccountId("chargeOff"))//
+                .chargeOffFraudExpenseAccountId(getExpenseAccountId("chargeOffFraud"))//
+                .dateFormat(LoanTestData.DATETIME_PATTERN)//
+                .locale("en_GB")//
+                .disallowExpectedDisbursements(true)//
+                .allowApprovedDisbursedAmountsOverApplied(true)//
+                .overAppliedCalculationType("percentage")//
+                .overAppliedNumber(50);
+    }
+
+    default PostLoanProductsRequest fourInstallmentsCumulative() {
+        return fourInstallmentsCumulativeTemplate().loanScheduleType(LoanScheduleType.CUMULATIVE.toString());
+    }
+
+    default PostLoanProductsRequest fourInstallmentsProgressive() {
+        return fourInstallmentsCumulativeTemplate().loanScheduleType("PROGRESSIVE").loanScheduleProcessingType("HORIZONTAL")
+                .rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD);
+    }
+
+    default PostLoanProductsRequest fourInstallmentsProgressiveWithCapitalizedIncome() {
+        return fourInstallmentsProgressive().enableIncomeCapitalization(true)//
+                .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)//
+                .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)//
+                .deferredIncomeLiabilityAccountId(getLiabilityAccountId("deferredIncomeLiability"))//
+                .incomeFromCapitalizationAccountId(getIncomeAccountId("feeIncome"))//
+                .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE);
+    }
+
+    default PostLoanProductsRequest fourInstallmentsCumulativeTemplate() {
+        return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("4I_PRODUCT_", 6))//
+                .shortName(Utils.uniqueRandomStringGenerator("", 4))//
+                .description("4 installment product")//
+                .includeInBorrowerCycle(false)//
+                .useBorrowerCycle(false)//
+                .currencyCode("EUR")//
+                .digitsAfterDecimal(2)//
+                .principal(1000.0)//
+                .minPrincipal(100.0)//
+                .maxPrincipal(10000.0)//
+                .numberOfRepayments(4)//
+                .repaymentEvery(1)//
+                .repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L)//
+                .interestRatePerPeriod(10.0)//
+                .minInterestRatePerPeriod(0.0)//
+                .maxInterestRatePerPeriod(120.0)//
+                .interestRateFrequencyType(InterestRateFrequencyType.YEARS)//
+                .isLinkedToFloatingInterestRates(false)//
+                .allowVariableInstallments(false)//
+                .amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
+                .interestType(InterestType.DECLINING_BALANCE)//
+                .interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
+                .allowPartialPeriodInterestCalcualtion(false)//
+                .creditAllocation(List.of())//
+                .overdueDaysForNPA(179)//
+                .daysInMonthType(DaysInMonthType.DAYS_30)//
+                .daysInYearType(DaysInYearType.DAYS_360)//
+                .isInterestRecalculationEnabled(false)//
+                .canDefineInstallmentAmount(true)//
+                .repaymentStartDateType(1)//
+                .charges(List.of())//
+                .principalVariationsForBorrowerCycle(List.of())//
+                .interestRateVariationsForBorrowerCycle(List.of())//
+                .numberOfRepaymentVariationsForBorrowerCycle(List.of())//
+                .accountingRule(3)//
+                .canUseForTopup(false)//
+                .fundSourceAccountId(getLiabilityAccountId("fundSource"))//
+                .loanPortfolioAccountId(getAssetAccountId("loansReceivable"))//
+                .transfersInSuspenseAccountId(getAssetAccountId("suspense"))//
+                .interestOnLoanAccountId(getIncomeAccountId("interestIncome"))//
+                .incomeFromFeeAccountId(getIncomeAccountId("feeIncome"))//
+                .incomeFromPenaltyAccountId(getIncomeAccountId("penaltyIncome"))//
+                .incomeFromRecoveryAccountId(getIncomeAccountId("recoveries"))//
+                .writeOffAccountId(getExpenseAccountId("writtenOff"))//
+                .overpaymentLiabilityAccountId(getLiabilityAccountId("overpayment"))//
+                .receivableInterestAccountId(getAssetAccountId("interestReceivable"))//
+                .receivableFeeAccountId(getAssetAccountId("feeReceivable"))//
+                .receivablePenaltyAccountId(getAssetAccountId("penaltyReceivable"))//
+                .goodwillCreditAccountId(getExpenseAccountId("goodwillExpense"))//
+                .incomeFromGoodwillCreditInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
+                .incomeFromGoodwillCreditFeesAccountId(getIncomeAccountId("feeChargeOff"))//
+                .incomeFromGoodwillCreditPenaltyAccountId(getIncomeAccountId("feeChargeOff"))//
+                .incomeFromChargeOffInterestAccountId(getIncomeAccountId("interestIncomeChargeOff"))//
+                .incomeFromChargeOffFeesAccountId(getIncomeAccountId("feeChargeOff"))//
+                .incomeFromChargeOffPenaltyAccountId(getIncomeAccountId("penaltyChargeOff"))//
+                .chargeOffExpenseAccountId(getExpenseAccountId("chargeOff"))//
+                .chargeOffFraudExpenseAccountId(getExpenseAccountId("chargeOffFraud"))//
+                .dateFormat(LoanTestData.DATETIME_PATTERN)//
+                .locale("en")//
+                .enableAccrualActivityPosting(false)//
+                .multiDisburseLoan(false)//
+                .disallowExpectedDisbursements(false)//
+                .allowApprovedDisbursedAmountsOverApplied(true)//
+                .overAppliedCalculationType("percentage")//
+                .overAppliedNumber(50)//
+                .principalThresholdForLastInstallment(50)//
+                .holdGuaranteeFunds(false)//
+                .accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
+                .allowAttributeOverrides(new AllowAttributeOverrides()//
+                        .amortizationType(true)//
+                        .interestType(true)//
+                        .transactionProcessingStrategyCode(true)//
+                        .interestCalculationPeriodType(true)//
+                        .inArrearsTolerance(true)//
+                        .repaymentEvery(true)//
+                        .graceOnPrincipalAndInterestPayment(true)//
+                        .graceOnArrearsAgeing(true))//
+                .isEqualAmortization(false)//
+                .enableDownPayment(false)//
+                .enableInstallmentLevelDelinquency(false)//
+                .transactionProcessingStrategyCode(
+                        LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY);
+    }
+
+    default PostLoanProductsRequest customizeProduct(PostLoanProductsRequest template,
+            Function<PostLoanProductsRequest, PostLoanProductsRequest> customizer) {
+        return customizer.apply(template);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanRequestBuilders.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanRequestBuilders.java
new file mode 100644
index 0000000000..ead6e9140c
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanRequestBuilders.java
@@ -0,0 +1,135 @@
+/**
+ * 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.feign.modules;
+
+import java.math.BigDecimal;
+import org.apache.fineract.client.models.PostLoansLoanIdRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansRequest;
+
+public final class LoanRequestBuilders {
+
+    private LoanRequestBuilders() {}
+
+    public static PostLoansRequest applyLoan(Long clientId, Long productId, String submittedOnDate, Double principal,
+            Integer numberOfRepayments) {
+        return new PostLoansRequest()//
+                .clientId(clientId)//
+                .productId(productId)//
+                .loanType("individual")//
+                .submittedOnDate(submittedOnDate)//
+                .expectedDisbursementDate(submittedOnDate)//
+                .principal(BigDecimal.valueOf(principal))//
+                .loanTermFrequency(numberOfRepayments)//
+                .loanTermFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
+                .numberOfRepayments(numberOfRepayments)//
+                .repaymentEvery(1)//
+                .repaymentFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
+                .interestRatePerPeriod(BigDecimal.ZERO)//
+                .amortizationType(LoanTestData.AmortizationType.EQUAL_INSTALLMENTS)//
+                .interestType(LoanTestData.InterestType.DECLINING_BALANCE)//
+                .interestCalculationPeriodType(LoanTestData.InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
+                .transactionProcessingStrategyCode("mifos-standard-strategy")//
+                .locale(LoanTestData.LOCALE)//
+                .dateFormat(LoanTestData.DATETIME_PATTERN);
+    }
+
+    public static PostLoansRequest applyCumulativeLoan(Long clientId, Long productId, String submittedOnDate, Double principal,
+            Integer numberOfRepayments, Double interestRate) {
+        return new PostLoansRequest()//
+                .clientId(clientId)//
+                .productId(productId)//
+                .submittedOnDate(submittedOnDate)//
+                .expectedDisbursementDate(submittedOnDate)//
+                .principal(BigDecimal.valueOf(principal))//
+                .loanTermFrequency(numberOfRepayments)//
+                .loanTermFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
+                .numberOfRepayments(numberOfRepayments)//
+                .repaymentEvery(1)//
+                .repaymentFrequencyType(LoanTestData.RepaymentFrequencyType.MONTHS)//
+                .interestRatePerPeriod(BigDecimal.valueOf(interestRate))//
+                .amortizationType(LoanTestData.AmortizationType.EQUAL_INSTALLMENTS)//
+                .interestType(LoanTestData.InterestType.DECLINING_BALANCE)//
+                .interestCalculationPeriodType(LoanTestData.InterestCalculationPeriodType.DAILY)//
+                .transactionProcessingStrategyCode("DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY")//
+                .loanType("individual")//
+                .locale(LoanTestData.LOCALE)//
+                .dateFormat(LoanTestData.DATETIME_PATTERN);
+    }
+
+    public static PostLoansRequest applyProgressiveLoan(Long clientId, Long productId, String submittedOnDate, Double principal,
+            Integer numberOfRepayments, Double interestRate) {
+        return applyCumulativeLoan(clientId, productId, submittedOnDate, principal, numberOfRepayments, interestRate);
+    }
+
+    public static PostLoansLoanIdRequest approveLoan(Double approvedAmount, String approvedOnDate) {
+        return new PostLoansLoanIdRequest()//
+                .approvedLoanAmount(BigDecimal.valueOf(approvedAmount))//
+                .approvedOnDate(approvedOnDate)//
+                .locale(LoanTestData.LOCALE)//
+                .dateFormat(LoanTestData.DATETIME_PATTERN);
+    }
+
+    public static PostLoansLoanIdRequest disburseLoan(Double disbursedAmount, String disbursedOnDate) {
+        return new PostLoansLoanIdRequest()//
+                .actualDisbursementDate(disbursedOnDate)//
+                .transactionAmount(BigDecimal.valueOf(disbursedAmount))//
+                .locale(LoanTestData.LOCALE)//
+                .dateFormat(LoanTestData.DATETIME_PATTERN);
+    }
+
+    public static PostLoansLoanIdTransactionsRequest repayLoan(Double amount, String transactionDate) {
+        PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
+        request.setTransactionDate(transactionDate);
+        request.setTransactionAmount(amount);
+        request.setLocale(LoanTestData.LOCALE);
+        request.setDateFormat(LoanTestData.DATETIME_PATTERN);
+        return request;
+    }
+
+    public static PostLoansLoanIdTransactionsRequest makeWaiver(Double amount, String transactionDate) {
+        PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
+        request.setTransactionDate(transactionDate);
+        request.setTransactionAmount(amount);
+        request.setLocale(LoanTestData.LOCALE);
+        request.setDateFormat(LoanTestData.DATETIME_PATTERN);
+        return request;
+    }
+
+    public static PostLoansLoanIdTransactionsRequest chargeOff(String transactionDate) {
+        PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
+        request.setTransactionDate(transactionDate);
+        request.setLocale(LoanTestData.LOCALE);
+        request.setDateFormat(LoanTestData.DATETIME_PATTERN);
+        return request;
+    }
+
+    public static PostLoansLoanIdTransactionsRequest addChargeback(Long transactionId, Double amount, String transactionDate) {
+        PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
+        request.setTransactionDate(transactionDate);
+        request.setTransactionAmount(amount);
+        request.setLocale(LoanTestData.LOCALE);
+        request.setDateFormat(LoanTestData.DATETIME_PATTERN);
+        return request;
+    }
+
+    public static PostLoansLoanIdTransactionsRequest waiveInterest(Double amount, String transactionDate) {
+        return makeWaiver(amount, transactionDate);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestAccounts.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestAccounts.java
new file mode 100644
index 0000000000..bed8fb5640
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestAccounts.java
@@ -0,0 +1,204 @@
+/**
+ * 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.feign.modules;
+
+import org.apache.fineract.integrationtests.client.feign.helpers.FeignAccountHelper;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+
+public class LoanTestAccounts implements LoanProductTemplates {
+
+    private final Account loansReceivableAccount;
+    private final Account interestReceivableAccount;
+    private final Account feeReceivableAccount;
+    private final Account penaltyReceivableAccount;
+    private final Account suspenseAccount;
+    private final Account fundSource;
+    private final Account overpaymentAccount;
+    private final Account interestIncomeAccount;
+    private final Account feeIncomeAccount;
+    private final Account penaltyIncomeAccount;
+    private final Account feeChargeOffAccount;
+    private final Account penaltyChargeOffAccount;
+    private final Account recoveriesAccount;
+    private final Account interestIncomeChargeOffAccount;
+    private final Account chargeOffExpenseAccount;
+    private final Account chargeOffFraudExpenseAccount;
+    private final Account writtenOffAccount;
+    private final Account goodwillExpenseAccount;
+    private final Account goodwillIncomeAccount;
+    private final Account deferredIncomeLiabilityAccount;
+    private final Account buyDownExpenseAccount;
+
+    public LoanTestAccounts(FeignAccountHelper accountHelper) {
+        this.loansReceivableAccount = accountHelper.createAssetAccount("loanPortfolio");
+        this.interestReceivableAccount = accountHelper.createAssetAccount("interestReceivable");
+        this.feeReceivableAccount = accountHelper.createAssetAccount("feeReceivable");
+        this.penaltyReceivableAccount = accountHelper.createAssetAccount("penaltyReceivable");
+        this.suspenseAccount = accountHelper.createAssetAccount("suspense");
+        this.fundSource = accountHelper.createLiabilityAccount("fundSource");
+        this.overpaymentAccount = accountHelper.createLiabilityAccount("overpayment");
+        this.interestIncomeAccount = accountHelper.createIncomeAccount("interestIncome");
+        this.feeIncomeAccount = accountHelper.createIncomeAccount("feeIncome");
+        this.penaltyIncomeAccount = accountHelper.createIncomeAccount("penaltyIncome");
+        this.feeChargeOffAccount = accountHelper.createIncomeAccount("feeChargeOff");
+        this.penaltyChargeOffAccount = accountHelper.createIncomeAccount("penaltyChargeOff");
+        this.recoveriesAccount = accountHelper.createIncomeAccount("recoveries");
+        this.interestIncomeChargeOffAccount = accountHelper.createIncomeAccount("interestIncomeChargeOff");
+        this.chargeOffExpenseAccount = accountHelper.createExpenseAccount("chargeOff");
+        this.chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud");
+        this.writtenOffAccount = accountHelper.createExpenseAccount("writtenOffAccount");
+        this.goodwillExpenseAccount = accountHelper.createExpenseAccount("goodwillExpenseAccount");
+        this.goodwillIncomeAccount = accountHelper.createIncomeAccount("goodwillIncomeAccount");
+        this.deferredIncomeLiabilityAccount = accountHelper.createLiabilityAccount("deferredIncomeLiabilityAccount");
+        this.buyDownExpenseAccount = accountHelper.createExpenseAccount("buyDownExpenseAccount");
+    }
+
+    @Override
+    public Long getAssetAccountId(String accountName) {
+        return switch (accountName) {
+            case "loansReceivable" -> loansReceivableAccount.getAccountID().longValue();
+            case "interestReceivable" -> interestReceivableAccount.getAccountID().longValue();
+            case "feeReceivable" -> feeReceivableAccount.getAccountID().longValue();
+            case "penaltyReceivable" -> penaltyReceivableAccount.getAccountID().longValue();
+            case "suspense" -> suspenseAccount.getAccountID().longValue();
+            default -> throw new IllegalArgumentException("Unknown asset account: " + accountName);
+        };
+    }
+
+    @Override
+    public Long getLiabilityAccountId(String accountName) {
+        return switch (accountName) {
+            case "fundSource" -> fundSource.getAccountID().longValue();
+            case "overpayment" -> overpaymentAccount.getAccountID().longValue();
+            case "deferredIncomeLiability" -> deferredIncomeLiabilityAccount.getAccountID().longValue();
+            default -> throw new IllegalArgumentException("Unknown liability account: " + accountName);
+        };
+    }
+
+    @Override
+    public Long getIncomeAccountId(String accountName) {
+        return switch (accountName) {
+            case "interestIncome" -> interestIncomeAccount.getAccountID().longValue();
+            case "feeIncome" -> feeIncomeAccount.getAccountID().longValue();
+            case "penaltyIncome" -> penaltyIncomeAccount.getAccountID().longValue();
+            case "feeChargeOff" -> feeChargeOffAccount.getAccountID().longValue();
+            case "penaltyChargeOff" -> penaltyChargeOffAccount.getAccountID().longValue();
+            case "recoveries" -> recoveriesAccount.getAccountID().longValue();
+            case "interestIncomeChargeOff" -> interestIncomeChargeOffAccount.getAccountID().longValue();
+            case "goodwillIncome" -> goodwillIncomeAccount.getAccountID().longValue();
+            default -> throw new IllegalArgumentException("Unknown income account: " + accountName);
+        };
+    }
+
+    @Override
+    public Long getExpenseAccountId(String accountName) {
+        return switch (accountName) {
+            case "chargeOff" -> chargeOffExpenseAccount.getAccountID().longValue();
+            case "chargeOffFraud" -> chargeOffFraudExpenseAccount.getAccountID().longValue();
+            case "writtenOff" -> writtenOffAccount.getAccountID().longValue();
+            case "goodwillExpense" -> goodwillExpenseAccount.getAccountID().longValue();
+            case "buyDownExpense" -> buyDownExpenseAccount.getAccountID().longValue();
+            default -> throw new IllegalArgumentException("Unknown expense account: " + accountName);
+        };
+    }
+
+    public Account getLoansReceivableAccount() {
+        return loansReceivableAccount;
+    }
+
+    public Account getInterestReceivableAccount() {
+        return interestReceivableAccount;
+    }
+
+    public Account getFeeReceivableAccount() {
+        return feeReceivableAccount;
+    }
+
+    public Account getPenaltyReceivableAccount() {
+        return penaltyReceivableAccount;
+    }
+
+    public Account getSuspenseAccount() {
+        return suspenseAccount;
+    }
+
+    public Account getFundSource() {
+        return fundSource;
+    }
+
+    public Account getOverpaymentAccount() {
+        return overpaymentAccount;
+    }
+
+    public Account getInterestIncomeAccount() {
+        return interestIncomeAccount;
+    }
+
+    public Account getFeeIncomeAccount() {
+        return feeIncomeAccount;
+    }
+
+    public Account getPenaltyIncomeAccount() {
+        return penaltyIncomeAccount;
+    }
+
+    public Account getFeeChargeOffAccount() {
+        return feeChargeOffAccount;
+    }
+
+    public Account getPenaltyChargeOffAccount() {
+        return penaltyChargeOffAccount;
+    }
+
+    public Account getRecoveriesAccount() {
+        return recoveriesAccount;
+    }
+
+    public Account getInterestIncomeChargeOffAccount() {
+        return interestIncomeChargeOffAccount;
+    }
+
+    public Account getChargeOffExpenseAccount() {
+        return chargeOffExpenseAccount;
+    }
+
+    public Account getChargeOffFraudExpenseAccount() {
+        return chargeOffFraudExpenseAccount;
+    }
+
+    public Account getWrittenOffAccount() {
+        return writtenOffAccount;
+    }
+
+    public Account getGoodwillExpenseAccount() {
+        return goodwillExpenseAccount;
+    }
+
+    public Account getGoodwillIncomeAccount() {
+        return goodwillIncomeAccount;
+    }
+
+    public Account getDeferredIncomeLiabilityAccount() {
+        return deferredIncomeLiabilityAccount;
+    }
+
+    public Account getBuyDownExpenseAccount() {
+        return buyDownExpenseAccount;
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestData.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestData.java
new file mode 100644
index 0000000000..0b42a26f9f
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestData.java
@@ -0,0 +1,231 @@
+/**
+ * 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.feign.modules;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+
+public final class LoanTestData {
+
+    public static final String DATETIME_PATTERN = "dd MMMM yyyy";
+    public static final String LOCALE = "en";
+
+    private LoanTestData() {}
+
+    @ToString
+    @AllArgsConstructor
+    public static class Transaction {
+
+        public Double amount;
+        public String type;
+        public String date;
+        public Boolean reversed;
+    }
+
+    @ToString
+    @AllArgsConstructor
+    public static class TransactionExt {
+
+        public Double amount;
+        public String type;
+        public String date;
+        public Double outstandingPrincipal;
+        public Double principalPortion;
+        public Double interestPortion;
+        public Double feePortion;
+        public Double penaltyPortion;
+        public Double unrecognizedPortion;
+        public Double overpaymentPortion;
+        public Boolean reversed;
+    }
+
+    @ToString
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class Journal {
+
+        public Double amount;
+        public Account account;
+        public String type;
+
+        public static Journal debit(Long glAccountId, double amount) {
+            Journal journal = new Journal();
+            journal.amount = amount;
+            journal.account = new Account(glAccountId.intValue(), null);
+            journal.type = "DEBIT";
+            return journal;
+        }
+
+        public static Journal credit(Long glAccountId, double amount) {
+            Journal journal = new Journal();
+            journal.amount = amount;
+            journal.account = new Account(glAccountId.intValue(), null);
+            journal.type = "CREDIT";
+            return journal;
+        }
+    }
+
+    @ToString
+    @AllArgsConstructor
+    public static class Installment {
+
+        public Double principalAmount;
+        public Double interestAmount;
+        public Double feeAmount;
+        public Double penaltyAmount;
+        public Double totalOutstandingAmount;
+        public Boolean completed;
+        public String dueDate;
+        public OutstandingAmounts outstandingAmounts;
+        public Double loanBalance;
+    }
+
+    @AllArgsConstructor
+    @ToString
+    public static class OutstandingAmounts {
+
+        public Double principalOutstanding;
+        public Double interestOutstanding;
+        public Double feeOutstanding;
+        public Double penaltyOutstanding;
+        public Double totalOutstanding;
+    }
+
+    public static final class AmortizationType {
+
+        public static final Integer EQUAL_INSTALLMENTS = 1;
+
+        private AmortizationType() {}
+    }
+
+    public static final class InterestType {
+
+        public static final Integer DECLINING_BALANCE = 0;
+        public static final Integer FLAT = 1;
+
+        private InterestType() {}
+    }
+
+    public static final class InterestRecalculationCompoundingMethod {
+
+        public static final Integer NONE = 0;
+
+        private InterestRecalculationCompoundingMethod() {}
+    }
+
+    public static final class RepaymentFrequencyType {
+
+        public static final Integer MONTHS = 2;
+        public static final Long MONTHS_L = 2L;
+        public static final String MONTHS_STRING = "MONTHS";
+        public static final Integer DAYS = 0;
+        public static final Long DAYS_L = 0L;
+        public static final String DAYS_STRING = "DAYS";
+
+        private RepaymentFrequencyType() {}
+    }
+
+    public static final class RecalculationRestFrequencyType {
+
+        public static final Integer SAME_AS_REPAYMENT_PERIOD = 1;
+        public static final Integer DAILY = 2;
+
+        private RecalculationRestFrequencyType() {}
+    }
+
+    public static final class InterestCalculationPeriodType {
+
+        public static final Integer DAILY = 0;
+        public static final Integer SAME_AS_REPAYMENT_PERIOD = 1;
+
+        private InterestCalculationPeriodType() {}
+    }
+
+    public static final class InterestRateFrequencyType {
+
+        public static final Integer MONTHS = 2;
+        public static final Integer YEARS = 3;
+        public static final Integer WHOLE_TERM = 4;
+
+        private InterestRateFrequencyType() {}
+    }
+
+    public static final class TransactionProcessingStrategyCode {
+
+        public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
+
+        private TransactionProcessingStrategyCode() {}
+    }
+
+    public static final class RescheduleStrategyMethod {
+
+        public static final Integer RESCHEDULE_NEXT_REPAYMENTS = 1;
+        public static final Integer REDUCE_EMI_AMOUNT = 3;
+        public static final Integer ADJUST_LAST_UNPAID_PERIOD = 4;
+
+        private RescheduleStrategyMethod() {}
+    }
+
+    public static final class DaysInYearType {
+
+        public static final Integer INVALID = 0;
+        public static final Integer ACTUAL = 1;
+        public static final Integer DAYS_360 = 360;
+        public static final Integer DAYS_364 = 364;
+        public static final Integer DAYS_365 = 365;
+
+        private DaysInYearType() {}
+    }
+
+    public static final class DaysInMonthType {
+
+        public static final Integer INVALID = 0;
+        public static final Integer ACTUAL = 1;
+        public static final Integer DAYS_30 = 30;
+
+        private DaysInMonthType() {}
+    }
+
+    public static final class FuturePaymentAllocationRule {
+
+        public static final String LAST_INSTALLMENT = "LAST_INSTALLMENT";
+        public static final String NEXT_INSTALLMENT = "NEXT_INSTALLMENT";
+        public static final String NEXT_LAST_INSTALLMENT = "NEXT_LAST_INSTALLMENT";
+
+        private FuturePaymentAllocationRule() {}
+    }
+
+    public static final class SupportedInterestRefundTypesItem {
+
+        public static final String MERCHANT_ISSUED_REFUND = "MERCHANT_ISSUED_REFUND";
+        public static final String PAYOUT_REFUND = "PAYOUT_REFUND";
+
+        private SupportedInterestRefundTypesItem() {}
+    }
+
+    public static final class DaysInYearCustomStrategy {
+
+        public static final String FEB_29_PERIOD_ONLY = "FEB_29_PERIOD_ONLY";
+        public static final String FULL_LEAP_YEAR = "FULL_LEAP_YEAR";
+
+        private DaysInYearCustomStrategy() {}
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestValidators.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestValidators.java
new file mode 100644
index 0000000000..211661b2cc
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/LoanTestValidators.java
@@ -0,0 +1,126 @@
+/**
+ * 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.feign.modules;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.function.Function;
+import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdStatus;
+import org.apache.fineract.integrationtests.common.Utils;
+
+public final class LoanTestValidators {
+
+    private LoanTestValidators() {}
+
+    public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
+            double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
+        GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
+                .filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
+        assertEquals(dueDate, period.getDueDate());
+        assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
+        assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
+        assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
+        assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
+        assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
+    }
+
+    public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, double principalDue, double principalPaid,
+            double principalOutstanding, double paidInAdvance, double paidLate) {
+        GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
+                .filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
+        assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
+        assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
+        assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
+        assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
+        assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
+    }
+
+    public static void validateFullyUnpaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
+            double principalDue, double feeDue, double penaltyDue, double interestDue) {
+        validateRepaymentPeriod(loanDetails, index,
+                LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue, 0,
+                principalDue, feeDue, 0, feeDue, penaltyDue, 0, penaltyDue, interestDue, 0, interestDue, 0, 0);
+    }
+
+    public static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
+            double principalDue, double feeDue, double penaltyDue, double interestDue) {
+        validateRepaymentPeriod(loanDetails, index,
+                LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue,
+                principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, 0, 0);
+    }
+
+    public static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
+            double principalDue, double feeDue, double penaltyDue, double interestDue, double paidLate) {
+        validateRepaymentPeriod(loanDetails, index,
+                LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue,
+                principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, 0, paidLate);
+    }
+
+    public static void validateFullyPaidRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, String dueDate,
+            double principalDue, double feeDue, double penaltyDue, double interestDue, double paidLate, double paidInAdvance) {
+        validateRepaymentPeriod(loanDetails, index,
+                LocalDate.parse(dueDate, DateTimeFormatter.ofPattern(LoanTestData.DATETIME_PATTERN, Locale.ENGLISH)), principalDue,
+                principalDue, 0, feeDue, feeDue, 0, penaltyDue, penaltyDue, 0, interestDue, interestDue, 0, paidInAdvance, paidLate);
+    }
+
+    public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
+            double feeDue, double penaltyDue, double interestDue) {
+        validateRepaymentPeriod(loanDetails, index, dueDate, principalDue, 0, principalDue, feeDue, 0, feeDue, penaltyDue, 0, penaltyDue,
+                interestDue, 0, interestDue, 0, 0);
+    }
+
+    public static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
+            double principalPaid, double principalOutstanding, double feeDue, double feePaid, double feeOutstanding, double penaltyDue,
+            double penaltyPaid, double penaltyOutstanding, double interestDue, double interestPaid, double interestOutstanding,
+            double paidInAdvance, double paidLate) {
+        GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
+                .filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
+        assertEquals(dueDate, period.getDueDate());
+        assertEquals(principalDue, Utils.getDoubleValue(period.getPrincipalDue()));
+        assertEquals(principalPaid, Utils.getDoubleValue(period.getPrincipalPaid()));
+        assertEquals(principalOutstanding, Utils.getDoubleValue(period.getPrincipalOutstanding()));
+        assertEquals(feeDue, Utils.getDoubleValue(period.getFeeChargesDue()));
+        assertEquals(feePaid, Utils.getDoubleValue(period.getFeeChargesPaid()));
+        assertEquals(feeOutstanding, Utils.getDoubleValue(period.getFeeChargesOutstanding()));
+        assertEquals(penaltyDue, Utils.getDoubleValue(period.getPenaltyChargesDue()));
+        assertEquals(penaltyPaid, Utils.getDoubleValue(period.getPenaltyChargesPaid()));
+        assertEquals(penaltyOutstanding, Utils.getDoubleValue(period.getPenaltyChargesOutstanding()));
+        assertEquals(interestDue, Utils.getDoubleValue(period.getInterestDue()));
+        assertEquals(interestPaid, Utils.getDoubleValue(period.getInterestPaid()));
+        assertEquals(interestOutstanding, Utils.getDoubleValue(period.getInterestOutstanding()));
+        assertEquals(paidInAdvance, Utils.getDoubleValue(period.getTotalPaidInAdvanceForPeriod()));
+        assertEquals(paidLate, Utils.getDoubleValue(period.getTotalPaidLateForPeriod()));
+    }
+
+    public static void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, Function<GetLoansLoanIdStatus, Boolean> extractor) {
+        assertNotNull(loanDetails);
+        assertNotNull(loanDetails.getStatus());
+        Boolean actualValue = extractor.apply(loanDetails.getStatus());
+        assertNotNull(actualValue);
+        assertTrue(actualValue);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanCreationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanCreationTest.java
new file mode 100644
index 0000000000..06ec3d4948
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanCreationTest.java
@@ -0,0 +1,159 @@
+/**
+ * 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.feign.tests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.time.LocalDate;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.integrationtests.client.feign.FeignLoanTestBase;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.junit.jupiter.api.Test;
+
+public class FeignLoanCreationTest extends FeignLoanTestBase {
+
+    @Test
+    void testCreateAndDisburseLoan_OnePeriodNoInterest() {
+        Long clientId = createClient("John", "Doe");
+        assertNotNull(clientId);
+
+        PostLoanProductsRequest productRequest = onePeriod30DaysNoInterest();
+        Long productId = createLoanProduct(productRequest);
+        assertNotNull(productId);
+
+        String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+        Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
+        assertNotNull(loanId);
+
+        GetLoansLoanIdResponse loan = getLoanDetails(loanId);
+        assertNotNull(loan);
+        verifyLoanStatus(loan, status -> status.getActive());
+
+        LocalDate expectedRepaymentDate = Utils.getLocalDateOfTenant().plusMonths(1);
+        validateRepaymentPeriod(loan, 1, expectedRepaymentDate, 1000.0, 0.0, 1000.0);
+
+        verifyJournalEntries(loanId, debit(getAccounts().getLoansReceivableAccount().getAccountID().longValue(), 1000.0),
+                credit(getAccounts().getFundSource().getAccountID().longValue(), 1000.0));
+    }
+
+    @Test
+    void testLoanRepayment_FullRepayment() {
+        Long clientId = createClient("Jane", "Smith");
+        Long productId = createLoanProduct(onePeriod30DaysNoInterest());
+        String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+        Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
+
+        Long repaymentId = addRepayment(loanId, repayment(1000.0, todayDate));
+        assertNotNull(repaymentId);
+
+        GetLoansLoanIdResponse loan = getLoanDetails(loanId);
+        verifyLoanStatus(loan, status -> status.getClosedObligationsMet());
+
+        assertEquals(0.0, Utils.getDoubleValue(loan.getSummary().getTotalOutstanding()));
+        assertEquals(1000.0, Utils.getDoubleValue(loan.getSummary().getTotalRepayment()));
+    }
+
+    @Test
+    void testLoanUndoApproval() {
+        Long clientId = createClient("Bob", "Johnson");
+        Long productId = createLoanProduct(onePeriod30DaysNoInterest());
+        String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+        Long loanId = createApprovedLoan(clientId, productId, todayDate, 1000.0, 1);
+
+        GetLoansLoanIdResponse loanBeforeUndo = getLoanDetails(loanId);
+        verifyLoanStatus(loanBeforeUndo, status -> status.getWaitingForDisbursal());
+
+        undoApproval(loanId);
+
+        GetLoansLoanIdResponse loanAfterUndo = getLoanDetails(loanId);
+        verifyLoanStatus(loanAfterUndo, status -> status.getPendingApproval());
+    }
+
+    @Test
+    void testLoanUndoDisbursement() {
+        Long clientId = createClient("Alice", "Williams");
+        Long productId = createLoanProduct(onePeriod30DaysNoInterest());
+        String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+        Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
+
+        GetLoansLoanIdResponse loanBeforeUndo = getLoanDetails(loanId);
+        verifyLoanStatus(loanBeforeUndo, status -> status.getActive());
+
+        undoDisbursement(loanId);
+
+        GetLoansLoanIdResponse loanAfterUndo = getLoanDetails(loanId);
+        verifyLoanStatus(loanAfterUndo, status -> status.getWaitingForDisbursal());
+    }
+
+    @Test
+    void testFourInstallmentLoan_CumulativeInterest() {
+        Long clientId = createClient("Charlie", "Brown");
+        Long productId = createLoanProduct(fourInstallmentsCumulative());
+        String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+        Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 4);
+
+        GetLoansLoanIdResponse loan = getLoanDetails(loanId);
+        verifyLoanStatus(loan, status -> status.getActive());
+
+        assertNotNull(loan.getRepaymentSchedule());
+        assertNotNull(loan.getRepaymentSchedule().getPeriods());
+        assertEquals(5, loan.getRepaymentSchedule().getPeriods().size());
+    }
+
+    @Test
+    void testPartialRepayment() {
+        Long clientId = createClient("David", "Miller");
+        Long productId = createLoanProduct(onePeriod30DaysNoInterest());
+        String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+        Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
+
+        Long repaymentId = addRepayment(loanId, repayment(500.0, todayDate));
+        assertNotNull(repaymentId);
+
+        GetLoansLoanIdResponse loan = getLoanDetails(loanId);
+        verifyLoanStatus(loan, status -> status.getActive());
+
+        assertEquals(500.0, Utils.getDoubleValue(loan.getSummary().getTotalOutstanding()));
+        assertEquals(500.0, Utils.getDoubleValue(loan.getSummary().getTotalRepayment()));
+    }
+
+    @Test
+    void testUndoRepayment() {
+        Long clientId = createClient("Eva", "Davis");
+        Long productId = createLoanProduct(onePeriod30DaysNoInterest());
+        String todayDate = Utils.dateFormatter.format(Utils.getLocalDateOfTenant());
+        Long loanId = createApproveAndDisburseLoan(clientId, productId, todayDate, 1000.0, 1);
+
+        Long repaymentId = addRepayment(loanId, repayment(1000.0, todayDate));
+        assertNotNull(repaymentId);
+
+        GetLoansLoanIdResponse loanAfterRepayment = getLoanDetails(loanId);
+        verifyLoanStatus(loanAfterRepayment, status -> status.getClosedObligationsMet());
+
+        undoRepayment(loanId, repaymentId, todayDate);
+
+        GetLoansLoanIdResponse loanAfterUndo = getLoanDetails(loanId);
+        verifyLoanStatus(loanAfterUndo, status -> status.getActive());
+
+        LocalDate expectedRepaymentDate = Utils.getLocalDateOfTenant().plusMonths(1);
+        validateRepaymentPeriod(loanAfterUndo, 1, expectedRepaymentDate, 1000.0, 0.0, 1000.0);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanTestBaseSmokeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanTestBaseSmokeTest.java
new file mode 100644
index 0000000000..02fd158502
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanTestBaseSmokeTest.java
@@ -0,0 +1,45 @@
+/**
+ * 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.feign.tests;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.fineract.integrationtests.client.feign.FeignLoanTestBase;
+import org.junit.jupiter.api.Test;
+
+public class FeignLoanTestBaseSmokeTest extends FeignLoanTestBase {
+
+    @Test
+    void testInfrastructureInitialization() {
+        assertNotNull(accountHelper);
+        assertNotNull(loanHelper);
+        assertNotNull(transactionHelper);
+        assertNotNull(journalHelper);
+        assertNotNull(businessDateHelper);
+        assertNotNull(clientHelper);
+        assertNotNull(getAccounts());
+        assertNotNull(getAccounts().getLoansReceivableAccount());
+    }
+
+    @Test
+    void testCreateClient() {
+        Long clientId = createClient("Test", "User");
+        assertNotNull(clientId);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/cob/CobPartitioningTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/cob/CobPartitioningTest.java
index c511e303b1..7d054cea69 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/cob/CobPartitioningTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/cob/CobPartitioningTest.java
@@ -59,6 +59,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuil
 import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
 import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -106,6 +107,12 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
         setProperFinancialActivity(TRANSFER_ACCOUNT);
     }
 
+    @AfterEach
+    public void tearDown() {
+        // Restore business date configuration after each test
+        cleanUpAndRestoreBusinessDate();
+    }
+
     private static void setProperFinancialActivity(Account transferAccount) {
         List<GetFinancialActivityAccountsResponse> financialMappings = FINANCIAL_ACTIVITY_ACCOUNT_HELPER.getAllFinancialActivityAccounts();
         financialMappings.forEach(mapping -> FINANCIAL_ACTIVITY_ACCOUNT_HELPER.deleteFinancialActivityAccount(mapping.getId()));
@@ -144,13 +151,16 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
 
             // Force close loans 3, 4, ... , N-3, N-2
             Collections.sort(loanIds);
+            List<Integer> forelosedLoanIds = new ArrayList<>();
             final CountDownLatch closeLatch = new CountDownLatch(N - 5);
             // Warm up (EclipseLink sometimes fails if JPQL cache is not warm up but concurrent queries are executed)
             LOAN_TRANSACTION_HELPER.forecloseLoan("02 March 2020", loanIds.get(2));
+            forelosedLoanIds.add(loanIds.get(2));
             for (int i = 3; i < N - 2; i++) {
                 final int idx = i;
                 futures.add(executorService.submit(() -> {
                     LOAN_TRANSACTION_HELPER.forecloseLoan("02 March 2020", loanIds.get(idx));
+                    forelosedLoanIds.add(loanIds.get(idx));
                     closeLatch.countDown();
                 }));
             }
@@ -163,24 +173,45 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
             });
             closeLatch.await();
 
-            // Let's retrieve the partitions
-            List<Map<String, Object>> cobPartitions = CobHelper.getCobPartitions(REQUEST_SPEC, RESPONSE_SPEC, 3, "");
-            log.info("\nLoans created: {},\nRetrieved partitions: {}", loanIds, cobPartitions);
-            Assertions.assertEquals(2, cobPartitions.size());
-
-            Assertions.assertEquals(0, cobPartitions.get(0).get("pageNo"));
-            Assertions.assertEquals(loanIds.get(0), cobPartitions.get(0).get("minId"));
-            Assertions.assertEquals(loanIds.get(8), cobPartitions.get(0).get("maxId"));
-            Assertions.assertEquals(3, cobPartitions.get(0).get("count"));
+            // Create list of active (non-foreclosed) loan IDs
+            List<Integer> activeLoanIds = new ArrayList<>(loanIds);
+            activeLoanIds.removeAll(forelosedLoanIds);
+            log.info("Active loan IDs after foreclosure: {}", activeLoanIds);
 
-            Assertions.assertEquals(1, cobPartitions.get(1).get("pageNo"));
-            Assertions.assertEquals(loanIds.get(9), cobPartitions.get(1).get("minId"));
-            Assertions.assertEquals(loanIds.get(9), cobPartitions.get(1).get("maxId"));
-            Assertions.assertEquals(1, cobPartitions.get(1).get("count"));
+            // Let's retrieve the partitions
+            List<Map<String, Object>> allPartitions = CobHelper.getCobPartitions(REQUEST_SPEC, RESPONSE_SPEC, 3, "");
+            log.info("\nLoans created by this test: {}\nAll partitions retrieved: {}", loanIds, allPartitions);
+
+            // Filter partitions to only include ACTIVE loans created by THIS test
+            // This ensures test isolation - we don't care about leftover loans from other tests
+            List<Map<String, Object>> testLoanPartitions = filterPartitionsForTestLoans(allPartitions, activeLoanIds);
+            log.info("Filtered partitions containing only our active test loans: {}", testLoanPartitions);
+
+            // Verify partitioning works correctly for our 4 active loans (10 created - 6 foreclosed)
+            // Expected: 2 partitions with page size 3
+            // Partition 0: loans 0, 1, 8 (3 loans)
+            // Partition 1: loan 9 (1 loan)
+            Assertions.assertEquals(2, testLoanPartitions.size(), "Expected 2 partitions for our 4 active test loans");
+
+            // Verify first partition
+            Assertions.assertEquals(0, testLoanPartitions.get(0).get("pageNo"));
+            Assertions.assertEquals(3, testLoanPartitions.get(0).get("count"));
+            Assertions.assertTrue(isLoanIdInRange(loanIds.get(0), testLoanPartitions.get(0)),
+                    "First active loan should be in first partition");
+            Assertions.assertTrue(isLoanIdInRange(loanIds.get(1), testLoanPartitions.get(0)),
+                    "Second active loan should be in first partition");
+            Assertions.assertTrue(isLoanIdInRange(loanIds.get(8), testLoanPartitions.get(0)),
+                    "Ninth active loan should be in first partition");
+
+            // Verify second partition
+            Assertions.assertEquals(1, testLoanPartitions.get(1).get("pageNo"));
+            Assertions.assertEquals(1, testLoanPartitions.get(1).get("count"));
+            Assertions.assertTrue(isLoanIdInRange(loanIds.get(9), testLoanPartitions.get(1)), "Tenth loan should be in second partition");
 
             executorService.shutdown();
-        } finally {
-            cleanUpAndRestoreBusinessDate();
+        } catch (Exception e) {
+            log.error("Test failed with exception", e);
+            throw e;
         }
     }
 
@@ -291,4 +322,41 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
         return collateral;
     }
 
+    /**
+     * Filters partitions to only include those containing loans from the test's loan IDs. This ensures test isolation
+     * by ignoring any leftover loans from previous test runs.
+     */
+    private List<Map<String, Object>> filterPartitionsForTestLoans(List<Map<String, Object>> allPartitions, List<Integer> testLoanIds) {
+        List<Map<String, Object>> filteredPartitions = new ArrayList<>();
+
+        for (Map<String, Object> partition : allPartitions) {
+            Integer minId = (Integer) partition.get("minId");
+            Integer maxId = (Integer) partition.get("maxId");
+
+            // Check if this partition contains any of our test loan IDs
+            boolean containsTestLoans = testLoanIds.stream().anyMatch(loanId -> loanId >= minId && loanId <= maxId);
+
+            if (containsTestLoans) {
+                // Count how many of OUR loans are in this partition's range
+                long testLoansInPartition = testLoanIds.stream().filter(loanId -> loanId >= minId && loanId <= maxId).count();
+
+                // Create a new partition entry with corrected count
+                Map<String, Object> filteredPartition = new HashMap<>(partition);
+                filteredPartition.put("count", (int) testLoansInPartition);
+                filteredPartitions.add(filteredPartition);
+            }
+        }
+
+        return filteredPartitions;
+    }
+
+    /**
+     * Checks if a loan ID falls within the min/max range of a partition.
+     */
+    private boolean isLoanIdInRange(Integer loanId, Map<String, Object> partition) {
+        Integer minId = (Integer) partition.get("minId");
+        Integer maxId = (Integer) partition.get("maxId");
+        return loanId >= minId && loanId <= maxId;
+    }
+
 }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/FineractFeignClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/FineractFeignClientHelper.java
new file mode 100644
index 0000000000..d909989db0
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/FineractFeignClientHelper.java
@@ -0,0 +1,58 @@
+/**
+ * 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.common;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.integrationtests.ConfigProperties;
+
+public final class FineractFeignClientHelper {
+
+    private static final FineractFeignClient DEFAULT_FINERACT_FEIGN_CLIENT = createNewFineractFeignClient(ConfigProperties.Backend.USERNAME,
+            ConfigProperties.Backend.PASSWORD);
+
+    private FineractFeignClientHelper() {}
+
+    public static FineractFeignClient getFineractFeignClient() {
+        return DEFAULT_FINERACT_FEIGN_CLIENT;
+    }
+
+    public static FineractFeignClient createNewFineractFeignClient(String username, String password) {
+        return createNewFineractFeignClient(username, password, Function.identity()::apply);
+    }
+
+    public static FineractFeignClient createNewFineractFeignClient(String username, String password, boolean debugEnabled) {
+        return createNewFineractFeignClient(username, password, builder -> builder.debug(debugEnabled));
+    }
+
+    public static FineractFeignClient createNewFineractFeignClient(String username, String password,
+            Consumer<FineractFeignClient.Builder> customizer) {
+        String url = System.getProperty("fineract.it.url", buildURI());
+        FineractFeignClient.Builder builder = FineractFeignClient.builder().baseUrl(url).credentials(username, password)
+                .disableSslVerification(true);
+        customizer.accept(builder);
+        return builder.build();
+    }
+
+    private static String buildURI() {
+        return ConfigProperties.Backend.PROTOCOL + "://" + ConfigProperties.Backend.HOST + ":" + ConfigProperties.Backend.PORT
+                + "/fineract-provider/api";
+    }
+}
diff --git a/settings.gradle b/settings.gradle
index de7530c1a1..c8cf0e47ae 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -67,6 +67,7 @@ include ':integration-tests'
 include ':twofactor-tests'
 include ':oauth2-tests'
 include ':fineract-client'
+include ':fineract-client-feign'
 include ':fineract-doc'
 include ':fineract-avro-schemas'
 include ':fineract-e2e-tests-core'