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);