From 5210bf741263db0bcfe022edd2d905007431c797 Mon Sep 17 00:00:00 2001 From: Janos Haber Date: Fri, 25 Nov 2022 00:09:59 +0100 Subject: [PATCH] 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 +- ...neLoanCOBBuildExecutionContextTasklet.java | 12 +- .../InlineLoanCOBExecutorServiceImpl.java | 10 +- .../commands/api/AuditsApiResource.java | 16 +- .../commands/data/AuditSearchData.java | 2 +- .../domain/CommandProcessingResultType.java | 4 +- .../commands/domain/CommandSource.java | 70 +++++--- .../domain/CommandSourceRepository.java | 4 +- .../handler/NewCommandSourceHandler.java | 4 + .../service/AuditReadPlatformServiceImpl.java | 10 +- .../service/CommandSourceService.java | 96 +++++++++++ .../service/IdempotencyKeyResolver.java | 49 ++++++ ...CommandSourceWritePlatformServiceImpl.java | 7 +- .../SynchronousCommandProcessingService.java | 147 +++++++++------- .../core/config/SecurityConfig.java | 8 +- .../AbstractIdempotentCommandException.java | 43 +++++ ...empotentCommandProcessFailedException.java | 39 +++++ ...mpotentCommandProcessSucceedException.java | 31 ++++ ...ommandProcessUnderProcessingException.java | 31 ++++ ...ntCommandProcessFailedExceptionMapper.java | 43 +++++ ...tCommandProcessSucceedExceptionMapper.java | 42 +++++ ...ProcessUnderProcessingExceptionMapper.java | 44 +++++ .../core/filters/IdempotencyStoreFilter.java | 91 ++++++++++ .../ExtendedJpaTransactionManager.java | 4 + .../GoogleGsonSerializerHelper.java | 2 +- .../jobs/ScheduledJobRunnerConfig.java | 3 +- .../LoanDelinquencyDomainServiceImpl.java | 4 +- .../src/main/resources/application.properties | 1 + .../db/changelog/tenant/changelog-tenant.xml | 3 + ...dd_result_and status_to_command_source.xml | 45 +++++ ...d_result_status_code_to_command_source.xml | 38 +++++ .../migrations/sample_data/barebones_db.sql | 14 +- .../sample_data/load_sample_data.sql | 14 +- .../service/CommandSourceServiceTest.java | 111 ++++++++++++ .../service/IdempotencyKeyResolverTest.java | 85 ++++++++++ ...nchronousCommandProcessingServiceTest.java | 153 +++++++++++++++++ .../integrationtests/IdempotencyTest.java | 158 ++++++++++++++++++ .../common/IdempotencyHelper.java | 95 +++++++++++ .../integrationtests/common/Utils.java | 11 ++ 46 files changed, 1428 insertions(+), 166 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractIdempotentCommandException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessFailedException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessSucceedException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessUnderProcessingException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessFailedExceptionMapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0072_add_result_and status_to_command_source.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0073_add_result_status_code_to_command_source.xml create mode 100644 fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/commands/service/IdempotencyKeyResolverTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/IdempotencyTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/IdempotencyHelper.java 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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); +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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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)'),('status',0,'invalid','Invalid'),('status',1,'processed','Processed'),('status',2,'awaiting.approval','Awaiting Approval'),('status',3,'rejected','Rejected'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); /*!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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); +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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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)'),('status',0,'invalid','Invalid'),('status',1,'processed','Processed'),('status',2,'awaiting.approval','Awaiting Approval'),('status',3,'rejected','Rejected'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); /*!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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); +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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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)'),('status',0,'invalid','Invalid'),('status',1,'processed','Processed'),('status',2,'awaiting.approval','Awaiting Approval'),('status',3,'rejected','Rejected'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); /*!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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); +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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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)'),('status',0,'invalid','Invalid'),('status',1,'processed','Processed'),('status',2,'awaiting.approval','Awaiting Approval'),('status',3,'rejected','Rejected'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); /*!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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); +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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',601,'Written-Off','Written-Off'),('loan_status_id',602,'Rescheduled','Rescheduled'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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)'),('status',0,'invalid','Invalid'),('status',1,'processed','Processed'),('status',2,'awaiting.approval','Awaiting Approval'),('status',3,'rejected','Rejected'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'),('transaction_type_enum',1,'Disbursement','Disbursement'),('transaction_type_enum',2,'Repayment','Repayment'),('transaction_type_enum',3,'Contra','Contra'),('transaction_type_enum',4,'Waive Interest','Waive Interest'),('transaction_type_enum',5,'Repayment At Disbursement','Repayment At Disbursement'),('transaction_type_enum',6,'Write-Off','Write-Off'),('transaction_type_enum',7,'Marked for Rescheduling','Marked for Rescheduling'),('transaction_type_enum',8,'Recovery Repayment','Recovery Repayment'),('transaction_type_enum',9,'Waive Charges','Waive Charges'),('transaction_type_enum',10,'Apply Charges','Apply Charges'),('transaction_type_enum',11,'Apply Interest','Apply Interest'); /*!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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'); +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 month','Per month'),('interest_period_frequency_enum',3,'Per year','Per year'),('loan_status_id',100,'Submitted and awaiting approval','Submitted and awaiting approval'),('loan_status_id',200,'Approved','Approved'),('loan_status_id',300,'Active','Active'),('loan_status_id',400,'Withdrawn by client','Withdrawn by client'),('loan_status_id',500,'Rejected','Rejected'),('loan_status_id',600,'Closed','Closed'),('loan_status_id',700,'Overpaid','Overpaid'),('loan_transaction_strategy_id',1,'mifos-standard-strategy','Mifos style'),('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)'),('status',0,'invalid','Invalid'),('status',1,'processed','Processed'),('status',2,'awaiting.approval','Awaiting Approval'),('status',3,'rejected','Rejected'),('repayment_period_frequency_enum',0,'Days','Days'),('repayment_period_frequency_enum',1,'Weeks','Weeks'),('repayment_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',0,'Days','Days'),('term_period_frequency_enum',1,'Weeks','Weeks'),('term_period_frequency_enum',2,'Months','Months'),('term_period_frequency_enum',3,'Years','Years'); /*!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, InitializingBean { +public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService { private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: "; @@ -80,12 +79,7 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService 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 appUsers; private final List actionNames; private final List entityNames; - private final Collection processingResults; + private final Collection 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, JpaSpecificationExecutor { - // 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 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 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> toApiJsonSerializer; private final ToApiJsonSerializer 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); - final NewCommandSourceHandler handler = findCommandHandler(wrapper); + // Store idempotency key to the request attribute + + 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 initialCommandSource = commandSourceService.findCommandSource(wrapper, idempotencyKey); + initialCommandSource.setResult(toApiJsonSerializer.serializeResult(result)); + initialCommandSource.updateResourceId(result.getResourceId()); + initialCommandSource.updateForAudit(result); - 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); - - 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 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/infrastructure/core/exception/AbstractIdempotentCommandException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractIdempotentCommandException.java new file mode 100644 index 000000000..50f7d6c50 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractIdempotentCommandException.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.exception; + +import lombok.Getter; + +public abstract class AbstractIdempotentCommandException extends AbstractPlatformException { + + public static final String IDEMPOTENT_CACHE_HEADER = "x-served-from-cache"; + @Getter + private final String action; + + @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/infrastructure/core/exception/IdempotentCommandProcessFailedException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessFailedException.java new file mode 100644 index 000000000..16af11090 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessFailedException.java @@ -0,0 +1,39 @@ +/** + * 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.exception; + +import org.apache.fineract.commands.domain.CommandSource; +import org.apache.fineract.commands.domain.CommandWrapper; + +/** + * Exception thrown when command is sent with same action, entity and idempotency key + */ +public class IdempotentCommandProcessFailedException extends AbstractIdempotentCommandException { + + 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/infrastructure/core/exception/IdempotentCommandProcessSucceedException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessSucceedException.java new file mode 100644 index 000000000..5b84ad550 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessSucceedException.java @@ -0,0 +1,31 @@ +/** + * 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.exception; + +import org.apache.fineract.commands.domain.CommandWrapper; + +/** + * Exception thrown when command is sent with same action, entity and idempotency key + */ +public class IdempotentCommandProcessSucceedException extends AbstractIdempotentCommandException { + + 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/infrastructure/core/exception/IdempotentCommandProcessUnderProcessingException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessUnderProcessingException.java new file mode 100644 index 000000000..4e6b3f611 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/exception/IdempotentCommandProcessUnderProcessingException.java @@ -0,0 +1,31 @@ +/** + * 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.exception; + +import org.apache.fineract.commands.domain.CommandWrapper; + +/** + * Exception thrown when command is sent with same action, entity and idempotency key + */ +public class IdempotentCommandProcessUnderProcessingException extends AbstractIdempotentCommandException { + + 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 { + + @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 { + + @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 { + + @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 wrapper = new MutableObject<>(); + if (isAllowedContentTypeRequest(request)) { + wrapper.setValue(new ContentCachingResponseWrapper(response)); + } + + filterChain.doFilter(request, wrapper.getValue() != null ? wrapper.getValue() : response); + Optional 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 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) { 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 @@ + + + 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 @@ + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + 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 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 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> toApiJsonSerializer; + @Mock + private ToApiJsonSerializer 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 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 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 batchRequests) { + return new Gson().toJson(new BusinessStepWrapper(batchRequests)); + } + + public static JobBusinessStepConfigData configuredBusinessStepFromJsonString(final String json) { + return new Gson().fromJson(json, new TypeToken() {}.getType()); + } + + private static JobBusinessStepDetail availableBusinessStepFromJsonString(final String json) { + return new Gson().fromJson(json, new TypeToken() {}.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 businessSteps; + + private BusinessStepWrapper(List 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 requestMapper) { + return requestMapper.apply(given().spec(requestSpec)).expect().spec(responseSpec).log().ifError().when().get(getURL).andReturn(); + } + public static 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 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);