FINERACT-2380: create feign client

This commit is contained in:
Attila Budai
2025-10-17 14:25:39 +02:00
committed by Adam Saghy
parent f193bbbdf1
commit 553347b320
56 changed files with 7906 additions and 21 deletions
+5
View File
@@ -44,6 +44,7 @@ buildscript {
'twofactor-tests', 'twofactor-tests',
'oauth2-tests', 'oauth2-tests',
'fineract-client', 'fineract-client',
'fineract-client-feign',
'fineract-avro-schemas', 'fineract-avro-schemas',
'fineract-e2e-tests-core', 'fineract-e2e-tests-core',
'fineract-e2e-tests-runner', 'fineract-e2e-tests-runner',
@@ -55,6 +56,7 @@ buildscript {
[ [
'fineract-avro-schemas', 'fineract-avro-schemas',
'fineract-client', 'fineract-client',
'fineract-client-feign',
'fineract-core', 'fineract-core',
'fineract-cob', 'fineract-cob',
'fineract-validation', 'fineract-validation',
@@ -558,6 +560,9 @@ configure(project.fineractJavaProjects) {
if (project.path == ':fineract-client') { if (project.path == ':fineract-client') {
excludedPaths = '.*/build/generated/java/src/main/java/.*' excludedPaths = '.*/build/generated/java/src/main/java/.*'
} }
if (project.path == ':fineract-client-feign') {
excludedPaths = '.*/build/generated/java/src/main/java/.*'
}
disable( disable(
// TODO Remove disabled checks from this list, by fixing remaining usages // TODO Remove disabled checks from this list, by fixing remaining usages
"UnusedVariable", "UnusedVariable",
@@ -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
+194
View File
@@ -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
}
+49
View File
@@ -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'
)
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
}
@@ -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;
}
}
@@ -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");
}
}
}
@@ -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);
}
@@ -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";
}
}
}
@@ -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);
}
@@ -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();
}
}
}
@@ -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();
}
}
@@ -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";
}
}
@@ -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}}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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");
}
}
@@ -32,11 +32,19 @@ public class BasicPasswordEncodablePlatformUser implements PlatformUser {
@Getter @Getter
private Long id; private Long id;
@Getter(onMethod_ = @Override)
private String username; private String username;
@Getter(onMethod_ = @Override)
private String password; private String password;
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override @Override
public Collection<GrantedAuthority> getAuthorities() { public Collection<GrantedAuthority> getAuthorities() {
return null; return null;
+58 -4
View File
@@ -88,15 +88,69 @@ cargo {
cargoRunLocal.dependsOn ':fineract-war:war' cargoRunLocal.dependsOn ':fineract-war:war'
cargoStartLocal.dependsOn ':fineract-war:war' cargoStartLocal.dependsOn ':fineract-war:war'
cargoStartLocal.mustRunAfter 'testClasses'
if (!project.hasProperty('cargoDisabled')) { import java.net.HttpURLConnection
test { import java.net.URL
dependsOn(cargoStartLocal) 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 finalizedBy cargoStopLocal
} }
} }
if (!project.hasProperty('cargoDisabled')) {
cargoStartLocal.mustRunAfter testClasses
waitForFineract.mustRunAfter cargoStartLocal
}
// Configure proper test output directories // Configure proper test output directories
sourceSets { sourceSets {
test { test {
+11 -2
View File
@@ -21,7 +21,8 @@ dependencies {
// Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly! // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly!
// //
tomcat 'org.apache.tomcat:tomcat:10.1.42@zip' 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-core', configuration: 'runtimeElements'),
project(path: ':fineract-accounting', configuration: 'runtimeElements'), project(path: ':fineract-accounting', configuration: 'runtimeElements'),
project(path: ':fineract-investor', configuration: 'runtimeElements'), project(path: ':fineract-investor', configuration: 'runtimeElements'),
@@ -32,9 +33,17 @@ dependencies {
project(path: ':fineract-savings', configuration: 'runtimeElements'), project(path: ':fineract-savings', configuration: 'runtimeElements'),
project(path: ':fineract-provider', configuration: 'runtimeElements'), project(path: ':fineract-provider', configuration: 'runtimeElements'),
project(path: ':fineract-avro-schemas', configuration: 'runtimeElements'), project(path: ':fineract-avro-schemas', configuration: 'runtimeElements'),
project(path: ':fineract-client', configuration: 'runtimeElements'),
project(path: ':fineract-progressive-loan', 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') { testImplementation ('org.mock-server:mockserver-junit-jupiter') {
exclude group: 'com.sun.mail', module: 'mailapi' exclude group: 'com.sun.mail', module: 'mailapi'
exclude group: 'javax.servlet', module: 'javax.servlet-api' exclude group: 'javax.servlet', module: 'javax.servlet-api'
@@ -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();
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
};
}
}
@@ -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());
}
}
}
}
@@ -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()));
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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")));
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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() {}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker; import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; 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.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -106,6 +107,12 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
setProperFinancialActivity(TRANSFER_ACCOUNT); setProperFinancialActivity(TRANSFER_ACCOUNT);
} }
@AfterEach
public void tearDown() {
// Restore business date configuration after each test
cleanUpAndRestoreBusinessDate();
}
private static void setProperFinancialActivity(Account transferAccount) { private static void setProperFinancialActivity(Account transferAccount) {
List<GetFinancialActivityAccountsResponse> financialMappings = FINANCIAL_ACTIVITY_ACCOUNT_HELPER.getAllFinancialActivityAccounts(); List<GetFinancialActivityAccountsResponse> financialMappings = FINANCIAL_ACTIVITY_ACCOUNT_HELPER.getAllFinancialActivityAccounts();
financialMappings.forEach(mapping -> FINANCIAL_ACTIVITY_ACCOUNT_HELPER.deleteFinancialActivityAccount(mapping.getId())); 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 // Force close loans 3, 4, ... , N-3, N-2
Collections.sort(loanIds); Collections.sort(loanIds);
List<Integer> forelosedLoanIds = new ArrayList<>();
final CountDownLatch closeLatch = new CountDownLatch(N - 5); final CountDownLatch closeLatch = new CountDownLatch(N - 5);
// Warm up (EclipseLink sometimes fails if JPQL cache is not warm up but concurrent queries are executed) // 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)); LOAN_TRANSACTION_HELPER.forecloseLoan("02 March 2020", loanIds.get(2));
forelosedLoanIds.add(loanIds.get(2));
for (int i = 3; i < N - 2; i++) { for (int i = 3; i < N - 2; i++) {
final int idx = i; final int idx = i;
futures.add(executorService.submit(() -> { futures.add(executorService.submit(() -> {
LOAN_TRANSACTION_HELPER.forecloseLoan("02 March 2020", loanIds.get(idx)); LOAN_TRANSACTION_HELPER.forecloseLoan("02 March 2020", loanIds.get(idx));
forelosedLoanIds.add(loanIds.get(idx));
closeLatch.countDown(); closeLatch.countDown();
})); }));
} }
@@ -163,24 +173,45 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
}); });
closeLatch.await(); closeLatch.await();
// 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);
// Let's retrieve the partitions // Let's retrieve the partitions
List<Map<String, Object>> cobPartitions = CobHelper.getCobPartitions(REQUEST_SPEC, RESPONSE_SPEC, 3, ""); List<Map<String, Object>> allPartitions = CobHelper.getCobPartitions(REQUEST_SPEC, RESPONSE_SPEC, 3, "");
log.info("\nLoans created: {},\nRetrieved partitions: {}", loanIds, cobPartitions); log.info("\nLoans created by this test: {}\nAll partitions retrieved: {}", loanIds, allPartitions);
Assertions.assertEquals(2, cobPartitions.size());
Assertions.assertEquals(0, cobPartitions.get(0).get("pageNo")); // Filter partitions to only include ACTIVE loans created by THIS test
Assertions.assertEquals(loanIds.get(0), cobPartitions.get(0).get("minId")); // This ensures test isolation - we don't care about leftover loans from other tests
Assertions.assertEquals(loanIds.get(8), cobPartitions.get(0).get("maxId")); List<Map<String, Object>> testLoanPartitions = filterPartitionsForTestLoans(allPartitions, activeLoanIds);
Assertions.assertEquals(3, cobPartitions.get(0).get("count")); log.info("Filtered partitions containing only our active test loans: {}", testLoanPartitions);
Assertions.assertEquals(1, cobPartitions.get(1).get("pageNo")); // Verify partitioning works correctly for our 4 active loans (10 created - 6 foreclosed)
Assertions.assertEquals(loanIds.get(9), cobPartitions.get(1).get("minId")); // Expected: 2 partitions with page size 3
Assertions.assertEquals(loanIds.get(9), cobPartitions.get(1).get("maxId")); // Partition 0: loans 0, 1, 8 (3 loans)
Assertions.assertEquals(1, cobPartitions.get(1).get("count")); // 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(); executorService.shutdown();
} finally { } catch (Exception e) {
cleanUpAndRestoreBusinessDate(); log.error("Test failed with exception", e);
throw e;
} }
} }
@@ -291,4 +322,41 @@ public class CobPartitioningTest extends BaseLoanIntegrationTest {
return collateral; 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;
}
} }
@@ -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";
}
}
+1
View File
@@ -67,6 +67,7 @@ include ':integration-tests'
include ':twofactor-tests' include ':twofactor-tests'
include ':oauth2-tests' include ':oauth2-tests'
include ':fineract-client' include ':fineract-client'
include ':fineract-client-feign'
include ':fineract-doc' include ':fineract-doc'
include ':fineract-avro-schemas' include ':fineract-avro-schemas'
include ':fineract-e2e-tests-core' include ':fineract-e2e-tests-core'