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

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 3aaa2ffe9 FINERACT-2119: Address jobs by name
3aaa2ffe9 is described below

commit 3aaa2ffe9c0fac97f688f3e244d142c808ac64f0
Author: Marta Jankovics <[email protected]>
AuthorDate: Wed Aug 28 03:09:34 2024 +0200

    FINERACT-2119: Address jobs by name
---
 .../db/custom-changelog/0001_acme_loan_job.xml     |   6 +
 .../infrastructure/core/api/IdTypeResolver.java    |  74 ++++++++
 .../core/exception/MultiException.java             |   8 +-
 .../PlatformApiDataValidationException.java        |  12 ++
 .../infrastructure/core/service/DefaultOption.java |  15 +-
 .../infrastructure/jobs/data/JobDetailData.java    |  21 +++
 .../jobs/exception/JobExecutionException.java      |  35 ++++
 .../service/SchedulerJobRunnerReadService.java     |  13 +-
 .../fineract/test/config/CacheConfiguration.java   |   2 +-
 .../apache/fineract/test/data/job/DefaultJob.java  |  18 +-
 .../org/apache/fineract/test/data/job/Job.java     |   2 +
 .../apache/fineract/test/data/job/JobResolver.java |  17 +-
 .../jobs/api/SchedulerJobApiConstants.java         |   1 +
 .../jobs/api/SchedulerJobApiResource.java          | 138 +++++++++++----
 .../jobs/api/SchedulerJobApiResourceSwagger.java   |   2 +
 .../jobs/domain/ScheduledJobDetail.java            |   6 +-
 .../jobs/domain/ScheduledJobDetailRepository.java  |  20 +++
 .../jobs/exception/JobNotFoundException.java       |   5 +
 .../service/SchedulerJobRunnerReadServiceImpl.java | 170 +++++++-----------
 .../resources/db/changelog/db.changelog-master.xml |   8 +-
 .../db/changelog/tenant/changelog-tenant.xml       |   1 +
 .../db/changelog/tenant/final-changelog-tenant.xml |  21 +--
 .../changelog/tenant/parts/0145_job_short_name.xml | 196 +++++++++++++++++++++
 .../tenant/parts/0146_add_final_constraints.xml    |  24 +--
 .../InstanceModeIntegrationTest.java               |   4 +-
 .../SavingsInterestPostingJobIntegrationTest.java  |  17 +-
 .../common/SchedulerJobHelper.java                 |  80 ++++++++-
 27 files changed, 689 insertions(+), 227 deletions(-)

diff --git 
a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
 
b/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
index f466fdaf3..b3a8f5443 100644
--- 
a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
+++ 
b/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
@@ -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>
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/IdTypeResolver.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/IdTypeResolver.java
new file mode 100644
index 000000000..28983324d
--- /dev/null
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/IdTypeResolver.java
@@ -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);
+    }
+}
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiException.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiException.java
index 17276de5a..b9fbab4dc 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiException.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiException.java
@@ -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);
     }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/PlatformApiDataValidationException.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/PlatformApiDataValidationException.java
index f20b4cf34..6faf601db 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/PlatformApiDataValidationException.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/PlatformApiDataValidationException.java
@@ -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;
     }
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/Job.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DefaultOption.java
similarity index 67%
copy from 
fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/Job.java
copy to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DefaultOption.java
index 76354b282..8249b961f 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/Job.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DefaultOption.java
@@ -16,9 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.test.data.job;
+package org.apache.fineract.infrastructure.core.service;
 
-public interface Job {
+public interface DefaultOption {
 
-    String getName();
+    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;
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobDetailData.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobDetailData.java
index e11b03dae..3e2b63692 100755
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobDetailData.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobDetailData.java
@@ -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);
+        }
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobExecutionException.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobExecutionException.java
index 02dc85776..bf7d29576 100755
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobExecutionException.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobExecutionException.java
@@ -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);
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadService.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadService.java
index f6567f717..ba40b7deb 100755
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadService.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadService.java
@@ -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();
-
 }
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/CacheConfiguration.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/CacheConfiguration.java
index 4465782c0..a9a3e485e 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/CacheConfiguration.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/config/CacheConfiguration.java
@@ -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;
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java
index cc80f2f3f..58d2a9100 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/DefaultJob.java
@@ -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;
+    }
 }
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/Job.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/Job.java
index 76354b282..54cc1fd4e 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/Job.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/Job.java
@@ -21,4 +21,6 @@ package org.apache.fineract.test.data.job;
 public interface Job {
 
     String getName();
+
+    String getShortName();
 }
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/JobResolver.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/JobResolver.java
index affecbc6f..4e562ce3a 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/JobResolver.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/job/JobResolver.java
@@ -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);
         }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiConstants.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiConstants.java
index 0a09bce0b..cf4487aaa 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiConstants.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiConstants.java
@@ -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";
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java
index efb9b06d0..7469dd70c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java
@@ -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);
-    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java
index 81bfec247..a981a4109 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java
@@ -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 = "")
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetail.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetail.java
index 036b8536b..a5b2d8d5a 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetail.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetail.java
@@ -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);
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java
index 6440be79b..be3d4cc4b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java
@@ -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);
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobNotFoundException.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobNotFoundException.java
index becff1983..95c9b493e 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobNotFoundException.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/exception/JobNotFoundException.java
@@ -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);
     }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java
index 70ef390fe..8f811e6b8 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobRunnerReadServiceImpl.java
@@ -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;
         }
-
     }
-
 }
diff --git 
a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml 
b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
index 4edca24bf..fd9f869f1 100644
--- a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
+++ b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml
@@ -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>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index ea46e8546..cf753dc26 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -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>
diff --git 
a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml
similarity index 52%
copy from 
custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
copy to 
fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml
index f466fdaf3..27b3335ca 100644
--- 
a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml
@@ -22,24 +22,5 @@
 <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="acme" id="1">
-        <insert tableName="job">
-            <column name="name" value="Acme Noop Job"/>
-            <column name="display_name" value="Acme Noop Job"/>
-            <column name="cron_expression" value="0 1 0 1/1 * ? *"/>
-            <column name="create_time" valueDate="${current_datetime}"/>
-            <column name="task_priority" valueNumeric="5"/>
-            <column name="group_name"/>
-            <column name="previous_run_start_time"/>
-            <column name="job_key" value="Acme Noop Job _ DEFAULT"/>
-            <column name="initializing_errorlog"/>
-            <column name="is_active" valueBoolean="false"/>
-            <column name="currently_running" valueBoolean="false"/>
-            <column name="updates_allowed" valueBoolean="true"/>
-            <column name="scheduler_group" valueNumeric="0"/>
-            <column name="is_misfired" valueBoolean="false"/>
-            <column name="node_id" valueNumeric="1"/>
-            <column name="is_mismatched_job" valueBoolean="true"/>
-        </insert>
-    </changeSet>
+    <include file="parts/0146_add_final_constraints.xml" 
relativeToChangelogFile="true"/>
 </databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0145_job_short_name.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0145_job_short_name.xml
new file mode 100644
index 000000000..5f1f87472
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0145_job_short_name.xml
@@ -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>
diff --git 
a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0146_add_final_constraints.xml
similarity index 52%
copy from 
custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
copy to 
fineract-provider/src/main/resources/db/changelog/tenant/parts/0146_add_final_constraints.xml
index f466fdaf3..e2f21abaa 100644
--- 
a/custom/acme/loan/job/src/main/resources/db/custom-changelog/0001_acme_loan_job.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0146_add_final_constraints.xml
@@ -22,24 +22,10 @@
 <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="acme" id="1">
-        <insert tableName="job">
-            <column name="name" value="Acme Noop Job"/>
-            <column name="display_name" value="Acme Noop Job"/>
-            <column name="cron_expression" value="0 1 0 1/1 * ? *"/>
-            <column name="create_time" valueDate="${current_datetime}"/>
-            <column name="task_priority" valueNumeric="5"/>
-            <column name="group_name"/>
-            <column name="previous_run_start_time"/>
-            <column name="job_key" value="Acme Noop Job _ DEFAULT"/>
-            <column name="initializing_errorlog"/>
-            <column name="is_active" valueBoolean="false"/>
-            <column name="currently_running" valueBoolean="false"/>
-            <column name="updates_allowed" valueBoolean="true"/>
-            <column name="scheduler_group" valueNumeric="0"/>
-            <column name="is_misfired" valueBoolean="false"/>
-            <column name="node_id" valueNumeric="1"/>
-            <column name="is_mismatched_job" valueBoolean="true"/>
-        </insert>
+    <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>
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstanceModeIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstanceModeIntegrationTest.java
index 251490c4c..a1d327674 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstanceModeIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstanceModeIntegrationTest.java
@@ -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)
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java
index 61ee96503..2f77b7c1b 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingJobIntegrationTest.java
@@ -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);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java
index 8f069ccf1..c343f25b2 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java
@@ -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");
+        };
+    }
 }


Reply via email to