FINERACT-2119: Address jobs by name
This commit is contained in:
committed by
Adam Saghy
parent
eef6a8da5e
commit
3aaa2ffe9c
@@ -42,4 +42,10 @@
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
<changeSet author="acme" id="2">
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="ACM_NOOP"/>
|
||||
<where>name='Acme Noop Job'</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.core.api;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.service.DefaultOption;
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public abstract class IdTypeResolver {
|
||||
|
||||
public enum IdType implements DefaultOption {
|
||||
|
||||
ID, //
|
||||
EXTERNAL_ID, //
|
||||
SHORT_NAME,; //
|
||||
|
||||
@Override
|
||||
public boolean isDefault() {
|
||||
return this == ID;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static IdType resolveDefault() {
|
||||
return IdType.ID;
|
||||
}
|
||||
|
||||
public static IdType resolve(String idType) {
|
||||
return resolve(IdType.class, idType);
|
||||
}
|
||||
|
||||
public static <T extends Enum<T>> T resolve(@NotNull Class<T> clazz, String idType) {
|
||||
if (idType == null) {
|
||||
return clazz.isAssignableFrom(DefaultOption.class) ? (T) DefaultOption.getDefault((Class) clazz) : null;
|
||||
}
|
||||
idType = formatIdType(idType);
|
||||
try {
|
||||
return Enum.valueOf(clazz, idType);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw resolveFailed(idType, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatIdType(String idType) {
|
||||
return idType == null ? null : idType.replaceAll("-", "_").toUpperCase();
|
||||
}
|
||||
|
||||
public static RuntimeException resolveFailed(String idType, Exception e) {
|
||||
return new PlatformApiDataValidationException("error.msg.id.type.not.found", "Provided type " + idType + " is not supported",
|
||||
"idType", e, idType);
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -51,14 +51,18 @@ public class MultiException extends Exception {
|
||||
private final List<Throwable> throwables;
|
||||
|
||||
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
|
||||
public MultiException(List<Throwable> problems) {
|
||||
super("MultiException with " + problems.size() + " contained causes (details available)");
|
||||
public MultiException(String message, List<Throwable> problems) {
|
||||
super(message);
|
||||
if (problems.isEmpty()) {
|
||||
throw new IllegalArgumentException("List of Throwables must not be empty");
|
||||
}
|
||||
this.throwables = new ArrayList<>(problems);
|
||||
}
|
||||
|
||||
public MultiException(List<Throwable> problems) {
|
||||
this("MultiException with " + problems.size() + " contained causes (details available)", problems);
|
||||
}
|
||||
|
||||
public List<Throwable> getCauses() {
|
||||
return Collections.unmodifiableList(throwables);
|
||||
}
|
||||
|
||||
+12
@@ -56,6 +56,18 @@ public class PlatformApiDataValidationException extends AbstractPlatformExceptio
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public PlatformApiDataValidationException(String messageCode, String userMessage, String parameterName, Throwable cause,
|
||||
final Object... userMessageArgs) {
|
||||
this("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
List.of(ApiParameterError.parameterError(messageCode, userMessage, parameterName, userMessageArgs)), cause);
|
||||
}
|
||||
|
||||
public PlatformApiDataValidationException(String messageCode, String userMessage, String parameterName,
|
||||
final Object... userMessageArgs) {
|
||||
this("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
List.of(ApiParameterError.parameterError(messageCode, userMessage, parameterName, userMessageArgs)), null);
|
||||
}
|
||||
|
||||
public List<ApiParameterError> getErrors() {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.core.service;
|
||||
|
||||
public interface DefaultOption {
|
||||
|
||||
boolean isDefault();
|
||||
|
||||
static <T extends Enum<T> & DefaultOption> T getDefault(Class<T> clazz) {
|
||||
for (T enumConstant : clazz.getEnumConstants()) {
|
||||
if (enumConstant.isDefault()) {
|
||||
return enumConstant;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+21
@@ -34,6 +34,9 @@ public class JobDetailData {
|
||||
@SuppressWarnings("unused")
|
||||
private String displayName;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private String shortName;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Date nextRunTime;
|
||||
|
||||
@@ -50,4 +53,22 @@ public class JobDetailData {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private JobDetailHistoryData lastRunHistory;
|
||||
|
||||
public JobDetailData(Long jobId, String displayName, String shortName, Date nextRunTime, String initializingError,
|
||||
String cronExpression, boolean active, boolean currentlyRunning, Long version, Date jobRunStartTime, Date jobRunEndTime,
|
||||
String status, String jobRunErrorMessage, String triggerType, String jobRunErrorLog) {
|
||||
this.jobId = jobId;
|
||||
this.displayName = displayName;
|
||||
this.shortName = shortName;
|
||||
this.nextRunTime = nextRunTime;
|
||||
this.initializingError = initializingError;
|
||||
this.cronExpression = cronExpression;
|
||||
this.active = active;
|
||||
this.currentlyRunning = currentlyRunning;
|
||||
if (version != null) {
|
||||
this.lastRunHistory = new JobDetailHistoryData().setVersion(version).setJobRunStartTime(jobRunStartTime)
|
||||
.setJobRunEndTime(jobRunEndTime).setStatus(status).setJobRunErrorMessage(jobRunErrorMessage).setTriggerType(triggerType)
|
||||
.setJobRunErrorLog(jobRunErrorLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -18,11 +18,23 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.exception;
|
||||
|
||||
import static java.util.stream.Collectors.mapping;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.fineract.infrastructure.core.exception.MultiException;
|
||||
|
||||
public class JobExecutionException extends MultiException {
|
||||
|
||||
public JobExecutionException(String message, List<Throwable> problems) {
|
||||
super(message, problems);
|
||||
}
|
||||
|
||||
public JobExecutionException(List<Throwable> problems) {
|
||||
super(problems);
|
||||
}
|
||||
@@ -30,4 +42,27 @@ public class JobExecutionException extends MultiException {
|
||||
public JobExecutionException(MultiException multiException) {
|
||||
super(multiException.getCauses());
|
||||
}
|
||||
|
||||
public static void throwErrors(@NotNull Map<Throwable, List<String>> errorMap) throws JobExecutionException {
|
||||
int size = errorMap.size();
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
List<Throwable> errors;
|
||||
StringBuilder msg = new StringBuilder("Job failed on ");
|
||||
Stream<Map.Entry<Throwable, List<String>>> entryStream = errorMap.entrySet().stream().filter(e -> e.getValue() != null);
|
||||
if (size < 10) {
|
||||
errors = new ArrayList<>(errorMap.keySet());
|
||||
Map<String, List<List<String>>> errorTypes = entryStream
|
||||
.collect(Collectors.groupingBy(e -> e.getKey().getClass().getSimpleName(), mapping(Map.Entry::getValue, toList())));
|
||||
errorTypes.forEach((key, value) -> msg.append(key).append(':').append(value.size()).append(':')
|
||||
.append(value.stream().flatMap(List::stream).collect(Collectors.joining(","))).append(";\n"));
|
||||
} else {
|
||||
errors = List.of(errorMap.keySet().iterator().next());
|
||||
Map<String, Long> errorTypes = entryStream
|
||||
.collect(Collectors.groupingBy(e -> e.getKey().getClass().getSimpleName(), Collectors.counting()));
|
||||
errorTypes.forEach((key, value) -> msg.append(key).append(':').append(value).append(";\n"));
|
||||
}
|
||||
throw new JobExecutionException(msg.toString(), errors);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.service;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.api.IdTypeResolver;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.core.service.SearchParameters;
|
||||
import org.apache.fineract.infrastructure.jobs.data.JobDetailData;
|
||||
@@ -26,14 +28,15 @@ import org.apache.fineract.infrastructure.jobs.data.JobDetailHistoryData;
|
||||
|
||||
public interface SchedulerJobRunnerReadService {
|
||||
|
||||
List<JobDetailData> findAllJobDeatils();
|
||||
List<JobDetailData> findAllJobDetails();
|
||||
|
||||
JobDetailData retrieveOne(Long jobId);
|
||||
JobDetailData retrieveOne(@NotNull IdTypeResolver.IdType idType, String identifier);
|
||||
|
||||
JobDetailData retrieveOneByName(String jobName);
|
||||
Page<JobDetailHistoryData> retrieveJobHistory(@NotNull IdTypeResolver.IdType idType, String identifier,
|
||||
SearchParameters searchParameters);
|
||||
|
||||
Page<JobDetailHistoryData> retrieveJobHistory(Long jobId, SearchParameters searchParameters);
|
||||
@NotNull
|
||||
Long retrieveId(@NotNull IdTypeResolver.IdType idType, String identifier);
|
||||
|
||||
boolean isUpdatesAllowed();
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ public class CacheConfiguration {
|
||||
public CacheManager cacheManager() {
|
||||
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
|
||||
simpleCacheManager.setCaches(List.of(new ConcurrentMapCache("paymentTypesByName"), //
|
||||
new ConcurrentMapCache("jobsByName"), //
|
||||
new ConcurrentMapCache("jobsByShortName"), //
|
||||
new ConcurrentMapCache("loanProductsByName"), //
|
||||
new ConcurrentMapCache("accountTypesByName")));//
|
||||
return simpleCacheManager;
|
||||
|
||||
+14
-4
@@ -20,18 +20,28 @@ package org.apache.fineract.test.data.job;
|
||||
|
||||
public enum DefaultJob implements Job {
|
||||
|
||||
ADD_ACCRUAL_TRANSACTIONS("Add Accrual Transactions"), ADD_PERIODIC_ACCRUAL_TRANSACTIONS(
|
||||
"Add Periodic Accrual Transactions"), INCREASE_BUSINESS_DAY("Increase Business Date by 1 day"), LOAN_DELINQUENCY_CLASSIFICATION(
|
||||
"Loan Delinquency Classification"), LOAN_COB("Loan COB");
|
||||
ADD_ACCRUAL_TRANSACTIONS("Add Accrual Transactions", "LA_AATR"), //
|
||||
ADD_PERIODIC_ACCRUAL_TRANSACTIONS("Add Periodic Accrual Transactions", "ACC_APTR"), //
|
||||
INCREASE_BUSINESS_DAY("Increase Business Date by 1 day", "BDT_INC1"), //
|
||||
LOAN_DELINQUENCY_CLASSIFICATION("Loan Delinquency Classification", "LA_DECL"), //
|
||||
LOAN_COB("Loan COB", "LA_ECOB"), //
|
||||
;
|
||||
|
||||
private final String customName;
|
||||
private final String shortName;
|
||||
|
||||
DefaultJob(String customName) {
|
||||
DefaultJob(String customName, String shortName) {
|
||||
this.customName = customName;
|
||||
this.shortName = shortName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return customName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,6 @@ package org.apache.fineract.test.data.job;
|
||||
public interface Job {
|
||||
|
||||
String getName();
|
||||
|
||||
String getShortName();
|
||||
}
|
||||
|
||||
+6
-11
@@ -19,7 +19,6 @@
|
||||
package org.apache.fineract.test.data.job;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.client.models.GetJobsResponse;
|
||||
@@ -35,20 +34,16 @@ public class JobResolver {
|
||||
|
||||
private final SchedulerJobApi schedulerJobApi;
|
||||
|
||||
@Cacheable(key = "#job.getName()", value = "jobsByName")
|
||||
@Cacheable(key = "#job.getShortName()", value = "jobsByShortName")
|
||||
public long resolve(Job job) {
|
||||
try {
|
||||
String jobName = job.getName();
|
||||
log.debug("Resolving job by name [{}]", jobName);
|
||||
Response<List<GetJobsResponse>> response = schedulerJobApi.retrieveAll8().execute();
|
||||
String shortName = job.getShortName();
|
||||
log.debug("Resolving job by short-name [{}]", shortName);
|
||||
Response<GetJobsResponse> response = schedulerJobApi.retrieveByShortName(shortName).execute();
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IllegalStateException("Unable to get jobs list. Status code was HTTP " + response.code());
|
||||
throw new IllegalStateException("Unable to get job. Status code was HTTP " + response.code());
|
||||
}
|
||||
|
||||
List<GetJobsResponse> jobsResponses = response.body();
|
||||
GetJobsResponse foundJob = jobsResponses.stream().filter(j -> jobName.equals(j.getDisplayName())).findAny()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Job [%s] not found".formatted(jobName)));
|
||||
return foundJob.getJobId();
|
||||
return response.body().getJobId();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ public final class SchedulerJobApiConstants {
|
||||
public static final String COMMAND_START_SCHEDULER = "start";
|
||||
public static final String COMMAND = "command";
|
||||
public static final String JOB_ID = "jobId";
|
||||
public static final String SHORT_NAME_PARAM = "short-name";
|
||||
public static final String JOB_RUN_HISTORY = "runhistory";
|
||||
public static final String SCHEDULER_STATUS_PATH = "scheduler";
|
||||
|
||||
|
||||
+106
-32
@@ -18,6 +18,11 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.api;
|
||||
|
||||
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.JOB_DETAIL_RESPONSE_DATA_PARAMETERS;
|
||||
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.JOB_HISTORY_RESPONSE_DATA_PARAMETERS;
|
||||
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME;
|
||||
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.SHORT_NAME_PARAM;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@@ -27,6 +32,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
@@ -41,12 +47,14 @@ import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.IdTypeResolver;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
@@ -88,10 +96,10 @@ public class SchedulerJobApiResource {
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsResponse.class)))) })
|
||||
public String retrieveAll(@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME);
|
||||
final List<JobDetailData> jobDetailDatas = this.schedulerJobRunnerReadService.findAllJobDeatils();
|
||||
this.context.authenticatedUser().validateHasReadPermission(SCHEDULER_RESOURCE_NAME);
|
||||
final List<JobDetailData> jobDetailDatas = this.schedulerJobRunnerReadService.findAllJobDetails();
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.toApiJsonSerializer.serialize(settings, jobDetailDatas, SchedulerJobApiConstants.JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
|
||||
return this.toApiJsonSerializer.serialize(settings, jobDetailDatas, JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -101,10 +109,19 @@ public class SchedulerJobApiResource {
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsResponse.class))) })
|
||||
public String retrieveOne(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME);
|
||||
final JobDetailData jobDetailData = this.schedulerJobRunnerReadService.retrieveOne(jobId);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.toApiJsonSerializer.serialize(settings, jobDetailData, SchedulerJobApiConstants.JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
|
||||
return retrieveOne(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), uriInfo);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(SHORT_NAME_PARAM + "/{shortName}")
|
||||
@Operation(summary = "Retrieve a Job", description = "Returns the details of a Job bu shortName.\n" + "\n" + "Example Requests:\n"
|
||||
+ "\n" + "jobs/short-name/SA_PINT")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsResponse.class))) })
|
||||
public String retrieveByShortName(
|
||||
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
|
||||
@Context final UriInfo uriInfo) {
|
||||
return retrieveOne(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, uriInfo);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -118,16 +135,22 @@ public class SchedulerJobApiResource {
|
||||
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
|
||||
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
|
||||
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(SchedulerJobApiConstants.SCHEDULER_RESOURCE_NAME);
|
||||
sqlValidator.validate(orderBy);
|
||||
sqlValidator.validate(sortOrder);
|
||||
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).offset(offset).orderBy(orderBy)
|
||||
.sortOrder(sortOrder).build();
|
||||
final Page<JobDetailHistoryData> jobhistoryDetailData = this.schedulerJobRunnerReadService.retrieveJobHistory(jobId,
|
||||
searchParameters);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.jobHistoryToApiJsonSerializer.serialize(settings, jobhistoryDetailData,
|
||||
SchedulerJobApiConstants.JOB_HISTORY_RESPONSE_DATA_PARAMETERS);
|
||||
return retrieveHistory(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), offset, limit, orderBy, sortOrder, uriInfo);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(SHORT_NAME_PARAM + "/{shortName}/" + SchedulerJobApiConstants.JOB_RUN_HISTORY)
|
||||
@Operation(summary = "Retrieve Job Run History", description = "Example Requests:\n" + "\n"
|
||||
+ "jobs/short-name/SA_PINT/runhistory?offset=0&limit=200")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.GetJobsJobIDJobRunHistoryResponse.class))) })
|
||||
public String retrieveHistoryByShortName(@Context final UriInfo uriInfo,
|
||||
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
|
||||
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
|
||||
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
|
||||
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
|
||||
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
|
||||
return retrieveHistory(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, offset, limit, orderBy, sortOrder, uriInfo);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -138,17 +161,78 @@ public class SchedulerJobApiResource {
|
||||
public Response executeJob(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId,
|
||||
@QueryParam(SchedulerJobApiConstants.COMMAND) @Parameter(description = "command") final String commandParam,
|
||||
@Parameter(hidden = true) final String jsonRequestBody) {
|
||||
// check the logged in user have permissions to execute scheduler jobs
|
||||
return executeJob(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), commandParam, jsonRequestBody);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(SHORT_NAME_PARAM + "/{shortName}")
|
||||
@Operation(summary = "Run a Job", description = "Manually Execute Specific Job.")
|
||||
@RequestBody(content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.ExecuteJobRequest.class)))
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "POST: jobs/short-name/SA_PINT?command=executeJob") })
|
||||
public Response executeJobByShortName(
|
||||
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
|
||||
@QueryParam(SchedulerJobApiConstants.COMMAND) @Parameter(description = "command") final String commandParam,
|
||||
@Parameter(hidden = true) final String jsonRequestBody) {
|
||||
return executeJob(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, commandParam, jsonRequestBody);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{" + SchedulerJobApiConstants.JOB_ID + "}")
|
||||
@Operation(summary = "Update a Job", description = "Updates the details of a job.")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.PutJobsJobIDRequest.class)))
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||
public String updateJobDetail(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId,
|
||||
@Parameter(hidden = true) final String jsonRequestBody) {
|
||||
return updateJobDetail(IdTypeResolver.resolveDefault(), Objects.toString(jobId, null), jsonRequestBody);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path(SHORT_NAME_PARAM + "/{shortName}")
|
||||
@Operation(summary = "Update a Job", description = "Updates the details of a job.")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.PutJobsJobIDRequest.class)))
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||
public String updateJobDetailByShortName(
|
||||
@PathParam("shortName") @Parameter(required = true, description = SchedulerJobApiConstants.SHORT_NAME_PARAM) final String shortName,
|
||||
@Parameter(hidden = true) final String jsonRequestBody) {
|
||||
return updateJobDetail(IdTypeResolver.resolve(SHORT_NAME_PARAM), shortName, jsonRequestBody);
|
||||
}
|
||||
|
||||
private boolean is(final String commandParam, final String commandValue) {
|
||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
||||
}
|
||||
|
||||
private String retrieveOne(@NotNull IdTypeResolver.IdType idType, String identifier, UriInfo uriInfo) {
|
||||
context.authenticatedUser().validateHasReadPermission(SCHEDULER_RESOURCE_NAME);
|
||||
final JobDetailData jobDetailData = schedulerJobRunnerReadService.retrieveOne(idType, identifier);
|
||||
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return toApiJsonSerializer.serialize(settings, jobDetailData, JOB_DETAIL_RESPONSE_DATA_PARAMETERS);
|
||||
}
|
||||
|
||||
private String retrieveHistory(@NotNull IdTypeResolver.IdType idType, String identifier, Integer offset, Integer limit, String orderBy,
|
||||
String sortOrder, UriInfo uriInfo) {
|
||||
context.authenticatedUser().validateHasReadPermission(SCHEDULER_RESOURCE_NAME);
|
||||
sqlValidator.validate(orderBy);
|
||||
sqlValidator.validate(sortOrder);
|
||||
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).offset(offset).orderBy(orderBy)
|
||||
.sortOrder(sortOrder).build();
|
||||
final Page<JobDetailHistoryData> jobHistoryData = schedulerJobRunnerReadService.retrieveJobHistory(idType, identifier,
|
||||
searchParameters);
|
||||
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return jobHistoryToApiJsonSerializer.serialize(settings, jobHistoryData, JOB_HISTORY_RESPONSE_DATA_PARAMETERS);
|
||||
}
|
||||
|
||||
private Response executeJob(@NotNull IdTypeResolver.IdType idType, String identifier, String commandParam, String jsonRequestBody) {
|
||||
// check the logged-in user have permissions to execute scheduler jobs
|
||||
Response response;
|
||||
if (fineractProperties.getMode().isBatchManagerEnabled()) {
|
||||
final boolean hasNotPermission = this.context.authenticatedUser().hasNotPermissionForAnyOf("ALL_FUNCTIONS",
|
||||
"EXECUTEJOB_SCHEDULER");
|
||||
final boolean hasNotPermission = context.authenticatedUser().hasNotPermissionForAnyOf("ALL_FUNCTIONS", "EXECUTEJOB_SCHEDULER");
|
||||
if (hasNotPermission) {
|
||||
final String authorizationMessage = "User has no authority to execute scheduler jobs";
|
||||
throw new NoAuthorizationException(authorizationMessage);
|
||||
}
|
||||
response = Response.status(400).build();
|
||||
if (is(commandParam, SchedulerJobApiConstants.COMMAND_EXECUTE_JOB)) {
|
||||
Long jobId = schedulerJobRunnerReadService.retrieveId(idType, identifier);
|
||||
jobRegisterService.executeJobWithParameters(jobId, jsonRequestBody);
|
||||
response = Response.status(202).build();
|
||||
} else {
|
||||
@@ -161,14 +245,8 @@ public class SchedulerJobApiResource {
|
||||
return response;
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{" + SchedulerJobApiConstants.JOB_ID + "}")
|
||||
@Operation(summary = "Update a Job", description = "Updates the details of a job.")
|
||||
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.PutJobsJobIDRequest.class)))
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||
public String updateJobDetail(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId,
|
||||
@Parameter(hidden = true) final String jsonRequestBody) {
|
||||
|
||||
private String updateJobDetail(@NotNull IdTypeResolver.IdType idType, String identifier, String jsonRequestBody) {
|
||||
Long jobId = schedulerJobRunnerReadService.retrieveId(idType, identifier);
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.updateJobDetail(jobId) //
|
||||
.withJson(jsonRequestBody) //
|
||||
@@ -180,8 +258,4 @@ public class SchedulerJobApiResource {
|
||||
}
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
private boolean is(final String commandParam, final String commandValue) {
|
||||
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ final class SchedulerJobApiResourceSwagger {
|
||||
public Long jobId;
|
||||
@Schema(example = "Update loan Summary")
|
||||
public String displayName;
|
||||
@Schema(example = "LA_USUM")
|
||||
public String shortName;
|
||||
@Schema(example = "")
|
||||
public Date nextRunTime;
|
||||
@Schema(example = "")
|
||||
|
||||
+5
-1
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Temporal;
|
||||
import jakarta.persistence.TemporalType;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@@ -36,7 +37,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants;
|
||||
|
||||
@Entity
|
||||
@Table(name = "job")
|
||||
@Table(name = "job", uniqueConstraints = { @UniqueConstraint(columnNames = { "short_name" }, name = "job_short_name_key") })
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@@ -97,6 +98,9 @@ public class ScheduledJobDetail extends AbstractPersistableCustom<Long> {
|
||||
@Column(name = "is_misfired")
|
||||
private boolean triggerMisfired;
|
||||
|
||||
@Column(name = "short_name", nullable = false)
|
||||
private String shortName;
|
||||
|
||||
public Map<String, Object> update(final JsonCommand command) {
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>(9);
|
||||
|
||||
|
||||
+20
@@ -20,6 +20,8 @@ package org.apache.fineract.infrastructure.jobs.domain;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.infrastructure.jobs.data.JobDetailData;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
@@ -47,4 +49,22 @@ public interface ScheduledJobDetailRepository
|
||||
|
||||
ScheduledJobDetail findByJobName(String jobName);
|
||||
|
||||
String GET_DATA = "select new org.apache.fineract.infrastructure.jobs.data.JobDetailData(j.id, j.jobDisplayName, j.shortName, j.nextRunTime, "
|
||||
+ "j.errorLog, j.cronExpression, j.activeSchedular, j.currentlyRunning, "
|
||||
+ "jh.version, jh.startTime, jh.endTime, jh.status, jh.errorMessage, jh.triggerType, jh.errorLog) "
|
||||
+ "from ScheduledJobDetail j left join ScheduledJobRunHistory jh on jh.scheduledJobDetail = j and j.previousRunStartTime = jh.startTime ";
|
||||
|
||||
@Query(GET_DATA + "where j.id = :jobId")
|
||||
JobDetailData getDataById(@Param("jobId") Long jobId);
|
||||
|
||||
@Query(GET_DATA + "where j.shortName = :shortName")
|
||||
JobDetailData getDataByShortName(@Param("shortName") String shortName);
|
||||
|
||||
@Query(GET_DATA + "order by j.id")
|
||||
List<JobDetailData> getAllData();
|
||||
|
||||
boolean existsByShortName(String shortName);
|
||||
|
||||
@Query("select j.id from ScheduledJobDetail j where j.shortName = :shortName")
|
||||
Optional<Long> findIdByShortName(@Param("shortName") String shortName);
|
||||
}
|
||||
|
||||
+5
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.IdTypeResolver;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
import org.springframework.batch.core.launch.NoSuchJobException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
@@ -31,6 +32,10 @@ public class JobNotFoundException extends AbstractPlatformResourceNotFoundExcept
|
||||
super("error.msg.sheduler.job.id.invalid", "Job with identifier " + identifier + " does not exist", identifier);
|
||||
}
|
||||
|
||||
public JobNotFoundException(IdTypeResolver.IdType idType, String jobId) {
|
||||
super("error.msg.sheduler.job.id.invalid", String.format("Job with %s: %s cannot be found", idType, jobId), idType, jobId);
|
||||
}
|
||||
|
||||
public JobNotFoundException(final String identifier, NoSuchJobException e) {
|
||||
super("error.msg.sheduler.job.id.invalid", "Job with identifier " + identifier + " does not exist", identifier, e);
|
||||
}
|
||||
|
||||
+63
-107
@@ -18,22 +18,24 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.service;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.api.IdTypeResolver;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.SearchParameters;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.jobs.data.JobDetailData;
|
||||
import org.apache.fineract.infrastructure.jobs.data.JobDetailHistoryData;
|
||||
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository;
|
||||
import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
|
||||
import org.apache.fineract.infrastructure.jobs.exception.OperationNotAllowedException;
|
||||
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -47,59 +49,62 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ColumnValidator columnValidator;
|
||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
private final ScheduledJobDetailRepository jobDetailRepository;
|
||||
|
||||
private final PaginationHelper paginationHelper;
|
||||
|
||||
@Autowired
|
||||
public SchedulerJobRunnerReadServiceImpl(final JdbcTemplate jdbcTemplate, final ColumnValidator columnValidator,
|
||||
DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) {
|
||||
DatabaseSpecificSQLGenerator sqlGenerator, ScheduledJobDetailRepository jobDetailRepository,
|
||||
PaginationHelper paginationHelper) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.columnValidator = columnValidator;
|
||||
this.sqlGenerator = sqlGenerator;
|
||||
this.jobDetailRepository = jobDetailRepository;
|
||||
this.paginationHelper = paginationHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JobDetailData> findAllJobDeatils() {
|
||||
final JobDetailMapper detailMapper = new JobDetailMapper(sqlGenerator);
|
||||
final String sql = detailMapper.schema();
|
||||
final List<JobDetailData> JobDeatils = this.jdbcTemplate.query(sql, detailMapper, new Object[] {});
|
||||
return JobDeatils;
|
||||
public List<JobDetailData> findAllJobDetails() {
|
||||
return jobDetailRepository.getAllData();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public JobDetailData retrieveOne(final Long jobId) {
|
||||
try {
|
||||
final JobDetailMapper detailMapper = new JobDetailMapper(sqlGenerator);
|
||||
final String sql = detailMapper.schema() + " where job.id=?";
|
||||
return this.jdbcTemplate.queryForObject(sql, detailMapper, new Object[] { jobId }); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new JobNotFoundException(String.valueOf(jobId), e);
|
||||
public JobDetailData retrieveOne(@NotNull IdTypeResolver.IdType idType, String identifier) {
|
||||
JobDetailData jobDetail = switch (idType) {
|
||||
case ID -> jobDetailRepository.getDataById(Long.valueOf(identifier));
|
||||
case SHORT_NAME -> jobDetailRepository.getDataByShortName(identifier);
|
||||
default -> null;
|
||||
};
|
||||
if (jobDetail == null) {
|
||||
throw new JobNotFoundException(idType, identifier);
|
||||
}
|
||||
return jobDetail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JobDetailData retrieveOneByName(String jobName) {
|
||||
try {
|
||||
final JobDetailMapper detailMapper = new JobDetailMapper(sqlGenerator);
|
||||
final String sql = detailMapper.schema() + " where job.name=?";
|
||||
return this.jdbcTemplate.queryForObject(sql, detailMapper, new Object[] { jobName }); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new JobNotFoundException(jobName, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<JobDetailHistoryData> retrieveJobHistory(final Long jobId, final SearchParameters searchParameters) {
|
||||
if (!isJobExist(jobId)) {
|
||||
throw new JobNotFoundException(String.valueOf(jobId));
|
||||
public Page<JobDetailHistoryData> retrieveJobHistory(@NotNull IdTypeResolver.IdType idType, String identifier,
|
||||
SearchParameters searchParameters) {
|
||||
if (!isJobExist(idType, identifier)) {
|
||||
throw new JobNotFoundException(idType, identifier);
|
||||
}
|
||||
final JobHistoryMapper jobHistoryMapper = new JobHistoryMapper(sqlGenerator);
|
||||
final StringBuilder sqlBuilder = new StringBuilder(200);
|
||||
sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " ");
|
||||
sqlBuilder.append(jobHistoryMapper.schema());
|
||||
sqlBuilder.append(" where job.id=?");
|
||||
final StringBuilder sqlBuilder = new StringBuilder("select " + sqlGenerator.calcFoundRows() + " ").append(jobHistoryMapper.schema())
|
||||
.append(" where job.");
|
||||
Object idParam;
|
||||
switch (idType) {
|
||||
case ID -> {
|
||||
sqlBuilder.append("id");
|
||||
idParam = Long.valueOf(identifier);
|
||||
}
|
||||
case SHORT_NAME -> {
|
||||
sqlBuilder.append("short_name");
|
||||
idParam = identifier;
|
||||
}
|
||||
default -> throw new JobNotFoundException(idType, identifier);
|
||||
}
|
||||
sqlBuilder.append(" = ?");
|
||||
if (searchParameters.hasOrderBy()) {
|
||||
sqlBuilder.append(" order by ").append(searchParameters.getOrderBy());
|
||||
this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getOrderBy());
|
||||
@@ -118,7 +123,18 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
|
||||
}
|
||||
}
|
||||
|
||||
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), new Object[] { jobId }, jobHistoryMapper);
|
||||
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), new Object[] { idParam }, jobHistoryMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Long retrieveId(@NotNull IdTypeResolver.IdType idType, String identifier) {
|
||||
return switch (idType) {
|
||||
case ID -> Long.valueOf(identifier);
|
||||
case SHORT_NAME ->
|
||||
jobDetailRepository.findIdByShortName(identifier).orElseThrow(() -> new JobNotFoundException(idType, identifier));
|
||||
default -> throw new JobNotFoundException(idType, identifier);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,84 +149,27 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isJobExist(final Long jobId) {
|
||||
boolean isJobPresent = false;
|
||||
try {
|
||||
final String sql = "select count(*) from job job where job.id= ?";
|
||||
final int count = this.jdbcTemplate.queryForObject(sql, Integer.class, new Object[] { jobId });
|
||||
if (count == 1) {
|
||||
isJobPresent = true;
|
||||
}
|
||||
return isJobPresent;
|
||||
} catch (EmptyResultDataAccessException e) {
|
||||
return isJobPresent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class JobDetailMapper implements RowMapper<JobDetailData> {
|
||||
|
||||
private final StringBuilder sqlBuilder;
|
||||
|
||||
JobDetailMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
sqlBuilder = new StringBuilder("select").append(
|
||||
" job.id,job.display_name as displayName,job.next_run_time as nextRunTime,job.initializing_errorlog as initializingError,job.cron_expression as cronExpression,job.is_active as active,job.currently_running as currentlyRunning,")
|
||||
.append(" runHistory.version,runHistory.start_time as lastRunStartTime,runHistory.end_time as lastRunEndTime,runHistory.")
|
||||
.append(sqlGenerator.escape("status"))
|
||||
.append(",runHistory.error_message as jobRunErrorMessage,runHistory.trigger_type as triggerType,runHistory.error_log as jobRunErrorLog ")
|
||||
.append(" from job job left join job_run_history runHistory ON job.id=runHistory.job_id and job.previous_run_start_time=runHistory.start_time ");
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.sqlBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JobDetailData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
final Long id = rs.getLong("id");
|
||||
final String displayName = rs.getString("displayName");
|
||||
final Date nextRunTime = rs.getTimestamp("nextRunTime");
|
||||
final String initializingError = rs.getString("initializingError");
|
||||
final String cronExpression = rs.getString("cronExpression");
|
||||
final boolean active = rs.getBoolean("active");
|
||||
final boolean currentlyRunning = rs.getBoolean("currentlyRunning");
|
||||
|
||||
final Long version = rs.getLong("version");
|
||||
final Date jobRunStartTime = rs.getTimestamp("lastRunStartTime");
|
||||
final Date jobRunEndTime = rs.getTimestamp("lastRunEndTime");
|
||||
final String status = rs.getString("status");
|
||||
final String jobRunErrorMessage = rs.getString("jobRunErrorMessage");
|
||||
final String triggerType = rs.getString("triggerType");
|
||||
final String jobRunErrorLog = rs.getString("jobRunErrorLog");
|
||||
|
||||
JobDetailHistoryData lastRunHistory = null;
|
||||
if (version > 0) {
|
||||
lastRunHistory = new JobDetailHistoryData().setVersion(version).setJobRunStartTime(jobRunStartTime)
|
||||
.setJobRunEndTime(jobRunEndTime).setStatus(status).setJobRunErrorMessage(jobRunErrorMessage)
|
||||
.setTriggerType(triggerType).setJobRunErrorLog(jobRunErrorLog);
|
||||
}
|
||||
final JobDetailData jobDetail = new JobDetailData().setJobId(id).setDisplayName(displayName).setNextRunTime(nextRunTime)
|
||||
.setInitializingError(initializingError).setCronExpression(cronExpression).setActive(active)
|
||||
.setCurrentlyRunning(currentlyRunning).setLastRunHistory(lastRunHistory);
|
||||
return jobDetail;
|
||||
}
|
||||
|
||||
private boolean isJobExist(@NotNull IdTypeResolver.IdType idType, @NotNull String jobId) {
|
||||
return switch (idType) {
|
||||
case ID -> jobDetailRepository.existsById(Long.valueOf(jobId));
|
||||
case SHORT_NAME -> jobDetailRepository.existsByShortName(jobId);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static final class JobHistoryMapper implements RowMapper<JobDetailHistoryData> {
|
||||
|
||||
private final StringBuilder sqlBuilder;
|
||||
private final String sql;
|
||||
|
||||
JobHistoryMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
sqlBuilder = new StringBuilder(200)
|
||||
.append(" runHistory.version,runHistory.start_time as runStartTime,runHistory.end_time as runEndTime,runHistory."
|
||||
+ sqlGenerator.escape("status")
|
||||
+ ",runHistory.error_message as jobRunErrorMessage,runHistory.trigger_type as triggerType,runHistory.error_log as jobRunErrorLog ")
|
||||
.append(" from job job join job_run_history runHistory ON job.id=runHistory.job_id");
|
||||
sql = " runHistory.version, runHistory.start_time as runStartTime, runHistory.end_time as runEndTime, " + "runHistory."
|
||||
+ sqlGenerator.escape("status")
|
||||
+ ", runHistory.error_message as jobRunErrorMessage, runHistory.trigger_type as triggerType, runHistory.error_log as jobRunErrorLog "
|
||||
+ "from job job join job_run_history runHistory ON job.id=runHistory.job_id";
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.sqlBuilder.toString();
|
||||
return sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -222,12 +181,9 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
|
||||
final String jobRunErrorMessage = rs.getString("jobRunErrorMessage");
|
||||
final String triggerType = rs.getString("triggerType");
|
||||
final String jobRunErrorLog = rs.getString("jobRunErrorLog");
|
||||
final JobDetailHistoryData jobDetailHistory = new JobDetailHistoryData().setVersion(version).setJobRunStartTime(jobRunStartTime)
|
||||
.setJobRunEndTime(jobRunEndTime).setStatus(status).setJobRunErrorMessage(jobRunErrorMessage).setTriggerType(triggerType)
|
||||
return new JobDetailHistoryData().setVersion(version).setJobRunStartTime(jobRunStartTime).setJobRunEndTime(jobRunEndTime)
|
||||
.setStatus(status).setJobRunErrorMessage(jobRunErrorMessage).setTriggerType(triggerType)
|
||||
.setJobRunErrorLog(jobRunErrorLog);
|
||||
return jobDetailHistory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
<include file="tenant-store/changelog-tenant-store.xml" relativeToChangelogFile="true" context="tenant_store_db AND !initial_switch"/>
|
||||
<include file="tenant/initial-switch-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND initial_switch"/>
|
||||
<include file="tenant/changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
|
||||
<includeAll path="db/changelog/tenant/module" errorIfMissingOrEmpty="false" context="tenant_db AND custom_changelog" filter="org.apache.fineract.infrastructure.core.service.migration.TenantModuleRootFilter" />
|
||||
<includeAll path="db/custom-changelog" errorIfMissingOrEmpty="false" context="tenant_db AND custom_changelog" />
|
||||
<!-- Add new module to the end of this modules list (to keep the existing auto-increment identifiers) -->
|
||||
<include file="db/changelog/tenant/module/loan/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<include file="db/changelog/tenant/module/investor/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<includeAll path="db/custom-changelog" errorIfMissingOrEmpty="false" context="tenant_db AND !initial_switch AND custom_changelog"/>
|
||||
<!-- Scripts to run after the modules were initialized -->
|
||||
<include file="tenant/final-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -164,4 +164,5 @@
|
||||
<include file="parts/0142_add_accrual_activity_transaction.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0143_add_accrual_activity_posting_job.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0144_transaction_summary_with_asset_owner_report_unc_allocation_fix.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0145_job_short_name.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<include file="parts/0146_add_final_constraints.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="job">
|
||||
<column name="short_name" type="VARCHAR(8)">
|
||||
<constraints unique="true" nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="2">
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_USUM"/>
|
||||
<where>name='Update loan Summary'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_ARAG"/>
|
||||
<where>name='Update Loan Arrears Ageing'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_PIAD"/>
|
||||
<where>name='Update Loan Paid In Advance'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_AHOL"/>
|
||||
<where>name='Apply Holidays To Loans'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_TFFS"/>
|
||||
<where>name='Transfer Fee For Loans From Savings'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_OPEN"/>
|
||||
<where>name='Apply penalty to overdue loans'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_UNPA"/>
|
||||
<where>name='Update Non Performing Assets'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_RINT"/>
|
||||
<where>name='Recalculate Interest For Loans'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_GLPR"/>
|
||||
<where>name='Generate Loan Loss Provisioning'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_AATR"/>
|
||||
<where>name='Add Accrual Transactions For Loans With Income Posted As Transactions'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_ECOB"/>
|
||||
<where>name='Loan COB'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="LA_DECL"/>
|
||||
<where>name='Loan Delinquency Classification'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SA_AANF"/>
|
||||
<where>name='Apply Annual Fee For Savings'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SA_PINT"/>
|
||||
<where>name='Post Interest For Savings'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SA_PDCH"/>
|
||||
<where>name='Pay Due Savings Charges'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SA_TINT"/>
|
||||
<where>name='Transfer Interest To Savings'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SA_MATD"/>
|
||||
<where>name='Update Deposit Accounts Maturity details'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SA_GSCH"/>
|
||||
<where>name='Generate Mandatory Savings Schedule'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SA_UDOR"/>
|
||||
<where>name='Update Savings Dormant Accounts'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SH_PDIV"/>
|
||||
<where>name='Post Dividends For Shares'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="ACC_RBAL"/>
|
||||
<where>name='Update Accounting Running Balances'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="ACC_AATR"/>
|
||||
<where>name='Add Accrual Transactions'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="ACC_APTR"/>
|
||||
<where>name='Add Periodic Accrual Transactions'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="STI_EXEC"/>
|
||||
<where>name='Execute Standing Instruction'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="RMJ_EXEC"/>
|
||||
<where>name='Execute Report Mailing Jobs'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="EM_EXEC"/>
|
||||
<where>name='Execute Email'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="EM_UOUT"/>
|
||||
<where>name='Update Email Outbound with campaign message'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SMS_UOUT"/>
|
||||
<where>name='Update SMS Outbound with Campaign Message'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SMS_SMSG"/>
|
||||
<where>name='Send Messages to SMS Gateway'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="SMS_DRPT"/>
|
||||
<where>name='Get Delivery Reports from SMS Gateway'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="ADH_GSCH"/>
|
||||
<where>name='Generate AdhocClient Schedule'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="TBL_UDET"/>
|
||||
<where>name='Update Trial Balance Details'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="JOB_EXEC"/>
|
||||
<where>name='Execute All Dirty Jobs'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="BDT_INC1"/>
|
||||
<where>name='Increase Business Date by 1 day'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="BDT_INC1"/>
|
||||
<where>name='Increase Business Date by 1 day'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="BDT_COB1"/>
|
||||
<where>name='Increase COB Date by 1 day'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="ASE_SEND"/>
|
||||
<where>name='Send Asynchronous Events'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="EXE_PURG"/>
|
||||
<where>name='Purge External Events'</where>
|
||||
</update>
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="COM_PURG"/>
|
||||
<where>name='Purge Processed Commands'</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="3">
|
||||
<update tableName="job">
|
||||
<column name="short_name" value="ACC_ACPO"/>
|
||||
<where>name='Accrual Activity Posting'</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="1" context="postgresql">
|
||||
<addNotNullConstraint tableName="job" columnName="short_name"/>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="2" context="mysql">
|
||||
<addNotNullConstraint tableName="job" columnName="short_name" columnDataType="VARCHAR(8)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+2
-2
@@ -57,8 +57,8 @@ public class InstanceModeIntegrationTest {
|
||||
responseSpec405 = new ResponseSpecBuilder().expectStatusCode(405).build();
|
||||
|
||||
schedulerJobHelper = new SchedulerJobHelper(requestSpec);
|
||||
String jobName = "Apply Annual Fee For Savings";
|
||||
jobId = schedulerJobHelper.getSchedulerJobIdByName(jobName);
|
||||
// Apply Annual Fee For Savings"
|
||||
jobId = schedulerJobHelper.getSchedulerJobIdByShortName("SA_AANF");
|
||||
}
|
||||
|
||||
@ConfigureInstanceMode(readEnabled = true, writeEnabled = false, batchWorkerEnabled = false, batchManagerEnabled = false)
|
||||
|
||||
+6
-11
@@ -59,6 +59,7 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SavingsInterestPostingJobIntegrationTest.class);
|
||||
public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
|
||||
public static final String POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME = "SA_PINT";
|
||||
|
||||
private static ResponseSpecification responseSpec;
|
||||
private static RequestSpecification requestSpec;
|
||||
@@ -83,7 +84,6 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
public void testSavingsBalanceCheckAfterDailyInterestPostingJob() {
|
||||
// client activation, savings activation and 1st transaction date
|
||||
final String startDate = "10 April 2022";
|
||||
final String jobName = "Post Interest For Savings";
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
|
||||
Assertions.assertNotNull(clientID);
|
||||
|
||||
@@ -95,7 +95,7 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
* Runs Post interest posting job and verify the new account created with accounting configuration set as none
|
||||
* is picked up by job
|
||||
*/
|
||||
this.scheduleJobHelper.executeAndAwaitJob(jobName);
|
||||
this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME);
|
||||
Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions");
|
||||
ArrayList<HashMap<String, Object>> transactions = (ArrayList<HashMap<String, Object>>) transactionObj;
|
||||
HashMap<String, Object> interestPostingTransaction = transactions.get(transactions.size() - 48);
|
||||
@@ -108,7 +108,6 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
@Test
|
||||
public void testSavingsDailyInterestPostingJobWithAccountingNone() {
|
||||
final String startDate = "10 April 2022";
|
||||
final String jobName = "Post Interest For Savings";
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
|
||||
Assertions.assertNotNull(clientID);
|
||||
this.accountHelper = new AccountHelper(requestSpec, responseSpec);
|
||||
@@ -126,7 +125,6 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
public void testDuplicateOverdraftInterestPostingJob() {
|
||||
// client activation, savings activation and 1st transaction date
|
||||
final String startDate = "01 July 2022";
|
||||
final String jobName = "Post Interest For Savings";
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
|
||||
Assertions.assertNotNull(clientID);
|
||||
|
||||
@@ -134,7 +132,7 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
|
||||
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
|
||||
|
||||
this.scheduleJobHelper.executeAndAwaitJob(jobName);
|
||||
this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME);
|
||||
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "1000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
|
||||
Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions");
|
||||
ArrayList<HashMap<String, Object>> transactions = (ArrayList<HashMap<String, Object>>) transactionObj;
|
||||
@@ -155,7 +153,6 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, today);
|
||||
// client activation, savings activation and 1st transaction date
|
||||
final String startDate = "10 April 2022";
|
||||
final String jobName = "Post Interest For Savings";
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
|
||||
Assertions.assertNotNull(clientID);
|
||||
|
||||
@@ -167,7 +164,7 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
* Runs Post interest posting job and verify the new account created with accounting configuration set as
|
||||
* none is picked up by job
|
||||
*/
|
||||
this.scheduleJobHelper.executeAndAwaitJob(jobName);
|
||||
this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME);
|
||||
Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions");
|
||||
ArrayList<HashMap<String, Object>> transactions = (ArrayList<HashMap<String, Object>>) transactionObj;
|
||||
HashMap<String, Object> interestPostingTransaction = transactions.get(transactions.size() - 3);
|
||||
@@ -190,7 +187,6 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
public void testSavingsDailyOverdraftInterestPostingJob() {
|
||||
// client activation, savings activation and 1st transaction date
|
||||
final String startDate = "10 April 2022";
|
||||
final String jobName = "Post Interest For Savings";
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
|
||||
Assertions.assertNotNull(clientID);
|
||||
|
||||
@@ -199,7 +195,7 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, "10000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
|
||||
|
||||
// Runs Post interest posting job and verify the new account created with Overdraft is posting negative interest
|
||||
this.scheduleJobHelper.executeAndAwaitJob(jobName);
|
||||
this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME);
|
||||
Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions");
|
||||
ArrayList<HashMap<String, Object>> transactions = (ArrayList<HashMap<String, Object>>) transactionObj;
|
||||
HashMap<String, Object> interestPostingTransaction = transactions.get(transactions.size() - 2);
|
||||
@@ -215,7 +211,6 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
@Test
|
||||
public void testAccountBalanceWithWithdrawalFeeAfterInterestPostingJob() {
|
||||
final String startDate = "21 June 2022";
|
||||
final String jobName = "Post Interest For Savings";
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDate);
|
||||
Assertions.assertNotNull(clientID);
|
||||
|
||||
@@ -227,7 +222,7 @@ public class SavingsInterestPostingJobIntegrationTest {
|
||||
Float balance = Float.parseFloat("800.0");
|
||||
assertEquals(balance, summary.get("accountBalance"), "Verifying account balance is 800");
|
||||
|
||||
this.scheduleJobHelper.executeAndAwaitJob(jobName);
|
||||
this.scheduleJobHelper.executeAndAwaitJobByShortName(POST_INTEREST_FOR_SAVINGS_JOB_SHORT_NAME);
|
||||
Object transactionObj = this.savingsAccountHelper.getSavingsDetails(savingsId, "transactions");
|
||||
ArrayList<HashMap<String, Object>> transactions = (ArrayList<HashMap<String, Object>>) transactionObj;
|
||||
HashMap<String, Object> interestPostingTransaction = transactions.get(transactions.size() - 5);
|
||||
|
||||
+73
-7
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.integrationtests.common;
|
||||
|
||||
import static java.time.Instant.now;
|
||||
import static org.apache.fineract.infrastructure.jobs.api.SchedulerJobApiConstants.SHORT_NAME_PARAM;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -28,6 +29,7 @@ import com.google.gson.Gson;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
@@ -37,7 +39,9 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.fineract.client.models.PutJobsJobIDRequest;
|
||||
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||
@@ -89,6 +93,15 @@ public class SchedulerJobHelper extends IntegrationTest {
|
||||
return response;
|
||||
}
|
||||
|
||||
private Map<String, Object> getSchedulerJobByShortName(String shortName) {
|
||||
final String GET_SCHEDULER_JOB_URL = "/fineract-provider/api/v1/jobs/" + SHORT_NAME_PARAM + "/" + shortName + "?"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
LOG.info("------------------------ RETRIEVING SCHEDULER JOB BY SHORT NAME -------------------------");
|
||||
Map<String, Object> response = Utils.performServerGet(requestSpec, response200Spec, GET_SCHEDULER_JOB_URL, "");
|
||||
assertNotNull(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
public Boolean getSchedulerStatus() {
|
||||
final String GET_SCHEDULER_STATUS_URL = "/fineract-provider/api/v1/scheduler?" + Utils.TENANT_IDENTIFIER;
|
||||
LOG.info("------------------------ RETRIEVING SCHEDULER STATUS -------------------------");
|
||||
@@ -131,6 +144,18 @@ public class SchedulerJobHelper extends IntegrationTest {
|
||||
Utils.performServerPost(requestSpec, responseSpec, RUN_SCHEDULER_JOB_URL, runSchedulerJobAsJSON(), null);
|
||||
}
|
||||
|
||||
public void runSchedulerJobByShortName(String shortName) {
|
||||
final ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(202).build();
|
||||
runSchedulerJobByShortName(shortName, responseSpec);
|
||||
}
|
||||
|
||||
public void runSchedulerJobByShortName(String shortName, ResponseSpecification responseSpec) {
|
||||
final String RUN_SCHEDULER_JOB_URL = "/fineract-provider/api/v1/jobs/" + SHORT_NAME_PARAM + "/" + shortName + "?command=executeJob&"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
LOG.info("------------------------ RUN SCHEDULER JOB -------------------------");
|
||||
Utils.performServerPost(requestSpec, responseSpec, RUN_SCHEDULER_JOB_URL, runSchedulerJobAsJSON(), null);
|
||||
}
|
||||
|
||||
private static String runSchedulerJobAsJSON() {
|
||||
final Map<String, String> map = new HashMap<>();
|
||||
String runSchedulerJob = new Gson().toJson(map);
|
||||
@@ -149,6 +174,16 @@ public class SchedulerJobHelper extends IntegrationTest {
|
||||
"No such named Job (see org.apache.fineract.infrastructure.jobs.service.JobName enum):" + jobName);
|
||||
}
|
||||
|
||||
public int getSchedulerJobIdByShortName(String shortName) {
|
||||
Map<String, Object> jobMap = getSchedulerJobByShortName(shortName);
|
||||
final String GET_SCHEDULER_JOB_URL = "/fineract-provider/api/v1/jobs/" + SHORT_NAME_PARAM + "/" + shortName + "?"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
LOG.info("------------------------ RETRIEVING SCHEDULER JOB ID BY SHORT NAME -------------------------");
|
||||
Integer response = (Integer) jobMap.get("jobId");
|
||||
assertNotNull(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a Job and awaits its completion.
|
||||
*
|
||||
@@ -158,23 +193,43 @@ public class SchedulerJobHelper extends IntegrationTest {
|
||||
* @author Michael Vorburger.ch
|
||||
*/
|
||||
public void executeAndAwaitJob(String jobName) {
|
||||
final Duration timeout = Duration.ofMinutes(4);
|
||||
final Duration pause = Duration.ofSeconds(2);
|
||||
DateTimeFormatter df = DateTimeFormatter.ISO_INSTANT; // FINERACT-926
|
||||
Instant beforeExecuteTime = now().truncatedTo(ChronoUnit.SECONDS);
|
||||
int jobId = getSchedulerJobIdByName(jobName);
|
||||
executeAndAwaitJob(jobId, (a) -> runSchedulerJob(jobId), () -> jobLastRunHistorySupplier(jobId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a Job and awaits its completion.
|
||||
*
|
||||
* @param shortName
|
||||
* shortName of Scheduler Job
|
||||
*
|
||||
* @author Michael Vorburger.ch
|
||||
*/
|
||||
public void executeAndAwaitJobByShortName(String shortName) {
|
||||
executeAndAwaitJob(shortName, (a) -> runSchedulerJobByShortName(shortName), () -> jobLastRunHistoryByShortName(shortName));
|
||||
}
|
||||
|
||||
public <T extends Serializable> void executeAndAwaitJob(T jobParam, Consumer<T> runSchedulerJob,
|
||||
Supplier<Callable<Map<String, String>>> retrievelastRunHistory) {
|
||||
// Stop the Scheduler while we manually trigger execution of job, to
|
||||
// avoid side effects and simplify debugging when readings logs
|
||||
updateSchedulerStatus(false);
|
||||
|
||||
Instant beforeExecuteTime = now().truncatedTo(ChronoUnit.SECONDS);
|
||||
// Executing Scheduler Job
|
||||
int jobId = getSchedulerJobIdByName(jobName);
|
||||
runSchedulerJob(jobId);
|
||||
runSchedulerJob.accept(jobParam);
|
||||
|
||||
awaitJob(beforeExecuteTime, retrievelastRunHistory);
|
||||
}
|
||||
|
||||
private void awaitJob(Instant beforeExecuteTime, Supplier<Callable<Map<String, String>>> retrieveLastRunHistory) {
|
||||
final Duration timeout = Duration.ofMinutes(4);
|
||||
final Duration pause = Duration.ofSeconds(2);
|
||||
DateTimeFormatter df = DateTimeFormatter.ISO_INSTANT; // FINERACT-926
|
||||
// Await JobDetailData.lastRunHistory [JobDetailHistoryData]
|
||||
// jobRunStartTime >= beforeExecuteTime (or timeout)
|
||||
// jobRunEndTime to be both set and >= jobRunStartTime (or timeout)
|
||||
Map<String, String> finalLastRunHistory = await().atMost(timeout).pollInterval(pause).until(jobLastRunHistorySupplier(jobId),
|
||||
Map<String, String> finalLastRunHistory = await().atMost(timeout).pollInterval(pause).until(retrieveLastRunHistory.get(),
|
||||
lastRunHistory -> {
|
||||
String jobRunStartText = lastRunHistory.get("jobRunStartTime");
|
||||
if (jobRunStartText == null) {
|
||||
@@ -223,4 +278,15 @@ public class SchedulerJobHelper extends IntegrationTest {
|
||||
return (Map<String, String>) job.get("lastRunHistory");
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Callable<Map<String, String>> jobLastRunHistoryByShortName(String shortName) {
|
||||
return () -> {
|
||||
Map<String, Object> job = getSchedulerJobByShortName(shortName);
|
||||
if (job == null) {
|
||||
return null;
|
||||
}
|
||||
return (Map<String, String>) job.get("lastRunHistory");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user