FINERACT-1744 - Idempotency support

- [x] Idempotency write move to filters
- [x] Idempotency integration test with success and failure
- [x] Added response status code to database
- [x] SynchronousCommandProcessingService not generate the command, just marked to the filter
- [x] IdempotencyStoreFilter store the command request if available.
- [x] Removed JpaExceptionHandler.java
This commit is contained in:
Janos Haber
2022-11-28 12:04:03 +01:00
committed by Arnold Gálovics
parent 25c0b6a08d
commit 5210bf7412
46 changed files with 1428 additions and 166 deletions
@@ -1447,7 +1447,7 @@ CREATE TABLE `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`), KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_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 `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_id` (`office_id`), KEY `client_id` (`office_id`),
@@ -1762,7 +1762,7 @@ CREATE TABLE `r_enum_value` (
LOCK TABLES `r_enum_value` WRITE; LOCK TABLES `r_enum_value` WRITE;
/*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */; /*!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 */; /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -1448,7 +1448,7 @@ CREATE TABLE `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`), KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_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 `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_id` (`office_id`), KEY `client_id` (`office_id`),
@@ -1763,7 +1763,7 @@ CREATE TABLE `r_enum_value` (
LOCK TABLES `r_enum_value` WRITE; LOCK TABLES `r_enum_value` WRITE;
/*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */; /*!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 */; /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -1373,7 +1373,7 @@ CREATE TABLE `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`), KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_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 `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_id` (`office_id`), KEY `client_id` (`office_id`),
@@ -1701,7 +1701,7 @@ CREATE TABLE `r_enum_value` (
LOCK TABLES `r_enum_value` WRITE; LOCK TABLES `r_enum_value` WRITE;
/*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */; /*!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 */; /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -1373,7 +1373,7 @@ CREATE TABLE `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`), KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_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 `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_id` (`office_id`), KEY `client_id` (`office_id`),
@@ -1701,7 +1701,7 @@ CREATE TABLE `r_enum_value` (
LOCK TABLES `r_enum_value` WRITE; LOCK TABLES `r_enum_value` WRITE;
/*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */; /*!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 */; /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -1448,7 +1448,7 @@ CREATE TABLE `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`), KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_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 `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_id` (`office_id`), KEY `client_id` (`office_id`),
@@ -1763,7 +1763,7 @@ CREATE TABLE `r_enum_value` (
LOCK TABLES `r_enum_value` WRITE; LOCK TABLES `r_enum_value` WRITE;
/*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */; /*!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 */; /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -1232,7 +1232,7 @@ CREATE TABLE `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`), KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_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 `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_id` (`office_id`), KEY `client_id` (`office_id`),
@@ -1679,7 +1679,7 @@ CREATE TABLE `r_enum_value` (
LOCK TABLES `r_enum_value` WRITE; LOCK TABLES `r_enum_value` WRITE;
/*!40000 ALTER TABLE `r_enum_value` DISABLE KEYS */; /*!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 */; /*!40000 ALTER TABLE `r_enum_value` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -304,7 +304,7 @@ CREATE TABLE `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_m_maker_m_appuser` (`maker_id`), KEY `FK_m_maker_m_appuser` (`maker_id`),
KEY `FK_m_checker_m_appuser` (`checker_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 `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_id` (`office_id`), KEY `client_id` (`office_id`),
@@ -47,10 +47,12 @@ VALUES
('loan_transaction_strategy_id',2,'heavensfamily-strategy','Heavensfamily'), ('loan_transaction_strategy_id',2,'heavensfamily-strategy','Heavensfamily'),
('loan_transaction_strategy_id',3,'creocore-strategy','Creocore'), ('loan_transaction_strategy_id',3,'creocore-strategy','Creocore'),
('loan_transaction_strategy_id',4,'rbi-india-strategy','RBI (India)'), ('loan_transaction_strategy_id',4,'rbi-india-strategy','RBI (India)'),
('processing_result_enum',0,'invalid','Invalid'), ('status',0,'invalid','Invalid'),
('processing_result_enum',1,'processed','Processed'), ('status',1,'processed','Processed'),
('processing_result_enum',2,'awaiting.approval','Awaiting Approval'), ('status',2,'awaiting.approval','Awaiting Approval'),
('processing_result_enum',3,'rejected','Rejected'), ('status',3,'rejected','Rejected'),
('status',4,'underProcessing','Under Processing'),
('status',5,'error','Error'),
('repayment_period_frequency_enum',0,'Days','Days'), ('repayment_period_frequency_enum',0,'Days','Days'),
('repayment_period_frequency_enum',1,'Weeks','Weeks'), ('repayment_period_frequency_enum',1,'Weeks','Weeks'),
('repayment_period_frequency_enum',2,'Months','Months'), ('repayment_period_frequency_enum',2,'Months','Months'),
@@ -37,22 +37,18 @@ import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus; import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class InlineLoanCOBBuildExecutionContextTasklet implements Tasklet, InitializingBean { @Component
public class InlineLoanCOBBuildExecutionContextTasklet implements Tasklet {
private final GoogleGsonSerializerHelper gsonFactory; private final GoogleGsonSerializerHelper gsonFactory;
private final COBBusinessStepService cobBusinessStepService; private final COBBusinessStepService cobBusinessStepService;
private final CustomJobParameterRepository customJobParameterRepository; private final CustomJobParameterRepository customJobParameterRepository;
private Gson gson; private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
@Override
public void afterPropertiesSet() throws Exception {
this.gson = gsonFactory.createSimpleGson();
}
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -55,7 +55,6 @@ import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.NoSuchJobException; import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
@@ -66,7 +65,7 @@ import org.springframework.transaction.support.TransactionTemplate;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<Long>, InitializingBean { public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<Long> {
private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: "; private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: ";
@@ -80,12 +79,7 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<L
private final CustomJobParameterRepository customJobParameterRepository; private final CustomJobParameterRepository customJobParameterRepository;
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private Gson gson; private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
@Override
public void afterPropertiesSet() throws Exception {
this.gson = gsonFactory.createSimpleGson();
}
@Override @Override
@Transactional(propagation = Propagation.NOT_SUPPORTED) @Transactional(propagation = Propagation.NOT_SUPPORTED)
@@ -68,7 +68,7 @@ public class AuditsApiResource {
"subresourceId", "maker", "madeOnDate", "checker", "checkedOnDate", "processingResult", "commandAsJson", "officeName", "subresourceId", "maker", "madeOnDate", "checker", "checkedOnDate", "processingResult", "commandAsJson", "officeName",
"groupLevelName", "groupName", "clientName", "loanAccountNo", "savingsAccountNo", "clientId", "loanId", "url")); "groupLevelName", "groupName", "clientName", "loanAccountNo", "savingsAccountNo", "clientId", "loanId", "url"));
private final String resourceNameForPermissions = "AUDIT"; private static final String RESOURCE_NAME_FOR_PERMISSIONS = "AUDIT";
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final AuditReadPlatformService auditReadPlatformService; private final AuditReadPlatformService auditReadPlatformService;
@@ -107,7 +107,7 @@ public class AuditsApiResource {
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); this.context.authenticatedUser().validateHasReadPermission(this.RESOURCE_NAME_FOR_PERMISSIONS);
final PaginationParameters parameters = PaginationParameters.instance(paged, offset, limit, orderBy, sortOrder); final PaginationParameters parameters = PaginationParameters.instance(paged, offset, limit, orderBy, sortOrder);
final SQLBuilder extraCriteria = getExtraCriteria(actionName, entityName, resourceId, makerId, makerDateTimeFrom, makerDateTimeTo, final SQLBuilder extraCriteria = getExtraCriteria(actionName, entityName, resourceId, makerId, makerDateTimeFrom, makerDateTimeTo,
checkerId, checkerDateTimeFrom, checkerDateTimeTo, processingResult, officeId, groupId, clientId, loanId, savingsAccountId); checkerId, checkerDateTimeFrom, checkerDateTimeTo, processingResult, officeId, groupId, clientId, loanId, savingsAccountId);
@@ -137,7 +137,7 @@ public class AuditsApiResource {
public String retrieveAuditEntry(@PathParam("auditId") @Parameter(description = "auditId") final Long auditId, public String retrieveAuditEntry(@PathParam("auditId") @Parameter(description = "auditId") final Long auditId,
@Context final UriInfo uriInfo) { @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); this.context.authenticatedUser().validateHasReadPermission(this.RESOURCE_NAME_FOR_PERMISSIONS);
final AuditData auditEntry = this.auditReadPlatformService.retrieveAuditEntry(auditId); final AuditData auditEntry = this.auditReadPlatformService.retrieveAuditEntry(auditId);
@@ -155,22 +155,22 @@ public class AuditsApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = MakercheckersApiResourceSwagger.GetMakerCheckersSearchTemplateResponse.class))) }) @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = MakercheckersApiResourceSwagger.GetMakerCheckersSearchTemplateResponse.class))) })
public String retrieveAuditSearchTemplate(@Context final UriInfo uriInfo) { public String retrieveAuditSearchTemplate(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); this.context.authenticatedUser().validateHasReadPermission(this.RESOURCE_NAME_FOR_PERMISSIONS);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
final AuditSearchData auditSearchData = this.auditReadPlatformService.retrieveSearchTemplate("audit"); final AuditSearchData auditSearchData = this.auditReadPlatformService.retrieveSearchTemplate("audit");
final Set<String> RESPONSE_DATA_PARAMETERS_SEARCH_TEMPLATE = new HashSet<>( final Set<String> RESPONSE_DATA_PARAMETERS_SEARCH_TEMPLATE = new HashSet<>(
Arrays.asList("appUsers", "actionNames", "entityNames", "processingResults")); Arrays.asList("appUsers", "actionNames", "entityNames", "status"));
return this.toApiJsonSerializerSearchTemplate.serialize(settings, auditSearchData, RESPONSE_DATA_PARAMETERS_SEARCH_TEMPLATE); 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, 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 makerDateTimeFrom, final String makerDateTimeTo, final Long checkerId, final String checkerDateTimeFrom,
final String checkerDateTimeTo, final Integer processingResult, final Integer officeId, final Integer groupId, final String checkerDateTimeTo, final Integer status, final Integer officeId, final Integer groupId, final Integer clientId,
final Integer clientId, final Integer loanId, final Integer savingsAccountId) { final Integer loanId, final Integer savingsAccountId) {
SQLBuilder extraCriteria = new SQLBuilder(); SQLBuilder extraCriteria = new SQLBuilder();
extraCriteria.addNonNullCriteria("aud.action_name = ", actionName); extraCriteria.addNonNullCriteria("aud.action_name = ", actionName);
@@ -184,7 +184,7 @@ public class AuditsApiResource {
extraCriteria.addNonNullCriteria("aud.made_on_date <= ", makerDateTimeTo); extraCriteria.addNonNullCriteria("aud.made_on_date <= ", makerDateTimeTo);
extraCriteria.addNonNullCriteria("aud.checked_on_date >= ", checkerDateTimeFrom); extraCriteria.addNonNullCriteria("aud.checked_on_date >= ", checkerDateTimeFrom);
extraCriteria.addNonNullCriteria("aud.checked_on_date <= ", checkerDateTimeTo); 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.office_id = ", officeId);
extraCriteria.addNonNullCriteria("aud.group_id = ", groupId); extraCriteria.addNonNullCriteria("aud.group_id = ", groupId);
extraCriteria.addNonNullCriteria("aud.client_id = ", clientId); extraCriteria.addNonNullCriteria("aud.client_id = ", clientId);
@@ -34,5 +34,5 @@ public final class AuditSearchData {
private final Collection<AppUserData> appUsers; private final Collection<AppUserData> appUsers;
private final List<String> actionNames; private final List<String> actionNames;
private final List<String> entityNames; private final List<String> entityNames;
private final Collection<ProcessingResultLookup> processingResults; private final Collection<ProcessingResultLookup> statuses;
} }
@@ -28,7 +28,9 @@ public enum CommandProcessingResultType {
INVALID(0, "commandProcessingResultType.invalid"), // INVALID(0, "commandProcessingResultType.invalid"), //
PROCESSED(1, "commandProcessingResultType.processed"), // PROCESSED(1, "commandProcessingResultType.processed"), //
AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), // 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 Integer value;
private final String code; private final String code;
@@ -24,7 +24,6 @@ import javax.persistence.Entity;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.Table; import javax.persistence.Table;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@@ -91,8 +90,8 @@ public class CommandSource extends AbstractPersistableCustom {
@JoinColumn(name = "checker_id", nullable = true) @JoinColumn(name = "checker_id", nullable = true)
private AppUser checker; private AppUser checker;
@Column(name = "processing_result_enum", nullable = false) @Column(name = "status", nullable = false)
private Integer processingResult; private Integer status;
@Column(name = "product_id") @Column(name = "product_id")
private Long productId; private Long productId;
@@ -118,10 +117,16 @@ public class CommandSource extends AbstractPersistableCustom {
@Column(name = "subresource_external_id") @Column(name = "subresource_external_id")
private ExternalId subResourceExternalId; 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, 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(), return new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(), command.subentityId(),
command.json(), maker, idempotencyKey); command.json(), maker, idempotencyKey, status);
} }
protected CommandSource() { 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, 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.actionName = actionName;
this.entityName = entityName; this.entityName = entityName;
this.resourceGetUrl = href; this.resourceGetUrl = href;
@@ -138,7 +144,7 @@ public class CommandSource extends AbstractPersistableCustom {
this.commandAsJson = commandSerializedAsJson; this.commandAsJson = commandSerializedAsJson;
this.maker = maker; this.maker = maker;
this.madeOnDate = DateUtils.getOffsetDateTimeOfTenant(); this.madeOnDate = DateUtils.getOffsetDateTimeOfTenant();
this.processingResult = CommandProcessingResultType.PROCESSED.getValue(); this.status = status;
this.idempotencyKey = idempotencyKey; this.idempotencyKey = idempotencyKey;
} }
@@ -165,13 +171,13 @@ public class CommandSource extends AbstractPersistableCustom {
public void markAsChecked(final AppUser checker) { public void markAsChecked(final AppUser checker) {
this.checker = checker; this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenant(); this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenant();
this.processingResult = CommandProcessingResultType.PROCESSED.getValue(); this.status = CommandProcessingResultType.PROCESSED.getValue();
} }
public void markAsRejected(final AppUser checker) { public void markAsRejected(final AppUser checker) {
this.checker = checker; this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenant(); this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenant();
this.processingResult = CommandProcessingResultType.REJECTED.getValue(); this.status = CommandProcessingResultType.REJECTED.getValue();
} }
public void updateResourceId(final Long resourceId) { public void updateResourceId(final Long resourceId) {
@@ -182,7 +188,11 @@ public class CommandSource extends AbstractPersistableCustom {
this.subResourceId = subResourceId; this.subResourceId = subResourceId;
} }
public void updateJsonTo(final String json) { public String getCommandJson() {
return this.commandAsJson;
}
public void setCommandJson(final String json) {
this.commandAsJson = json; this.commandAsJson = json;
} }
@@ -194,14 +204,6 @@ public class CommandSource extends AbstractPersistableCustom {
return this.subResourceId; return this.subResourceId;
} }
public boolean hasJson() {
return StringUtils.isNotBlank(this.commandAsJson);
}
public String json() {
return this.commandAsJson;
}
public String getActionName() { public String getActionName() {
return this.actionName; return this.actionName;
} }
@@ -223,15 +225,11 @@ public class CommandSource extends AbstractPersistableCustom {
} }
public void markAsAwaitingApproval() { public void markAsAwaitingApproval() {
this.processingResult = CommandProcessingResultType.AWAITING_APPROVAL.getValue(); this.status = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
} }
public boolean isMarkedAsAwaitingApproval() { public boolean isMarkedAsAwaitingApproval() {
if (this.processingResult.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue())) { return this.status.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue());
return true;
}
return false;
} }
public void updateForAudit(final CommandProcessingResult result) { public void updateForAudit(final CommandProcessingResult result) {
@@ -309,4 +307,28 @@ public class CommandSource extends AbstractPersistableCustom {
public void setIdempotencyKey(String idempotencyKey) { public void setIdempotencyKey(String idempotencyKey) {
this.idempotencyKey = 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;
}
} }
@@ -22,5 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface CommandSourceRepository extends JpaRepository<CommandSource, Long>, JpaSpecificationExecutor<CommandSource> { public interface CommandSourceRepository extends JpaRepository<CommandSource, Long>, JpaSpecificationExecutor<CommandSource> {
// no added behaviour
CommandSource findByActionNameAndEntityNameAndIdempotencyKey(String actionName, String entityName, String idempotencyKey);
} }
@@ -20,8 +20,12 @@ package org.apache.fineract.commands.handler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; 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 { public interface NewCommandSourceHandler {
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
CommandProcessingResult processCommand(JsonCommand command); CommandProcessingResult processCommand(JsonCommand command);
} }
@@ -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_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_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 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 // data scoping: head office (hierarchy = ".") can see all audit
// entries // entries
@@ -206,7 +206,7 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
@Override @Override
public Collection<AuditData> retrieveAllEntriesToBeChecked(final SQLBuilder extraCriteria, final boolean includeJson) { public Collection<AuditData> retrieveAllEntriesToBeChecked(final SQLBuilder extraCriteria, final boolean includeJson) {
extraCriteria.addCriteria("aud.processing_result_enum = ", 2); extraCriteria.addCriteria("aud.status = ", 2);
return retrieveEntries("makerchecker", extraCriteria, " order by aud.id, mk.username", includeJson); return retrieveEntries("makerchecker", extraCriteria, " order by aud.id, mk.username", includeJson);
} }
@@ -491,13 +491,13 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
@Override @Override
public ProcessingResultLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { public ProcessingResultLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = JdbcSupport.getLong(rs, "id"); 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() { 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"; + " order by enum_id";
} }
} }
@@ -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;
}
}
@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.commands.service;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Component
@RequiredArgsConstructor
public class IdempotencyKeyResolver {
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
private final FineractProperties fineractProperties;
public String resolve(CommandWrapper wrapper) {
return Optional.ofNullable(wrapper.getIdempotencyKey())
.orElseGet(() -> getHeaderAttribute().orElseGet(idempotencyKeyGenerator::create));
}
private Optional<String> getHeaderAttribute() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) //
.filter(ServletRequestAttributes.class::isInstance) //
.map(ServletRequestAttributes.class::cast) //
.map(ServletRequestAttributes::getRequest) //
.map(request -> request.getHeader(fineractProperties.getIdempotencyKeyHeaderName()));
}
}
@@ -87,16 +87,15 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(),
commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getCreditBureauId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getIdempotencyKey()); commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getIdempotencyKey());
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.json()); final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.getCommandJson());
final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.json(), parsedCommand, final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.getCommandJson(), parsedCommand,
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(), this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(),
commandSourceInput.subResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.subResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(),
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(), commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName()); commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName());
final boolean makerCheckerApproval = true; return this.processAndLogCommandService.executeCommand(wrapper, command, true);
return this.processAndLogCommandService.executeCommand(wrapper, command, makerCheckerApproval);
} }
@Transactional @Transactional
@@ -18,6 +18,10 @@
*/ */
package org.apache.fineract.commands.service; 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.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.retry.annotation.Retry;
@@ -25,23 +29,27 @@ import java.lang.reflect.Type;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo; 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.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.domain.CommandWrapper; 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.RollbackTransactionAsCommandIsNotApprovedByCheckerException;
import org.apache.fineract.commands.exception.UnsupportedCommandException; import org.apache.fineract.commands.exception.UnsupportedCommandException;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.commands.provider.CommandHandlerProvider; import org.apache.fineract.commands.provider.CommandHandlerProvider;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand; 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.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; 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.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.hooks.event.HookEvent; 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.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class SynchronousCommandProcessingService implements CommandProcessingService { 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 PlatformSecurityContext context;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final ToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer; private final ToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
private final ToApiJsonSerializer<CommandProcessingResult> toApiResultJsonSerializer; private final ToApiJsonSerializer<CommandProcessingResult> toApiResultJsonSerializer;
private final CommandSourceRepository commandSourceRepository;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final CommandHandlerProvider commandHandlerProvider; private final CommandHandlerProvider commandHandlerProvider;
private final IdempotencyKeyResolver idempotencyKeyResolver;
private final IdempotencyKeyGenerator idempotencyKeyGenerator; private final IdempotencyKeyGenerator idempotencyKeyGenerator;
private final FineractProperties fineractProperties; private final CommandSourceService commandSourceService;
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
@Override @Override
@Transactional
@Retry(name = "executeCommand", fallbackMethod = "fallbackExecuteCommand") @Retry(name = "executeCommand", fallbackMethod = "fallbackExecuteCommand")
public CommandProcessingResult executeCommand(final CommandWrapper wrapper, final JsonCommand command, public CommandProcessingResult executeCommand(final CommandWrapper wrapper, final JsonCommand command,
final boolean isApprovedByChecker) { final boolean isApprovedByChecker) {
// Do not store the idempotency key because of the exception handling
setIdempotencyKeyStoreFlag(false);
final boolean rollbackTransaction = configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName()); 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; final CommandProcessingResult result;
try { try {
result = handler.processCommand(command); result = findCommandHandler(wrapper).processCommand(command);
} catch (Throwable t) { } catch (Throwable t) { // NOSONAR
commandSourceService.saveFailed(commandSourceService.findCommandSource(wrapper, idempotencyKey));
publishHookErrorEvent(wrapper, command, t); publishHookErrorEvent(wrapper, command, t);
throw 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; boolean rollBack = (rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker;
if (result.hasChanges() && !rollBack) { if (result.hasChanges() && !rollBack) {
changesOnlyJson = toApiJsonSerializer.serializeResult(result.getChanges()); initialCommandSource.setCommandJson(toApiJsonSerializer.serializeResult(result.getChanges()));
commandSourceResult.updateJsonTo(changesOnlyJson);
} }
if (!result.hasChanges() && wrapper.isUpdateOperation() && !wrapper.isUpdateDatatable()) { initialCommandSource.setStatus(CommandProcessingResultType.PROCESSED.getValue());
commandSourceResult.updateJsonTo(null); commandSourceService.saveResult(initialCommandSource);
}
if (commandSourceResult.hasJson()) {
commandSourceRepository.save(commandSourceResult);
}
if ((rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker) { 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 * transactionId, because as there are no entries are created with new transactionId, will throw an error
* when checker approves the transaction * 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 * Update CommandSource json data with JsonCommand json data, line 77 and 81 may update the json data
*/ */
commandSourceResult.updateJsonTo(command.json()); initialCommandSource.setCommandJson(command.json());
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(commandSourceResult); throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(initialCommandSource);
} }
result.setRollbackTransaction(null); result.setRollbackTransaction(null);
@@ -148,15 +140,55 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
return result; return result;
} }
private void storeCommandToIdempotentFilter(CommandSource savedCommandSource) {
if (savedCommandSource.getId() == null) {
throw new IllegalStateException("Command source not saved");
}
saveCommandToRequest(savedCommandSource);
}
private static void saveCommandToRequest(CommandSource savedCommandSource) {
Optional.ofNullable(RequestContextHolder.getRequestAttributes()).ifPresent(requestAttributes -> requestAttributes
.setAttribute(COMMAND_SOURCE_ID, savedCommandSource.getId(), RequestAttributes.SCOPE_REQUEST));
}
private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
ErrorInfo ex = commandSourceService.generateErrorException(t);
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, gson.toJson(ex));
}
private void exceptionWhenTheRequestAlreadyProcessed(CommandWrapper wrapper, String idempotencyKey) {
CommandSource existingCommand = commandSourceService.findCommandSource(wrapper, idempotencyKey);
if (existingCommand != null) {
idempotentExceptionByStatus(UNDER_PROCESSING, existingCommand,
command -> new IdempotentCommandProcessUnderProcessingException(wrapper));
idempotentExceptionByStatus(ERROR, existingCommand, command -> new IdempotentCommandProcessFailedException(wrapper, command));
idempotentExceptionByStatus(PROCESSED, existingCommand,
command -> new IdempotentCommandProcessSucceedException(wrapper, command.getResult()));
}
}
private void idempotentExceptionByStatus(CommandProcessingResultType status, CommandSource command,
Function<CommandSource, AbstractIdempotentCommandException> exceptionMapper) {
if (status.getValue().equals(command.getStatus())) {
throw exceptionMapper.apply(command);
}
}
private void setIdempotencyKeyStoreFlag(boolean flag) {
Optional.ofNullable(RequestContextHolder.getRequestAttributes()).ifPresent(
requestAttributes -> requestAttributes.setAttribute(IDEMPOTENCY_KEY_STORE_FLAG, flag, RequestAttributes.SCOPE_REQUEST));
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult logCommand(CommandSource commandSourceResult) { public CommandProcessingResult logCommand(CommandSource commandSourceResult) {
commandSourceResult.markAsAwaitingApproval(); commandSourceResult.markAsAwaitingApproval();
if (commandSourceResult.getIdempotencyKey() == null) { if (commandSourceResult.getIdempotencyKey() == null) {
commandSourceResult.setIdempotencyKey(idempotencyKeyGenerator.create()); commandSourceResult.setIdempotencyKey(idempotencyKeyGenerator.create());
} }
commandSourceResult = commandSourceRepository.saveAndFlush(commandSourceResult); commandSourceResult = commandSourceService.saveResult(commandSourceResult);
return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId()) return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId())
.withEntityId(commandSourceResult.getResourceId()).build(); .withEntityId(commandSourceResult.getResourceId()).build();
@@ -236,20 +268,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
return rollbackTransaction; 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) { private void publishHookEvent(final String entityName, final String actionName, JsonCommand command, final Object result) {
Gson gson = new Gson();
try { try {
final AppUser appUser = context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null)); final AppUser appUser = context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null));
@@ -301,5 +321,4 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
log.error("Error", e); log.error("Error", e);
} }
} }
} }
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.core.config; 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.instancemode.filter.FineractInstanceModeApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter; import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter;
import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter; 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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder; 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.BasicAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.SecurityContextPersistenceFilter;
@@ -67,6 +69,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired @Autowired
private ServerProperties serverProperties; private ServerProperties serverProperties;
@Autowired
private IdempotencyStoreFilter idempotencyStoreFilter;
@Override @Override
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
http // http //
@@ -91,7 +96,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.addFilterAfter(fineractInstanceModeApiFilter, SecurityContextPersistenceFilter.class) // .addFilterAfter(fineractInstanceModeApiFilter, SecurityContextPersistenceFilter.class) //
.addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) // .addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) //
.addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class) // .addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class) //
.addFilterAfter(loanCOBApiFilter, InsecureTwoFactorAuthenticationFilter.class); .addFilterAfter(loanCOBApiFilter, InsecureTwoFactorAuthenticationFilter.class)
.addFilterBefore(idempotencyStoreFilter, ExceptionTranslationFilter.class);
if (serverProperties.getSsl().isEnabled()) { if (serverProperties.getSsl().isEnabled()) {
http.requiresChannel(channel -> channel.antMatchers("/api/**").requiresSecure()); http.requiresChannel(channel -> channel.antMatchers("/api/**").requiresSecure());
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.exceptionmapper;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class IdempotentCommandProcessFailedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessFailedException> {
@Override
public Response toResponse(final IdempotentCommandProcessFailedException exception) {
log.debug("Idempotent processing failed request: {}", exception.getMessage());
Status statusCode = Status.fromStatusCode(exception.getStatusCode());
return Response.status(statusCode).entity(exception.getResponse())
.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON).build();
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.exceptionmapper;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class IdempotentCommandProcessSucceedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessSucceedException> {
@Override
public Response toResponse(final IdempotentCommandProcessSucceedException exception) {
log.debug("Idempotent processing success request: {}", exception.getMessage());
return Response.status(Status.OK).entity(exception.getResponse())
.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON).build();
}
}
@@ -0,0 +1,44 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.exceptionmapper;
import static org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class IdempotentCommandProcessUnderProcessingExceptionMapper
implements ExceptionMapper<IdempotentCommandProcessUnderProcessingException> {
@Override
public Response toResponse(final IdempotentCommandProcessUnderProcessingException exception) {
log.debug("Idempotent under processing request: {}", exception.getMessage());
return Response.status(Status.CONFLICT).entity(exception.getResponse()).header(IDEMPOTENT_CACHE_HEADER, "true")
.type(MediaType.APPLICATION_JSON).build();
}
}
@@ -0,0 +1,91 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.core.filters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.mutable.Mutable;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.service.CommandSourceService;
import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;
@RequiredArgsConstructor
@Slf4j
@Component
public class IdempotencyStoreFilter extends OncePerRequestFilter {
private final CommandSourceRepository commandSourceRepository;
private final CommandSourceService commandSourceService;
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException {
Mutable<ContentCachingResponseWrapper> wrapper = new MutableObject<>();
if (isAllowedContentTypeRequest(request)) {
wrapper.setValue(new ContentCachingResponseWrapper(response));
}
filterChain.doFilter(request, wrapper.getValue() != null ? wrapper.getValue() : response);
Optional<Long> commandId = getCommandId(request);
boolean isSuccessWithoutStored = isStoreIdempotencyKey(request) && commandId.isPresent() && isAllowedContentTypeResponse(response)
&& wrapper.getValue() != null;
if (isSuccessWithoutStored) {
commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
commandSource.setResultStatusCode(response.getStatus());
commandSource.setResult(new String(wrapper.getValue().getContentAsByteArray(), StandardCharsets.UTF_8));
commandSourceService.saveResult(commandSource);
});
}
if (wrapper.getValue() != null) {
wrapper.getValue().copyBodyToResponse();
}
}
private boolean isAllowedContentTypeResponse(HttpServletResponse response) {
return Optional.ofNullable(response.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
.orElse(false);
}
private boolean isAllowedContentTypeRequest(HttpServletRequest request) {
return Optional.ofNullable(request.getContentType()).map(String::toLowerCase).map(ct -> ct.contains("application/json"))
.orElse(false);
}
private boolean isStoreIdempotencyKey(HttpServletRequest request) {
return Optional.ofNullable(request.getAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_STORE_FLAG))
.filter(Boolean.class::isInstance).map(Boolean.class::cast).orElse(false);
}
private Optional<Long> getCommandId(HttpServletRequest request) {
return Optional.ofNullable(request.getAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID))
.filter(Long.class::isInstance).map(Long.class::cast);
}
}
@@ -29,6 +29,10 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
public class ExtendedJpaTransactionManager extends JpaTransactionManager { public class ExtendedJpaTransactionManager extends JpaTransactionManager {
public ExtendedJpaTransactionManager() {
setValidateExistingTransaction(true);
}
@Override @Override
protected void doBegin(Object transaction, TransactionDefinition definition) { protected void doBegin(Object transaction, TransactionDefinition definition) {
super.doBegin(transaction, definition); super.doBegin(transaction, definition);
@@ -98,7 +98,7 @@ public final class GoogleGsonSerializerHelper {
return serializer.toJson(singleDataObject); return serializer.toJson(singleDataObject);
} }
public Gson createSimpleGson() { public static Gson createSimpleGson() {
return createGsonBuilder().create(); return createGsonBuilder().create();
} }
@@ -41,7 +41,8 @@ public class ScheduledJobRunnerConfig {
@Bean @Bean
public PlatformTransactionManager transactionManager(ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) { public PlatformTransactionManager transactionManager(ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
ExtendedJpaTransactionManager transactionManager = new ExtendedJpaTransactionManager(); ExtendedJpaTransactionManager transactionManager = new ExtendedJpaTransactionManager();
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager)); transactionManager.setValidateExistingTransaction(true);
transactionManagerCustomizers.ifAvailable(customizers -> customizers.customize(transactionManager));
return transactionManager; return transactionManager;
} }
@@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.delinquency.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import javax.transaction.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; 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.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j @Slf4j
@Service @Service
public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainService { public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainService {
@Override @Override
@Transactional @Transactional(readOnly = true)
public CollectionData getOverdueCollectionData(final Loan loan) { public CollectionData getOverdueCollectionData(final Loan loan) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate(); final LocalDate businessDate = DateUtils.getBusinessLocalDate();
@@ -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.connectionTimeout=${FINERACT_HIKARI_CONNECTION_TIMEOUT:20000}
spring.datasource.hikari.connectionTestquery=${FINERACT_HIKARI_TEST_QUERY:SELECT 1} spring.datasource.hikari.connectionTestquery=${FINERACT_HIKARI_TEST_QUERY:SELECT 1}
spring.datasource.hikari.autoCommit=${FINERACT_HIKARI_AUTO_COMMIT:true} 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['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['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} spring.datasource.hikari.dataSourceProperties['prepStmtCacheSqlLimit']=${FINERACT_HIKARI_DS_PROPERTIES_PREP_STMT_CACHE_SQL_LIMIT:2048}
@@ -91,4 +91,7 @@
<include file="parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml" relativeToChangelogFile="true"/> <include file="parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml" relativeToChangelogFile="true"/>
<include file="parts/0070_add_event_configuration_for_delinquency_range_change_event.xml" relativeToChangelogFile="true"/> <include file="parts/0070_add_event_configuration_for_delinquency_range_change_event.xml" relativeToChangelogFile="true"/>
<include file="parts/0071_add_external_id_support_for_loan_transaction.xml" relativeToChangelogFile="true"/> <include file="parts/0071_add_external_id_support_for_loan_transaction.xml" relativeToChangelogFile="true"/>
<include file="parts/0072_add_result_and status_to_command_source.xml" relativeToChangelogFile="true" />
<include file="parts/0073_add_result_status_code_to_command_source.xml" relativeToChangelogFile="true" />
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="1" author="fineract" context="postgresql">
<renameColumn newColumnName="status"
oldColumnName="processing_result_enum"
tableName="m_portfolio_command_source"/>
</changeSet>
<changeSet id="2" author="fineract" context="mysql">
<renameColumn newColumnName="status"
oldColumnName="processing_result_enum"
tableName="m_portfolio_command_source"
columnDataType="smallint(6)" />
</changeSet>
<changeSet id="3" author="fineract">
<addColumn tableName="m_portfolio_command_source">
<column name="result" type="TEXT"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="1" author="fineract" context="postgresql">
<addColumn tableName="m_portfolio_command_source">
<column name="result_status_code" type="int"></column>
</addColumn>
</changeSet>
<changeSet id="2" author="fineract" context="mysql">
<addColumn tableName="m_portfolio_command_source">
<column name="result_status_code" type="int"></column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -3882,7 +3882,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
`product_id` BIGINT DEFAULT NULL, `product_id` BIGINT DEFAULT NULL,
`transaction_id` varchar(100) DEFAULT NULL, `transaction_id` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
@@ -3892,7 +3892,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
KEY `entity_name` (`entity_name`,`resource_id`), KEY `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_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', 2, 'SAVING', 'EXPENSE', 0),
('portfolio_account_type_enum', 3, 'PROVISIONING', 'PROVISIONING', 0), ('portfolio_account_type_enum', 3, 'PROVISIONING', 'PROVISIONING', 0),
('portfolio_account_type_enum', 4, 'SHARES', 'SHARES', 0), ('portfolio_account_type_enum', 4, 'SHARES', 'SHARES', 0),
('processing_result_enum', 0, 'invalid', 'Invalid', 0), ('status', 0, 'invalid', 'Invalid', 0),
('processing_result_enum', 1, 'processed', 'Processed', 0), ('status', 1, 'processed', 'Processed', 0),
('processing_result_enum', 2, 'awaiting.approval', 'Awaiting Approval', 0), ('status', 2, 'awaiting.approval', 'Awaiting Approval', 0),
('processing_result_enum', 3, 'rejected', 'Rejected', 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', 0, 'Days', 'Days', 0),
('repayment_period_frequency_enum', 1, 'Weeks', 'Weeks', 0), ('repayment_period_frequency_enum', 1, 'Weeks', 'Weeks', 0),
('repayment_period_frequency_enum', 2, 'Months', 'Months', 0), ('repayment_period_frequency_enum', 2, 'Months', 'Months', 0),
@@ -4054,7 +4054,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
`made_on_date` datetime NOT NULL, `made_on_date` datetime NOT NULL,
`checker_id` BIGINT DEFAULT NULL, `checker_id` BIGINT DEFAULT NULL,
`checked_on_date` datetime DEFAULT NULL, `checked_on_date` datetime DEFAULT NULL,
`processing_result_enum` SMALLINT NOT NULL, `status` SMALLINT NOT NULL,
`product_id` BIGINT DEFAULT NULL, `product_id` BIGINT DEFAULT NULL,
`transaction_id` varchar(100) DEFAULT NULL, `transaction_id` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
@@ -4064,7 +4064,7 @@ CREATE TABLE IF NOT EXISTS `m_portfolio_command_source` (
KEY `entity_name` (`entity_name`,`resource_id`), KEY `entity_name` (`entity_name`,`resource_id`),
KEY `made_on_date` (`made_on_date`), KEY `made_on_date` (`made_on_date`),
KEY `checked_on_date` (`checked_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 `office_id` (`office_id`),
KEY `group_id` (`office_id`), KEY `group_id` (`office_id`),
KEY `client_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) -- Dumping data for table mifostenant-reference.m_portfolio_command_source: ~72 rows (approximately)
/*!40000 ALTER TABLE `m_portfolio_command_source` DISABLE KEYS */; /*!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), (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), (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), (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', 2, 'SAVING', 'EXPENSE', 0),
('portfolio_account_type_enum', 3, 'PROVISIONING', 'PROVISIONING', 0), ('portfolio_account_type_enum', 3, 'PROVISIONING', 'PROVISIONING', 0),
('portfolio_account_type_enum', 4, 'SHARES', 'SHARES', 0), ('portfolio_account_type_enum', 4, 'SHARES', 'SHARES', 0),
('processing_result_enum', 0, 'invalid', 'Invalid', 0), ('status', 0, 'invalid', 'Invalid', 0),
('processing_result_enum', 1, 'processed', 'Processed', 0), ('status', 1, 'processed', 'Processed', 0),
('processing_result_enum', 2, 'awaiting.approval', 'Awaiting Approval', 0), ('status', 2, 'awaiting.approval', 'Awaiting Approval', 0),
('processing_result_enum', 3, 'rejected', 'Rejected', 0), ('status', 3, 'rejected', 'Rejected', 0),
('repayment_period_frequency_enum', 0, 'Days', 'Days', 0), ('repayment_period_frequency_enum', 0, 'Days', 'Days', 0),
('repayment_period_frequency_enum', 1, 'Weeks', 'Weeks', 0), ('repayment_period_frequency_enum', 1, 'Weeks', 'Weeks', 0),
('repayment_period_frequency_enum', 2, 'Months', 'Months', 0), ('repayment_period_frequency_enum', 2, 'Months', 'Months', 0),
@@ -0,0 +1,111 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
import java.time.ZoneId;
import java.util.Optional;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.infrastructure.codes.exception.CodeNotFoundException;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.useradministration.domain.AppUser;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
public class CommandSourceServiceTest {
@Mock
private CommandSourceRepository commandSourceRepository;
@InjectMocks
private CommandSourceService underTest;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
}
@AfterEach
public void tearDown() {
ThreadLocalContextUtil.reset();
}
@Test
public void testCreateFromWrapper() {
CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
JsonCommand jsonCommand = JsonCommand.from("{}");
AppUser appUser = Mockito.mock(AppUser.class);
FineractPlatformTenant ft = new FineractPlatformTenant(1L, "t1", "n1", ZoneId.systemDefault().toString(), null);
ThreadLocalContextUtil.setTenant(ft);
String idk = "idk";
underTest.saveInitial(wrapper, jsonCommand, appUser, idk);
ArgumentCaptor<CommandSource> commandSourceArgumentCaptor = ArgumentCaptor.forClass(CommandSource.class);
Mockito.verify(commandSourceRepository).saveAndFlush(commandSourceArgumentCaptor.capture());
CommandSource captured = commandSourceArgumentCaptor.getValue();
Assertions.assertEquals(idk, captured.getIdempotencyKey());
Assertions.assertEquals(UNDER_PROCESSING.getValue(), captured.getStatus());
}
@Test
public void testCreateFromExisting() {
CommandWrapper wrapper = CommandWrapper.wrap("act", "ent", 1L, 1L);
long commandId = 1L;
JsonCommand jsonCommand = JsonCommand.fromExistingCommand(commandId, "", null, null, null, 1L, null, null, null, null, null, null,
null, null, null, null, null);
CommandSource commandMock = Mockito.mock(CommandSource.class);
Mockito.when(commandSourceRepository.saveAndFlush(commandMock)).thenReturn(commandMock);
Mockito.when(commandSourceRepository.findById(commandId)).thenReturn(Optional.of(commandMock));
AppUser appUser = Mockito.mock(AppUser.class);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "t1", "n1", ZoneId.systemDefault().toString(), null));
CommandSource actual = underTest.saveInitial(wrapper, jsonCommand, appUser, "idk");
ArgumentCaptor<CommandSource> commandSourceArgumentCaptor = ArgumentCaptor.forClass(CommandSource.class);
Mockito.verify(commandSourceRepository).saveAndFlush(commandSourceArgumentCaptor.capture());
CommandSource captured = commandSourceArgumentCaptor.getValue();
Assertions.assertEquals(actual, captured);
}
@Test
public void testGenerateErrorException() {
ErrorInfo result = underTest.generateErrorException(new CodeNotFoundException("foo"));
Assertions.assertEquals(404, result.getStatusCode());
Assertions.assertEquals(1001, result.getErrorCode());
Assertions.assertTrue(result.getMessage().contains("Code with name `foo` does not exist"));
}
}
@@ -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);
}
}
@@ -0,0 +1,153 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.commands.service;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.commands.provider.CommandHandlerProvider;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.useradministration.domain.AppUser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class SynchronousCommandProcessingServiceTest {
@Mock
private PlatformSecurityContext context;
@Mock
private ApplicationContext applicationContext;
@Mock
private ToApiJsonSerializer<Map<String, Object>> toApiJsonSerializer;
@Mock
private ToApiJsonSerializer<CommandProcessingResult> toApiResultJsonSerializer;
@Mock
private ConfigurationDomainService configurationDomainService;
@Mock
private CommandHandlerProvider commandHandlerProvider;
@Mock
private IdempotencyKeyResolver idempotencyKeyResolver;
@Mock
private IdempotencyKeyGenerator idempotencyKeyGenerator;
@Mock
private CommandSourceService commandSourceService;
@InjectMocks
private SynchronousCommandProcessingService underTest;
@Mock
private HttpServletRequest request;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
}
@Test
public void testExecuteCommandSuccess() {
CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class);
when(commandWrapper.isDatatableResource()).thenReturn(false);
when(commandWrapper.isNoteResource()).thenReturn(false);
when(commandWrapper.isSurveyResource()).thenReturn(false);
when(commandWrapper.isLoanDisburseDetailResource()).thenReturn(false);
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
NewCommandSourceHandler newCommandSourceHandler = Mockito.mock(NewCommandSourceHandler.class);
CommandProcessingResult commandProcessingResult = Mockito.mock(CommandProcessingResult.class);
when(commandProcessingResult.isRollbackTransaction()).thenReturn(false);
when(newCommandSourceHandler.processCommand(jsonCommand)).thenReturn(commandProcessingResult);
when(commandHandlerProvider.getHandler(Mockito.any(), Mockito.any())).thenReturn(newCommandSourceHandler);
when(configurationDomainService.isMakerCheckerEnabledForTask(Mockito.any())).thenReturn(false);
String idk = "idk";
when(idempotencyKeyResolver.resolve(commandWrapper)).thenReturn(idk);
CommandSource commandSource = Mockito.mock(CommandSource.class);
when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(null).thenReturn(commandSource);
AppUser appUser = Mockito.mock(AppUser.class);
when(commandSourceService.saveInitial(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
CommandProcessingResult actualCommandProcessingResult = underTest.executeCommand(commandWrapper, jsonCommand, false);
verify(commandSourceService).saveInitial(commandWrapper, jsonCommand, appUser, idk);
verify(commandSource).setStatus(CommandProcessingResultType.PROCESSED.getValue());
verify(commandSourceService).saveResult(commandSource);
Assertions.assertEquals(commandProcessingResult, actualCommandProcessingResult);
}
@Test
public void testExecuteCommandFails() {
CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class);
when(commandWrapper.isDatatableResource()).thenReturn(false);
when(commandWrapper.isNoteResource()).thenReturn(false);
when(commandWrapper.isSurveyResource()).thenReturn(false);
when(commandWrapper.isLoanDisburseDetailResource()).thenReturn(false);
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
NewCommandSourceHandler newCommandSourceHandler = Mockito.mock(NewCommandSourceHandler.class);
CommandProcessingResult commandProcessingResult = Mockito.mock(CommandProcessingResult.class);
CommandSource commandSource = Mockito.mock(CommandSource.class);
when(commandProcessingResult.isRollbackTransaction()).thenReturn(false);
RuntimeException runtimeException = new RuntimeException("foo");
when(newCommandSourceHandler.processCommand(jsonCommand)).thenThrow(runtimeException);
when(commandHandlerProvider.getHandler(Mockito.any(), Mockito.any())).thenReturn(newCommandSourceHandler);
when(configurationDomainService.isMakerCheckerEnabledForTask(Mockito.any())).thenReturn(false);
String idk = "idk";
when(idempotencyKeyResolver.resolve(commandWrapper)).thenReturn(idk);
when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(null);
AppUser appUser = Mockito.mock(AppUser.class);
when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
when(commandSourceService.saveInitial(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
CommandSource initialCommandSource = Mockito.mock(CommandSource.class);
when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(initialCommandSource);
Assertions.assertThrows(RuntimeException.class, () -> {
underTest.executeCommand(commandWrapper, jsonCommand, false);
});
verify(commandSourceService).saveInitial(commandWrapper, jsonCommand, appUser, idk);
verify(commandSourceService).generateErrorException(runtimeException);
}
}
@@ -0,0 +1,158 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.apache.fineract.cob.data.BusinessStep;
import org.apache.fineract.cob.data.JobBusinessStepConfigData;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.integrationtests.common.IdempotencyHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class IdempotencyTest {
private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
public static final String LOAN_JOB_NAME = "LOAN_CLOSE_OF_BUSINESS";
public static final String LOAN_CATEGORY_NAME = "loan";
public static final String APPLY_CHARGE_TO_OVERDUE_LOANS = "APPLY_CHARGE_TO_OVERDUE_LOANS";
public static final String NOT_BELONGING_BUSINESS_STEP_NAME = "APPLY_CHARGE_TO_OVERDUE_LOANS_2";
public static final String LOAN_DELINQUENCY_CLASSIFICATION = "LOAN_DELINQUENCY_CLASSIFICATION";
@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
}
@Test
public void shouldUpdateStepOrder() {
JobBusinessStepConfigData originalStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec,
LOAN_JOB_NAME);
String idempotencyKeyHeader = UUID.randomUUID().toString();
List<BusinessStep> requestBody = new ArrayList<>();
requestBody.add(getBusinessSteps(1L, APPLY_CHARGE_TO_OVERDUE_LOANS));
Response response = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
Response responseSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
Assertions.assertEquals(response.getBody().asString(), responseSecond.getBody().asString());
Assertions.assertNull(response.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertNotNull(responseSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
idempotencyKeyHeader = UUID.randomUUID().toString();
JobBusinessStepConfigData newStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec,
LOAN_JOB_NAME);
BusinessStep applyChargeStep = newStepConfig.getBusinessSteps().stream()
.filter(businessStep -> APPLY_CHARGE_TO_OVERDUE_LOANS.equals(businessStep.getStepName())).findFirst().get();
assertEquals(1, newStepConfig.getBusinessSteps().size());
assertEquals(1L, applyChargeStep.getOrder());
requestBody.add(getBusinessSteps(2L, LOAN_DELINQUENCY_CLASSIFICATION));
Response update = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
Response updateSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
Assertions.assertNull(update.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertNotNull(updateSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertEquals(update.getBody().asString(), updateSecond.getBody().asString());
newStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec, LOAN_JOB_NAME);
applyChargeStep = newStepConfig.getBusinessSteps().stream()
.filter(businessStep -> APPLY_CHARGE_TO_OVERDUE_LOANS.equals(businessStep.getStepName())).findFirst().get();
BusinessStep loanDelinquencyStep = newStepConfig.getBusinessSteps().stream()
.filter(businessStep -> LOAN_DELINQUENCY_CLASSIFICATION.equals(businessStep.getStepName())).findFirst().get();
assertEquals(2, newStepConfig.getBusinessSteps().size());
assertEquals(1L, applyChargeStep.getOrder());
assertEquals(2L, loanDelinquencyStep.getOrder());
requestBody.remove(1);
idempotencyKeyHeader = UUID.randomUUID().toString();
update = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
updateSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKeyHeader);
Assertions.assertNull(update.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertNotNull(updateSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertEquals(update.getBody().asString(), updateSecond.getBody().asString());
newStepConfig = IdempotencyHelper.getConfiguredBusinessStepsByJobName(requestSpec, responseSpec, LOAN_JOB_NAME);
applyChargeStep = newStepConfig.getBusinessSteps().stream()
.filter(businessStep -> APPLY_CHARGE_TO_OVERDUE_LOANS.equals(businessStep.getStepName())).findFirst().get();
assertEquals(1, newStepConfig.getBusinessSteps().size());
assertEquals(1L, applyChargeStep.getOrder());
idempotencyKeyHeader = UUID.randomUUID().toString();
update = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(originalStepConfig.getBusinessSteps()), idempotencyKeyHeader);
updateSecond = IdempotencyHelper.updateBusinessStepOrder(requestSpec, responseSpec, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(originalStepConfig.getBusinessSteps()), idempotencyKeyHeader);
Assertions.assertNull(update.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertNotNull(updateSecond.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertEquals(update.getBody().asString(), updateSecond.getBody().asString());
}
@Test
public void shoudTheSecondRequestWithSameIdempotencyKeyWillFailureToo() {
ResponseSpecification responseSpecForError = new ResponseSpecBuilder().expectStatusCode(400).build();
List<BusinessStep> requestBody = new ArrayList<>();
String idempotencyKey = UUID.randomUUID().toString();
// IdempotencyHelper.configuredApiParameterErrorFromJsonString(response.getBody().asString())
Response response1 = IdempotencyHelper.updateBusinessStepOrderWithError(requestSpec, responseSpecForError, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKey);
Assertions.assertNull(response1.getHeader(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
String originalBody = response1.getBody().asString();
Response response2 = IdempotencyHelper.updateBusinessStepOrderWithError(requestSpec, responseSpecForError, LOAN_JOB_NAME,
IdempotencyHelper.toJsonString(requestBody), idempotencyKey);
Assertions.assertNotNull(response2.getHeader(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER));
Assertions.assertEquals(originalBody, response2.getBody().asString());
}
private BusinessStep getBusinessSteps(Long order, String stepName) {
BusinessStep businessStep = new BusinessStep();
businessStep.setStepName(stepName);
businessStep.setOrder(order);
return businessStep;
}
}
@@ -0,0 +1,95 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.integrationtests.common;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.data.BusinessStep;
import org.apache.fineract.cob.data.JobBusinessStepConfigData;
import org.apache.fineract.cob.data.JobBusinessStepDetail;
@Slf4j
public final class IdempotencyHelper {
private static final String BUSINESS_STEPS_API_URL_START = "/fineract-provider/api/v1/jobs/";
private static final String BUSINESS_STEPS_API_URL_END = "/steps?" + Utils.TENANT_IDENTIFIER;
private static final String GET_AVAILABLE_BUSINESS_STEPS_API_URL_END = "/available-steps?" + Utils.TENANT_IDENTIFIER;
private IdempotencyHelper() {
}
public static String toJsonString(final List<BusinessStep> batchRequests) {
return new Gson().toJson(new BusinessStepWrapper(batchRequests));
}
public static JobBusinessStepConfigData configuredBusinessStepFromJsonString(final String json) {
return new Gson().fromJson(json, new TypeToken<JobBusinessStepConfigData>() {}.getType());
}
private static JobBusinessStepDetail availableBusinessStepFromJsonString(final String json) {
return new Gson().fromJson(json, new TypeToken<JobBusinessStepDetail>() {}.getType());
}
public static JobBusinessStepConfigData getConfiguredBusinessStepsByJobName(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, String jobName) {
final String response = Utils.performServerGet(requestSpec, responseSpec,
BUSINESS_STEPS_API_URL_START + jobName + BUSINESS_STEPS_API_URL_END);
log.info("BusinessStepConfigurationHelper Response: {}", response);
return configuredBusinessStepFromJsonString(response);
}
public static JobBusinessStepDetail getAvailableBusinessStepsByJobName(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, String jobName) {
final String response = Utils.performServerGet(requestSpec, responseSpec,
BUSINESS_STEPS_API_URL_START + jobName + GET_AVAILABLE_BUSINESS_STEPS_API_URL_END);
log.info("BusinessStepConfigurationHelper Response: {}", response);
return availableBusinessStepFromJsonString(response);
}
public static Response updateBusinessStepOrder(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
String jobName, String jsonBodyToSend, String idempotencyKey) {
Response response = Utils.performServerPutRaw(requestSpec, responseSpec,
BUSINESS_STEPS_API_URL_START + jobName + BUSINESS_STEPS_API_URL_END,
request -> request.header("Idempotency-Key", idempotencyKey).body(jsonBodyToSend));
log.info("BusinessStepConfigurationHelper Response: {}", response.getBody().asString());
return response;
}
public static Response updateBusinessStepOrderWithError(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, String jobName, String jsonBodyToSend, String idempotencyKey) {
String url = BUSINESS_STEPS_API_URL_START + jobName + BUSINESS_STEPS_API_URL_END;
return Utils.performServerPutRaw(requestSpec, responseSpec, url,
request -> request.header("Idempotency-Key", idempotencyKey).body(jsonBodyToSend));
}
private static final class BusinessStepWrapper {
private List<BusinessStep> businessSteps;
private BusinessStepWrapper(List<BusinessStep> businessSteps) {
this.businessSteps = businessSteps;
}
}
}
@@ -53,6 +53,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.HttpHostConnectException; import org.apache.http.conn.HttpHostConnectException;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -164,6 +165,11 @@ public final class Utils {
return performServerGet(requestSpec, responseSpec, url, null); return performServerGet(requestSpec, responseSpec, url, null);
} }
public static Response performServerGetRaw(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final String getURL, Function<RequestSpecification, RequestSpecification> requestMapper) {
return requestMapper.apply(given().spec(requestSpec)).expect().spec(responseSpec).log().ifError().when().get(getURL).andReturn();
}
public static <T> T performServerGet(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, public static <T> T performServerGet(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final String getURL, final String jsonAttributeToGetBack) { final String getURL, final String jsonAttributeToGetBack) {
final String json = given().spec(requestSpec).expect().spec(responseSpec).log().ifError().when().get(getURL).andReturn().asString(); 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); return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
} }
public static Response performServerPutRaw(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final String putURL, Function<RequestSpecification, RequestSpecification> bodyMapper) {
return bodyMapper.apply(given().spec(requestSpec)).expect().spec(responseSpec).log().ifError().when().put(putURL).andReturn();
}
public static String performServerPut(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, public static String performServerPut(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final String putURL, final String jsonBodyToSend) { final String putURL, final String jsonBodyToSend) {
return performServerPut(requestSpec, responseSpec, putURL, jsonBodyToSend, null); return performServerPut(requestSpec, responseSpec, putURL, jsonBodyToSend, null);