You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2022/11/28 11:04:09 UTC

[fineract] branch develop updated: FINERACT-1744 - Idempotency support - [x] Idempotency write move to filters - [x] Idempotency integration test with success and failure - [x] Added response status code to database - [x] SynchronousCommandProcessingService not generate the command, just marked to the filter - [x] IdempotencyStoreFilter store the command request if available. - [x] Removed JpaExceptionHandler.java

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

arnold 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 5210bf741 FINERACT-1744 - Idempotency support - [x] Idempotency write move to filters - [x] Idempotency integration test with success and failure - [x] Added response status code to database - [x] SynchronousCommandProcessingService not generate the command, just marked to the filter - [x] IdempotencyStoreFilter store the command request if available. - [x] Removed JpaExceptionHandler.java
5210bf741 is described below

commit 5210bf741263db0bcfe022edd2d905007431c797
Author: Janos Haber <ja...@finesolution.hu>
AuthorDate: Fri Nov 25 00:09:59 2022 +0100

    FINERACT-1744 - Idempotency support
    - [x] Idempotency write move to filters
    - [x] Idempotency integration test with success and failure
    - [x] Added response status code to database
    - [x] SynchronousCommandProcessingService not generate the command, just marked to the filter
    - [x] IdempotencyStoreFilter store the command request if available.
    - [x] Removed JpaExceptionHandler.java
---
 .../bare-bones-demo/bk_bare_bones_demo.sql         |   6 +-
 .../bk_mifostenant_default.sql                     |   6 +-
 .../ceda/bk_ceda_trial.sql                         |   6 +-
 .../ceda/bk_core_with_custom_and_coa.sql           |   6 +-
 .../default-demo/bk_mifostenant-default.sql        |   6 +-
 .../latam-demo/bk_latam.sql                        |   6 +-
 .../0001a-mifosplatform-core-ddl-latest.sql        |   4 +-
 .../0002-mifosx-base-reference-data-utf8.sql       |  10 +-
 .../InlineLoanCOBBuildExecutionContextTasklet.java |  12 +-
 .../service/InlineLoanCOBExecutorServiceImpl.java  |  10 +-
 .../fineract/commands/api/AuditsApiResource.java   |  16 +--
 .../fineract/commands/data/AuditSearchData.java    |   2 +-
 .../domain/CommandProcessingResultType.java        |   4 +-
 .../fineract/commands/domain/CommandSource.java    |  70 +++++----
 .../commands/domain/CommandSourceRepository.java   |   4 +-
 .../commands/handler/NewCommandSourceHandler.java  |   4 +
 .../service/AuditReadPlatformServiceImpl.java      |  10 +-
 .../commands/service/CommandSourceService.java     |  96 +++++++++++++
 .../commands/service/IdempotencyKeyResolver.java   |  49 +++++++
 ...folioCommandSourceWritePlatformServiceImpl.java |   7 +-
 .../SynchronousCommandProcessingService.java       | 147 ++++++++++---------
 .../infrastructure/core/config/SecurityConfig.java |   8 +-
 .../AbstractIdempotentCommandException.java}       |  30 ++--
 .../IdempotentCommandProcessFailedException.java}  |  29 ++--
 .../IdempotentCommandProcessSucceedException.java} |  22 ++-
 ...entCommandProcessUnderProcessingException.java} |  22 ++-
 ...mpotentCommandProcessFailedExceptionMapper.java |  43 ++++++
 ...potentCommandProcessSucceedExceptionMapper.java |  42 ++++++
 ...mmandProcessUnderProcessingExceptionMapper.java |  44 ++++++
 .../core/filters/IdempotencyStoreFilter.java       |  91 ++++++++++++
 .../persistence/ExtendedJpaTransactionManager.java |   4 +
 .../serialization/GoogleGsonSerializerHelper.java  |   2 +-
 .../jobs/ScheduledJobRunnerConfig.java             |   3 +-
 .../service/LoanDelinquencyDomainServiceImpl.java  |   4 +-
 .../src/main/resources/application.properties      |   1 +
 .../db/changelog/tenant/changelog-tenant.xml       |   3 +
 ...072_add_result_and status_to_command_source.xml |  45 ++++++
 ...73_add_result_status_code_to_command_source.xml |  38 +++++
 .../sql/migrations/sample_data/barebones_db.sql    |  14 +-
 .../migrations/sample_data/load_sample_data.sql    |  14 +-
 .../commands/service/CommandSourceServiceTest.java | 111 +++++++++++++++
 .../service/IdempotencyKeyResolverTest.java        |  85 +++++++++++
 .../SynchronousCommandProcessingServiceTest.java   | 153 ++++++++++++++++++++
 .../fineract/integrationtests/IdempotencyTest.java | 158 +++++++++++++++++++++
 .../integrationtests/common/IdempotencyHelper.java |  95 +++++++++++++
 .../fineract/integrationtests/common/Utils.java    |  11 ++
 46 files changed, 1336 insertions(+), 217 deletions(-)

diff --git a/fineract-db/multi-tenant-demo-backups/bare-bones-demo/bk_bare_bones_demo.sql b/fineract-db/multi-tenant-demo-backups/bare-bones-demo/bk_bare_bones_demo.sql
index 0c34151db..a14eb7e55 100644
--- a/fineract-db/multi-tenant-demo-backups/bare-bones-demo/bk_bare_bones_demo.sql
+++ b/fineract-db/multi-tenant-demo-backups/bare-bones-demo/bk_bare_bones_demo.sql
@@ -1447,7 +1447,7 @@ CREATE TABLE `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_m_maker_m_appuser` (`maker_id`),
   KEY `FK_m_checker_m_appuser` (`checker_id`),
@@ -1455,7 +1455,7 @@ CREATE TABLE `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -1762,7 +1762,7 @@ CREATE TABLE `r_enum_value` (
 
 LOCK TABLES `r_enum_value` WRITE;
 /*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */;
-INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
+INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
 /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/fineract-db/multi-tenant-demo-backups/bk_mifostenant_default.sql b/fineract-db/multi-tenant-demo-backups/bk_mifostenant_default.sql
index ef70237b6..63308294c 100644
--- a/fineract-db/multi-tenant-demo-backups/bk_mifostenant_default.sql
+++ b/fineract-db/multi-tenant-demo-backups/bk_mifostenant_default.sql
@@ -1448,7 +1448,7 @@ CREATE TABLE `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_m_maker_m_appuser` (`maker_id`),
   KEY `FK_m_checker_m_appuser` (`checker_id`),
@@ -1456,7 +1456,7 @@ CREATE TABLE `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -1763,7 +1763,7 @@ CREATE TABLE `r_enum_value` (
 
 LOCK TABLES `r_enum_value` WRITE;
 /*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */;
-INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
+INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
 /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/fineract-db/multi-tenant-demo-backups/ceda/bk_ceda_trial.sql b/fineract-db/multi-tenant-demo-backups/ceda/bk_ceda_trial.sql
index 5ed8ef07d..a4924c96a 100644
--- a/fineract-db/multi-tenant-demo-backups/ceda/bk_ceda_trial.sql
+++ b/fineract-db/multi-tenant-demo-backups/ceda/bk_ceda_trial.sql
@@ -1373,7 +1373,7 @@ CREATE TABLE `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_m_maker_m_appuser` (`maker_id`),
   KEY `FK_m_checker_m_appuser` (`checker_id`),
@@ -1381,7 +1381,7 @@ CREATE TABLE `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -1701,7 +1701,7 @@ CREATE TABLE `r_enum_value` (
 
 LOCK TABLES `r_enum_value` WRITE;
 /*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */;
-INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
+INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
 /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/fineract-db/multi-tenant-demo-backups/ceda/bk_core_with_custom_and_coa.sql b/fineract-db/multi-tenant-demo-backups/ceda/bk_core_with_custom_and_coa.sql
index f4672ec00..58d2caf3d 100644
--- a/fineract-db/multi-tenant-demo-backups/ceda/bk_core_with_custom_and_coa.sql
+++ b/fineract-db/multi-tenant-demo-backups/ceda/bk_core_with_custom_and_coa.sql
@@ -1373,7 +1373,7 @@ CREATE TABLE `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_m_maker_m_appuser` (`maker_id`),
   KEY `FK_m_checker_m_appuser` (`checker_id`),
@@ -1381,7 +1381,7 @@ CREATE TABLE `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -1701,7 +1701,7 @@ CREATE TABLE `r_enum_value` (
 
 LOCK TABLES `r_enum_value` WRITE;
 /*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */;
-INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
+INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
 /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/fineract-db/multi-tenant-demo-backups/default-demo/bk_mifostenant-default.sql b/fineract-db/multi-tenant-demo-backups/default-demo/bk_mifostenant-default.sql
index ef70237b6..63308294c 100644
--- a/fineract-db/multi-tenant-demo-backups/default-demo/bk_mifostenant-default.sql
+++ b/fineract-db/multi-tenant-demo-backups/default-demo/bk_mifostenant-default.sql
@@ -1448,7 +1448,7 @@ CREATE TABLE `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_m_maker_m_appuser` (`maker_id`),
   KEY `FK_m_checker_m_appuser` (`checker_id`),
@@ -1456,7 +1456,7 @@ CREATE TABLE `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -1763,7 +1763,7 @@ CREATE TABLE `r_enum_value` (
 
 LOCK TABLES `r_enum_value` WRITE;
 /*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */;
-INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
+INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
 /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/fineract-db/multi-tenant-demo-backups/latam-demo/bk_latam.sql b/fineract-db/multi-tenant-demo-backups/latam-demo/bk_latam.sql
index 9900ab12f..f45087d7c 100644
--- a/fineract-db/multi-tenant-demo-backups/latam-demo/bk_latam.sql
+++ b/fineract-db/multi-tenant-demo-backups/latam-demo/bk_latam.sql
@@ -1232,7 +1232,7 @@ CREATE TABLE `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_m_maker_m_appuser` (`maker_id`),
   KEY `FK_m_checker_m_appuser` (`checker_id`),
@@ -1240,7 +1240,7 @@ CREATE TABLE `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -1679,7 +1679,7 @@ CREATE TABLE `r_enum_value` (
 
 LOCK TABLES `r_enum_value` WRITE;
 /*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */;
-INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
+INSERT INTO `r_enum_value` VALUES ('amortization_method_enum',0,'Equal principle payments','Equal principle payments'),('amortization_method_enum',1,'Equal installments','Equal installments'),('interest_calculated_in_period_enum',0,'Daily','Daily'),('interest_calculated_in_period_enum',1,'Same as repayment period','Same as repayment period'),('interest_method_enum',0,'Declining Balance','Declining Balance'),('interest_method_enum',1,'Flat','Flat'),('interest_period_frequency_enum',2,'Per [...]
 /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
 UNLOCK TABLES;
 
diff --git a/fineract-db/old-schema-files/0001a-mifosplatform-core-ddl-latest.sql b/fineract-db/old-schema-files/0001a-mifosplatform-core-ddl-latest.sql
index a07826136..ca9c7e8a9 100644
--- a/fineract-db/old-schema-files/0001a-mifosplatform-core-ddl-latest.sql
+++ b/fineract-db/old-schema-files/0001a-mifosplatform-core-ddl-latest.sql
@@ -304,7 +304,7 @@ CREATE TABLE `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   PRIMARY KEY (`id`),
   KEY `FK_m_maker_m_appuser` (`maker_id`),
   KEY `FK_m_checker_m_appuser` (`checker_id`),
@@ -312,7 +312,7 @@ CREATE TABLE `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
diff --git a/fineract-db/old-schema-files/0002-mifosx-base-reference-data-utf8.sql b/fineract-db/old-schema-files/0002-mifosx-base-reference-data-utf8.sql
index 9810e799c..e74d3cc01 100644
--- a/fineract-db/old-schema-files/0002-mifosx-base-reference-data-utf8.sql
+++ b/fineract-db/old-schema-files/0002-mifosx-base-reference-data-utf8.sql
@@ -47,10 +47,12 @@ VALUES
 ('loan_transaction_strategy_id',2,'heavensfamily-strategy','Heavensfamily'),
 ('loan_transaction_strategy_id',3,'creocore-strategy','Creocore'),
 ('loan_transaction_strategy_id',4,'rbi-india-strategy','RBI (India)'),
-('processing_result_enum',0,'invalid','Invalid'),
-('processing_result_enum',1,'processed','Processed'),
-('processing_result_enum',2,'awaiting.approval','Awaiting Approval'),
-('processing_result_enum',3,'rejected','Rejected'),
+('status',0,'invalid','Invalid'),
+('status',1,'processed','Processed'),
+('status',2,'awaiting.approval','Awaiting Approval'),
+('status',3,'rejected','Rejected'),
+('status',4,'underProcessing','Under Processing'),
+('status',5,'error','Error'),
 ('repayment_period_frequency_enum',0,'Days','Days'),
 ('repayment_period_frequency_enum',1,'Weeks','Weeks'),
 ('repayment_period_frequency_enum',2,'Months','Months'),
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java
index e3856fd05..cb84aa1fc 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineLoanCOBBuildExecutionContextTasklet.java
@@ -37,22 +37,18 @@ import org.springframework.batch.core.StepContribution;
 import org.springframework.batch.core.scope.context.ChunkContext;
 import org.springframework.batch.core.step.tasklet.Tasklet;
 import org.springframework.batch.repeat.RepeatStatus;
-import org.springframework.beans.factory.InitializingBean;
+import org.springframework.stereotype.Component;
 
 @Slf4j
 @RequiredArgsConstructor
-public class InlineLoanCOBBuildExecutionContextTasklet implements Tasklet, InitializingBean {
+@Component
+public class InlineLoanCOBBuildExecutionContextTasklet implements Tasklet {
 
     private final GoogleGsonSerializerHelper gsonFactory;
     private final COBBusinessStepService cobBusinessStepService;
     private final CustomJobParameterRepository customJobParameterRepository;
 
-    private Gson gson;
-
-    @Override
-    public void afterPropertiesSet() throws Exception {
-        this.gson = gsonFactory.createSimpleGson();
-    }
+    private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
 
     @Override
     public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java
index cb56e4664..5e2f4aac1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java
@@ -55,7 +55,6 @@ import org.springframework.batch.core.configuration.JobLocator;
 import org.springframework.batch.core.explore.JobExplorer;
 import org.springframework.batch.core.launch.JobLauncher;
 import org.springframework.batch.core.launch.NoSuchJobException;
-import org.springframework.beans.factory.InitializingBean;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.TransactionStatus;
 import org.springframework.transaction.annotation.Propagation;
@@ -66,7 +65,7 @@ import org.springframework.transaction.support.TransactionTemplate;
 @Service
 @Slf4j
 @RequiredArgsConstructor
-public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<Long>, InitializingBean {
+public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<Long> {
 
     private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: ";
 
@@ -80,12 +79,7 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<L
     private final CustomJobParameterRepository customJobParameterRepository;
     private final PlatformSecurityContext context;
 
-    private Gson gson;
-
-    @Override
-    public void afterPropertiesSet() throws Exception {
-        this.gson = gsonFactory.createSimpleGson();
-    }
+    private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
 
     @Override
     @Transactional(propagation = Propagation.NOT_SUPPORTED)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/api/AuditsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/commands/api/AuditsApiResource.java
index e5f80132d..5e1eff7c9 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/api/AuditsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/api/AuditsApiResource.java
@@ -68,7 +68,7 @@ public class AuditsApiResource {
             "subresourceId", "maker", "madeOnDate", "checker", "checkedOnDate", "processingResult", "commandAsJson", "officeName",
             "groupLevelName", "groupName", "clientName", "loanAccountNo", "savingsAccountNo", "clientId", "loanId", "url"));
 
-    private final String resourceNameForPermissions = "AUDIT";
+    private static final String RESOURCE_NAME_FOR_PERMISSIONS = "AUDIT";
 
     private final PlatformSecurityContext context;
     private final AuditReadPlatformService auditReadPlatformService;
@@ -107,7 +107,7 @@ public class AuditsApiResource {
             @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
             @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
 
-        this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
+        this.context.authenticatedUser().validateHasReadPermission(this.RESOURCE_NAME_FOR_PERMISSIONS);
         final PaginationParameters parameters = PaginationParameters.instance(paged, offset, limit, orderBy, sortOrder);
         final SQLBuilder extraCriteria = getExtraCriteria(actionName, entityName, resourceId, makerId, makerDateTimeFrom, makerDateTimeTo,
                 checkerId, checkerDateTimeFrom, checkerDateTimeTo, processingResult, officeId, groupId, clientId, loanId, savingsAccountId);
@@ -137,7 +137,7 @@ public class AuditsApiResource {
     public String retrieveAuditEntry(@PathParam("auditId") @Parameter(description = "auditId") final Long auditId,
             @Context final UriInfo uriInfo) {
 
-        this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
+        this.context.authenticatedUser().validateHasReadPermission(this.RESOURCE_NAME_FOR_PERMISSIONS);
 
         final AuditData auditEntry = this.auditReadPlatformService.retrieveAuditEntry(auditId);
 
@@ -155,22 +155,22 @@ public class AuditsApiResource {
             @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = MakercheckersApiResourceSwagger.GetMakerCheckersSearchTemplateResponse.class))) })
     public String retrieveAuditSearchTemplate(@Context final UriInfo uriInfo) {
 
-        this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
+        this.context.authenticatedUser().validateHasReadPermission(this.RESOURCE_NAME_FOR_PERMISSIONS);
 
         final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
 
         final AuditSearchData auditSearchData = this.auditReadPlatformService.retrieveSearchTemplate("audit");
 
         final Set<String> RESPONSE_DATA_PARAMETERS_SEARCH_TEMPLATE = new HashSet<>(
-                Arrays.asList("appUsers", "actionNames", "entityNames", "processingResults"));
+                Arrays.asList("appUsers", "actionNames", "entityNames", "status"));
 
         return this.toApiJsonSerializerSearchTemplate.serialize(settings, auditSearchData, RESPONSE_DATA_PARAMETERS_SEARCH_TEMPLATE);
     }
 
     private SQLBuilder getExtraCriteria(final String actionName, final String entityName, final Long resourceId, final Long makerId,
             final String makerDateTimeFrom, final String makerDateTimeTo, final Long checkerId, final String checkerDateTimeFrom,
-            final String checkerDateTimeTo, final Integer processingResult, final Integer officeId, final Integer groupId,
-            final Integer clientId, final Integer loanId, final Integer savingsAccountId) {
+            final String checkerDateTimeTo, final Integer status, final Integer officeId, final Integer groupId, final Integer clientId,
+            final Integer loanId, final Integer savingsAccountId) {
 
         SQLBuilder extraCriteria = new SQLBuilder();
         extraCriteria.addNonNullCriteria("aud.action_name = ", actionName);
@@ -184,7 +184,7 @@ public class AuditsApiResource {
         extraCriteria.addNonNullCriteria("aud.made_on_date <= ", makerDateTimeTo);
         extraCriteria.addNonNullCriteria("aud.checked_on_date >= ", checkerDateTimeFrom);
         extraCriteria.addNonNullCriteria("aud.checked_on_date <= ", checkerDateTimeTo);
-        extraCriteria.addNonNullCriteria("aud.processing_result_enum = ", processingResult);
+        extraCriteria.addNonNullCriteria("aud.status = ", status);
         extraCriteria.addNonNullCriteria("aud.office_id = ", officeId);
         extraCriteria.addNonNullCriteria("aud.group_id = ", groupId);
         extraCriteria.addNonNullCriteria("aud.client_id = ", clientId);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditSearchData.java b/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditSearchData.java
index 35fe02100..047e80dee 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditSearchData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditSearchData.java
@@ -34,5 +34,5 @@ public final class AuditSearchData {
     private final Collection<AppUserData> appUsers;
     private final List<String> actionNames;
     private final List<String> entityNames;
-    private final Collection<ProcessingResultLookup> processingResults;
+    private final Collection<ProcessingResultLookup> statuses;
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
index f25287e7f..770f579d0 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
@@ -28,7 +28,9 @@ public enum CommandProcessingResultType {
     INVALID(0, "commandProcessingResultType.invalid"), //
     PROCESSED(1, "commandProcessingResultType.processed"), //
     AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), //
-    REJECTED(3, "commandProcessingResultType.rejected");
+    REJECTED(3, "commandProcessingResultType.rejected"), //
+    UNDER_PROCESSING(4, "commandProcessingResultType.underProcessing"), //
+    ERROR(5, "commandProcessingResultType.error");
 
     private final Integer value;
     private final String code;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java
index c9a2286d2..67e12fecd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSource.java
@@ -24,7 +24,6 @@ import javax.persistence.Entity;
 import javax.persistence.JoinColumn;
 import javax.persistence.ManyToOne;
 import javax.persistence.Table;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.fineract.infrastructure.core.api.JsonCommand;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
 import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@@ -91,8 +90,8 @@ public class CommandSource extends AbstractPersistableCustom {
     @JoinColumn(name = "checker_id", nullable = true)
     private AppUser checker;
 
-    @Column(name = "processing_result_enum", nullable = false)
-    private Integer processingResult;
+    @Column(name = "status", nullable = false)
+    private Integer status;
 
     @Column(name = "product_id")
     private Long productId;
@@ -118,10 +117,16 @@ public class CommandSource extends AbstractPersistableCustom {
     @Column(name = "subresource_external_id")
     private ExternalId subResourceExternalId;
 
+    @Column(name = "result")
+    private String result;
+
+    @Column(name = "result_status_code")
+    private Integer resultStatusCode;
+
     public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
-            String idempotencyKey) {
+            String idempotencyKey, Integer status) {
         return new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(), command.subentityId(),
-                command.json(), maker, idempotencyKey);
+                command.json(), maker, idempotencyKey, status);
     }
 
     protected CommandSource() {
@@ -129,7 +134,8 @@ public class CommandSource extends AbstractPersistableCustom {
     }
 
     private CommandSource(final String actionName, final String entityName, final String href, final Long resourceId,
-            final Long subResourceId, final String commandSerializedAsJson, final AppUser maker, final String idempotencyKey) {
+            final Long subResourceId, final String commandSerializedAsJson, final AppUser maker, final String idempotencyKey,
+            final Integer status) {
         this.actionName = actionName;
         this.entityName = entityName;
         this.resourceGetUrl = href;
@@ -138,7 +144,7 @@ public class CommandSource extends AbstractPersistableCustom {
         this.commandAsJson = commandSerializedAsJson;
         this.maker = maker;
         this.madeOnDate = DateUtils.getOffsetDateTimeOfTenant();
-        this.processingResult = CommandProcessingResultType.PROCESSED.getValue();
+        this.status = status;
         this.idempotencyKey = idempotencyKey;
     }
 
@@ -165,13 +171,13 @@ public class CommandSource extends AbstractPersistableCustom {
     public void markAsChecked(final AppUser checker) {
         this.checker = checker;
         this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenant();
-        this.processingResult = CommandProcessingResultType.PROCESSED.getValue();
+        this.status = CommandProcessingResultType.PROCESSED.getValue();
     }
 
     public void markAsRejected(final AppUser checker) {
         this.checker = checker;
         this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenant();
-        this.processingResult = CommandProcessingResultType.REJECTED.getValue();
+        this.status = CommandProcessingResultType.REJECTED.getValue();
     }
 
     public void updateResourceId(final Long resourceId) {
@@ -182,7 +188,11 @@ public class CommandSource extends AbstractPersistableCustom {
         this.subResourceId = subResourceId;
     }
 
-    public void updateJsonTo(final String json) {
+    public String getCommandJson() {
+        return this.commandAsJson;
+    }
+
+    public void setCommandJson(final String json) {
         this.commandAsJson = json;
     }
 
@@ -194,14 +204,6 @@ public class CommandSource extends AbstractPersistableCustom {
         return this.subResourceId;
     }
 
-    public boolean hasJson() {
-        return StringUtils.isNotBlank(this.commandAsJson);
-    }
-
-    public String json() {
-        return this.commandAsJson;
-    }
-
     public String getActionName() {
         return this.actionName;
     }
@@ -223,15 +225,11 @@ public class CommandSource extends AbstractPersistableCustom {
     }
 
     public void markAsAwaitingApproval() {
-        this.processingResult = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
+        this.status = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
     }
 
     public boolean isMarkedAsAwaitingApproval() {
-        if (this.processingResult.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue())) {
-            return true;
-        }
-
-        return false;
+        return this.status.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue());
     }
 
     public void updateForAudit(final CommandProcessingResult result) {
@@ -309,4 +307,28 @@ public class CommandSource extends AbstractPersistableCustom {
     public void setIdempotencyKey(String idempotencyKey) {
         this.idempotencyKey = idempotencyKey;
     }
+
+    public String getResult() {
+        return result;
+    }
+
+    public void setResult(String result) {
+        this.result = result;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public void setStatus(Integer status) {
+        this.status = status;
+    }
+
+    public Integer getResultStatusCode() {
+        return resultStatusCode;
+    }
+
+    public void setResultStatusCode(Integer resultStatusCode) {
+        this.resultStatusCode = resultStatusCode;
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSourceRepository.java b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSourceRepository.java
index 158ce0a1a..1977c6ede 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSourceRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandSourceRepository.java
@@ -22,5 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
 public interface CommandSourceRepository extends JpaRepository<CommandSource, Long>, JpaSpecificationExecutor<CommandSource> {
-    // no added behaviour
+
+    CommandSource findByActionNameAndEntityNameAndIdempotencyKey(String actionName, String entityName, String idempotencyKey);
+
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java b/fineract-provider/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java
index d2fd82ce1..d07e04cd7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/handler/NewCommandSourceHandler.java
@@ -20,8 +20,12 @@ package org.apache.fineract.commands.handler;
 
 import org.apache.fineract.infrastructure.core.api.JsonCommand;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.springframework.transaction.annotation.Isolation;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
 
 public interface NewCommandSourceHandler {
 
+    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
     CommandProcessingResult processCommand(JsonCommand command);
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java
index 6744d296b..0485b37f8 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java
@@ -115,7 +115,7 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
                     + " left join m_office o on o.id = aud.office_id" + " left join m_group g on g.id = aud.group_id"
                     + " left join m_group_level gl on gl.id = g.level_id" + " left join m_client c on c.id = aud.client_id"
                     + " left join m_loan l on l.id = aud.loan_id" + " left join m_savings_account s on s.id = aud.savings_account_id"
-                    + " left join r_enum_value ev on ev.enum_name = 'processing_result_enum' and ev.enum_id = aud.processing_result_enum";
+                    + " left join r_enum_value ev on ev.enum_name = 'status' and ev.enum_id = aud.status";
 
             // data scoping: head office (hierarchy = ".") can see all audit
             // entries
@@ -206,7 +206,7 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
 
     @Override
     public Collection<AuditData> retrieveAllEntriesToBeChecked(final SQLBuilder extraCriteria, final boolean includeJson) {
-        extraCriteria.addCriteria("aud.processing_result_enum = ", 2);
+        extraCriteria.addCriteria("aud.status = ", 2);
         return retrieveEntries("makerchecker", extraCriteria, " order by aud.id, mk.username", includeJson);
     }
 
@@ -491,13 +491,13 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
         @Override
         public ProcessingResultLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
             final Long id = JdbcSupport.getLong(rs, "id");
-            final String processingResult = rs.getString("processingResult");
+            final String status = rs.getString("status");
 
-            return new ProcessingResultLookup(id, processingResult);
+            return new ProcessingResultLookup(id, status);
         }
 
         public String schema() {
-            return " select enum_id as id, enum_message_property as processingResult from r_enum_value where enum_name = 'processing_result_enum' "
+            return " select enum_id as id, enum_message_property as status from r_enum_value where enum_name = 'status' "
                     + " order by enum_id";
         }
     }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java
new file mode 100644
index 000000000..882e14041
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java
@@ -0,0 +1,96 @@
+/**
+ * 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.commands.service;
+
+import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
+import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.batch.exception.ErrorHandler;
+import org.apache.fineract.batch.exception.ErrorInfo;
+import org.apache.fineract.commands.domain.CommandSource;
+import org.apache.fineract.commands.domain.CommandSourceRepository;
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.commands.exception.CommandNotFoundException;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Isolation;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Two phase transactional command processing: save initial...work...finish/failed to handle idempotent requests. As the
+ * default isolation level for MYSQL is REPEATABLE_READ and a lower value READ_COMMITED for postgres, we can force to
+ * use the same for both database backends to be consistent.
+ */
+@Component
+@RequiredArgsConstructor
+public class CommandSourceService {
+
+    private final CommandSourceRepository commandSourceRepository;
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
+    public CommandSource saveInitial(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
+        CommandSource initialCommandSource = getInitialCommandSource(wrapper, jsonCommand, maker, idempotencyKey);
+
+        if (initialCommandSource.getCommandJson() == null) {
+            initialCommandSource.setCommandJson("{}");
+        }
+
+        return commandSourceRepository.saveAndFlush(initialCommandSource);
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
+    public void saveFailed(CommandSource commandSource) {
+        commandSource.setStatus(ERROR.getValue());
+        commandSourceRepository.saveAndFlush(commandSource);
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
+    public CommandSource saveResult(CommandSource commandSource) {
+        return commandSourceRepository.saveAndFlush(commandSource);
+    }
+
+    public ErrorInfo generateErrorException(Throwable t) {
+        if (t instanceof final RuntimeException e) {
+            return ErrorHandler.handler(e);
+        } else {
+            return new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}");
+        }
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
+    public CommandSource findCommandSource(CommandWrapper wrapper, String idempotencyKey) {
+        return commandSourceRepository.findByActionNameAndEntityNameAndIdempotencyKey(wrapper.actionName(), wrapper.entityName(),
+                idempotencyKey);
+    }
+
+    private CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
+        CommandSource commandSourceResult;
+        if (jsonCommand.commandId() != null) {
+            commandSourceResult = commandSourceRepository.findById(jsonCommand.commandId())
+                    .orElseThrow(() -> new CommandNotFoundException(jsonCommand.commandId()));
+            commandSourceResult.markAsChecked(maker);
+        } else {
+            commandSourceResult = CommandSource.fullEntryFrom(wrapper, jsonCommand, maker, idempotencyKey, UNDER_PROCESSING.getValue());
+        }
+        return commandSourceResult;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java
new file mode 100644
index 000000000..880aa0438
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.commands.service;
+
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+@Component
+@RequiredArgsConstructor
+public class IdempotencyKeyResolver {
+
+    private final IdempotencyKeyGenerator idempotencyKeyGenerator;
+
+    private final FineractProperties fineractProperties;
+
+    public String resolve(CommandWrapper wrapper) {
+        return Optional.ofNullable(wrapper.getIdempotencyKey())
+                .orElseGet(() -> getHeaderAttribute().orElseGet(idempotencyKeyGenerator::create));
+    }
+
+    private Optional<String> getHeaderAttribute() {
+        return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) //
+                .filter(ServletRequestAttributes.class::isInstance) //
+                .map(ServletRequestAttributes.class::cast) //
+                .map(ServletRequestAttributes::getRequest) //
+                .map(request -> request.getHeader(fineractProperties.getIdempotencyKeyHeaderName()));
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
index a37861114..42cc37b42 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
@@ -87,16 +87,15 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
                 commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(),
                 commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getCreditBureauId(),
                 commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getIdempotencyKey());
-        final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.json());
-        final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.json(), parsedCommand,
+        final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.getCommandJson());
+        final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.getCommandJson(), parsedCommand,
                 this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(),
                 commandSourceInput.subResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
                 commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(),
                 commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(),
                 commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName());
 
-        final boolean makerCheckerApproval = true;
-        return this.processAndLogCommandService.executeCommand(wrapper, command, makerCheckerApproval);
+        return this.processAndLogCommandService.executeCommand(wrapper, command, true);
     }
 
     @Transactional
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
index 479eb6d7e..7216babe2 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
@@ -18,6 +18,10 @@
  */
 package org.apache.fineract.commands.service;
 
+import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
+import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED;
+import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
+
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
 import io.github.resilience4j.retry.annotation.Retry;
@@ -25,23 +29,27 @@ import java.lang.reflect.Type;
 import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.batch.exception.ErrorHandler;
 import org.apache.fineract.batch.exception.ErrorInfo;
+import org.apache.fineract.commands.domain.CommandProcessingResultType;
 import org.apache.fineract.commands.domain.CommandSource;
-import org.apache.fineract.commands.domain.CommandSourceRepository;
 import org.apache.fineract.commands.domain.CommandWrapper;
-import org.apache.fineract.commands.exception.CommandNotFoundException;
 import org.apache.fineract.commands.exception.RollbackTransactionAsCommandIsNotApprovedByCheckerException;
 import org.apache.fineract.commands.exception.UnsupportedCommandException;
 import org.apache.fineract.commands.handler.NewCommandSourceHandler;
 import org.apache.fineract.commands.provider.CommandHandlerProvider;
 import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
 import org.apache.fineract.infrastructure.core.api.JsonCommand;
-import org.apache.fineract.infrastructure.core.config.FineractProperties;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
+import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
 import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.infrastructure.hooks.event.HookEvent;
@@ -53,80 +61,64 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.RequestContextHolder;
-import org.springframework.web.context.request.ServletRequestAttributes;
 
 @Service
 @Slf4j
 @RequiredArgsConstructor
 public class SynchronousCommandProcessingService implements CommandProcessingService {
 
+    public static final String IDEMPOTENCY_KEY_STORE_FLAG = "idempotencyKeyStoreFlag";
+    public static final String COMMAND_SOURCE_ID = "commandSourceId";
     private final PlatformSecurityContext context;
     private final ApplicationContext applicationContext;
     private final ToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
     private final ToApiJsonSerializer<CommandProcessingResult> toApiResultJsonSerializer;
-    private final CommandSourceRepository commandSourceRepository;
     private final ConfigurationDomainService configurationDomainService;
     private final CommandHandlerProvider commandHandlerProvider;
+    private final IdempotencyKeyResolver idempotencyKeyResolver;
     private final IdempotencyKeyGenerator idempotencyKeyGenerator;
-    private final FineractProperties fineractProperties;
+    private final CommandSourceService commandSourceService;
+    private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
 
     @Override
-    @Transactional
     @Retry(name = "executeCommand", fallbackMethod = "fallbackExecuteCommand")
     public CommandProcessingResult executeCommand(final CommandWrapper wrapper, final JsonCommand command,
             final boolean isApprovedByChecker) {
+        // Do not store the idempotency key because of the exception handling
+        setIdempotencyKeyStoreFlag(false);
 
         final boolean rollbackTransaction = configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName());
+        String idempotencyKey = idempotencyKeyResolver.resolve(wrapper);
+        exceptionWhenTheRequestAlreadyProcessed(wrapper, idempotencyKey);
+
+        // Store idempotency key to the request attribute
 
-        final NewCommandSourceHandler handler = findCommandHandler(wrapper);
+        CommandSource savedCommandSource = commandSourceService.saveInitial(wrapper, command, context.authenticatedUser(wrapper),
+                idempotencyKey);
+        storeCommandToIdempotentFilter(savedCommandSource);
+        setIdempotencyKeyStoreFlag(true);
 
         final CommandProcessingResult result;
         try {
-            result = handler.processCommand(command);
-        } catch (Throwable t) {
+            result = findCommandHandler(wrapper).processCommand(command);
+        } catch (Throwable t) { // NOSONAR
+            commandSourceService.saveFailed(commandSourceService.findCommandSource(wrapper, idempotencyKey));
             publishHookErrorEvent(wrapper, command, t);
             throw t;
         }
 
-        final AppUser maker = context.authenticatedUser(wrapper);
-
-        CommandSource commandSourceResult;
-        if (command.commandId() != null) {
-            commandSourceResult = commandSourceRepository.findById(command.commandId())
-                    .orElseThrow(() -> new CommandNotFoundException(command.commandId()));
-            commandSourceResult.markAsChecked(maker);
-        } else {
-            String requestIdempotencyKey = null;
-            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
-            if (requestAttributes != null) {
-                if (requestAttributes instanceof ServletRequestAttributes) {
-                    requestIdempotencyKey = ((ServletRequestAttributes) requestAttributes).getRequest()
-                            .getHeader(fineractProperties.getIdempotencyKeyHeaderName());
-                }
-            }
-
-            commandSourceResult = CommandSource.fullEntryFrom(wrapper, command, maker,
-                    wrapper.getIdempotencyKey() == null
-                            ? (requestIdempotencyKey == null ? idempotencyKeyGenerator.create() : requestIdempotencyKey)
-                            : wrapper.getIdempotencyKey());
-        }
-
-        commandSourceResult.updateForAudit(result);
+        CommandSource initialCommandSource = commandSourceService.findCommandSource(wrapper, idempotencyKey);
+        initialCommandSource.setResult(toApiJsonSerializer.serializeResult(result));
+        initialCommandSource.updateResourceId(result.getResourceId());
+        initialCommandSource.updateForAudit(result);
 
-        String changesOnlyJson;
         boolean rollBack = (rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker;
         if (result.hasChanges() && !rollBack) {
-            changesOnlyJson = toApiJsonSerializer.serializeResult(result.getChanges());
-            commandSourceResult.updateJsonTo(changesOnlyJson);
+            initialCommandSource.setCommandJson(toApiJsonSerializer.serializeResult(result.getChanges()));
         }
 
-        if (!result.hasChanges() && wrapper.isUpdateOperation() && !wrapper.isUpdateDatatable()) {
-            commandSourceResult.updateJsonTo(null);
-        }
-
-        if (commandSourceResult.hasJson()) {
-            commandSourceRepository.save(commandSourceResult);
-        }
+        initialCommandSource.setStatus(CommandProcessingResultType.PROCESSED.getValue());
+        commandSourceService.saveResult(initialCommandSource);
 
         if ((rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker) {
             /*
@@ -134,12 +126,12 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
              * transactionId, because as there are no entries are created with new transactionId, will throw an error
              * when checker approves the transaction
              */
-            commandSourceResult.updateTransaction(command.getTransactionId());
+            initialCommandSource.updateTransaction(command.getTransactionId());
             /*
              * Update CommandSource json data with JsonCommand json data, line 77 and 81 may update the json data
              */
-            commandSourceResult.updateJsonTo(command.json());
-            throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(commandSourceResult);
+            initialCommandSource.setCommandJson(command.json());
+            throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(initialCommandSource);
         }
         result.setRollbackTransaction(null);
 
@@ -148,15 +140,55 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
         return result;
     }
 
+    private void storeCommandToIdempotentFilter(CommandSource savedCommandSource) {
+        if (savedCommandSource.getId() == null) {
+            throw new IllegalStateException("Command source not saved");
+        }
+        saveCommandToRequest(savedCommandSource);
+    }
+
+    private static void saveCommandToRequest(CommandSource savedCommandSource) {
+        Optional.ofNullable(RequestContextHolder.getRequestAttributes()).ifPresent(requestAttributes -> requestAttributes
+                .setAttribute(COMMAND_SOURCE_ID, savedCommandSource.getId(), RequestAttributes.SCOPE_REQUEST));
+    }
+
+    private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
+        ErrorInfo ex = commandSourceService.generateErrorException(t);
+        publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, gson.toJson(ex));
+    }
+
+    private void exceptionWhenTheRequestAlreadyProcessed(CommandWrapper wrapper, String idempotencyKey) {
+        CommandSource existingCommand = commandSourceService.findCommandSource(wrapper, idempotencyKey);
+        if (existingCommand != null) {
+            idempotentExceptionByStatus(UNDER_PROCESSING, existingCommand,
+                    command -> new IdempotentCommandProcessUnderProcessingException(wrapper));
+            idempotentExceptionByStatus(ERROR, existingCommand, command -> new IdempotentCommandProcessFailedException(wrapper, command));
+            idempotentExceptionByStatus(PROCESSED, existingCommand,
+                    command -> new IdempotentCommandProcessSucceedException(wrapper, command.getResult()));
+        }
+    }
+
+    private void idempotentExceptionByStatus(CommandProcessingResultType status, CommandSource command,
+            Function<CommandSource, AbstractIdempotentCommandException> exceptionMapper) {
+        if (status.getValue().equals(command.getStatus())) {
+            throw exceptionMapper.apply(command);
+        }
+    }
+
+    private void setIdempotencyKeyStoreFlag(boolean flag) {
+        Optional.ofNullable(RequestContextHolder.getRequestAttributes()).ifPresent(
+                requestAttributes -> requestAttributes.setAttribute(IDEMPOTENCY_KEY_STORE_FLAG, flag, RequestAttributes.SCOPE_REQUEST));
+
+    }
+
     @Transactional
     @Override
     public CommandProcessingResult logCommand(CommandSource commandSourceResult) {
-
         commandSourceResult.markAsAwaitingApproval();
         if (commandSourceResult.getIdempotencyKey() == null) {
             commandSourceResult.setIdempotencyKey(idempotencyKeyGenerator.create());
         }
-        commandSourceResult = commandSourceRepository.saveAndFlush(commandSourceResult);
+        commandSourceResult = commandSourceService.saveResult(commandSourceResult);
 
         return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId())
                 .withEntityId(commandSourceResult.getResourceId()).build();
@@ -236,20 +268,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
         return rollbackTransaction;
     }
 
-    private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
-
-        ErrorInfo ex;
-        if (t instanceof final RuntimeException e) {
-            ex = ErrorHandler.handler(e);
-        } else {
-            ex = new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}");
-        }
-
-        publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, ex);
-    }
-
     private void publishHookEvent(final String entityName, final String actionName, JsonCommand command, final Object result) {
-        Gson gson = new Gson();
+
         try {
             final AppUser appUser = context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null));
 
@@ -301,5 +321,4 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
             log.error("Error", e);
         }
     }
-
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java
index 05b59f2bf..175fd8e37 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java
@@ -19,6 +19,7 @@
 
 package org.apache.fineract.infrastructure.core.config;
 
+import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter;
 import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter;
 import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter;
 import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter;
@@ -41,6 +42,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.crypto.factory.PasswordEncoderFactories;
 import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.access.ExceptionTranslationFilter;
 import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
@@ -67,6 +69,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
     @Autowired
     private ServerProperties serverProperties;
 
+    @Autowired
+    private IdempotencyStoreFilter idempotencyStoreFilter;
+
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http //
@@ -91,7 +96,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
                 .addFilterAfter(fineractInstanceModeApiFilter, SecurityContextPersistenceFilter.class) //
                 .addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) //
                 .addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class) //
-                .addFilterAfter(loanCOBApiFilter, InsecureTwoFactorAuthenticationFilter.class);
+                .addFilterAfter(loanCOBApiFilter, InsecureTwoFactorAuthenticationFilter.class)
+                .addFilterBefore(idempotencyStoreFilter, ExceptionTranslationFilter.class);
 
         if (serverProperties.getSsl().isEnabled()) {
             http.requiresChannel(channel -> channel.antMatchers("/api/**").requiresSecure());
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractIdempotentCommandException.java
similarity index 53%
copy from fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractIdempotentCommandException.java
index f25287e7f..50f7d6c50 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractIdempotentCommandException.java
@@ -16,20 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.commands.domain;
+package org.apache.fineract.infrastructure.core.exception;
 
 import lombok.Getter;
-import lombok.RequiredArgsConstructor;
 
-@Getter
-@RequiredArgsConstructor
-public enum CommandProcessingResultType {
+public abstract class AbstractIdempotentCommandException extends AbstractPlatformException {
 
-    INVALID(0, "commandProcessingResultType.invalid"), //
-    PROCESSED(1, "commandProcessingResultType.processed"), //
-    AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), //
-    REJECTED(3, "commandProcessingResultType.rejected");
+    public static final String IDEMPOTENT_CACHE_HEADER = "x-served-from-cache";
+    @Getter
+    private final String action;
 
-    private final Integer value;
-    private final String code;
+    @Getter
+    private final String entity;
+    @Getter
+    private final String idempotencyKey;
+    @Getter
+    private final String response;
+
+    protected AbstractIdempotentCommandException(String action, String entity, String idempotencyKey, String response) {
+        super(null, null);
+        this.action = action;
+        this.entity = entity;
+        this.idempotencyKey = idempotencyKey;
+        this.response = response;
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditSearchData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessFailedException.java
similarity index 51%
copy from fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditSearchData.java
copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessFailedException.java
index 35fe02100..16af11090 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditSearchData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessFailedException.java
@@ -16,23 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.commands.data;
+package org.apache.fineract.infrastructure.core.exception;
 
-import java.util.Collection;
-import java.util.List;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.apache.fineract.useradministration.data.AppUserData;
+import org.apache.fineract.commands.domain.CommandSource;
+import org.apache.fineract.commands.domain.CommandWrapper;
 
 /**
- * Immutable data object representing audit search results.
+ * Exception thrown when command is sent with same action, entity and idempotency key
  */
-@RequiredArgsConstructor
-@Getter
-public final class AuditSearchData {
+public class IdempotentCommandProcessFailedException extends AbstractIdempotentCommandException {
 
-    private final Collection<AppUserData> appUsers;
-    private final List<String> actionNames;
-    private final List<String> entityNames;
-    private final Collection<ProcessingResultLookup> processingResults;
+    private final Integer statusCode;
+
+    public IdempotentCommandProcessFailedException(CommandWrapper wrapper, CommandSource commandSource) {
+        super(wrapper.actionName(), wrapper.entityName(), wrapper.getIdempotencyKey(), commandSource.getResult());
+        this.statusCode = commandSource.getResultStatusCode();
+    }
+
+    public Integer getStatusCode() {
+        return statusCode;
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessSucceedException.java
similarity index 61%
copy from fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessSucceedException.java
index f25287e7f..5b84ad550 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessSucceedException.java
@@ -16,20 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.commands.domain;
+package org.apache.fineract.infrastructure.core.exception;
 
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.domain.CommandWrapper;
 
-@Getter
-@RequiredArgsConstructor
-public enum CommandProcessingResultType {
-
-    INVALID(0, "commandProcessingResultType.invalid"), //
-    PROCESSED(1, "commandProcessingResultType.processed"), //
-    AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), //
-    REJECTED(3, "commandProcessingResultType.rejected");
+/**
+ * Exception thrown when command is sent with same action, entity and idempotency key
+ */
+public class IdempotentCommandProcessSucceedException extends AbstractIdempotentCommandException {
 
-    private final Integer value;
-    private final String code;
+    public IdempotentCommandProcessSucceedException(CommandWrapper wrapper, String response) {
+        super(wrapper.actionName(), wrapper.entityName(), wrapper.getIdempotencyKey(), response);
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessUnderProcessingException.java
similarity index 60%
copy from fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
copy to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessUnderProcessingException.java
index f25287e7f..4e6b3f611 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessUnderProcessingException.java
@@ -16,20 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.commands.domain;
+package org.apache.fineract.infrastructure.core.exception;
 
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.domain.CommandWrapper;
 
-@Getter
-@RequiredArgsConstructor
-public enum CommandProcessingResultType {
-
-    INVALID(0, "commandProcessingResultType.invalid"), //
-    PROCESSED(1, "commandProcessingResultType.processed"), //
-    AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), //
-    REJECTED(3, "commandProcessingResultType.rejected");
+/**
+ * Exception thrown when command is sent with same action, entity and idempotency key
+ */
+public class IdempotentCommandProcessUnderProcessingException extends AbstractIdempotentCommandException {
 
-    private final Integer value;
-    private final String code;
+    public IdempotentCommandProcessUnderProcessingException(CommandWrapper wrapper) {
+        super(wrapper.actionName(), wrapper.entityName(), wrapper.getIdempotencyKey(), wrapper.getJson());
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessFailedExceptionMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessFailedExceptionMapper.java
new file mode 100644
index 000000000..831191a23
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessFailedExceptionMapper.java
@@ -0,0 +1,43 @@
+/**
+ * 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.exceptionmapper;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
+import org.springframework.stereotype.Component;
+
+@Provider
+@Component
+@Slf4j
+public class IdempotentCommandProcessFailedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessFailedException> {
+
+    @Override
+    public Response toResponse(final IdempotentCommandProcessFailedException exception) {
+        log.debug("Idempotent processing failed request: {}", exception.getMessage());
+        Status statusCode = Status.fromStatusCode(exception.getStatusCode());
+        return Response.status(statusCode).entity(exception.getResponse())
+                .header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON).build();
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java
new file mode 100644
index 000000000..7d5494a90
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.exceptionmapper;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
+import org.springframework.stereotype.Component;
+
+@Provider
+@Component
+@Slf4j
+public class IdempotentCommandProcessSucceedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessSucceedException> {
+
+    @Override
+    public Response toResponse(final IdempotentCommandProcessSucceedException exception) {
+        log.debug("Idempotent processing success request: {}", exception.getMessage());
+        return Response.status(Status.OK).entity(exception.getResponse())
+                .header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON).build();
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java
new file mode 100644
index 000000000..579e7b3a5
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java
@@ -0,0 +1,44 @@
+/**
+ * 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.exceptionmapper;
+
+import static org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
+import org.springframework.stereotype.Component;
+
+@Provider
+@Component
+@Slf4j
+public class IdempotentCommandProcessUnderProcessingExceptionMapper
+        implements ExceptionMapper<IdempotentCommandProcessUnderProcessingException> {
+
+    @Override
+    public Response toResponse(final IdempotentCommandProcessUnderProcessingException exception) {
+        log.debug("Idempotent under processing request: {}", exception.getMessage());
+        return Response.status(Status.CONFLICT).entity(exception.getResponse()).header(IDEMPOTENT_CACHE_HEADER, "true")
+                .type(MediaType.APPLICATION_JSON).build();
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java
new file mode 100644
index 000000000..9a9a19c12
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java
@@ -0,0 +1,91 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.filters;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.apache.fineract.commands.domain.CommandSourceRepository;
+import org.apache.fineract.commands.service.CommandSourceService;
+import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingResponseWrapper;
+
+@RequiredArgsConstructor
+@Slf4j
+@Component
+public class IdempotencyStoreFilter extends OncePerRequestFilter {
+
+    private final CommandSourceRepository commandSourceRepository;
+    private final CommandSourceService commandSourceService;
+
+    @Override
+    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
+            @NotNull FilterChain filterChain) throws ServletException, IOException {
+        Mutable<ContentCachingResponseWrapper> wrapper = new MutableObject<>();
+        if (isAllowedContentTypeRequest(request)) {
+            wrapper.setValue(new ContentCachingResponseWrapper(response));
+        }
+
+        filterChain.doFilter(request, wrapper.getValue() != null ? wrapper.getValue() : response);
+        Optional<Long> commandId = getCommandId(request);
+        boolean isSuccessWithoutStored = isStoreIdempotencyKey(request) && commandId.isPresent() && isAllowedContentTypeResponse(response)
+                && wrapper.getValue() != null;
+        if (isSuccessWithoutStored) {
+            commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
+                commandSource.setResultStatusCode(response.getStatus());
+                commandSource.setResult(new String(wrapper.getValue().getContentAsByteArray(), StandardCharsets.UTF_8));
+                commandSourceService.saveResult(commandSource);
+            });
+        }
+        if (wrapper.getValue() != null) {
+            wrapper.getValue().copyBodyToResponse();
+        }
+    }
+
+    private boolean isAllowedContentTypeResponse(HttpServletResponse response) {
+        return Optional.ofNullable(response.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
+                .orElse(false);
+    }
+
+    private boolean isAllowedContentTypeRequest(HttpServletRequest request) {
+        return Optional.ofNullable(request.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
+                .orElse(false);
+    }
+
+    private boolean isStoreIdempotencyKey(HttpServletRequest request) {
+        return Optional.ofNullable(request.getAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_STORE_FLAG))
+                .filter(Boolean.class::isInstance).map(Boolean.class::cast).orElse(false);
+    }
+
+    private Optional<Long> getCommandId(HttpServletRequest request) {
+        return Optional.ofNullable(request.getAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID))
+                .filter(Long.class::isInstance).map(Long.class::cast);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
index c04d6321a..1355da65f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
@@ -29,6 +29,10 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
 
 public class ExtendedJpaTransactionManager extends JpaTransactionManager {
 
+    public ExtendedJpaTransactionManager() {
+        setValidateExistingTransaction(true);
+    }
+
     @Override
     protected void doBegin(Object transaction, TransactionDefinition definition) {
         super.doBegin(transaction, definition);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java
index 04be13dca..146f44e0c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/GoogleGsonSerializerHelper.java
@@ -98,7 +98,7 @@ public final class GoogleGsonSerializerHelper {
         return serializer.toJson(singleDataObject);
     }
 
-    public Gson createSimpleGson() {
+    public static Gson createSimpleGson() {
         return createGsonBuilder().create();
     }
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java
index 3f53eb3c6..1062c3c28 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/ScheduledJobRunnerConfig.java
@@ -41,7 +41,8 @@ public class ScheduledJobRunnerConfig {
     @Bean
     public PlatformTransactionManager transactionManager(ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
         ExtendedJpaTransactionManager transactionManager = new ExtendedJpaTransactionManager();
-        transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
+        transactionManager.setValidateExistingTransaction(true);
+        transactionManagerCustomizers.ifAvailable(customizers -> customizers.customize(transactionManager));
         return transactionManager;
     }
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
index df694fac9..26ddea7a8 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
@@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.delinquency.service;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
-import javax.transaction.Transactional;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -30,13 +29,14 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 @Slf4j
 @Service
 public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainService {
 
     @Override
-    @Transactional
+    @Transactional(readOnly = true)
     public CollectionData getOverdueCollectionData(final Loan loan) {
         final LocalDate businessDate = DateUtils.getBusinessLocalDate();
 
diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties
index db24d4f67..a50db382f 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -130,6 +130,7 @@ spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
 spring.datasource.hikari.connectionTimeout=${FINERACT_HIKARI_CONNECTION_TIMEOUT:20000}
 spring.datasource.hikari.connectionTestquery=${FINERACT_HIKARI_TEST_QUERY:SELECT 1}
 spring.datasource.hikari.autoCommit=${FINERACT_HIKARI_AUTO_COMMIT:true}
+spring.datasource.hikari.transactionIsolation=${FINERACT_HIKARI_TRANSACTION_ISOLATION:TRANSACTION_REPEATABLE_READ}
 spring.datasource.hikari.dataSourceProperties['cachePrepStmts']=${FINERACT_HIKARI_DS_PROPERTIES_CACHE_PREP_STMTS:true}
 spring.datasource.hikari.dataSourceProperties['prepStmtCacheSize']=${FINERACT_HIKARI_DS_PROPERTIES_PREP_STMT_CACHE_SIZE:250}
 spring.datasource.hikari.dataSourceProperties['prepStmtCacheSqlLimit']=${FINERACT_HIKARI_DS_PROPERTIES_PREP_STMT_CACHE_SQL_LIMIT:2048}
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 e6719b6aa..c521010ca 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
@@ -91,4 +91,7 @@
     <include file="parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml" relativeToChangelogFile="true"/>
     <include file="parts/0070_add_event_configuration_for_delinquency_range_change_event.xml" relativeToChangelogFile="true"/>
     <include file="parts/0071_add_external_id_support_for_loan_transaction.xml" relativeToChangelogFile="true"/>
+    <include file="parts/0072_add_result_and status_to_command_source.xml" relativeToChangelogFile="true" />
+    <include file="parts/0073_add_result_status_code_to_command_source.xml" relativeToChangelogFile="true" />
+
 </databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0072_add_result_and status_to_command_source.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0072_add_result_and status_to_command_source.xml
new file mode 100644
index 000000000..1b72369f7
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0072_add_result_and status_to_command_source.xml	
@@ -0,0 +1,45 @@
+<?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 id="1" author="fineract" context="postgresql">
+        <renameColumn newColumnName="status"
+                       oldColumnName="processing_result_enum"
+                       tableName="m_portfolio_command_source"/>
+    </changeSet>
+
+    <changeSet id="2" author="fineract" context="mysql">
+        <renameColumn newColumnName="status"
+                      oldColumnName="processing_result_enum"
+                      tableName="m_portfolio_command_source"
+                      columnDataType="smallint(6)" />
+    </changeSet>
+
+    <changeSet id="3" author="fineract">
+        <addColumn tableName="m_portfolio_command_source">
+            <column name="result" type="TEXT"/>
+        </addColumn>
+    </changeSet>
+
+</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0073_add_result_status_code_to_command_source.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0073_add_result_status_code_to_command_source.xml
new file mode 100644
index 000000000..8bfe9d955
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0073_add_result_status_code_to_command_source.xml
@@ -0,0 +1,38 @@
+<?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 id="1" author="fineract" context="postgresql">
+        <addColumn tableName="m_portfolio_command_source">
+            <column name="result_status_code" type="int"></column>
+        </addColumn>
+    </changeSet>
+
+    <changeSet id="2" author="fineract" context="mysql">
+        <addColumn tableName="m_portfolio_command_source">
+            <column name="result_status_code" type="int"></column>
+        </addColumn>
+    </changeSet>
+
+</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql b/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql
index 7ad6deb4c..e39c00140 100644
--- a/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql
+++ b/fineract-provider/src/main/resources/sql/migrations/sample_data/barebones_db.sql
@@ -3882,7 +3882,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   `product_id` BIGINT DEFAULT NULL,
   `transaction_id` varchar(100) DEFAULT NULL,
   PRIMARY KEY (`id`),
@@ -3892,7 +3892,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -5632,10 +5632,12 @@ INSERT INTO `r_enum_value` (`enum_name`, `enum_id`, `enum_message_property`, `en
     ('portfolio_account_type_enum', 2, 'SAVING', 'EXPENSE', 0),
     ('portfolio_account_type_enum', 3, 'PROVISIONING', 'PROVISIONING', 0),
     ('portfolio_account_type_enum', 4, 'SHARES', 'SHARES', 0),
-    ('processing_result_enum', 0, 'invalid', 'Invalid', 0),
-    ('processing_result_enum', 1, 'processed', 'Processed', 0),
-    ('processing_result_enum', 2, 'awaiting.approval', 'Awaiting Approval', 0),
-    ('processing_result_enum', 3, 'rejected', 'Rejected', 0),
+    ('status', 0, 'invalid', 'Invalid', 0),
+    ('status', 1, 'processed', 'Processed', 0),
+    ('status', 2, 'awaiting.approval', 'Awaiting Approval', 0),
+    ('status', 3, 'rejected', 'Rejected', 0),
+    ('status', 4, 'underProcessing', 'Under Processing', 0),
+    ('status', 5, 'error', 'Error', 0),
     ('repayment_period_frequency_enum', 0, 'Days', 'Days', 0),
     ('repayment_period_frequency_enum', 1, 'Weeks', 'Weeks', 0),
     ('repayment_period_frequency_enum', 2, 'Months', 'Months', 0),
diff --git a/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql b/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql
index 61e4fdf63..a5d1c7f29 100644
--- a/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql
+++ b/fineract-provider/src/main/resources/sql/migrations/sample_data/load_sample_data.sql
@@ -4054,7 +4054,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
   `made_on_date` datetime NOT NULL,
   `checker_id` BIGINT DEFAULT NULL,
   `checked_on_date` datetime DEFAULT NULL,
-  `processing_result_enum` SMALLINT NOT NULL,
+  `status` SMALLINT NOT NULL,
   `product_id` BIGINT DEFAULT NULL,
   `transaction_id` varchar(100) DEFAULT NULL,
   PRIMARY KEY (`id`),
@@ -4064,7 +4064,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
   KEY `entity_name` (`entity_name`,`resource_id`),
   KEY `made_on_date` (`made_on_date`),
   KEY `checked_on_date` (`checked_on_date`),
-  KEY `processing_result_enum` (`processing_result_enum`),
+  KEY `status` (`status`),
   KEY `office_id` (`office_id`),
   KEY `group_id` (`office_id`),
   KEY `client_id` (`office_id`),
@@ -4075,7 +4075,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
 
 -- Dumping data for table mifostenant-reference.m_portfolio_command_source: ~72 rows (approximately)
 /*!40000 ALTER TABLE `m_portfolio_command_source` DISABLE KEYS */;
-INSERT INTO `m_portfolio_command_source` (`id`, `action_name`, `entity_name`, `office_id`, `group_id`, `client_id`, `loan_id`, `savings_account_id`, `api_get_url`, `resource_id`, `subresource_id`, `command_as_json`, `maker_id`, `made_on_date`, `checker_id`, `checked_on_date`, `processing_result_enum`, `product_id`, `transaction_id`) VALUES
+INSERT INTO `m_portfolio_command_source` (`id`, `action_name`, `entity_name`, `office_id`, `group_id`, `client_id`, `loan_id`, `savings_account_id`, `api_get_url`, `resource_id`, `subresource_id`, `command_as_json`, `maker_id`, `made_on_date`, `checker_id`, `checked_on_date`, `status`, `product_id`, `transaction_id`) VALUES
     (1, 'CREATE', 'STAFF', 1, NULL, NULL, NULL, NULL, '/staff/template', 1, NULL, '{"isLoanOfficer":true,"officeId":1,"firstname":"Aliya","lastname":"A"}', 1, '2014-03-07 19:10:05', NULL, NULL, 1, NULL, NULL),
     (2, 'CREATE', 'USER', 1, NULL, NULL, NULL, NULL, '/users/template', 2, NULL, '{"sendPasswordToEmail":true,"officeId":1,"username":"adama","firstname":"Adam","lastname":"A","email":"adama@123.com","roles":["1"]}', 1, '2014-03-07 19:19:31', NULL, NULL, 1, NULL, NULL),
     (3, 'CREATE', 'CLIENT', 1, NULL, 1, NULL, NULL, '/clients/template', 1, NULL, '{"officeId":1,"staffId":1,"firstname":"Smith","lastname":"R","active":true,"locale":"en","dateFormat":"dd MMMM yyyy","activationDate":"07 March 2014","submittedOnDate":"01 January 2010","savingsProductId":null}', 1, '2014-03-07 19:23:36', NULL, NULL, 1, NULL, NULL),
@@ -5889,10 +5889,10 @@ INSERT INTO `r_enum_value` (`enum_name`, `enum_id`, `enum_message_property`, `en
     ('portfolio_account_type_enum', 2, 'SAVING', 'EXPENSE', 0),
     ('portfolio_account_type_enum', 3, 'PROVISIONING', 'PROVISIONING', 0),
     ('portfolio_account_type_enum', 4, 'SHARES', 'SHARES', 0),
-    ('processing_result_enum', 0, 'invalid', 'Invalid', 0),
-    ('processing_result_enum', 1, 'processed', 'Processed', 0),
-    ('processing_result_enum', 2, 'awaiting.approval', 'Awaiting Approval', 0),
-    ('processing_result_enum', 3, 'rejected', 'Rejected', 0),
+    ('status', 0, 'invalid', 'Invalid', 0),
+    ('status', 1, 'processed', 'Processed', 0),
+    ('status', 2, 'awaiting.approval', 'Awaiting Approval', 0),
+    ('status', 3, 'rejected', 'Rejected', 0),
     ('repayment_period_frequency_enum', 0, 'Days', 'Days', 0),
     ('repayment_period_frequency_enum', 1, 'Weeks', 'Weeks', 0),
     ('repayment_period_frequency_enum', 2, 'Months', 'Months', 0),
diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java
new file mode 100644
index 000000000..b28cc7de7
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.commands.service;
+
+import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
+
+import java.time.ZoneId;
+import java.util.Optional;
+import org.apache.fineract.batch.exception.ErrorInfo;
+import org.apache.fineract.commands.domain.CommandSource;
+import org.apache.fineract.commands.domain.CommandSourceRepository;
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.infrastructure.codes.exception.CodeNotFoundException;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+public class CommandSourceServiceTest {
+
+    @Mock
+    private CommandSourceRepository commandSourceRepository;
+
+    @InjectMocks
+    private CommandSourceService underTest;
+
+    @BeforeEach
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        ThreadLocalContextUtil.reset();
+    }
+
+    @Test
+    public void testCreateFromWrapper() {
+        CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
+        JsonCommand jsonCommand = JsonCommand.from("{}");
+        AppUser appUser = Mockito.mock(AppUser.class);
+
+        FineractPlatformTenant ft = new FineractPlatformTenant(1L, "t1", "n1", ZoneId.systemDefault().toString(), null);
+        ThreadLocalContextUtil.setTenant(ft);
+
+        String idk = "idk";
+        underTest.saveInitial(wrapper, jsonCommand, appUser, idk);
+
+        ArgumentCaptor<CommandSource> commandSourceArgumentCaptor = ArgumentCaptor.forClass(CommandSource.class);
+        Mockito.verify(commandSourceRepository).saveAndFlush(commandSourceArgumentCaptor.capture());
+
+        CommandSource captured = commandSourceArgumentCaptor.getValue();
+        Assertions.assertEquals(idk, captured.getIdempotencyKey());
+        Assertions.assertEquals(UNDER_PROCESSING.getValue(), captured.getStatus());
+    }
+
+    @Test
+    public void testCreateFromExisting() {
+        CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
+        long commandId = 1L;
+        JsonCommand jsonCommand = JsonCommand.fromExistingCommand(commandId, "", null, null, null, 1L, null, null, null, null, null, null,
+                null, null, null, null, null);
+        CommandSource commandMock = Mockito.mock(CommandSource.class);
+        Mockito.when(commandSourceRepository.saveAndFlush(commandMock)).thenReturn(commandMock);
+        Mockito.when(commandSourceRepository.findById(commandId)).thenReturn(Optional.of(commandMock));
+        AppUser appUser = Mockito.mock(AppUser.class);
+
+        ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "t1", "n1", ZoneId.systemDefault().toString(), null));
+
+        CommandSource actual = underTest.saveInitial(wrapper, jsonCommand, appUser, "idk");
+
+        ArgumentCaptor<CommandSource> commandSourceArgumentCaptor = ArgumentCaptor.forClass(CommandSource.class);
+        Mockito.verify(commandSourceRepository).saveAndFlush(commandSourceArgumentCaptor.capture());
+
+        CommandSource captured = commandSourceArgumentCaptor.getValue();
+        Assertions.assertEquals(actual, captured);
+    }
+
+    @Test
+    public void testGenerateErrorException() {
+        ErrorInfo result = underTest.generateErrorException(new CodeNotFoundException("foo"));
+        Assertions.assertEquals(404, result.getStatusCode());
+        Assertions.assertEquals(1001, result.getErrorCode());
+        Assertions.assertTrue(result.getMessage().contains("Code with name `foo` does not exist"));
+    }
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java
new file mode 100644
index 000000000..cc9acc7e3
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java
@@ -0,0 +1,85 @@
+/**
+ * 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.commands.service;
+
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+public class IdempotencyKeyResolverTest {
+
+    @Mock
+    private IdempotencyKeyGenerator idempotencyKeyGenerator;
+
+    @Mock
+    private FineractProperties fineractProperties;
+
+    @InjectMocks
+    private IdempotencyKeyResolver underTest;
+
+    @BeforeEach
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+    }
+
+    @Test
+    public void testIPKResolveFromRequest() {
+        String idkh = "foo";
+        String idk = "bar";
+        Mockito.when(fineractProperties.getIdempotencyKeyHeaderName()).thenReturn(idkh);
+
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader(idkh, idk);
+        RequestAttributes requestAttributes = new ServletRequestAttributes(request);
+        RequestContextHolder.setRequestAttributes(requestAttributes);
+        CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
+        String resolvedIdk = underTest.resolve(wrapper);
+        Assertions.assertEquals(idk, resolvedIdk);
+    }
+
+    @Test
+    public void testIPKResolveFromGenerate() {
+        String idk = "idk";
+        Mockito.when(idempotencyKeyGenerator.create()).thenReturn(idk);
+        RequestContextHolder.setRequestAttributes(null);
+        CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
+        String resolvedIdk = underTest.resolve(wrapper);
+        Assertions.assertEquals(idk, resolvedIdk);
+    }
+
+    @Test
+    public void testIPKResolveFromWrapper() {
+        RequestContextHolder.setRequestAttributes(null);
+        String idk = "idk";
+        CommandWrapper wrapper = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+                null, null, null, idk);
+        String resolvedIdk = underTest.resolve(wrapper);
+        Assertions.assertEquals(idk, resolvedIdk);
+    }
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java
new file mode 100644
index 000000000..66a00ec9e
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java
@@ -0,0 +1,153 @@
+/**
+ * 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.commands.service;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.fineract.commands.domain.CommandProcessingResultType;
+import org.apache.fineract.commands.domain.CommandSource;
+import org.apache.fineract.commands.domain.CommandWrapper;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.commands.provider.CommandHandlerProvider;
+import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.springframework.context.ApplicationContext;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+public class SynchronousCommandProcessingServiceTest {
+
+    @Mock
+    private PlatformSecurityContext context;
+    @Mock
+    private ApplicationContext applicationContext;
+    @Mock
+    private ToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
+    @Mock
+    private ToApiJsonSerializer<CommandProcessingResult> toApiResultJsonSerializer;
+    @Mock
+    private ConfigurationDomainService configurationDomainService;
+    @Mock
+    private CommandHandlerProvider commandHandlerProvider;
+    @Mock
+    private IdempotencyKeyResolver idempotencyKeyResolver;
+    @Mock
+    private IdempotencyKeyGenerator idempotencyKeyGenerator;
+    @Mock
+    private CommandSourceService commandSourceService;
+
+    @InjectMocks
+    private SynchronousCommandProcessingService underTest;
+
+    @Mock
+    private HttpServletRequest request;
+
+    @BeforeEach
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
+    }
+
+    @Test
+    public void testExecuteCommandSuccess() {
+
+        CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class);
+        when(commandWrapper.isDatatableResource()).thenReturn(false);
+        when(commandWrapper.isNoteResource()).thenReturn(false);
+        when(commandWrapper.isSurveyResource()).thenReturn(false);
+        when(commandWrapper.isLoanDisburseDetailResource()).thenReturn(false);
+        JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
+
+        NewCommandSourceHandler newCommandSourceHandler = Mockito.mock(NewCommandSourceHandler.class);
+        CommandProcessingResult commandProcessingResult = Mockito.mock(CommandProcessingResult.class);
+        when(commandProcessingResult.isRollbackTransaction()).thenReturn(false);
+        when(newCommandSourceHandler.processCommand(jsonCommand)).thenReturn(commandProcessingResult);
+        when(commandHandlerProvider.getHandler(Mockito.any(), Mockito.any())).thenReturn(newCommandSourceHandler);
+
+        when(configurationDomainService.isMakerCheckerEnabledForTask(Mockito.any())).thenReturn(false);
+        String idk = "idk";
+        when(idempotencyKeyResolver.resolve(commandWrapper)).thenReturn(idk);
+        CommandSource commandSource = Mockito.mock(CommandSource.class);
+        when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(null).thenReturn(commandSource);
+
+        AppUser appUser = Mockito.mock(AppUser.class);
+        when(commandSourceService.saveInitial(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
+        when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
+
+        CommandProcessingResult actualCommandProcessingResult = underTest.executeCommand(commandWrapper, jsonCommand, false);
+
+        verify(commandSourceService).saveInitial(commandWrapper, jsonCommand, appUser, idk);
+        verify(commandSource).setStatus(CommandProcessingResultType.PROCESSED.getValue());
+        verify(commandSourceService).saveResult(commandSource);
+
+        Assertions.assertEquals(commandProcessingResult, actualCommandProcessingResult);
+    }
+
+    @Test
+    public void testExecuteCommandFails() {
+        CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class);
+        when(commandWrapper.isDatatableResource()).thenReturn(false);
+        when(commandWrapper.isNoteResource()).thenReturn(false);
+        when(commandWrapper.isSurveyResource()).thenReturn(false);
+        when(commandWrapper.isLoanDisburseDetailResource()).thenReturn(false);
+        JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
+
+        NewCommandSourceHandler newCommandSourceHandler = Mockito.mock(NewCommandSourceHandler.class);
+        CommandProcessingResult commandProcessingResult = Mockito.mock(CommandProcessingResult.class);
+        CommandSource commandSource = Mockito.mock(CommandSource.class);
+        when(commandProcessingResult.isRollbackTransaction()).thenReturn(false);
+        RuntimeException runtimeException = new RuntimeException("foo");
+        when(newCommandSourceHandler.processCommand(jsonCommand)).thenThrow(runtimeException);
+        when(commandHandlerProvider.getHandler(Mockito.any(), Mockito.any())).thenReturn(newCommandSourceHandler);
+
+        when(configurationDomainService.isMakerCheckerEnabledForTask(Mockito.any())).thenReturn(false);
+        String idk = "idk";
+        when(idempotencyKeyResolver.resolve(commandWrapper)).thenReturn(idk);
+        when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(null);
+
+        AppUser appUser = Mockito.mock(AppUser.class);
+        when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
+        when(commandSourceService.saveInitial(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
+
+        CommandSource initialCommandSource = Mockito.mock(CommandSource.class);
+
+        when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(initialCommandSource);
+
+        Assertions.assertThrows(RuntimeException.class, () -> {
+            underTest.executeCommand(commandWrapper, jsonCommand, false);
+        });
+
+        verify(commandSourceService).saveInitial(commandWrapper, jsonCommand, appUser, idk);
+        verify(commandSourceService).generateErrorException(runtimeException);
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/IdempotencyTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/IdempotencyTest.java
new file mode 100644
index 000000000..f550fdb3a
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/IdempotencyTest.java
@@ -0,0 +1,158 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.response.Response;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import org.apache.fineract.cob.data.BusinessStep;
+import org.apache.fineract.cob.data.JobBusinessStepConfigData;
+import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
+import org.apache.fineract.integrationtests.common.IdempotencyHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class IdempotencyTest {
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    public static final String LOAN_JOB_NAME = "LOAN_CLOSE_OF_BUSINESS";
+    public static final String LOAN_CATEGORY_NAME = "loan";
+    public static final String APPLY_CHARGE_TO_OVERDUE_LOANS = "APPLY_CHARGE_TO_OVERDUE_LOANS";
+    public static final String NOT_BELONGING_BUSINESS_STEP_NAME = "APPLY_CHARGE_TO_OVERDUE_LOANS_2";
+    public static final String LOAN_DELINQUENCY_CLASSIFICATION = "LOAN_DELINQUENCY_CLASSIFICATION";
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+    }
+
+    @Test
+    public void shouldUpdateStepOrder() {
+        JobBusinessStepConfigData originalStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec,
+                LOAN_JOB_NAME);
+
+        String idempotencyKeyHeader = UUID.randomUUID().toString();
+
+        List<BusinessStep> requestBody = new ArrayList<>();
+        requestBody.add(getBusinessSteps(1L, APPLY_CHARGE_TO_OVERDUE_LOANS));
+        Response response = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
+        Response responseSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
+        Assertions.assertEquals(response.getBody().asString(), responseSecond.getBody().asString());
+        Assertions.assertNull(response.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertNotNull(responseSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+
+        idempotencyKeyHeader = UUID.randomUUID().toString();
+
+        JobBusinessStepConfigData newStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec,
+                LOAN_JOB_NAME);
+        BusinessStep applyChargeStep = newStepConfig.getBusinessSteps().stream()
+                .filter(businessStep -> APPLY_CHARGE_TO_OVERDUE_LOANS.equals(businessStep.getStepName())).findFirst().get();
+        assertEquals(1, newStepConfig.getBusinessSteps().size());
+        assertEquals(1L, applyChargeStep.getOrder());
+
+        requestBody.add(getBusinessSteps(2L, LOAN_DELINQUENCY_CLASSIFICATION));
+
+        Response update = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
+        Response updateSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
+        Assertions.assertNull(update.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertNotNull(updateSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertEquals(update.getBody().asString(), updateSecond.getBody().asString());
+
+        newStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec, LOAN_JOB_NAME);
+        applyChargeStep = newStepConfig.getBusinessSteps().stream()
+                .filter(businessStep -> APPLY_CHARGE_TO_OVERDUE_LOANS.equals(businessStep.getStepName())).findFirst().get();
+        BusinessStep loanDelinquencyStep = newStepConfig.getBusinessSteps().stream()
+                .filter(businessStep -> LOAN_DELINQUENCY_CLASSIFICATION.equals(businessStep.getStepName())).findFirst().get();
+        assertEquals(2, newStepConfig.getBusinessSteps().size());
+        assertEquals(1L, applyChargeStep.getOrder());
+        assertEquals(2L, loanDelinquencyStep.getOrder());
+
+        requestBody.remove(1);
+        idempotencyKeyHeader = UUID.randomUUID().toString();
+        update = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
+        updateSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
+
+        Assertions.assertNull(update.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertNotNull(updateSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertEquals(update.getBody().asString(), updateSecond.getBody().asString());
+
+        newStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec, LOAN_JOB_NAME);
+        applyChargeStep = newStepConfig.getBusinessSteps().stream()
+                .filter(businessStep -> APPLY_CHARGE_TO_OVERDUE_LOANS.equals(businessStep.getStepName())).findFirst().get();
+        assertEquals(1, newStepConfig.getBusinessSteps().size());
+        assertEquals(1L, applyChargeStep.getOrder());
+
+        idempotencyKeyHeader = UUID.randomUUID().toString();
+
+        update = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(originalStepConfig.getBusinessSteps()), idempotencyKeyHeader);
+        updateSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(originalStepConfig.getBusinessSteps()), idempotencyKeyHeader);
+
+        Assertions.assertNull(update.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertNotNull(updateSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertEquals(update.getBody().asString(), updateSecond.getBody().asString());
+
+    }
+
+    @Test
+    public void shoudTheSecondRequestWithSameIdempotencyKeyWillFailureToo() {
+        ResponseSpecification responseSpecForError = new ResponseSpecBuilder().expectStatusCode(400).build();
+        List<BusinessStep> requestBody = new ArrayList<>();
+        String idempotencyKey = UUID.randomUUID().toString();
+        // IdempotencyHelper.configuredApiParameterErrorFromJsonString(response.getBody().asString())
+
+        Response response1 = IdempotencyHelper.updateBusinessStepOrderWithError(requestSpec, responseSpecForError, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKey);
+        Assertions.assertNull(response1.getHeader(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        String originalBody = response1.getBody().asString();
+
+        Response response2 = IdempotencyHelper.updateBusinessStepOrderWithError(requestSpec, responseSpecForError, LOAN_JOB_NAME,
+                IdempotencyHelper.toJsonString(requestBody), idempotencyKey);
+        Assertions.assertNotNull(response2.getHeader(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
+        Assertions.assertEquals(originalBody, response2.getBody().asString());
+    }
+
+    private BusinessStep getBusinessSteps(Long order, String stepName) {
+        BusinessStep businessStep = new BusinessStep();
+        businessStep.setStepName(stepName);
+        businessStep.setOrder(order);
+        return businessStep;
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/IdempotencyHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/IdempotencyHelper.java
new file mode 100644
index 000000000..b10eb5d56
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/IdempotencyHelper.java
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.common;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import io.restassured.response.Response;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.data.BusinessStep;
+import org.apache.fineract.cob.data.JobBusinessStepConfigData;
+import org.apache.fineract.cob.data.JobBusinessStepDetail;
+
+@Slf4j
+public final class IdempotencyHelper {
+
+    private static final String BUSINESS_STEPS_API_URL_START = "/fineract-provider/api/v1/jobs/";
+    private static final String BUSINESS_STEPS_API_URL_END = "/steps?" + Utils.TENANT_IDENTIFIER;
+    private static final String GET_AVAILABLE_BUSINESS_STEPS_API_URL_END = "/available-steps?" + Utils.TENANT_IDENTIFIER;
+
+    private IdempotencyHelper() {
+
+    }
+
+    public static String toJsonString(final List<BusinessStep> batchRequests) {
+        return new Gson().toJson(new BusinessStepWrapper(batchRequests));
+    }
+
+    public static JobBusinessStepConfigData configuredBusinessStepFromJsonString(final String json) {
+        return new Gson().fromJson(json, new TypeToken<JobBusinessStepConfigData>() {}.getType());
+    }
+
+    private static JobBusinessStepDetail availableBusinessStepFromJsonString(final String json) {
+        return new Gson().fromJson(json, new TypeToken<JobBusinessStepDetail>() {}.getType());
+    }
+
+    public static JobBusinessStepConfigData getConfiguredBusinessStepsByJobName(final RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec, String jobName) {
+        final String response = Utils.performServerGet(requestSpec, responseSpec,
+                BUSINESS_STEPS_API_URL_START + jobName + BUSINESS_STEPS_API_URL_END);
+        log.info("BusinessStepConfigurationHelper Response: {}", response);
+        return configuredBusinessStepFromJsonString(response);
+    }
+
+    public static JobBusinessStepDetail getAvailableBusinessStepsByJobName(final RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec, String jobName) {
+        final String response = Utils.performServerGet(requestSpec, responseSpec,
+                BUSINESS_STEPS_API_URL_START + jobName + GET_AVAILABLE_BUSINESS_STEPS_API_URL_END);
+        log.info("BusinessStepConfigurationHelper Response: {}", response);
+        return availableBusinessStepFromJsonString(response);
+    }
+
+    public static Response updateBusinessStepOrder(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
+            String jobName, String jsonBodyToSend, String idempotencyKey) {
+        Response response = Utils.performServerPutRaw(requestSpec, responseSpec,
+                BUSINESS_STEPS_API_URL_START + jobName + BUSINESS_STEPS_API_URL_END,
+                request -> request.header("Idempotency-Key", idempotencyKey).body(jsonBodyToSend));
+        log.info("BusinessStepConfigurationHelper Response: {}", response.getBody().asString());
+        return response;
+    }
+
+    public static Response updateBusinessStepOrderWithError(final RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec, String jobName, String jsonBodyToSend, String idempotencyKey) {
+        String url = BUSINESS_STEPS_API_URL_START + jobName + BUSINESS_STEPS_API_URL_END;
+        return Utils.performServerPutRaw(requestSpec, responseSpec, url,
+                request -> request.header("Idempotency-Key", idempotencyKey).body(jsonBodyToSend));
+    }
+
+    private static final class BusinessStepWrapper {
+
+        private List<BusinessStep> businessSteps;
+
+        private BusinessStepWrapper(List<BusinessStep> businessSteps) {
+            this.businessSteps = businessSteps;
+        }
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
index 6ba031238..858d13db3 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
@@ -53,6 +53,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Random;
 import java.util.TimeZone;
+import java.util.function.Function;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.http.conn.HttpHostConnectException;
 import org.slf4j.Logger;
@@ -164,6 +165,11 @@ public final class Utils {
         return performServerGet(requestSpec, responseSpec, url, null);
     }
 
+    public static Response performServerGetRaw(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
+            final String getURL, Function<RequestSpecification, RequestSpecification> requestMapper) {
+        return requestMapper.apply(given().spec(requestSpec)).expect().spec(responseSpec).log().ifError().when().get(getURL).andReturn();
+    }
+
     public static <T> T performServerGet(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
             final String getURL, final String jsonAttributeToGetBack) {
         final String json = given().spec(requestSpec).expect().spec(responseSpec).log().ifError().when().get(getURL).andReturn().asString();
@@ -212,6 +218,11 @@ public final class Utils {
         return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
     }
 
+    public static Response performServerPutRaw(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
+            final String putURL, Function<RequestSpecification, RequestSpecification> bodyMapper) {
+        return bodyMapper.apply(given().spec(requestSpec)).expect().spec(responseSpec).log().ifError().when().put(putURL).andReturn();
+    }
+
     public static String performServerPut(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
             final String putURL, final String jsonBodyToSend) {
         return performServerPut(requestSpec, responseSpec, putURL, jsonBodyToSend, null);