FINERACT-2421: Allow reage preview API via Batch API
This commit is contained in:
+9
-2
@@ -47,12 +47,12 @@ public class CommandStrategyProvider {
|
||||
/**
|
||||
* Regex pattern for specifying any number of query params or not specific any query param
|
||||
*/
|
||||
private static final String OPTIONAL_QUERY_PARAM_REGEX = "(\\?(\\w+(?:\\=[\\w,]+|&)+)+)?";
|
||||
private static final String OPTIONAL_QUERY_PARAM_REGEX = "(\\?(\\w+=[^&]+)(?:&\\w+=[^&]+)*)?";
|
||||
|
||||
/**
|
||||
* Regex pattern for specifying query params
|
||||
*/
|
||||
private static final String MANDATORY_QUERY_PARAM_REGEX = "(\\?(\\w+(?:\\=[\\w\\-,]+|&)+)+)";
|
||||
private static final String MANDATORY_QUERY_PARAM_REGEX = "(\\?(\\w+=[^&]+)(?:&\\w+=[^&]+)*)";
|
||||
|
||||
/**
|
||||
* Regex pattern for specifying any query param that has key = 'command' or not specific anything.
|
||||
@@ -201,6 +201,13 @@ public class CommandStrategyProvider {
|
||||
CommandContext.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/transactions\\/external-id\\/"
|
||||
+ UUID_PARAM_REGEX + OPTIONAL_QUERY_PARAM_REGEX).method(GET).build(),
|
||||
"getLoanTransactionByExternalIdCommandStrategy");
|
||||
commandStrategies.put(
|
||||
CommandContext.resource("v1\\/loans\\/" + NUMBER_REGEX + "\\/transactions\\/reage-preview" + OPTIONAL_QUERY_PARAM_REGEX)
|
||||
.method(GET).build(),
|
||||
"getReagePreviewByLoanIdCommandStrategy");
|
||||
commandStrategies.put(CommandContext
|
||||
.resource("v1\\/loans\\/external-id\\/" + UUID_PARAM_REGEX + "\\/transactions\\/reage-preview" + OPTIONAL_QUERY_PARAM_REGEX)
|
||||
.method(GET).build(), "getReagePreviewByLoanExternalIdCommandStrategy");
|
||||
commandStrategies.put(CommandContext.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX)
|
||||
.method(POST).build(), "createDatatableEntryCommandStrategy");
|
||||
commandStrategies.put(CommandContext
|
||||
|
||||
+89
-2
@@ -18,6 +18,11 @@
|
||||
*/
|
||||
package org.apache.fineract.batch.command;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -39,7 +44,7 @@ public final class CommandStrategyUtils {
|
||||
*
|
||||
* @param relativeUrl
|
||||
* the relative URL
|
||||
* @return the query parameters in a map
|
||||
* @return the query parameters in a map with URL-decoded values
|
||||
*/
|
||||
public static Map<String, String> getQueryParameters(final String relativeUrl) {
|
||||
final String queryParameterStr = StringUtils.substringAfter(relativeUrl, "?");
|
||||
@@ -47,7 +52,14 @@ public final class CommandStrategyUtils {
|
||||
final Map<String, String> queryParametersMap = new HashMap<>();
|
||||
for (String parameterStr : queryParametersArray) {
|
||||
String[] keyValue = StringUtils.split(parameterStr, "=");
|
||||
queryParametersMap.put(keyValue[0], keyValue[1]);
|
||||
String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
|
||||
String value = "";
|
||||
if (keyValue.length > 1 && StringUtils.isNotEmpty(keyValue[1])) {
|
||||
value = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8);
|
||||
} else if (keyValue.length > 1) {
|
||||
value = keyValue[1];
|
||||
}
|
||||
queryParametersMap.put(key, value);
|
||||
}
|
||||
return queryParametersMap;
|
||||
}
|
||||
@@ -86,4 +98,79 @@ public final class CommandStrategyUtils {
|
||||
return m.matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a request object from query parameters using reflection and the builder pattern. This method automatically
|
||||
* detects field types and converts string values to the appropriate type (Integer, String, etc.).
|
||||
*
|
||||
* @param queryParameters
|
||||
* map of query parameter names to their string values
|
||||
* @param requestClass
|
||||
* the class of the request object to build (must have a builder() method)
|
||||
* @param <T>
|
||||
* the type of the request object
|
||||
* @return the built request object with fields populated from query parameters
|
||||
* @throws RuntimeException
|
||||
* if the request class doesn't have a builder or if reflection fails
|
||||
*/
|
||||
public static <T> T buildRequestFromQueryParameters(final Map<String, String> queryParameters, final Class<T> requestClass) {
|
||||
try {
|
||||
// Get the builder
|
||||
Method builderMethod = requestClass.getMethod("builder");
|
||||
Object builder = builderMethod.invoke(null);
|
||||
Class<?> builderClass = builder.getClass();
|
||||
|
||||
// Iterate through all declared fields of the request class
|
||||
for (Field field : requestClass.getDeclaredFields()) {
|
||||
String fieldName = field.getName();
|
||||
String paramValue = queryParameters.get(fieldName);
|
||||
|
||||
// Skip if parameter is not present or is the serialVersionUID field
|
||||
if (paramValue == null || "serialVersionUID".equals(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the builder method for this field
|
||||
Method builderSetter = builderClass.getMethod(fieldName, field.getType());
|
||||
|
||||
// Convert the string value to the appropriate type and invoke builder method
|
||||
Object convertedValue = convertValue(paramValue, field.getType());
|
||||
builderSetter.invoke(builder, convertedValue);
|
||||
}
|
||||
|
||||
// Call build() method to create the final object
|
||||
Method buildMethod = builderClass.getMethod("build");
|
||||
return requestClass.cast(buildMethod.invoke(builder));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to build request object from query parameters", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string value to the target type.
|
||||
*
|
||||
* @param value
|
||||
* the string value to convert
|
||||
* @param targetType
|
||||
* the target type
|
||||
* @return the converted value
|
||||
*/
|
||||
private static Object convertValue(final String value, final Class<?> targetType) {
|
||||
if (targetType == String.class) {
|
||||
return value;
|
||||
} else if (targetType == Integer.class || targetType == int.class) {
|
||||
return Integer.parseInt(value);
|
||||
} else if (targetType == Long.class || targetType == long.class) {
|
||||
return Long.parseLong(value);
|
||||
} else if (targetType == BigDecimal.class) {
|
||||
return new BigDecimal(value);
|
||||
} else if (targetType == Double.class || targetType == double.class) {
|
||||
return Double.parseDouble(value);
|
||||
} else if (targetType == Boolean.class || targetType == boolean.class) {
|
||||
return Boolean.parseBoolean(value);
|
||||
} else {
|
||||
// Default to string for unknown types
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+95
@@ -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.batch.command.internal;
|
||||
|
||||
import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.batch.command.CommandStrategy;
|
||||
import org.apache.fineract.batch.command.CommandStrategyUtils;
|
||||
import org.apache.fineract.batch.domain.BatchRequest;
|
||||
import org.apache.fineract.batch.domain.BatchResponse;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionsApiResource;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Implements {@link CommandStrategy} to calculate the reage preview for the given term for a particular loan by its
|
||||
* loan external id. It passes the contents of the body from the BatchRequest to {@link LoanTransactionsApiResource} and
|
||||
* gets back the response. This class will also catch any errors raised by {@link LoanTransactionsApiResource} and map
|
||||
* those errors to appropriate status codes in BatchResponse.
|
||||
*
|
||||
* @see CommandStrategy
|
||||
* @see BatchRequest
|
||||
* @see BatchResponse
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GetReagePreviewByLoanExternalIdCommandStrategy implements CommandStrategy {
|
||||
|
||||
/**
|
||||
* Loan transactions api resource {@link LoanTransactionsApiResource}.
|
||||
*/
|
||||
private final LoanTransactionsApiResource loanTransactionsApiResource;
|
||||
|
||||
/**
|
||||
* The toApiJsonSerializer to convert json to object
|
||||
*/
|
||||
private final DefaultToApiJsonSerializer<LoanScheduleData> toApiJsonSerializer;
|
||||
|
||||
@Override
|
||||
public BatchResponse execute(final BatchRequest request, final UriInfo uriInfo) {
|
||||
final BatchResponse response = new BatchResponse();
|
||||
|
||||
response.setRequestId(request.getRequestId());
|
||||
response.setHeaders(request.getHeaders());
|
||||
|
||||
final String relativeUrl = relativeUrlWithoutVersion(request);
|
||||
|
||||
// Expected pattern - loans/external-id/" + UUID_PARAM_REGEX +
|
||||
// "/transactions/reage-preview?queryParam1=<blah>&queryParam2=<blah>&....
|
||||
// Get the loan external id
|
||||
final List<String> pathParameters = Splitter.on('/').splitToList(relativeUrl);
|
||||
final String loanExternalId = pathParameters.get(2);
|
||||
Map<String, String> queryParameters = new HashMap<>();
|
||||
if (relativeUrl.indexOf('?') > 0) {
|
||||
queryParameters = CommandStrategyUtils.getQueryParameters(relativeUrl);
|
||||
}
|
||||
|
||||
// Build ReAgePreviewRequest from query parameters using generic utility
|
||||
final ReAgePreviewRequest reAgePreviewRequest = CommandStrategyUtils.buildRequestFromQueryParameters(queryParameters,
|
||||
ReAgePreviewRequest.class);
|
||||
|
||||
// Calls 'previewReAgeSchedule' function from 'loanTransactionsApiResource' using external id
|
||||
response.setStatusCode(HttpStatus.SC_OK);
|
||||
// Sets the body of the response after getting reage preview
|
||||
response.setBody(
|
||||
toApiJsonSerializer.serialize(loanTransactionsApiResource.previewReAgeSchedule(loanExternalId, reAgePreviewRequest)));
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
+93
@@ -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.batch.command.internal;
|
||||
|
||||
import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.batch.command.CommandStrategy;
|
||||
import org.apache.fineract.batch.command.CommandStrategyUtils;
|
||||
import org.apache.fineract.batch.domain.BatchRequest;
|
||||
import org.apache.fineract.batch.domain.BatchResponse;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionsApiResource;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Implements {@link CommandStrategy} to calculate the reage preview for the given term for a particular loan by its
|
||||
* loan id. It passes the contents of the body from the BatchRequest to {@link LoanTransactionsApiResource} and gets
|
||||
* back the response. This class will also catch any errors raised by {@link LoanTransactionsApiResource} and map those
|
||||
* errors to appropriate status codes in BatchResponse.
|
||||
*
|
||||
* @see CommandStrategy
|
||||
* @see BatchRequest
|
||||
* @see BatchResponse
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GetReagePreviewByLoanIdCommandStrategy implements CommandStrategy {
|
||||
|
||||
/**
|
||||
* Loan transactions api resource {@link LoanTransactionsApiResource}.
|
||||
*/
|
||||
private final LoanTransactionsApiResource loanTransactionsApiResource;
|
||||
|
||||
/**
|
||||
* The toApiJsonSerializer to convert json to object
|
||||
*/
|
||||
private final DefaultToApiJsonSerializer<LoanScheduleData> toApiJsonSerializer;
|
||||
|
||||
@Override
|
||||
public BatchResponse execute(final BatchRequest request, final UriInfo uriInfo) {
|
||||
final BatchResponse response = new BatchResponse();
|
||||
|
||||
response.setRequestId(request.getRequestId());
|
||||
response.setHeaders(request.getHeaders());
|
||||
|
||||
final String relativeUrl = relativeUrlWithoutVersion(request);
|
||||
|
||||
// Expected pattern - loans/" + NUMBER + "/transactions/reage-preview?queryParam1=<blah>&queryParam2=<blah>&....
|
||||
// Get the loan external id
|
||||
final List<String> pathParameters = Splitter.on('/').splitToList(relativeUrl);
|
||||
final Long loanId = Long.parseLong(pathParameters.get(1));
|
||||
Map<String, String> queryParameters = new HashMap<>();
|
||||
if (relativeUrl.indexOf('?') > 0) {
|
||||
queryParameters = CommandStrategyUtils.getQueryParameters(relativeUrl);
|
||||
}
|
||||
|
||||
// Build ReAgePreviewRequest from query parameters using generic utility
|
||||
final ReAgePreviewRequest reAgePreviewRequest = CommandStrategyUtils.buildRequestFromQueryParameters(queryParameters,
|
||||
ReAgePreviewRequest.class);
|
||||
|
||||
// Calls 'previewReAgeSchedule' function from 'loanTransactionsApiResource' using external id
|
||||
response.setStatusCode(HttpStatus.SC_OK);
|
||||
// Sets the body of the response after getting reage preview
|
||||
response.setBody(toApiJsonSerializer.serialize(loanTransactionsApiResource.previewReAgeSchedule(loanId, reAgePreviewRequest)));
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -77,6 +77,7 @@ import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactio
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@@ -129,7 +130,7 @@ public class LoanReAgingService {
|
||||
.with(changes).build();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
|
||||
public LoanScheduleData previewReAge(final Long loanId, final String loanExternalId, final ReAgePreviewRequest reAgePreviewRequest) {
|
||||
final Loan loan = loanId != null ? loanAssembler.assembleFrom(loanId)
|
||||
: loanAssembler.assembleFrom(ExternalIdFactory.produce(loanExternalId), false);
|
||||
|
||||
+20
-1
@@ -52,6 +52,8 @@ import org.apache.fineract.batch.command.internal.GetLoanByExternalIdCommandStra
|
||||
import org.apache.fineract.batch.command.internal.GetLoanByIdCommandStrategy;
|
||||
import org.apache.fineract.batch.command.internal.GetLoanTransactionByExternalIdCommandStrategy;
|
||||
import org.apache.fineract.batch.command.internal.GetLoanTransactionByIdCommandStrategy;
|
||||
import org.apache.fineract.batch.command.internal.GetReagePreviewByLoanExternalIdCommandStrategy;
|
||||
import org.apache.fineract.batch.command.internal.GetReagePreviewByLoanIdCommandStrategy;
|
||||
import org.apache.fineract.batch.command.internal.LoanStateTransistionsByExternalIdCommandStrategy;
|
||||
import org.apache.fineract.batch.command.internal.ModifyLoanApplicationCommandStrategy;
|
||||
import org.apache.fineract.batch.command.internal.UnknownCommandStrategy;
|
||||
@@ -206,7 +208,24 @@ public class CommandStrategyProviderTest {
|
||||
Arguments.of("loans/external-id/8dfad438-2319-48ce-8520-10a62801e9a1/interest-pauses", HttpMethod.POST,
|
||||
"createLoanInterestPauseByExternalIdCommandStrategy", mock(CommandStrategy.class)),
|
||||
Arguments.of("loans/external-id/8dfad438-2319-48ce-8520-10a62801e9a1/interest-pauses/123", HttpMethod.PUT,
|
||||
"updateLoanInterestPauseByExternalIdCommandStrategy", mock(CommandStrategy.class)));
|
||||
"updateLoanInterestPauseByExternalIdCommandStrategy", mock(CommandStrategy.class)),
|
||||
Arguments.of("loans/123/transactions/reage-preview", HttpMethod.GET, "getReagePreviewByLoanIdCommandStrategy",
|
||||
mock(GetReagePreviewByLoanIdCommandStrategy.class)),
|
||||
Arguments.of("loans/123/transactions/reage-preview?frequencyNumber=1&frequencyType=MONTHS", HttpMethod.GET,
|
||||
"getReagePreviewByLoanIdCommandStrategy", mock(GetReagePreviewByLoanIdCommandStrategy.class)),
|
||||
Arguments.of(
|
||||
"loans/123/transactions/reage-preview?frequencyType=MONTHS&locale=en_US&frequencyNumber=1&dateFormat=MM%2Fdd%2Fyyyy&startDate=02%2F05%2F2026&numberOfInstallments=6",
|
||||
HttpMethod.GET, "getReagePreviewByLoanIdCommandStrategy", mock(GetReagePreviewByLoanIdCommandStrategy.class)),
|
||||
Arguments.of("loans/external-id/8dfad438-2319-48ce-8520-10a62801e9a1/transactions/reage-preview", HttpMethod.GET,
|
||||
"getReagePreviewByLoanExternalIdCommandStrategy", mock(GetReagePreviewByLoanExternalIdCommandStrategy.class)),
|
||||
Arguments.of(
|
||||
"loans/external-id/8dfad438-2319-48ce-8520-10a62801e9a1/transactions/reage-preview?frequencyNumber=2&frequencyType=WEEKS",
|
||||
HttpMethod.GET, "getReagePreviewByLoanExternalIdCommandStrategy",
|
||||
mock(GetReagePreviewByLoanExternalIdCommandStrategy.class)),
|
||||
Arguments.of(
|
||||
"loans/external-id/0083477d-ea2a-45a4-a244-cb79a9ecf741/transactions/reage-preview?frequencyType=MONTHS&locale=en_US&frequencyNumber=1&dateFormat=MM%2Fdd%2Fyyyy&startDate=02%2F05%2F2026&numberOfInstallments=6",
|
||||
HttpMethod.GET, "getReagePreviewByLoanExternalIdCommandStrategy",
|
||||
mock(GetReagePreviewByLoanExternalIdCommandStrategy.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+305
@@ -19,9 +19,21 @@
|
||||
package org.apache.fineract.batch.command;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.fineract.batch.domain.BatchRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
class CommandStrategyUtilsTest {
|
||||
|
||||
@@ -90,4 +102,297 @@ class CommandStrategyUtilsTest {
|
||||
// then
|
||||
assertThat(result).isEqualTo("clients/123?command=action&something=else");
|
||||
}
|
||||
|
||||
// Tests for buildRequestFromQueryParameters method
|
||||
|
||||
private static Stream<Arguments> typeConversionTestCases() {
|
||||
return Stream.of(Arguments.of("stringField", "testValue", (Object) "testValue"), Arguments.of("integerField", "42", 42),
|
||||
Arguments.of("longField", "9876543210", 9876543210L), Arguments.of("bigDecimalField", "123.456", new BigDecimal("123.456")),
|
||||
Arguments.of("bigDecimalField", "0.1000000000000000055511151231257827021181583404541015625",
|
||||
new BigDecimal("0.1000000000000000055511151231257827021181583404541015625")),
|
||||
Arguments.of("doubleField", "3.14", 3.14), Arguments.of("booleanField", "true", true),
|
||||
Arguments.of("booleanField", "false", false), Arguments.of("booleanField", "TRUE", true));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("typeConversionTestCases")
|
||||
public void testBuildRequestFromQueryParametersTypeConversion(String fieldName, String value, Object expectedValue) {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put(fieldName, value);
|
||||
|
||||
// when
|
||||
TestRequest result = CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, TestRequest.class);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
Object actualValue = getFieldValue(result, fieldName);
|
||||
if (expectedValue instanceof BigDecimal) {
|
||||
assertThat((BigDecimal) actualValue).isEqualByComparingTo((BigDecimal) expectedValue);
|
||||
} else {
|
||||
assertThat(actualValue).isEqualTo(expectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> invalidTypeConversionTestCases() {
|
||||
return Stream.of(Arguments.of("integerField", "notANumber"), Arguments.of("longField", "notANumber"),
|
||||
Arguments.of("bigDecimalField", "notANumber"), Arguments.of("doubleField", "notANumber"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidTypeConversionTestCases")
|
||||
public void testBuildRequestFromQueryParametersInvalidTypeConversion(String fieldName, String invalidValue) {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put(fieldName, invalidValue);
|
||||
|
||||
// when/then
|
||||
assertThatThrownBy(() -> CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, TestRequest.class))
|
||||
.isInstanceOf(RuntimeException.class).hasMessageContaining("Failed to build request object from query parameters")
|
||||
.hasCauseInstanceOf(NumberFormatException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildRequestFromQueryParametersWithAllFieldTypes() {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put("stringField", "testValue");
|
||||
queryParams.put("integerField", "42");
|
||||
queryParams.put("longField", "9876543210");
|
||||
queryParams.put("bigDecimalField", "3.14");
|
||||
queryParams.put("doubleField", "2.71");
|
||||
queryParams.put("booleanField", "true");
|
||||
|
||||
// when
|
||||
TestRequest result = CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, TestRequest.class);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getStringField()).isEqualTo("testValue");
|
||||
assertThat(result.getIntegerField()).isEqualTo(42);
|
||||
assertThat(result.getLongField()).isEqualTo(9876543210L);
|
||||
assertThat(result.getBigDecimalField()).isEqualByComparingTo(new BigDecimal("3.14"));
|
||||
assertThat(result.getDoubleField()).isEqualTo(2.71);
|
||||
assertThat(result.getBooleanField()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildRequestFromQueryParametersWithMissingFields() {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put("stringField", "testValue");
|
||||
|
||||
// when
|
||||
TestRequest result = CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, TestRequest.class);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getStringField()).isEqualTo("testValue");
|
||||
assertThat(result.getIntegerField()).isNull();
|
||||
assertThat(result.getLongField()).isNull();
|
||||
assertThat(result.getBigDecimalField()).isNull();
|
||||
assertThat(result.getDoubleField()).isNull();
|
||||
assertThat(result.getBooleanField()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildRequestFromQueryParametersWithEmptyMap() {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
|
||||
// when
|
||||
TestRequest result = CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, TestRequest.class);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getStringField()).isNull();
|
||||
assertThat(result.getIntegerField()).isNull();
|
||||
assertThat(result.getLongField()).isNull();
|
||||
assertThat(result.getBigDecimalField()).isNull();
|
||||
assertThat(result.getDoubleField()).isNull();
|
||||
assertThat(result.getBooleanField()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildRequestFromQueryParametersSkipsSerialVersionUID() {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put("serialVersionUID", "12345");
|
||||
queryParams.put("stringField", "testValue");
|
||||
|
||||
// when
|
||||
TestRequest result = CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, TestRequest.class);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getStringField()).isEqualTo("testValue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildRequestFromQueryParametersWithClassWithoutBuilder() {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put("field", "value");
|
||||
|
||||
// when/then
|
||||
assertThatThrownBy(() -> CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, ClassWithoutBuilder.class))
|
||||
.isInstanceOf(RuntimeException.class).hasMessageContaining("Failed to build request object from query parameters");
|
||||
}
|
||||
|
||||
private Object getFieldValue(TestRequest request, String fieldName) {
|
||||
switch (fieldName) {
|
||||
case "stringField":
|
||||
return request.getStringField();
|
||||
case "integerField":
|
||||
return request.getIntegerField();
|
||||
case "longField":
|
||||
return request.getLongField();
|
||||
case "bigDecimalField":
|
||||
return request.getBigDecimalField();
|
||||
case "doubleField":
|
||||
return request.getDoubleField();
|
||||
case "booleanField":
|
||||
return request.getBooleanField();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
static class TestRequest {
|
||||
|
||||
private String stringField;
|
||||
private Integer integerField;
|
||||
private Long longField;
|
||||
private BigDecimal bigDecimalField;
|
||||
private Double doubleField;
|
||||
private Boolean booleanField;
|
||||
}
|
||||
|
||||
static class ClassWithoutBuilder {
|
||||
|
||||
private String field;
|
||||
|
||||
ClassWithoutBuilder(String field) {
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for getQueryParameters method with URL decoding
|
||||
|
||||
private static Stream<Arguments> urlDecodingTestCases() {
|
||||
return Stream.of(
|
||||
// URL-encoded forward slashes
|
||||
Arguments.of("loans/123?dateFormat=MM%2Fdd%2Fyyyy", "dateFormat", "MM/dd/yyyy"),
|
||||
Arguments.of("loans/123?startDate=02%2F05%2F2026", "startDate", "02/05/2026"),
|
||||
// URL-encoded spaces
|
||||
Arguments.of("loans/123?name=John%20Doe", "name", "John Doe"),
|
||||
// URL-encoded special characters
|
||||
Arguments.of("loans/123?email=user%40example.com", "email", "user@example.com"),
|
||||
Arguments.of("loans/123?query=hello%26world", "query", "hello&world"),
|
||||
// URL-encoded percentage sign
|
||||
Arguments.of("loans/123?discount=50%25", "discount", "50%"),
|
||||
// Non-encoded values (should remain unchanged)
|
||||
Arguments.of("loans/123?locale=en_US", "locale", "en_US"),
|
||||
Arguments.of("loans/123?frequencyType=MONTHS", "frequencyType", "MONTHS"),
|
||||
Arguments.of("loans/123?frequencyNumber=1", "frequencyNumber", "1"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("urlDecodingTestCases")
|
||||
public void testGetQueryParametersWithUrlDecoding(String relativeUrl, String expectedKey, String expectedValue) {
|
||||
// when
|
||||
Map<String, String> result = CommandStrategyUtils.getQueryParameters(relativeUrl);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).containsKey(expectedKey);
|
||||
assertThat(result.get(expectedKey)).isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetQueryParametersWithMultipleUrlEncodedParams() {
|
||||
// given
|
||||
String relativeUrl = "loans/123?dateFormat=MM%2Fdd%2Fyyyy&startDate=02%2F05%2F2026&locale=en_US&name=John%20Doe";
|
||||
|
||||
// when
|
||||
Map<String, String> result = CommandStrategyUtils.getQueryParameters(relativeUrl);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).hasSize(4);
|
||||
assertThat(result.get("dateFormat")).isEqualTo("MM/dd/yyyy");
|
||||
assertThat(result.get("startDate")).isEqualTo("02/05/2026");
|
||||
assertThat(result.get("locale")).isEqualTo("en_US");
|
||||
assertThat(result.get("name")).isEqualTo("John Doe");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetQueryParametersWithComplexUrlEncodedQuery() {
|
||||
// given - using the actual sample URL from the requirement
|
||||
String relativeUrl = "loans/external-id/0083477d-ea2a-45a4-a244-cb79a9ecf741/transactions/reage-preview"
|
||||
+ "?frequencyType=MONTHS&locale=en_US&frequencyNumber=1&dateFormat=MM%2Fdd%2Fyyyy"
|
||||
+ "&startDate=02%2F05%2F2026&numberOfInstallments=6";
|
||||
|
||||
// when
|
||||
Map<String, String> result = CommandStrategyUtils.getQueryParameters(relativeUrl);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).hasSize(6);
|
||||
assertThat(result.get("frequencyType")).isEqualTo("MONTHS");
|
||||
assertThat(result.get("locale")).isEqualTo("en_US");
|
||||
assertThat(result.get("frequencyNumber")).isEqualTo("1");
|
||||
assertThat(result.get("dateFormat")).isEqualTo("MM/dd/yyyy");
|
||||
assertThat(result.get("startDate")).isEqualTo("02/05/2026");
|
||||
assertThat(result.get("numberOfInstallments")).isEqualTo("6");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetQueryParametersWithEmptyValue() {
|
||||
// given
|
||||
String relativeUrl = "loans/123?param1=value1¶m2=¶m3=value3";
|
||||
|
||||
// when
|
||||
Map<String, String> result = CommandStrategyUtils.getQueryParameters(relativeUrl);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).hasSize(3);
|
||||
assertThat(result.get("param1")).isEqualTo("value1");
|
||||
assertThat(result.get("param2")).isEqualTo("");
|
||||
assertThat(result.get("param3")).isEqualTo("value3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetQueryParametersWithNoQueryParams() {
|
||||
// given
|
||||
String relativeUrl = "loans/123";
|
||||
|
||||
// when
|
||||
Map<String, String> result = CommandStrategyUtils.getQueryParameters(relativeUrl);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildRequestFromQueryParametersWithUrlEncodedValues() {
|
||||
// given
|
||||
Map<String, String> queryParams = new HashMap<>();
|
||||
queryParams.put("stringField", "Hello World"); // Already decoded by getQueryParameters
|
||||
queryParams.put("integerField", "42");
|
||||
|
||||
// when
|
||||
TestRequest result = CommandStrategyUtils.buildRequestFromQueryParameters(queryParams, TestRequest.class);
|
||||
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getStringField()).isEqualTo("Hello World");
|
||||
assertThat(result.getIntegerField()).isEqualTo(42);
|
||||
}
|
||||
}
|
||||
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 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.batch.command.internal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import jakarta.ws.rs.HttpMethod;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.fineract.batch.domain.BatchRequest;
|
||||
import org.apache.fineract.batch.domain.BatchResponse;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionsApiResource;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Test class for {@link GetReagePreviewByLoanExternalIdCommandStrategy}.
|
||||
*/
|
||||
public class GetReagePreviewByLoanExternalIdCommandStrategyTest {
|
||||
|
||||
/**
|
||||
* The query parameter provider.
|
||||
*
|
||||
* @return Arguments.
|
||||
*/
|
||||
private static Stream<Arguments> provideQueryParameters() {
|
||||
return Stream.of(Arguments.of(1, "MONTHS", "2024-01-15", 12, "dd MMMM yyyy", "en"),
|
||||
Arguments.of(2, "WEEKS", "2024-02-01", 24, "yyyy-MM-dd", "en_US"),
|
||||
Arguments.of(3, "DAYS", "2024-03-10", 6, "dd/MM/yyyy", "en_GB"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test {@link GetReagePreviewByLoanExternalIdCommandStrategy#execute} with wrong parameter names.
|
||||
*/
|
||||
@Test
|
||||
public void testExecuteWithWrongParameterNames() {
|
||||
// given
|
||||
final TestContext testContext = new TestContext();
|
||||
|
||||
final String loanExternalId = UUID.randomUUID().toString();
|
||||
// Build request with WRONG parameter names
|
||||
final BatchRequest request = getBatchRequestWithWrongParameterNames(loanExternalId);
|
||||
|
||||
// Mock LoanScheduleData since it doesn't have a default constructor
|
||||
final LoanScheduleData loanScheduleData = mock(LoanScheduleData.class);
|
||||
final String responseBody = "{\"periods\":[]}";
|
||||
|
||||
given(testContext.loanTransactionsApiResource.previewReAgeSchedule(eq(loanExternalId), any(ReAgePreviewRequest.class)))
|
||||
.willReturn(loanScheduleData);
|
||||
given(testContext.toApiJsonSerializer.serialize(eq(loanScheduleData))).willReturn(responseBody);
|
||||
|
||||
// when
|
||||
final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo);
|
||||
|
||||
// then
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
|
||||
|
||||
// Verify the API resource was called
|
||||
verify(testContext.loanTransactionsApiResource).previewReAgeSchedule(eq(loanExternalId),
|
||||
testContext.reAgePreviewRequestCaptor.capture());
|
||||
|
||||
// Verify the serializer was invoked
|
||||
verify(testContext.toApiJsonSerializer).serialize(eq(loanScheduleData));
|
||||
|
||||
// Verify that wrong parameter names result in null values
|
||||
final ReAgePreviewRequest capturedRequest = testContext.reAgePreviewRequestCaptor.getValue();
|
||||
assertThat(capturedRequest.getFrequencyNumber()).isNull(); // frequencyNo was sent, not frequencyNumber
|
||||
assertThat(capturedRequest.getFrequencyType()).isNull(); // freqType was sent, not frequencyType
|
||||
assertThat(capturedRequest.getStartDate()).isNull(); // start was sent, not startDate
|
||||
assertThat(capturedRequest.getNumberOfInstallments()).isNull(); // installments was sent, not
|
||||
// numberOfInstallments
|
||||
assertThat(capturedRequest.getDateFormat()).isNull(); // format was sent, not dateFormat
|
||||
assertThat(capturedRequest.getLocale()).isNull(); // lang was sent, not locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Test {@link GetReagePreviewByLoanExternalIdCommandStrategy#execute} happy path scenario.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideQueryParameters")
|
||||
public void testExecuteSuccessScenario(final Integer frequencyNumber, final String frequencyType, final String startDate,
|
||||
final Integer numberOfInstallments, final String dateFormat, final String locale) {
|
||||
// given
|
||||
final TestContext testContext = new TestContext();
|
||||
|
||||
final String loanExternalId = UUID.randomUUID().toString();
|
||||
final BatchRequest request = getBatchRequest(loanExternalId, frequencyNumber, frequencyType, startDate, numberOfInstallments,
|
||||
dateFormat, locale);
|
||||
|
||||
// Mock LoanScheduleData since it doesn't have a default constructor
|
||||
final LoanScheduleData loanScheduleData = mock(LoanScheduleData.class);
|
||||
final String responseBody = "{\"periods\":[]}";
|
||||
|
||||
given(testContext.loanTransactionsApiResource.previewReAgeSchedule(eq(loanExternalId), any(ReAgePreviewRequest.class)))
|
||||
.willReturn(loanScheduleData);
|
||||
given(testContext.toApiJsonSerializer.serialize(eq(loanScheduleData))).willReturn(responseBody);
|
||||
|
||||
// when
|
||||
final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo);
|
||||
|
||||
// then
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
|
||||
assertThat(response.getRequestId()).isEqualTo(request.getRequestId());
|
||||
assertThat(response.getHeaders()).isEqualTo(request.getHeaders());
|
||||
assertThat(response.getBody()).isEqualTo(responseBody);
|
||||
|
||||
// Verify the API resource was called with correct parameters
|
||||
verify(testContext.loanTransactionsApiResource).previewReAgeSchedule(eq(loanExternalId),
|
||||
testContext.reAgePreviewRequestCaptor.capture());
|
||||
|
||||
// Verify the serializer was invoked with the returned LoanScheduleData
|
||||
verify(testContext.toApiJsonSerializer).serialize(eq(loanScheduleData));
|
||||
|
||||
// Verify the ReAgePreviewRequest was built correctly from query parameters
|
||||
final ReAgePreviewRequest capturedRequest = testContext.reAgePreviewRequestCaptor.getValue();
|
||||
assertThat(capturedRequest.getFrequencyNumber()).isEqualTo(frequencyNumber);
|
||||
assertThat(capturedRequest.getFrequencyType()).isEqualTo(frequencyType);
|
||||
assertThat(capturedRequest.getStartDate()).isEqualTo(startDate);
|
||||
assertThat(capturedRequest.getNumberOfInstallments()).isEqualTo(numberOfInstallments);
|
||||
assertThat(capturedRequest.getDateFormat()).isEqualTo(dateFormat);
|
||||
assertThat(capturedRequest.getLocale()).isEqualTo(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a batch request with the given parameters.
|
||||
*
|
||||
* @param loanExternalId
|
||||
* the loan external id
|
||||
* @param frequencyNumber
|
||||
* the frequency number
|
||||
* @param frequencyType
|
||||
* the frequency type
|
||||
* @param startDate
|
||||
* the start date
|
||||
* @param numberOfInstallments
|
||||
* the number of installments
|
||||
* @param dateFormat
|
||||
* the date format
|
||||
* @param locale
|
||||
* the locale
|
||||
* @return BatchRequest
|
||||
*/
|
||||
private BatchRequest getBatchRequest(final String loanExternalId, final Integer frequencyNumber, final String frequencyType,
|
||||
final String startDate, final Integer numberOfInstallments, final String dateFormat, final String locale) {
|
||||
|
||||
final BatchRequest br = new BatchRequest();
|
||||
String relativeUrl = "loans/external-id/" + loanExternalId + "/transactions/reage-preview";
|
||||
|
||||
Set<String> queryParams = new HashSet<>();
|
||||
queryParams.add("frequencyNumber=" + frequencyNumber);
|
||||
queryParams.add("frequencyType=" + frequencyType);
|
||||
queryParams.add("startDate=" + startDate);
|
||||
queryParams.add("numberOfInstallments=" + numberOfInstallments);
|
||||
queryParams.add("dateFormat=" + dateFormat);
|
||||
queryParams.add("locale=" + locale);
|
||||
|
||||
relativeUrl = relativeUrl + "?" + String.join("&", queryParams);
|
||||
|
||||
br.setRequestId(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setRelativeUrl(relativeUrl);
|
||||
br.setMethod(HttpMethod.GET);
|
||||
br.setReference(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setBody("{}");
|
||||
|
||||
return br;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a batch request with WRONG parameter names to test validation.
|
||||
*
|
||||
* @param loanExternalId
|
||||
* the loan external id
|
||||
* @return BatchRequest
|
||||
*/
|
||||
private BatchRequest getBatchRequestWithWrongParameterNames(final String loanExternalId) {
|
||||
|
||||
final BatchRequest br = new BatchRequest();
|
||||
String relativeUrl = "loans/external-id/" + loanExternalId + "/transactions/reage-preview";
|
||||
|
||||
Set<String> queryParams = new HashSet<>();
|
||||
// Using wrong parameter names
|
||||
queryParams.add("frequencyNo=1"); // should be frequencyNumber
|
||||
queryParams.add("freqType=MONTHS"); // should be frequencyType
|
||||
queryParams.add("start=2024-01-15"); // should be startDate
|
||||
queryParams.add("installments=12"); // should be numberOfInstallments
|
||||
queryParams.add("format=dd MMMM yyyy"); // should be dateFormat
|
||||
queryParams.add("lang=en"); // should be locale
|
||||
|
||||
relativeUrl = relativeUrl + "?" + String.join("&", queryParams);
|
||||
|
||||
br.setRequestId(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setRelativeUrl(relativeUrl);
|
||||
br.setMethod(HttpMethod.GET);
|
||||
br.setReference(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setBody("{}");
|
||||
|
||||
return br;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private test context class used since testng runs in parallel to avoid state between tests
|
||||
*/
|
||||
private static class TestContext {
|
||||
|
||||
/**
|
||||
* The Mock UriInfo
|
||||
*/
|
||||
@Mock
|
||||
private UriInfo uriInfo;
|
||||
|
||||
/**
|
||||
* The Mock {@link LoanTransactionsApiResource}
|
||||
*/
|
||||
@Mock
|
||||
private LoanTransactionsApiResource loanTransactionsApiResource;
|
||||
|
||||
/**
|
||||
* The Mock {@link DefaultToApiJsonSerializer}
|
||||
*/
|
||||
@Mock
|
||||
private DefaultToApiJsonSerializer<LoanScheduleData> toApiJsonSerializer;
|
||||
|
||||
/**
|
||||
* The Captor for ReAgePreviewRequest
|
||||
*/
|
||||
@Captor
|
||||
private ArgumentCaptor<ReAgePreviewRequest> reAgePreviewRequestCaptor;
|
||||
|
||||
/**
|
||||
* The class under test.
|
||||
*/
|
||||
private final GetReagePreviewByLoanExternalIdCommandStrategy subjectToTest;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
TestContext() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
subjectToTest = new GetReagePreviewByLoanExternalIdCommandStrategy(loanTransactionsApiResource, toApiJsonSerializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 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.batch.command.internal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import jakarta.ws.rs.HttpMethod;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.fineract.batch.domain.BatchRequest;
|
||||
import org.apache.fineract.batch.domain.BatchResponse;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionsApiResource;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Test class for {@link GetReagePreviewByLoanIdCommandStrategy}.
|
||||
*/
|
||||
public class GetReagePreviewByLoanIdCommandStrategyTest {
|
||||
|
||||
/**
|
||||
* The query parameter provider.
|
||||
*
|
||||
* @return Arguments.
|
||||
*/
|
||||
private static Stream<Arguments> provideQueryParameters() {
|
||||
return Stream.of(Arguments.of(1L, 1, "MONTHS", "2024-01-15", 12, "dd MMMM yyyy", "en"),
|
||||
Arguments.of(2L, 2, "WEEKS", "2024-02-01", 24, "yyyy-MM-dd", "en_US"),
|
||||
Arguments.of(3L, 3, "DAYS", "2024-03-10", 6, "dd/MM/yyyy", "en_GB"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test {@link GetReagePreviewByLoanIdCommandStrategy#execute} with wrong parameter names.
|
||||
*/
|
||||
@Test
|
||||
public void testExecuteWithWrongParameterNames() {
|
||||
// given
|
||||
final TestContext testContext = new TestContext();
|
||||
|
||||
final Long loanId = 1L;
|
||||
// Build request with WRONG parameter names
|
||||
final BatchRequest request = getBatchRequestWithWrongParameterNames(loanId);
|
||||
|
||||
// Mock LoanScheduleData since it doesn't have a default constructor
|
||||
final LoanScheduleData loanScheduleData = mock(LoanScheduleData.class);
|
||||
final String responseBody = "{\"periods\":[]}";
|
||||
|
||||
given(testContext.loanTransactionsApiResource.previewReAgeSchedule(eq(loanId), any(ReAgePreviewRequest.class)))
|
||||
.willReturn(loanScheduleData);
|
||||
given(testContext.toApiJsonSerializer.serialize(eq(loanScheduleData))).willReturn(responseBody);
|
||||
|
||||
// when
|
||||
final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo);
|
||||
|
||||
// then
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
|
||||
|
||||
// Verify the API resource was called
|
||||
verify(testContext.loanTransactionsApiResource).previewReAgeSchedule(eq(loanId), testContext.reAgePreviewRequestCaptor.capture());
|
||||
|
||||
// Verify the serializer was invoked
|
||||
verify(testContext.toApiJsonSerializer).serialize(eq(loanScheduleData));
|
||||
|
||||
// Verify that wrong parameter names result in null values
|
||||
final ReAgePreviewRequest capturedRequest = testContext.reAgePreviewRequestCaptor.getValue();
|
||||
assertThat(capturedRequest.getFrequencyNumber()).isNull(); // frequencyNo was sent, not frequencyNumber
|
||||
assertThat(capturedRequest.getFrequencyType()).isNull(); // freqType was sent, not frequencyType
|
||||
assertThat(capturedRequest.getStartDate()).isNull(); // start was sent, not startDate
|
||||
assertThat(capturedRequest.getNumberOfInstallments()).isNull(); // installments was sent, not
|
||||
// numberOfInstallments
|
||||
assertThat(capturedRequest.getDateFormat()).isNull(); // format was sent, not dateFormat
|
||||
assertThat(capturedRequest.getLocale()).isNull(); // lang was sent, not locale
|
||||
}
|
||||
|
||||
/**
|
||||
* Test {@link GetReagePreviewByLoanIdCommandStrategy#execute} happy path scenario.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideQueryParameters")
|
||||
public void testExecuteSuccessScenario(final Long loanId, final Integer frequencyNumber, final String frequencyType,
|
||||
final String startDate, final Integer numberOfInstallments, final String dateFormat, final String locale) {
|
||||
// given
|
||||
final TestContext testContext = new TestContext();
|
||||
|
||||
final BatchRequest request = getBatchRequest(loanId, frequencyNumber, frequencyType, startDate, numberOfInstallments, dateFormat,
|
||||
locale);
|
||||
|
||||
// Mock LoanScheduleData since it doesn't have a default constructor
|
||||
final LoanScheduleData loanScheduleData = mock(LoanScheduleData.class);
|
||||
final String responseBody = "{\"periods\":[]}";
|
||||
|
||||
given(testContext.loanTransactionsApiResource.previewReAgeSchedule(eq(loanId), any(ReAgePreviewRequest.class)))
|
||||
.willReturn(loanScheduleData);
|
||||
given(testContext.toApiJsonSerializer.serialize(eq(loanScheduleData))).willReturn(responseBody);
|
||||
|
||||
// when
|
||||
final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo);
|
||||
|
||||
// then
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
|
||||
assertThat(response.getRequestId()).isEqualTo(request.getRequestId());
|
||||
assertThat(response.getHeaders()).isEqualTo(request.getHeaders());
|
||||
assertThat(response.getBody()).isEqualTo(responseBody);
|
||||
|
||||
// Verify the API resource was called with correct parameters
|
||||
verify(testContext.loanTransactionsApiResource).previewReAgeSchedule(eq(loanId), testContext.reAgePreviewRequestCaptor.capture());
|
||||
|
||||
// Verify the serializer was invoked with the returned LoanScheduleData
|
||||
verify(testContext.toApiJsonSerializer).serialize(eq(loanScheduleData));
|
||||
|
||||
// Verify the ReAgePreviewRequest was built correctly from query parameters
|
||||
final ReAgePreviewRequest capturedRequest = testContext.reAgePreviewRequestCaptor.getValue();
|
||||
assertThat(capturedRequest.getFrequencyNumber()).isEqualTo(frequencyNumber);
|
||||
assertThat(capturedRequest.getFrequencyType()).isEqualTo(frequencyType);
|
||||
assertThat(capturedRequest.getStartDate()).isEqualTo(startDate);
|
||||
assertThat(capturedRequest.getNumberOfInstallments()).isEqualTo(numberOfInstallments);
|
||||
assertThat(capturedRequest.getDateFormat()).isEqualTo(dateFormat);
|
||||
assertThat(capturedRequest.getLocale()).isEqualTo(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a batch request with the given parameters.
|
||||
*
|
||||
* @param loanId
|
||||
* the loan id
|
||||
* @param frequencyNumber
|
||||
* the frequency number
|
||||
* @param frequencyType
|
||||
* the frequency type
|
||||
* @param startDate
|
||||
* the start date
|
||||
* @param numberOfInstallments
|
||||
* the number of installments
|
||||
* @param dateFormat
|
||||
* the date format
|
||||
* @param locale
|
||||
* the locale
|
||||
* @return BatchRequest
|
||||
*/
|
||||
private BatchRequest getBatchRequest(final Long loanId, final Integer frequencyNumber, final String frequencyType,
|
||||
final String startDate, final Integer numberOfInstallments, final String dateFormat, final String locale) {
|
||||
|
||||
final BatchRequest br = new BatchRequest();
|
||||
String relativeUrl = "loans/" + loanId + "/transactions/reage-preview";
|
||||
|
||||
Set<String> queryParams = new HashSet<>();
|
||||
queryParams.add("frequencyNumber=" + frequencyNumber);
|
||||
queryParams.add("frequencyType=" + frequencyType);
|
||||
queryParams.add("startDate=" + startDate);
|
||||
queryParams.add("numberOfInstallments=" + numberOfInstallments);
|
||||
queryParams.add("dateFormat=" + dateFormat);
|
||||
queryParams.add("locale=" + locale);
|
||||
|
||||
relativeUrl = relativeUrl + "?" + String.join("&", queryParams);
|
||||
|
||||
br.setRequestId(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setRelativeUrl(relativeUrl);
|
||||
br.setMethod(HttpMethod.GET);
|
||||
br.setReference(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setBody("{}");
|
||||
|
||||
return br;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a batch request with WRONG parameter names to test validation.
|
||||
*
|
||||
* @param loanId
|
||||
* the loan id
|
||||
* @return BatchRequest
|
||||
*/
|
||||
private BatchRequest getBatchRequestWithWrongParameterNames(final Long loanId) {
|
||||
|
||||
final BatchRequest br = new BatchRequest();
|
||||
String relativeUrl = "loans/" + loanId + "/transactions/reage-preview";
|
||||
|
||||
Set<String> queryParams = new HashSet<>();
|
||||
// Using wrong parameter names
|
||||
queryParams.add("frequencyNo=1"); // should be frequencyNumber
|
||||
queryParams.add("freqType=MONTHS"); // should be frequencyType
|
||||
queryParams.add("start=2024-01-15"); // should be startDate
|
||||
queryParams.add("installments=12"); // should be numberOfInstallments
|
||||
queryParams.add("format=dd MMMM yyyy"); // should be dateFormat
|
||||
queryParams.add("lang=en"); // should be locale
|
||||
|
||||
relativeUrl = relativeUrl + "?" + String.join("&", queryParams);
|
||||
|
||||
br.setRequestId(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setRelativeUrl(relativeUrl);
|
||||
br.setMethod(HttpMethod.GET);
|
||||
br.setReference(Long.valueOf(RandomStringUtils.randomNumeric(5)));
|
||||
br.setBody("{}");
|
||||
|
||||
return br;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private test context class used since testng runs in parallel to avoid state between tests
|
||||
*/
|
||||
private static class TestContext {
|
||||
|
||||
/**
|
||||
* The Mock UriInfo
|
||||
*/
|
||||
@Mock
|
||||
private UriInfo uriInfo;
|
||||
|
||||
/**
|
||||
* The Mock {@link LoanTransactionsApiResource}
|
||||
*/
|
||||
@Mock
|
||||
private LoanTransactionsApiResource loanTransactionsApiResource;
|
||||
|
||||
/**
|
||||
* The Mock {@link DefaultToApiJsonSerializer}
|
||||
*/
|
||||
@Mock
|
||||
private DefaultToApiJsonSerializer<LoanScheduleData> toApiJsonSerializer;
|
||||
|
||||
/**
|
||||
* The Captor for ReAgePreviewRequest
|
||||
*/
|
||||
@Captor
|
||||
private ArgumentCaptor<ReAgePreviewRequest> reAgePreviewRequestCaptor;
|
||||
|
||||
/**
|
||||
* The class under test.
|
||||
*/
|
||||
private final GetReagePreviewByLoanIdCommandStrategy subjectToTest;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
TestContext() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
subjectToTest = new GetReagePreviewByLoanIdCommandStrategy(loanTransactionsApiResource, toApiJsonSerializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user