FINERACT-2421: Allow reage preview API via Batch API

This commit is contained in:
Adam Saghy
2026-01-15 13:35:54 +01:00
parent 1a2faa9c62
commit 633acf204b
9 changed files with 1163 additions and 6 deletions
@@ -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
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
@@ -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)));
}
/**
@@ -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&param2=&param3=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);
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}