Merge branch 'develop'

This commit is contained in:
Keith Woodlock
2013-06-10 14:54:47 +01:00
216 changed files with 8863 additions and 3796 deletions
+546 -38
View File
@@ -11,7 +11,7 @@
var result = {};
if (window.location.search) {
// split up the query string and store in an associative array
var params = window.location.search.slice(1).split("&"); x
var params = window.location.search.slice(1).split("&");
for ( var i = 0; i < params.length; i++) {
var tmp = params[i].split("=");
result[tmp[0]] = unescape(tmp[1]);
@@ -90,7 +90,7 @@
<body>
<div id="page-wrapper">
<div id="flybar">
<div id="nav-logo">1.2.1.RELEASE API</div>
<div id="nav-logo">1.3.0.RELEASE API</div>
<div class="flybar-nav">
<h2 class="flybar-button">Overview</h2>
<div class="flybar-menu-overview">
@@ -1063,11 +1063,19 @@
<tr>
<td><a href="#datatables">Data Table</a></td>
<td>datatables</td>
<td></td>
<td><a href="#datatables_createTable">Create Data Table</a></td>
<td><a href="#datatables_list">List Data Tables</a></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>datatables/{datatable}</td>
<td></td>
<td><a href="#datatables_getTable">Retrieve Data Table Details</a></td>
<td><a href="#datatables_updateTable">Update Data Table</a></td>
<td><a href="#datatables_deleteTable">Delete Data Table</a></td>
</tr>
<tr class="alt">
<td></td>
<td>datatables/register/{datatable}/{apptable}</td>
@@ -3813,6 +3821,34 @@ GET https://DomainName/mifosng-provider/api/v1/groups
<span>Example Values:</span> 2=Per month, 3=Per year
</td>
</tr>
<tr class=alt>
<td>graceOnPrincipalPayment</td>
</tr>
<tr>
<td class=fielddesc>Optional: Integer - represents the number of repayment periods that grace should apply to the principal component of a repayment period.
</td>
</tr>
<tr class=alt>
<td>graceOnInterestPayment</td>
</tr>
<tr>
<td class=fielddesc>Optional: Integer - represents the number of repayment periods that grace should apply to the interest component of a repayment period. Interest is still calculated but offset to later repayment periods.
</td>
</tr>
<tr class=alt>
<td>graceOnInterestCharged</td>
</tr>
<tr>
<td class=fielddesc>Optional: Integer - represents the number of repayment periods that should be interest-free.
</td>
</tr>
<tr class=alt>
<td>interestChargedFromDate</td>
</tr>
<tr>
<td class=fielddesc>Optional: Date - The date from with interest is to start being charged.</td>
</tr>
<tr class=alt>
<td>expectedDisbursementDate</td>
</tr>
@@ -4841,6 +4877,15 @@ Request Body:
expectedDisbursementDate, submittedOnDate, loanType</td>
</tr>
</table>
<br/>
<table class=matrixHeading>
<tr class="matrixHeadingBG">
<td><div class="mifosXHeading2">Optional Fields</div></td>
</tr>
<tr class=alt>
<td>graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged</td>
</tr>
</table>
</div>
<div class="method-example">
<code class="method-declaration">
@@ -6959,6 +7004,28 @@ Request Body:
</ol>
</td>
</tr>
<tr class=alt>
<td>graceOnPrincipalPayment</td>
</tr>
<tr>
<td class=fielddesc>Optional: Integer - represents the number of repayment periods that grace should apply to the principal component of a repayment period.
</td>
</tr>
<tr class=alt>
<td>graceOnInterestPayment</td>
</tr>
<tr>
<td class=fielddesc>Optional: Integer - represents the number of repayment periods that grace should apply to the interest component of a repayment period. Interest is still calculated but offset to later repayment periods.
</td>
</tr>
<tr class=alt>
<td>graceOnInterestCharged</td>
</tr>
<tr>
<td class=fielddesc>Optional: Integer - represents the number of repayment periods that should be interest-free.
</td>
</tr>
<tr class=alt>
<td>accountingRule</td>
</tr>
@@ -7516,13 +7583,14 @@ GET https://DomainName/api/v1/loanproducts/{productId}
<td>name, currencyCode, digitsAfterDecimal, principal,
numberOfRepayments, repaymentEvery, repaymentFrequencyType,
interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType,
interestCalculationPeriodType, transactionProcessingStrategyId, accountingRule</td>
interestCalculationPeriodType, transactionProcessingStrategyId,
accountingRule</td>
</tr>
<tr class="matrixHeadingBG">
<td><div class="mifosXHeading2">Optional Fields</div></td>
</tr>
<tr class=alt>
<td>inArrearsTolerance, charges,paymentChannelToFundSourceMappings</td>
<td>inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, charges, paymentChannelToFundSourceMappings</td>
</tr>
<tr class="matrixHeadingBG">
@@ -9426,6 +9494,30 @@ Content-Type: application/json
<tr>
<td class=fielddesc>Credit account for creating accounting rule</td>
</tr>
<tr class=alt>
<td>debitTags</td>
</tr>
<tr>
<td class=fielddesc>Debit tags for creating accounting rule, These tags fetches the all chart of accounts that are tagged with these tagIds</td>
</tr>
<tr class=alt>
<td>creditTags</td>
</tr>
<tr>
<td class=fielddesc>Credit tags for creating accounting rule, These tags fetches the all chart of accounts that are tagged with these tagIds</td>
</tr>
<tr class=alt>
<td>allowMultipleDebitEntries</td>
</tr>
<tr>
<td class=fielddesc>Allows to add multiple debits if the value is true, while creating journal entries with this rule</td>
</tr>
<tr class=alt>
<td>allowMultipleCreditEntries</td>
</tr>
<tr>
<td class=fielddesc>Allows to add multiple credits if the value is true, while creating journal entries with this rule</td>
</tr>
</table>
</div>
</div>
@@ -9537,6 +9629,64 @@ Request Body:
{
"officeId":1,
"resourceId":1
}
</code>
<code class="method-request">
POST accountingrules
Content-Type: application/json
Request Body:
{
"name": "fordocs",
"officeId": "1",
"debitTags": ["10","11"],
"allowMultipleDebitEntries": "true",
"creditTags": ["12","13"],
"allowMultipleCreditEntries": "true",
"description": "for api docs"
}
</code>
<code class="method-response">
{
"officeId":1,
"resourceId":2
}
</code>
<code class="method-request">
POST accountingrules
Content-Type: application/json
Request Body:
{
"name": "test123",
"officeId": "1",
"debitAccountHead": "21",
"creditTags": ["12", "13"],
"allowMultipleCreditEntries": "false",
"description": "Employee salary"
}
</code>
<code class="method-response">
{
"officeId":1,
"resourceId":3
}
</code>
<code class="method-request">
POST accountingrules
Content-Type: application/json
Request Body:
{
"name": "fordocstest",
"officeId": "1",
"debitTags": ["10", "11"],
"allowMultipleDebitEntries": "false",
"creditAccountHead": "9",
"description": "for api docs"
}
</code>
<code class="method-response">
{
"officeId":1,
"resourceId":4
}
</code>
</div>
@@ -9734,6 +9884,23 @@ Content-Type: application/json
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>lockinPeriodFrequency</i> to indicate the length of time that the savings account is 'locked in' and withdrawals are not allowed. 0=Days, 1=Weeks, 2=Months, 3=Years
e.g. 6 <b>Months</b></td></tr>
<tr class=alt><td>withdrawalFeeAmount</td></tr>
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>withdrawalFeeType</i> to indicate the amount to be charged on the account for withdrawing. e.g. <b>5</b> Flat or <b>5</b> % of Amount</td></tr>
<tr class=alt><td>withdrawalFeeType</td></tr>
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>withdrawalFeeAmount</i> to indicate the amount to be charged on the account for withdrawing. 1=Flat, 2=% of Amount, e.g. 5 <b>Flat</b> or 5 <b>% of Amount</b></td></tr>
<tr class=alt><td>annualFeeAmount</td></tr>
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>annualFeeOnMonthDay</i> to indicate the amount to be charged on the account annually on a given day of month. e.g. <b>10</b> on 10 May. <i>monthDayFormat</i> indicates day and month formatting used e.g. "dd MMM" for 10 May
</td>
</tr>
<tr class=alt><td>annualFeeOnMonthDay</td></tr>
<tr>
<td class=fielddesc><b>Optional</b>: If provided, used along with <i>annualFeeAmount</i> and <i>monthDayFormat</i> to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on <b>10 May</b>. <i>monthDayFormat</i> indicates day and month formatting used e.g. "dd MMM" for 10 May
</td>
</tr>
<tr class=alt><td>accountingRule</td></tr>
<tr>
<td class=fielddesc>Specifies if accounting is enabled for the particular
@@ -10252,7 +10419,7 @@ Content-Type: application/json
<br/>
<table class=matrixHeading>
<tr class="matrixHeadingBG"><td><div class="mifosXHeading2">Optional Fields</div></td></tr>
<tr class=alt><td>minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, paymentChannelToFundSourceMappings</tr>
<tr class=alt><td>minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeAmount, withdrawalFeeType, annualFeeAmount, annualFeeOnMonthDay, monthDayFormat, paymentChannelToFundSourceMappings</tr>
</table>
</div>
<div class="method-example">
@@ -10558,6 +10725,23 @@ Content-Type: application/json
<tr class=alt><td>lockinPeriodFrequencyType</td></tr>
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>lockinPeriodFrequency</i> to indicate the length of time that the savings account is 'locked in' and withdrawals are not allowed. 0=Days, 1=Weeks, 2=Months, 3=Years
e.g. 6 <b>Months</b></td></tr>
<tr class=alt><td>withdrawalFeeAmount</td></tr>
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>withdrawalFeeType</i> to indicate the amount to be charged on the account for withdrawing. e.g. <b>5</b> Flat or <b>5</b> % of Amount</td></tr>
<tr class=alt><td>withdrawalFeeType</td></tr>
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>withdrawalFeeAmount</i> to indicate the amount to be charged on the account for withdrawing. 1=Flat, 2=% of Amount, e.g. 5 <b>Flat</b> or 5 <b>% of Amount</b></td></tr>
<tr class=alt><td>annualFeeAmount</td></tr>
<tr><td class=fielddesc><b>Optional</b>: If provided, used along with <i>annualFeeOnMonthDay</i> to indicate the amount to be charged on the account annually on a given day of month. e.g. <b>10</b> on 10 May. <i>monthDayFormat</i> indicates day and month formatting used e.g. "dd MMM" for 10 May
</td>
</tr>
<tr class=alt><td>annualFeeOnMonthDay</td></tr>
<tr>
<td class=fielddesc><b>Optional</b>: If provided, used along with <i>annualFeeAmount</i> and <i>monthDayFormat</i> to indicate the amount to be charged on the account annually on a given day of month. e.g. 10 on <b>10 May</b>. <i>monthDayFormat</i> indicates day and month formatting used e.g. "dd MMM" for 10 May
</td>
</tr>
</table>
</div>
</div>
@@ -10758,7 +10942,9 @@ Content-Type: application/json
<br/>
<table class=matrixHeading>
<tr class="matrixHeadingBG"><td><div class="mifosXHeading2">Inherited from Product (if not provided)</div></td></tr>
<tr class=alt><td>nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType</td></tr>
<tr class=alt><td>nominalAnnualInterestRate, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency,
lockinPeriodFrequencyType, withdrawalFeeAmount, withdrawalFeeType, annualFeeAmount, annualFeeOnMonthDay</td>
</tr>
</table>
</div>
<div class="method-example">
@@ -11240,6 +11426,57 @@ No Request Body:
</div>
</div>
<a id="datatables_createTable" name="datatables_createTable"
class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Create Data Table</h4>
<p>Create a new data table and registers it with the Mifos X Core application
table.</p>
</div>
<div class="method-example">
<code class="method-declaration">
POST https://DomainName/api/v1/datatables
</code>
<code class="method-request">
POST https://DomainName/api/v1/datatables
Content-Type: application/json
Request Body:
{
"datatableName": "extra_client_details",
"apptableName": "m_client",
"columns": [
{
"name": "Gender",
"type": "Dropdown",
"code": "Gender"
},
{
"name": "Some Decimal",
"type": "Decimal",
"mandatory": true
},
{
"name": "Birth Date",
"type": "Date"
},
{
"name": "Question 2",
"type": "String",
"length": 100,
"mandatory": false
}
]
}
</code>
<code class="method-response">
{
"resourceIdentifier": "extra_client_details"
}
</code>
</div>
</div>
<a id="datatables_list" name="datatables_list"
class="old-syle-anchor">&nbsp;</a>
@@ -11267,25 +11504,191 @@ No Request Body:
<code class="method-declaration">
GET https://DomainName/api/v1/datatables
</code>
<code class="method-response">
<code class="method-response">
[
{
"applicationTableName": "m_client",
"registeredTableName": "Additional Client Fields Data"
},
{
"applicationTableName": "m_client",
"registeredTableName": "Extra Family Details Data"
},
{
"applicationTableName": "m_loan",
"registeredTableName": "Additional Loan Fields Data"
}
]
{
"applicationTableName": "m_client",
"registeredTableName": "extra_client_details",
"columnHeaderData": [
{
"columnName": "client_id",
"columnType": "bigint",
"columnLength": 0,
"columnDisplayType": "INTEGER",
"isColumnNullable": false,
"isColumnPrimaryKey": true,
"columnValues": []
},
{
"columnName": "Gender_cd_Question",
"columnType": "int",
"columnLength": 0,
"columnDisplayType": "INTEGER",
"isColumnNullable": true,
"isColumnPrimaryKey": false,
"columnValues": []
},
{
"columnName": "Some Decimal",
"columnType": "decimal",
"columnLength": 0,
"columnDisplayType": "DECIMAL",
"isColumnNullable": true,
"isColumnPrimaryKey": false,
"columnValues": []
},
{
"columnName": "Birth Date",
"columnType": "date",
"columnLength": 0,
"columnDisplayType": "DATE",
"isColumnNullable": true,
"isColumnPrimaryKey": false,
"columnValues": []
}
]
}
]
</code>
</div>
</div>
<a id="datatables_getTable" name="datatables_getTable"
class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Retrieve Data Table Details</h4>
<p>Lists a registered data table details and the Mifos X Core
application table they are registered to.</p>
</div>
<div class="method-example">
<code class="method-declaration">
GET https://DomainName/api/v1/datatables/{datatable}
</code>
<code class="method-response">
{
"applicationTableName": "m_client",
"registeredTableName": "extra_client_details",
"columnHeaderData": [
{
"columnName": "client_id",
"columnType": "bigint",
"columnLength": 0,
"columnDisplayType": "INTEGER",
"isColumnNullable": false,
"isColumnPrimaryKey": true,
"columnValues": []
},
{
"columnName": "Gender_cd_Question",
"columnType": "int",
"columnLength": 0,
"columnDisplayType": "INTEGER",
"isColumnNullable": true,
"isColumnPrimaryKey": false,
"columnValues": []
},
{
"columnName": "Some Decimal",
"columnType": "decimal",
"columnLength": 0,
"columnDisplayType": "DECIMAL",
"isColumnNullable": true,
"isColumnPrimaryKey": false,
"columnValues": []
},
{
"columnName": "Birth Date",
"columnType": "date",
"columnLength": 0,
"columnDisplayType": "DATE",
"isColumnNullable": true,
"isColumnPrimaryKey": false,
"columnValues": []
}
]
}
</code>
</div>
</div>
<a id="datatables_updateTable" name="datatables_updateTable"
class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Update Data Table</h4>
<p>Modifies fields of a data table. If the apptableName parameter is passed,
data table is deregistered and registered with the new application table.</p>
</div>
<div class="method-example">
<code class="method-declaration">
PUT https://DomainName/api/v1/datatables/{datatables}
</code>
<code class="method-request">
PUT https://DomainName/api/v1/datatables/extra_client_details
Content-Type: application/json
Request Body:
{
"apptableName": "m_client",
"columns": [
{
"name": "Gender",
"type": "Dropdown",
"code": "Gender"
},
{
"name": "Some Decimal",
"type": "Decimal",
"mandatory": true
},
{
"name": "Birth Date",
"type": "Date"
},
{
"name": "Question 2",
"type": "String",
"length": 100,
"mandatory": false
}
]
}
</code>
<code class="method-response">
{
"resourceIdentifier": "extra_client_details"
}
</code>
</div>
</div>
<a id="datatables_deleteTable" name="datatables_deleteTable"
class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Delete Data Table</h4>
<p>Deletes a data table and unregisters it from the Mifos X Core
application table.</p>
</div>
<div class="method-example">
<code class="method-declaration">
DELETE https://DomainName/api/v1/datatables/{datatables}
</code>
<code class="method-request">
DELETE https://DomainName/api/v1/datatables/extra_client_details
Content-Type: application/json
No Request Body
</code>
<code class="method-response">
{
"resourceIdentifier": "extra_client_details"
}
</code>
</div>
</div>
<a id="datatables_register" name="datatables_register"
class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
@@ -12720,8 +13123,7 @@ GET https://DomainName/api/v1/runreports/{reportName}
</div>
</div>
<a id="authenticate_request" name="authenticate_request"
class="old-syle-anchor">&nbsp;</a>
<a id="authenticate_request" name="authenticate_request" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Verify authentication</h4>
@@ -12736,24 +13138,60 @@ POST authentication?username=mifos&password=password
Content-Type: application/json
No Request Body
</code>
<p>Example response of autentication for user that is not linked with any staff.</p>
<code class="method-response">
{
"username": "mifos",
"userId": 1,
"base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=",
"authenticated": true,
"roles": [
{
"id": 1,
"name": "Super user",
"description": "This role provides all application permissions."
}
],
"permissions": [
"ALL_FUNCTIONS"
]
"username": "mifos",
"userId": 1,
"base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=",
"authenticated": true,
"officeId": 1,
"officeName": "Head Office",
"roles": [
{
"id": 1,
"name": "Super user",
"description": "This role provides all application permissions."
}
],
"permissions": [
"ALL_FUNCTIONS"
]
}
</code>
<p>Example response of autentication for user that is linked with a staff member and role.</p>
<code class="method-response">
{
"username": "mifos",
"userId": 1,
"base64EncodedAuthenticationKey": "bWlmb3M6cGFzc3dvcmQ=",
"authenticated": true,
"officeId": 1,
"officeName": "Head Office",
"staffId": 1,
"staffDisplayName": "Director, Program",
"organisationalRole": {
"id": 100,
"code": "staffOrganisationalRoleType.programDirector",
"value": "Program Director"
},
"roles": [
{
"id": 1,
"name": "Super user",
"description": "This role provides all application permissions."
}
],
"permissions": [
"ALL_FUNCTIONS"
]
}
</code>
<code class="method-request">
POST authentication?username=mifos&password=fail
Content-Type: application/json
@@ -14534,6 +14972,68 @@ Request Body:
{
"officeId":1,
"transactionId":"RS9MCISID4WK1ZM"
}
</code>
<code class="method-declaration">
POST https://DomainName/api/v1/journalentries
</code>
<code class="method-request">
POST journalentries
Content-Type: application/json
Request Body:
{
"locale": "en",
"dateFormat": "dd MMMM yyyy",
"officeId": "1",
"transactionDate": "23 May 2013",
"referenceNumber": "547",
"comments": "asdf",
"accountingRule": "2",
"credits":[{"glAccountId":1,
"amount":5000},
{"glAccountId":2,
"amount":5000}
],
"debits":[{"glAccountId":3,
"amount":5000},
{"glAccountId":4,
"amount":5000}
]
}
</code>
<code class="method-response">
{
"officeId":1,
"transactionId":"RS9MCISID4WK1ZM"
}
</code>
<code class="method-declaration">
POST https://DomainName/api/v1/journalentries
</code>
<code class="method-request">
POST journalentries
Content-Type: application/json
Request Body:
{
"locale": "en",
"dateFormat": "dd MMMM yyyy",
"officeId": "1",
"transactionDate": "23 May 2013",
"referenceNumber": "547",
"comments": "asdf",
"accountingRule": "3",
"credits":[{"glAccountId":1,
"amount":5000},
{"glAccountId":2,
"amount":5000}
],
"amount": "10000"
}
</code>
<code class="method-response">
{
"officeId":1,
"transactionId":"RS9MCISID4WK1ZM"
}
</code>
</div>
@@ -15286,11 +15786,19 @@ GET https://DomainName/api/v1/search?query=000000111
<tr class="alt">
<td><a href="#datatables">Data Table</a></td>
<td>datatables</td>
<td></td>
<td><a href="#datatables_createTable">Create Data Table</a></td>
<td><a href="#datatables_list">List Data Tables</a></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>datatables/{datatable}</td>
<td></td>
<td><a href="#datatables_getTable">Retrieve Data Table Details</a></td>
<td><a href="#datatables_updateTable">Update Data Table</a></td>
<td><a href="#datatables_deleteTable">Delete Data Table</a></td>
</tr>
<tr>
<td></td>
<td>datatables/register/{datatable}/{apptable}</td>
@@ -0,0 +1,11 @@
create table `acc_rule_tags` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`acc_rule_id` BIGINT(20) NOT NULL,
`tag_id` INT(11) NOT NULL,
`acc_type_enum` SMALLINT(5) NOT NULL,
primary key(`id`),
INDEX `FK_acc_accounting_rule_id` (`acc_rule_id`),
INDEX `FK_m_code_value_id` (`tag_id`),
CONSTRAINT `FK_acc_accounting_rule_id` FOREIGN KEY (`acc_rule_id`) REFERENCES `acc_accounting_rule` (`id`),
CONSTRAINT `FK_m_code_value_id` FOREIGN KEY (`tag_id`) REFERENCES `m_code_value` (`id`)
);
@@ -0,0 +1,10 @@
INSERT INTO `m_permission` (`grouping`, `code`, `entity_name`, `action_name`, `can_maker_checker`)
VALUES
('configuration', 'CREATE_DATATABLE', 'DATATABLE', 'CREATE', 0),
('configuration', 'CREATE_DATATABLE_CHECKER', 'DATATABLE', 'CREATE', 0),
('configuration', 'UPDATE_DATATABLE', 'DATATABLE', 'UPDATE', 0),
('configuration', 'UPDATE_DATATABLE_CHECKER', 'DATATABLE', 'UPDATE', 0),
('configuration', 'DELETE_DATATABLE', 'DATATABLE', 'DELETE', 0),
('configuration', 'DELETE_DATATABLE_CHECKER', 'DATATABLE', 'DELETE', 0);
@@ -0,0 +1,14 @@
ALTER TABLE `m_appuser`
ADD COLUMN `staff_id` BIGINT(20) NULL DEFAULT NULL AFTER `office_id` ;
ALTER TABLE m_appuser
ADD CONSTRAINT `fk_m_appuser_002`
FOREIGN KEY (`staff_id`)
REFERENCES m_staff (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION
,ADD INDEX `fk_m_appuser_002x` (`staff_id` ASC);
ALTER TABLE `m_staff`
ADD COLUMN `organisational_role_enum` SMALLINT NULL DEFAULT NULL AFTER `external_id`,
ADD COLUMN `organisational_role_parent_staff_id` BIGINT(20) NULL DEFAULT NULL AFTER `organisational_role_enum`;
+78
View File
@@ -0,0 +1,78 @@
INSERT INTO c_configuration (name,enabled) VALUES ('amazon-S3',0);
/*New table for storing details of external services used*/
CREATE TABLE `c_external_service` (
`name` VARCHAR(150) NOT NULL,
`value` VARCHAR(250) NULL DEFAULT NULL,
UNIQUE INDEX `name` (`name`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
INSERT INTO c_external_service (name) VALUES ('s3_bucket_name');
INSERT INTO c_external_service (name) VALUES ('s3_access_key');
INSERT INTO c_external_service (name) VALUES ('s3_secret_key');
/*Image tables stores details of all images*/
CREATE TABLE IF NOT EXISTS `m_image`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`location` varchar(500),
`storage_type_enum` SMALLINT(5),
PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
/*Client points to image table*/
ALTER TABLE `m_client`
ADD COLUMN `image_id` BIGINT(20) NULL DEFAULT NULL AFTER `display_name`,
ADD CONSTRAINT `FK_m_client_m_image` FOREIGN KEY (`image_id`) REFERENCES `m_image` (`id`);
/*Move existing image locations to new table*/
drop procedure if exists migrate_customer_image_data;
delimiter #
create procedure migrate_customer_image_data()
begin
DECLARE v_counter INT DEFAULT 0;
DECLARE num_of_clients INT DEFAULT 0;
DECLARE curr_image INT DEFAULT 0;
DECLARE prev_image INT DEFAULT 0;
SELECT COUNT(*) FROM m_client INTO num_of_clients;
SET @curr_client := 0;
while v_counter < num_of_clients do
SET @s = CONCAT('INSERT INTO m_image(`location`,`storage_type_enum`) select image_key, 1 FROM m_client where image_key is not null LIMIT ', v_counter , ', ', 1);
PREPARE stmt1 FROM @s;
EXECUTE stmt1;
DEALLOCATE PREPARE stmt1;
select ifnull(max(`id`),0) from m_image INTO curr_image;
SET @z = CONCAT('select id INTO @curr_client FROM m_client where image_key is not null limit ', v_counter , ', ', 1);
PREPARE stmt2 FROM @z;
EXECUTE stmt2;
IF (prev_image != curr_image) THEN UPDATE m_client set image_id = curr_image where id=@curr_client;
END IF;
set prev_image=curr_image;
set v_counter=v_counter+1;
end while;
delete from m_image where `location` is null;
end#
delimiter ;
call migrate_customer_image_data();
/*Now drop the procedure*/
drop procedure if exists migrate_customer_image_data;
/*Delete image key from client*/
ALTER TABLE `m_client` DROP COLUMN `image_key`;
/*Add storage type for m_document table and update existing documents to file storage*/
ALTER TABLE m_document ADD COLUMN storage_type_enum SMALLINT(5);
UPDATE m_document set storage_type_enum=1;
@@ -0,0 +1,14 @@
CREATE TABLE `m_loan_charge_paid_by` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`loan_transaction_id` BIGINT(20) NOT NULL,
`loan_charge_id` BIGINT(20) NOT NULL,
`amount` DECIMAL(19,6) NOT NULL,
PRIMARY KEY (`id`),
INDEX `FK__m_loan_transaction` (`loan_transaction_id`),
INDEX `FK__m_loan_charge` (`loan_charge_id`),
CONSTRAINT `FK__m_loan_charge` FOREIGN KEY (`loan_charge_id`) REFERENCES `m_loan_charge` (`id`),
CONSTRAINT `FK__m_loan_transaction` FOREIGN KEY (`loan_transaction_id`) REFERENCES `m_loan_transaction` (`id`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
@@ -0,0 +1,9 @@
ALTER TABLE `m_product_loan`
ADD COLUMN `grace_on_principal_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `max_number_of_repayments`,
ADD COLUMN `grace_on_interest_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_principal_periods`,
ADD COLUMN `grace_interest_free_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_interest_periods`;
ALTER TABLE `m_loan`
ADD COLUMN `grace_on_principal_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `number_of_repayments`,
ADD COLUMN `grace_on_interest_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_principal_periods`,
ADD COLUMN `grace_interest_free_periods` SMALLINT(5) NULL DEFAULT NULL AFTER `grace_on_interest_periods`;
@@ -0,0 +1,5 @@
ALTER TABLE `m_loan_repayment_schedule`
ADD COLUMN `obligations_met_on_date` DATE NULL DEFAULT NULL AFTER `completed_derived`;
ALTER TABLE `m_loan_repayment_schedule`
CHANGE COLUMN `interest_waived_derived` `interest_waived_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `interest_writtenoff_derived` ;
@@ -0,0 +1,6 @@
ALTER TABLE `acc_accounting_rule`
ADD COLUMN `allow_multiple_debits` TINYINT(1) NOT NULL DEFAULT '0' AFTER `debit_account_id`,
ADD COLUMN `allow_multiple_credits` TINYINT(1) NOT NULL DEFAULT '0' AFTER `credit_account_id`;
ALTER TABLE acc_rule_tags
ADD UNIQUE KEY `UNIQUE_ACCOUNT_RULE_TAGS` (`acc_rule_id`,`tag_id`,`acc_type_enum`);
@@ -0,0 +1,3 @@
ALTER TABLE `m_loan_repayment_schedule`
ADD COLUMN `total_paid_in_advance_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `penalty_charges_waived_derived`,
ADD COLUMN `total_paid_late_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `total_paid_in_advance_derived`;
@@ -0,0 +1,3 @@
ALTER TABLE `acc_product_mapping`
ADD COLUMN `charge_id` BIGINT(20) NULL DEFAULT NULL AFTER `payment_type`,
ADD CONSTRAINT `FK_acc_product_mapping_m_charge` FOREIGN KEY (`charge_id`) REFERENCES `m_charge` (`id`);
@@ -0,0 +1,12 @@
ALTER TABLE `ref_loan_transaction_processing_strategy`
DROP COLUMN `lastmodified_date` ,
DROP COLUMN `lastmodifiedby_id` ,
DROP COLUMN `created_date` ,
DROP COLUMN `createdby_id` ;
INSERT INTO `ref_loan_transaction_processing_strategy` (`id`, `code`, `name`) VALUES
(5,'principal-interest-penalties-fees-order-strategy', 'Principal Interest Penalties Fees Order');
INSERT INTO `ref_loan_transaction_processing_strategy` (`id`,`code`, `name`)
VALUES (6,'interest-principal-penalties-fees-order-strategy', 'Interest Principal Penalties Fees Order');
@@ -0,0 +1,2 @@
ALTER TABLE `m_loan`
ADD COLUMN `total_overpaid_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `total_outstanding_derived`;
+2 -1
View File
@@ -160,7 +160,8 @@ dependencies {
[group: 'com.lowagie', name: 'itext-rtf', version: '2.1.7'],
[group: 'org.mnode.ical4j', name: 'ical4j', version: '1.0.4'],
[group: 'com.googlecode.flyway', name: 'flyway-maven-plugin', version: '2.1.1'],
[group: 'org.quartz-scheduler', name: 'quartz', version: '2.1.7']
[group: 'org.quartz-scheduler', name: 'quartz', version: '2.1.7'],
[group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.2.1']
)
testCompile 'junit:junit:4.11',
+1 -1
View File
@@ -1 +1 @@
releaseVersion=1.2.1.RELEASE
releaseVersion=1.3.0.RELEASE
@@ -9,10 +9,10 @@ import java.util.HashMap;
import org.junit.Before;
import org.junit.Test;
import org.mifosplatform.integrationtests.common.ClientHelper;
import org.mifosplatform.integrationtests.common.Utils;
import org.mifosplatform.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.mifosplatform.integrationtests.common.loans.LoanProductTestBuilder;
import org.mifosplatform.integrationtests.common.loans.LoanTransactionHelper;
import org.mifosplatform.integrationtests.common.Utils;
import com.jayway.restassured.builder.RequestSpecBuilder;
import com.jayway.restassured.builder.ResponseSpecBuilder;
@@ -30,6 +30,7 @@ public class ClientLoanIntegrationTest {
private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
private LoanTransactionHelper loanTransactionHelper;
@Before
public void setup() {
Utils.initializeRESTAssured();
@@ -40,7 +41,7 @@ public class ClientLoanIntegrationTest {
@Test
public void checkClientLoanCreateAndDisburseFlow() {
loanTransactionHelper = new LoanTransactionHelper(requestSpec,responseSpec);
loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
Integer clientID = ClientHelper.createClient(requestSpec, responseSpec);
ClientHelper.verifyClientCreatedOnServer(requestSpec, responseSpec, clientID);
@@ -53,21 +54,35 @@ public class ClientLoanIntegrationTest {
private Integer createLoanProduct() {
System.out.println("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
String loanProductJSON = new LoanProductTestBuilder().withPrincipal("12,000.00").withNumberOfRepayments("4")
.withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("2")
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
.withinterestRatePerPeriod("1").build();
String loanProductJSON = new LoanProductTestBuilder() //
.withPrincipal("12,000.00") //
.withNumberOfRepayments("4") //
.withRepaymentAfterEvery("1") //
.withRepaymentTypeAsMonth() //
.withinterestRatePerPeriod("1") //
.withInterestRateFrequencyTypeAsMonths() //
.withAmortizationTypeAsEqualInstallments() //
.withInterestTypeAsDecliningBalance() //
.build();
return loanTransactionHelper.getLoanProductId(loanProductJSON);
}
private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID) {
System.out.println("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("12,000.00").withLoanTermFrequency("4")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("4").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2")
.withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("20 September 2011")
.withSubmittedOnDate("20 September 2011").build(clientID.toString(), loanProductID.toString());
String loanApplicationJSON = new LoanApplicationTestBuilder() //
.withPrincipal("12,000.00") //
.withLoanTermFrequency("4") //
.withLoanTermFrequencyAsMonths() //
.withNumberOfRepayments("4") //
.withRepaymentEveryAfter("1") //
.withRepaymentFrequencyTypeAsMonths() //
.withInterestRatePerPeriod("2") //
.withAmortizationTypeAsEqualInstallments() //
.withInterestTypeAsDecliningBalance() //
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod() //
.withExpectedDisbursementDate("20 September 2011") //
.withSubmittedOnDate("20 September 2011") //
.build(clientID.toString(), loanProductID.toString());
return loanTransactionHelper.getLoanId(loanApplicationJSON);
}
@@ -81,17 +96,17 @@ public class ClientLoanIntegrationTest {
assertEquals("Checking for Due Date for 2nd Month", new ArrayList<Integer>(Arrays.asList(2011, 11, 20)),
loanSchedule.get(2).get("dueDate"));
assertEquals("Checking for Principal Due for 2nd Month", new Float("2969.72"), loanSchedule.get(2).get("principalDue"));
assertEquals("Checking for Principal Due for 2nd Month", new Float("2969.71"), loanSchedule.get(2).get("principalDue"));
assertEquals("Checking for Interest Due for 2nd Month", new Float("181.77"), loanSchedule.get(2).get("interestOriginalDue"));
assertEquals("Checking for Due Date for 3rd Month", new ArrayList<Integer>(Arrays.asList(2011, 12, 20)),
loanSchedule.get(3).get("dueDate"));
assertEquals("Checking for Principal Due for 3rd Month", new Float("3029.11"), loanSchedule.get(3).get("principalDue"));
assertEquals("Checking for Principal Due for 3rd Month", new Float("3029.1"), loanSchedule.get(3).get("principalDue"));
assertEquals("Checking for Interest Due for 3rd Month", new Float("122.38"), loanSchedule.get(3).get("interestOriginalDue"));
assertEquals("Checking for Due Date for 4th Month", new ArrayList<Integer>(Arrays.asList(2012, 1, 20)),
loanSchedule.get(4).get("dueDate"));
assertEquals("Checking for Principal Due for 4th Month", new Float("3089.68"), loanSchedule.get(4).get("principalDue"));
assertEquals("Checking for Principal Due for 4th Month", new Float("3089.7"), loanSchedule.get(4).get("principalDue"));
assertEquals("Checking for Interest Due for 4th Month", new Float("61.79"), loanSchedule.get(4).get("interestOriginalDue"));
}
}
@@ -31,7 +31,7 @@ public class LoanApplicationTestBuilder {
private final String transactionProcessingID = MIFOS_STANDARD_STRATEGY;
private String expectedDisbursmentDate = "";
private String submittedOnDate = "";
private String loanType = "individual"; //default it to individual
private String loanType = "individual";
public String build(final String clientID, final String loanProductId) {
@@ -42,7 +42,7 @@ public class LoanApplicationTestBuilder {
map.put("productId", loanProductId);
map.put("principal", principal);
map.put("loanTermFrequency", loanTermFrequency);
map.put("loanTermFrequencyType", loanTermFrequencyType); // FIXME - Should this be using
map.put("loanTermFrequencyType", loanTermFrequencyType);
map.put("numberOfRepayments", numberOfRepayment);
map.put("repaymentEvery", repaymentPeriod);
map.put("repaymentFrequencyType", repaymentFrequencyType);
@@ -53,7 +53,7 @@ public class LoanApplicationTestBuilder {
map.put("transactionProcessingStrategyId", transactionProcessingID);
map.put("expectedDisbursementDate", expectedDisbursmentDate);
map.put("submittedOnDate", submittedOnDate);
map.put("loanType", "individual");
map.put("loanType", loanType);
return new Gson().toJson(map);
}
@@ -85,7 +85,8 @@ public class AccountingConstants {
"incomeFromFeeAccountId"), INCOME_FROM_PENALTIES("incomeFromPenaltyAccountId"), LOSSES_WRITTEN_OFF("writeOffAccountId"), INTEREST_RECEIVABLE(
"receivableInterestAccountId"), FEES_RECEIVABLE("receivableFeeAccountId"), PENALTIES_RECEIVABLE(
"receivablePenaltyAccountId"), PAYMENT_CHANNEL_FUND_SOURCE_MAPPING("paymentChannelToFundSourceMappings"), PAYMENT_TYPE(
"paymentTypeId");
"paymentTypeId"), FEE_INCOME_ACCOUNT_MAPPING("feeToIncomeAccountMappings"), PENALTY_INCOME_ACCOUNT_MAPPING(
"penaltyToIncomeAccountMappings"), CHARGE_ID("chargeId"), INCOME_ACCOUNT_ID("incomeAccountId");
private final String value;
@@ -50,9 +50,10 @@ import org.springframework.stereotype.Component;
public class GLAccountsApiResource {
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("id", "name", "parentId", "glCode",
"disabled", "manualEntriesAllowed", "type", "usage", "description", "assetHeaderAccountOptions", "liabilityHeaderAccountOptions",
"equityHeaderAccountOptions", "incomeHeaderAccountOptions", "expenseHeaderAccountOptions", "nameDecorated", "tagId",
"allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions", "allowedIncomeTagOptions", "allowedExpensesTagOptions"));
"disabled", "manualEntriesAllowed", "type", "usage", "description", "assetHeaderAccountOptions",
"liabilityHeaderAccountOptions", "equityHeaderAccountOptions", "incomeHeaderAccountOptions", "expenseHeaderAccountOptions",
"nameDecorated", "tagId", "allowedAssetsTagOptions", "allowedLiabilitiesTagOptions", "allowedEquityTagOptions",
"allowedIncomeTagOptions", "allowedExpensesTagOptions", "creditAccounts", "debitAccounts"));
private final String resourceNameForPermission = "GLACCOUNT";
@@ -63,12 +64,13 @@ public class GLAccountsApiResource {
private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final CodeValueReadPlatformService codeValueReadPlatformService;
@Autowired
public GLAccountsApiResource(final PlatformSecurityContext context, final GLAccountReadPlatformService glAccountReadPlatformService,
final DefaultToApiJsonSerializer<GLAccountData> toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
final AccountingDropdownReadPlatformService dropdownReadPlatformService, final CodeValueReadPlatformService codeValueReadPlatformService) {
final AccountingDropdownReadPlatformService dropdownReadPlatformService,
final CodeValueReadPlatformService codeValueReadPlatformService) {
this.context = context;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
@@ -82,7 +84,7 @@ public class GLAccountsApiResource {
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveNewAccountDetails(@Context final UriInfo uriInfo, @QueryParam("type") final Integer type) {
public String retrieveNewAccountDetails(@Context final UriInfo uriInfo, @QueryParam("type") final Integer type) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
@@ -167,22 +169,32 @@ public class GLAccountsApiResource {
private GLAccountData handleTemplate(final GLAccountData glAccountData) {
final List<EnumOptionData> accountTypeOptions = this.dropdownReadPlatformService.retrieveGLAccountTypeOptions();
final List<EnumOptionData> usageOptions = this.dropdownReadPlatformService.retrieveGLAccountUsageOptions();
final List<GLAccountData> assetHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.ASSET));
final List<GLAccountData> liabilityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.LIABILITY));
final List<GLAccountData> equityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EQUITY));
final List<GLAccountData> incomeHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.INCOME));
final List<GLAccountData> expenseHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EXPENSE));
final Collection<CodeValueData> allowedAssetsTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedLiabilitiesTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedEquityTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedIncomeTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedExpensesTagOptions = this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME);
final List<GLAccountData> assetHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService
.retrieveAllEnabledHeaderGLAccounts(GLAccountType.ASSET));
final List<GLAccountData> liabilityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService
.retrieveAllEnabledHeaderGLAccounts(GLAccountType.LIABILITY));
final List<GLAccountData> equityHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService
.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EQUITY));
final List<GLAccountData> incomeHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService
.retrieveAllEnabledHeaderGLAccounts(GLAccountType.INCOME));
final List<GLAccountData> expenseHeaderAccountOptions = defaultIfEmpty(this.glAccountReadPlatformService
.retrieveAllEnabledHeaderGLAccounts(GLAccountType.EXPENSE));
final Collection<CodeValueData> allowedAssetsTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedLiabilitiesTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedEquityTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedIncomeTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME);
final Collection<CodeValueData> allowedExpensesTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME);
return new GLAccountData(glAccountData, accountTypeOptions, usageOptions, assetHeaderAccountOptions, liabilityHeaderAccountOptions,
equityHeaderAccountOptions, incomeHeaderAccountOptions, expenseHeaderAccountOptions, allowedAssetsTagOptions,
equityHeaderAccountOptions, incomeHeaderAccountOptions, expenseHeaderAccountOptions, allowedAssetsTagOptions,
allowedLiabilitiesTagOptions, allowedEquityTagOptions, allowedIncomeTagOptions, allowedExpensesTagOptions);
}
private List<GLAccountData> defaultIfEmpty(final List<GLAccountData> list) {
List<GLAccountData> returnList = null;
if (list != null && !list.isEmpty()) {
@@ -190,4 +202,4 @@ public class GLAccountsApiResource {
}
return returnList;
}
}
}
@@ -71,17 +71,17 @@ public class GLAccountData {
this.expenseHeaderAccountOptions = null;
this.allowedAssetsTagOptions = null;
this.allowedLiabilitiesTagOptions = null;
this.allowedEquityTagOptions=null;
this.allowedIncomeTagOptions=null;
this.allowedExpensesTagOptions=null;
this.allowedEquityTagOptions = null;
this.allowedIncomeTagOptions = null;
this.allowedExpensesTagOptions = null;
}
public GLAccountData(final GLAccountData accountData, final List<EnumOptionData> accountTypeOptions,
final List<EnumOptionData> usageOptions, final List<GLAccountData> assetHeaderAccountOptions,
final List<GLAccountData> liabilityHeaderAccountOptions, final List<GLAccountData> equityHeaderAccountOptions,
final List<GLAccountData> incomeHeaderAccountOptions, final List<GLAccountData> expenseHeaderAccountOptions,
final Collection<CodeValueData> allowedAssetsTagOptions, final Collection<CodeValueData> allowedLiabilitiesTagOptions,
final Collection<CodeValueData> allowedEquityTagOptions, final Collection<CodeValueData> allowedIncomeTagOptions,
final List<GLAccountData> incomeHeaderAccountOptions, final List<GLAccountData> expenseHeaderAccountOptions,
final Collection<CodeValueData> allowedAssetsTagOptions, final Collection<CodeValueData> allowedLiabilitiesTagOptions,
final Collection<CodeValueData> allowedEquityTagOptions, final Collection<CodeValueData> allowedIncomeTagOptions,
final Collection<CodeValueData> allowedExpensesTagOptions) {
this.id = accountData.id;
this.name = accountData.name;
@@ -116,19 +116,20 @@ public class GLAccountData {
final boolean disabled = false;
final boolean manualEntriesAllowed = true;
final EnumOptionData type;
if(glAccType != null && glAccType >= GLAccountType.getMinValue() && glAccType <= GLAccountType.getMaxValue())
if (glAccType != null && glAccType >= GLAccountType.getMinValue() && glAccType <= GLAccountType.getMaxValue()) {
type = AccountingEnumerations.gLAccountType(glAccType);
else
} else {
type = AccountingEnumerations.gLAccountType(GLAccountType.ASSET);
}
final EnumOptionData usage = AccountingEnumerations.gLAccountUsage(GLAccountUsage.DETAIL);
final String description = null;
final String nameDecorated = null;
final CodeValueData tagId = null;
return new GLAccountData(id, name, parentId, glCode, disabled, manualEntriesAllowed, type, usage, description, nameDecorated, tagId);
}
public GLAccountData(final Long id, final String name, final String glCode) {
this.id = id;
this.id = id;
this.name = name;
this.parentId = null;
this.glCode = glCode;
@@ -148,23 +149,21 @@ public class GLAccountData {
this.expenseHeaderAccountOptions = null;
this.allowedAssetsTagOptions = null;
this.allowedLiabilitiesTagOptions = null;
this.allowedEquityTagOptions=null;
this.allowedIncomeTagOptions=null;
this.allowedExpensesTagOptions=null;
}
this.allowedEquityTagOptions = null;
this.allowedIncomeTagOptions = null;
this.allowedExpensesTagOptions = null;
}
public Long getId() {
return this.id;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getName() {
return this.name;
}
public String getGlCode() {
return this.glCode;
}
public String getGlCode() {
return this.glCode;
}
}
@@ -0,0 +1,26 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.glaccount.data;
public class GLAccountDataForLookup {
private final Long id;
@SuppressWarnings("unused")
private final String name;
@SuppressWarnings("unused")
private final String glCode;
public GLAccountDataForLookup(final Long id, final String name, final String glCode) {
this.id = id;
this.name = name;
this.glCode = glCode;
}
public Long getId() {
return this.id;
}
}
@@ -1,11 +1,17 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.glaccount.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class InvalidParentGLAccountHeadException extends AbstractPlatformDomainRuleException {
public InvalidParentGLAccountHeadException(Long glAccountId, Long parentId) {
super("error.msg.glaccount.id.and.parentid.must.not.same", "parentId:"+parentId+", id"+glAccountId+" should not be same", glAccountId, parentId);
}
public InvalidParentGLAccountHeadException(final Long glAccountId, final Long parentId) {
super("error.msg.glaccount.id.and.parentid.must.not.same", "parentId:" + parentId + ", id" + glAccountId + " should not be same",
glAccountId, parentId);
}
}
@@ -8,6 +8,7 @@ package org.mifosplatform.accounting.glaccount.service;
import java.util.List;
import org.mifosplatform.accounting.glaccount.data.GLAccountData;
import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup;
import org.mifosplatform.accounting.glaccount.domain.GLAccountType;
public interface GLAccountReadPlatformService {
@@ -24,4 +25,6 @@ public interface GLAccountReadPlatformService {
List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(GLAccountType accountType);
GLAccountData retrieveNewGLAccountDetails(final Integer type);
List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType);
}
@@ -13,6 +13,7 @@ import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.accounting.common.AccountingEnumerations;
import org.mifosplatform.accounting.glaccount.data.GLAccountData;
import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup;
import org.mifosplatform.accounting.glaccount.domain.GLAccountType;
import org.mifosplatform.accounting.glaccount.domain.GLAccountUsage;
import org.mifosplatform.accounting.glaccount.exception.GLAccountInvalidClassificationException;
@@ -65,7 +66,7 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe
final String nameDecorated = rs.getString("nameDecorated");
final Long codeId = rs.wasNull() ? null : rs.getLong("codeId");
final String codeValue = rs.getString("codeValue");
CodeValueData tagId = CodeValueData.instance(codeId, codeValue);
final CodeValueData tagId = CodeValueData.instance(codeId, codeValue);
return new GLAccountData(id, name, parentId, glCode, disabled, manualEntriesAllowed, accountType, usage, description,
nameDecorated, tagId);
}
@@ -202,4 +203,27 @@ public class GLAccountReadPlatformServiceImpl implements GLAccountReadPlatformSe
public List<GLAccountData> retrieveAllEnabledHeaderGLAccounts(final GLAccountType accountType) {
return retrieveAllGLAccounts(accountType.getValue(), null, GLAccountUsage.HEADER.getValue(), null, false);
}
@Override
public List<GLAccountDataForLookup> retrieveAccountsByTagId(final Long ruleId, final Integer transactionType) {
final GLAccountDataLookUpMapper mapper = new GLAccountDataLookUpMapper();
final String sql = "Select " + mapper.schema() + " where rule.id=? and tags.acc_type_enum=?";
return this.jdbcTemplate.query(sql, mapper, new Object[] { ruleId, transactionType });
}
private static final class GLAccountDataLookUpMapper implements RowMapper<GLAccountDataForLookup> {
public String schema() {
return " gl.id as id, gl.name as name, gl.gl_code as glCode from acc_accounting_rule rule join acc_rule_tags tags on tags.acc_rule_id = rule.id join acc_gl_account gl on gl.tag_id=tags.tag_id";
}
@Override
public GLAccountDataForLookup mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = JdbcSupport.getLong(rs, "id");
final String name = rs.getString("name");
final String glCode = rs.getString("glCode");
return new GLAccountDataForLookup(id, name, glCode);
}
}
}
@@ -30,8 +30,8 @@ public class JournalEntryCommand {
private final SingleDebitOrCreditEntryCommand[] debits;
public JournalEntryCommand(final Long officeId, final LocalDate transactionDate, final String comments,
final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits,
final String referenceNumber, final Long accountingRuleId, final BigDecimal amount) {
final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits, final String referenceNumber,
final Long accountingRuleId, final BigDecimal amount) {
this.officeId = officeId;
this.transactionDate = transactionDate;
this.comments = comments;
@@ -53,46 +53,42 @@ public class JournalEntryCommand {
baseDataValidator.reset().parameter("officeId").value(this.officeId).notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter("comments").value(this.comments).ignoreIfNull().notExceedingLengthOf(500);
baseDataValidator.reset().parameter("referenceNumber").value(this.referenceNumber).ignoreIfNull().notExceedingLengthOf(100);
baseDataValidator.reset().parameter("accountingRule").value(this.accountingRuleId).ignoreIfNull().longGreaterThanZero();
if (this.accountingRuleId == null) {
baseDataValidator.reset().parameter("credits").value(this.credits).notNull();
baseDataValidator.reset().parameter("debits").value(this.debits).notNull();
// validation for credit array elements
if (this.credits != null) {
if (this.credits.length == 0) {
validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null));
} else {
int i = 0;
for (final SingleDebitOrCreditEntryCommand credit : this.credits) {
validateSingleDebitOrCredit(baseDataValidator, "credits", i, credit);
i++;
}
baseDataValidator.reset().parameter("credits").value(this.credits).ignoreIfNull();
baseDataValidator.reset().parameter("debits").value(this.debits).ignoreIfNull();
// validation for credit array elements
if (this.credits != null) {
if (this.credits.length == 0) {
validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null));
} else {
int i = 0;
for (final SingleDebitOrCreditEntryCommand credit : this.credits) {
validateSingleDebitOrCredit(baseDataValidator, "credits", i, credit);
i++;
}
}
// validation for debit array elements
if (this.debits != null) {
if (this.debits.length == 0) {
validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null));
} else {
int i = 0;
for (final SingleDebitOrCreditEntryCommand debit : this.debits) {
validateSingleDebitOrCredit(baseDataValidator, "debits", i, debit);
i++;
}
}
}
} else {
baseDataValidator.reset().parameter("amount").value(this.amount).notNull().zeroOrPositiveAmount();
}
// validation for debit array elements
if (this.debits != null) {
if (this.debits.length == 0) {
validateSingleDebitOrCredit(baseDataValidator, "credits", 0, new SingleDebitOrCreditEntryCommand(null, null, null, null));
} else {
int i = 0;
for (final SingleDebitOrCreditEntryCommand debit : this.debits) {
validateSingleDebitOrCredit(baseDataValidator, "debits", i, debit);
i++;
}
}
}
baseDataValidator.reset().parameter("amount").value(this.amount).ignoreIfNull().zeroOrPositiveAmount();
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
@@ -134,8 +130,8 @@ public class JournalEntryCommand {
return this.referenceNumber;
}
public Long getAccountingRuleId() {
return this.accountingRuleId;
}
public Long getAccountingRuleId() {
return this.accountingRuleId;
}
}
@@ -0,0 +1,29 @@
package org.mifosplatform.accounting.journalentry.data;
import java.math.BigDecimal;
public class ChargePaymentDTO {
private final Long chargeId;
private final BigDecimal amount;
private final Long loanChargeId;
public ChargePaymentDTO(Long chargeId, Long loanChargeId, BigDecimal amount) {
this.chargeId = chargeId;
this.amount = amount;
this.loanChargeId = loanChargeId;
}
public Long getChargeId() {
return this.chargeId;
}
public BigDecimal getAmount() {
return this.amount;
}
public Long getLoanChargeId() {
return this.loanChargeId;
}
}
@@ -7,6 +7,7 @@ package org.mifosplatform.accounting.journalentry.data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
@@ -28,9 +29,14 @@ public class LoanTransactionDTO {
/*** Boolean values determines if the transaction is reversed ***/
private final boolean reversed;
/** Breakdowns of fees and penalties this Transaction pays **/
private final List<ChargePaymentDTO> penaltyPayments;
private final List<ChargePaymentDTO> feePayments;
public LoanTransactionDTO(final Long paymentTypeId, final String transactionId, final Date transactionDate,
final LoanTransactionEnumData transactionType, final BigDecimal amount, final BigDecimal principal, final BigDecimal interest,
final BigDecimal fees, final BigDecimal penalties, final boolean reversed) {
final BigDecimal fees, final BigDecimal penalties, final boolean reversed, final List<ChargePaymentDTO> feePayments,
final List<ChargePaymentDTO> penaltyPayments) {
this.paymentTypeId = paymentTypeId;
this.transactionId = transactionId;
this.transactionDate = transactionDate;
@@ -41,6 +47,8 @@ public class LoanTransactionDTO {
this.penalties = penalties;
this.reversed = reversed;
this.transactionType = transactionType;
this.feePayments = feePayments;
this.penaltyPayments = penaltyPayments;
}
public String getTransactionId() {
@@ -83,4 +91,12 @@ public class LoanTransactionDTO {
return this.paymentTypeId;
}
public List<ChargePaymentDTO> getPenaltyPayments() {
return this.penaltyPayments;
}
public List<ChargePaymentDTO> getFeePayments() {
return this.feePayments;
}
}
@@ -73,7 +73,7 @@ public class JournalEntry extends AbstractAuditableCustom<AppUser, Long> {
@SuppressWarnings("unused")
@Column(name = "entity_id")
private Long entityId;
@Column(name = "ref_num")
private String referenceNumber;
@@ -104,7 +104,7 @@ public class JournalEntry extends AbstractAuditableCustom<AppUser, Long> {
this.entityType = entityType;
this.entityId = entityId;
this.referenceNumber = referenceNumber;
}
public boolean isDebitEntry() {
@@ -139,8 +139,8 @@ public class JournalEntry extends AbstractAuditableCustom<AppUser, Long> {
this.reversed = reversed;
}
public String getReferenceNumber() {
return this.referenceNumber;
}
public String getReferenceNumber() {
return this.referenceNumber;
}
}
@@ -45,4 +45,12 @@ public enum JournalEntryType {
return name().toString();
}
public boolean isDebitType() {
return this.value.equals(JournalEntryType.DEBIT.getValue());
}
public boolean isCreditType() {
return this.value.equals(JournalEntryType.CREDIT.getValue());
}
}
@@ -16,7 +16,7 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce
/*** enum of reasons for invalid Journal Entry **/
public static enum GL_JOURNAL_ENTRY_INVALID_REASON {
FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, GL_ACCOUNT_DISABLED, GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED;
FUTURE_DATE, ACCOUNTING_CLOSED, NO_DEBITS_OR_CREDITS, DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT, DEBIT_CREDIT_SUM_MISMATCH, DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, GL_ACCOUNT_DISABLED, GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED;
public String errorMessage() {
if (name().toString().equalsIgnoreCase("FUTURE_DATE")) {
@@ -25,6 +25,8 @@ public class JournalEntryInvalidException extends AbstractPlatformDomainRuleExce
return "Journal entry cannot be made prior to last account closing date for the branch";
} else if (name().toString().equalsIgnoreCase("NO_DEBITS_OR_CREDITS")) {
return "Journal Entry must have atleast one Debit and one Credit";
} else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT")) {
return "Sum of All Debits OR Credits must equal the Amount for a Journal Entry";
} else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_SUM_MISMATCH")) {
return "Sum of All Debits must equal the sum of all Credits for a Journal Entry";
} else if (name().toString().equalsIgnoreCase("DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY")) {
@@ -8,6 +8,7 @@ package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -18,6 +19,7 @@ import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_
import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_SAVINGS;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.accounting.journalentry.data.ChargePaymentDTO;
import org.mifosplatform.accounting.journalentry.data.LoanDTO;
import org.mifosplatform.accounting.journalentry.data.LoanTransactionDTO;
import org.mifosplatform.accounting.journalentry.data.SavingsDTO;
@@ -30,6 +32,7 @@ import org.mifosplatform.accounting.journalentry.exception.JournalEntryInvalidEx
import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.organisation.office.domain.Office;
import org.mifosplatform.organisation.office.domain.OfficeRepository;
import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData;
@@ -77,8 +80,28 @@ public class AccountingProcessorHelper {
final boolean reversed = (Boolean) map.get("reversed");
final Long paymentTypeId = (Long) map.get("paymentTypeId");
List<ChargePaymentDTO> feePayments = new ArrayList<ChargePaymentDTO>();
List<ChargePaymentDTO> penaltyPayments = new ArrayList<ChargePaymentDTO>();
// extract charge payment details (if exists)
if (map.containsKey("loanChargesPaid")) {
@SuppressWarnings("unchecked")
final List<Map<String, Object>> loanChargesPaidData = (List<Map<String, Object>>) map.get("loanChargesPaid");
for (final Map<String, Object> loanChargePaid : loanChargesPaidData) {
final Long chargeId = (Long) loanChargePaid.get("chargeId");
final Long loanChargeId = (Long) loanChargePaid.get("chargeId");
final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty");
final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount");
ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, loanChargeId, chargeAmountPaid);
if (isPenalty) {
penaltyPayments.add(chargePaymentDTO);
} else {
feePayments.add(chargePaymentDTO);
}
}
}
final LoanTransactionDTO transaction = new LoanTransactionDTO(paymentTypeId, transactionId, transactionDate, transactionType,
amount, principal, interest, fees, penalties, reversed);
amount, principal, interest, fees, penalties, reversed, feePayments, penaltyPayments);
newLoanTransactions.add(transaction);
@@ -304,6 +327,48 @@ public class AccountingProcessorHelper {
}
}
public void createCreditJournalEntryOrReversalForLoanCharges(final Office office, final int accountMappingTypeId,
final Long loanProductId, final Long loanId, final String transactionId, final Date transactionDate,
final BigDecimal totalAmount, final Boolean isReversal, final List<ChargePaymentDTO> chargePaymentDTOs) {
/***
* Map to track each account and the net credit to be made for a
* particular account
***/
Map<GLAccount, BigDecimal> creditDetailsMap = new LinkedHashMap<GLAccount, BigDecimal>();
for (ChargePaymentDTO chargePaymentDTO : chargePaymentDTOs) {
Long chargeId = chargePaymentDTO.getChargeId();
GLAccount chargeSpecificAccount = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId);
BigDecimal chargeSpecificAmount = chargePaymentDTO.getAmount();
// adjust net credit amount if the account is already present in the
// map
if (creditDetailsMap.containsKey(chargeSpecificAccount)) {
BigDecimal existingAmount = creditDetailsMap.get(chargeSpecificAccount);
chargeSpecificAmount = chargeSpecificAmount.add(existingAmount);
}
creditDetailsMap.put(chargeSpecificAccount, chargeSpecificAmount);
}
BigDecimal totalCreditedAmount = BigDecimal.ZERO;
for (Map.Entry<GLAccount, BigDecimal> entry : creditDetailsMap.entrySet()) {
GLAccount account = entry.getKey();
BigDecimal amount = entry.getValue();
totalCreditedAmount = totalCreditedAmount.add(amount);
if (isReversal) {
createDebitJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount);
} else {
createCreditJournalEntryForLoan(office, account, loanId, transactionId, transactionDate, amount);
}
}
// TODO: Vishwas Temporary validation to be removed before moving to
// release branch
if (totalAmount.compareTo(totalCreditedAmount) != 0) { throw new PlatformDataIntegrityException(
"Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction",
"Meltdown in advanced accounting...sum of all charges is not equal to the fee charge for a transaction",
totalCreditedAmount, totalAmount); }
}
private void createCreditJournalEntryOrReversalForLoan(final Office office, final int accountMappingTypeId, final Long loanProductId,
final Long paymentTypeId, final Long loanId, final String transactionId, final Date transactionDate, final BigDecimal amount,
final Boolean isReversal) {
@@ -363,6 +428,30 @@ public class AccountingProcessorHelper {
accountMapping = paymentChannelSpecificAccountMapping;
}
}
return accountMapping.getGlAccount();
}
private GLAccount getLinkedGLAccountForLoanCharges(final Long loanProductId, final int accountMappingTypeId, final Long chargeId) {
ProductToGLAccountMapping accountMapping = this.accountMappingRepository.findCoreProductToFinAccountMapping(loanProductId,
PortfolioProductType.LOAN.getValue(), accountMappingTypeId);
/*****
* Get more specific mappings for Charges and penalties (based on the
* actual charge /penalty coupled with the loan product). Note the
* income from fees and income from penalties placeholder ID would be
* the same for both cash and accrual based accounts
*****/
// Vishwas TODO: remove this condition as it should always be true
if (accountMappingTypeId == CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue()
|| accountMappingTypeId == CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue()) {
ProductToGLAccountMapping chargeSpecificIncomeAccountMapping = this.accountMappingRepository
.findByProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(loanProductId, PortfolioProductType.LOAN.getValue(),
accountMappingTypeId, chargeId);
if (chargeSpecificIncomeAccountMapping != null) {
accountMapping = chargeSpecificIncomeAccountMapping;
}
}
return accountMapping.getGlAccount();
}
@@ -134,14 +134,15 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES, loanProductId, paymentTypeId,
loanId, transactionId, transactionDate, feesAmount, isReversal);
helper.createCreditJournalEntryOrReversalForLoanCharges(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES.getValue(),
loanProductId, loanId, transactionId, transactionDate, feesAmount, isReversal, loanTransactionDTO.getFeePayments());
}
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
helper.createCreditJournalEntryOrReversalForLoan(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES, loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, penaltiesAmount, isReversal);
helper.createCreditJournalEntryOrReversalForLoanCharges(office, CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES.getValue(),
loanProductId, loanId, transactionId, transactionDate, penaltiesAmount, isReversal,
loanTransactionDTO.getPenaltyPayments());
}
/*** create a single debit entry (or reversal) for the entire amount **/
@@ -6,6 +6,7 @@
package org.mifosplatform.accounting.journalentry.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -15,9 +16,11 @@ import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate;
import org.mifosplatform.accounting.closure.domain.GLClosure;
import org.mifosplatform.accounting.closure.domain.GLClosureRepository;
import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository;
import org.mifosplatform.accounting.glaccount.exception.GLAccountNotFoundException;
import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService;
import org.mifosplatform.accounting.journalentry.api.JournalEntryJsonInputParams;
import org.mifosplatform.accounting.journalentry.command.JournalEntryCommand;
import org.mifosplatform.accounting.journalentry.command.SingleDebitOrCreditEntryCommand;
@@ -61,6 +64,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
private final AccountingProcessorHelper helper;
private final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final AccountingRuleRepository accountingRuleRepository;
private final GLAccountReadPlatformService glAccountReadPlatformService;
@Autowired
public JournalEntryWritePlatformServiceJpaRepositoryImpl(final GLClosureRepository glClosureRepository,
@@ -68,7 +72,8 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
final GLAccountRepository glAccountRepository, final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer,
final AccountingProcessorHelper accountingProcessorHelper, final AccountingRuleRepository accountingRuleRepository,
final AccountingProcessorForLoanFactory accountingProcessorForLoanFactory,
final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory) {
final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory,
final GLAccountReadPlatformService glAccountReadPlatformService) {
this.glClosureRepository = glClosureRepository;
this.officeRepository = officeRepository;
this.glJournalEntryRepository = glJournalEntryRepository;
@@ -78,6 +83,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
this.accountingProcessorForSavingsFactory = accountingProcessorForSavingsFactory;
this.helper = accountingProcessorHelper;
this.accountingRuleRepository = accountingRuleRepository;
this.glAccountReadPlatformService = glAccountReadPlatformService;
}
@Transactional
@@ -99,32 +105,65 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
/** Set a transaction Id and save these Journal entries **/
final Date transactionDate = command.DateValueOfParameterNamed(JournalEntryJsonInputParams.TRANSACTION_DATE.getValue());
final String transactionId = generateTransactionId();
final String referenceNumber = journalEntryCommand.getReferenceNumber();
final String referenceNumber = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.REFERENCE_NUMBER.getValue());
final boolean manualEntry = true;
if (accountRuleId != null) {
final Long accountingRuleId = command.longValueOfParameterNamed(JournalEntryJsonInputParams.ACCOUNTING_RULE.getValue());
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(JournalEntryJsonInputParams.AMOUNT.getValue());
final AccountingRule accountingRule = this.accountingRuleRepository.findOne(accountingRuleId);
if (accountingRule == null) { throw new AccountingRuleNotFoundException(accountingRuleId); }
final GLAccount creditAccountHead = accountingRule.getAccountToCredit();
validateGLAccountForTransaction(creditAccountHead);
final GLAccount debitAccountHead = accountingRule.getAccountToDebit();
validateGLAccountForTransaction(debitAccountHead);
String description = accountingRule.getDescription();
String description = null;
if (StringUtils.isNotBlank(journalEntryCommand.getComments())) {
description = journalEntryCommand.getComments();
}
JournalEntry creditEntry = JournalEntry.createNew(office, creditAccountHead, transactionId, manualEntry, transactionDate,
JournalEntryType.CREDIT, amount, description, null, null, referenceNumber);
this.glJournalEntryRepository.saveAndFlush(creditEntry);
final AccountingRule accountingRule = this.accountingRuleRepository.findOne(accountRuleId);
if (accountingRule == null) { throw new AccountingRuleNotFoundException(accountRuleId); }
JournalEntry debitEntry = JournalEntry.createNew(office, debitAccountHead, transactionId, manualEntry, transactionDate,
JournalEntryType.DEBIT, amount, description, null, null, referenceNumber);
this.glJournalEntryRepository.saveAndFlush(debitEntry);
if (accountingRule.getAccountToCredit() == null) {
if (journalEntryCommand.getCredits() == null) { throw new RuntimeException(
"You have to pass atleast one value for credits"); }
if (journalEntryCommand.getDebits() != null) {
checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(),
journalEntryCommand.getDebits());
checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits());
} else {
checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), amount);
}
saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getCredits(),
transactionId, JournalEntryType.CREDIT, referenceNumber);
} else {
if (journalEntryCommand.getCredits() != null) { throw new RuntimeException(
"You can not pass credits for this rule please verify your rule"); }
final GLAccount creditAccountHead = accountingRule.getAccountToCredit();
validateGLAccountForTransaction(creditAccountHead);
final JournalEntry creditEntry = JournalEntry.createNew(office, creditAccountHead, transactionId, manualEntry,
transactionDate, JournalEntryType.CREDIT, amount, description, null, null, referenceNumber);
this.glJournalEntryRepository.saveAndFlush(creditEntry);
}
if (accountingRule.getAccountToDebit() == null) {
if (journalEntryCommand.getDebits() == null) { throw new RuntimeException(
"You have to pass atleast one parameter for debits"); }
if (journalEntryCommand.getCredits() != null) {
checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(),
journalEntryCommand.getDebits());
checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits());
} else {
checkDebitAndCreditAmounts(journalEntryCommand.getDebits(), amount);
}
saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getDebits(),
transactionId, JournalEntryType.DEBIT, referenceNumber);
} else {
if (journalEntryCommand.getDebits() != null) { throw new RuntimeException(
"You can not pass debits for this rule please verify your rule"); }
final GLAccount debitAccountHead = accountingRule.getAccountToDebit();
validateGLAccountForTransaction(debitAccountHead);
final JournalEntry debitEntry = JournalEntry.createNew(office, debitAccountHead, transactionId, manualEntry,
transactionDate, JournalEntryType.DEBIT, amount, description, null, null, referenceNumber);
this.glJournalEntryRepository.saveAndFlush(debitEntry);
}
} else {
saveAllDebitOrCreditEntries(journalEntryCommand, office, transactionDate, journalEntryCommand.getDebits(), transactionId,
@@ -143,7 +182,76 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
}
}
private void validateGLAccountForTransaction(GLAccount creditOrDebitAccountHead) {
@SuppressWarnings("null")
private void checkDebitOrCreditAccountsAreValid(final AccountingRule accountingRule, final SingleDebitOrCreditEntryCommand[] credits,
final SingleDebitOrCreditEntryCommand[] debits) {
// Validate the debit and credit arrays are appropriate accounts
List<GLAccountDataForLookup> allowedCreditGLAccounts = new ArrayList<GLAccountDataForLookup>();
List<GLAccountDataForLookup> allowedDebitGLAccounts = new ArrayList<GLAccountDataForLookup>();
final SingleDebitOrCreditEntryCommand[] validCredits = new SingleDebitOrCreditEntryCommand[credits.length];
final SingleDebitOrCreditEntryCommand[] validDebits = new SingleDebitOrCreditEntryCommand[debits.length];
if (credits != null && credits.length > 0) {
allowedCreditGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(),
JournalEntryType.CREDIT.getValue());
for (final GLAccountDataForLookup accountDataForLookup : allowedCreditGLAccounts) {
for (int i = 0; i < credits.length; i++) {
final SingleDebitOrCreditEntryCommand credit = credits[i];
if (credit.getGlAccountId().equals(accountDataForLookup.getId())) {
validCredits[i] = credit;
}
}
}
if (credits.length != validCredits.length) { throw new RuntimeException("Invalid credits"); }
}
if (debits != null && debits.length > 0) {
allowedDebitGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(),
JournalEntryType.DEBIT.getValue());
for (final GLAccountDataForLookup accountDataForLookup : allowedDebitGLAccounts) {
for (int i = 0; i < debits.length; i++) {
final SingleDebitOrCreditEntryCommand debit = debits[i];
if (debit.getGlAccountId().equals(accountDataForLookup.getId())) {
validDebits[i] = debit;
}
}
}
if (debits.length != validDebits.length) { throw new RuntimeException("Invalid debits"); }
}
}
private void checkDebitAndCreditAmounts(final SingleDebitOrCreditEntryCommand[] credits, final SingleDebitOrCreditEntryCommand[] debits) {
// sum of all debits must be = sum of all credits
BigDecimal creditsSum = BigDecimal.ZERO;
BigDecimal debitsSum = BigDecimal.ZERO;
for (final SingleDebitOrCreditEntryCommand creditEntryCommand : credits) {
if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); }
creditsSum = creditsSum.add(creditEntryCommand.getAmount());
}
for (final SingleDebitOrCreditEntryCommand debitEntryCommand : debits) {
if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); }
debitsSum = debitsSum.add(debitEntryCommand.getAmount());
}
if (creditsSum.compareTo(debitsSum) != 0) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); }
}
private void checkDebitAndCreditAmounts(final SingleDebitOrCreditEntryCommand[] creditOrDebits, final BigDecimal amount) {
if (amount == null) { throw new JournalEntryInvalidException(GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH_WITH_AMOUNT,
null, null, null); }
BigDecimal creditOrDebitSum = BigDecimal.ZERO;
for (final SingleDebitOrCreditEntryCommand creditEntryCommand : creditOrDebits) {
if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); }
creditOrDebitSum = creditOrDebitSum.add(creditEntryCommand.getAmount());
}
if (creditOrDebitSum.compareTo(amount) != 0) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); }
}
private void validateGLAccountForTransaction(final GLAccount creditOrDebitAccountHead) {
/***
* validate that the account allows manual adjustments and is not
* disabled
@@ -199,7 +307,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled");
if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) {
final LoanDTO loanDTO = helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
final LoanDTO loanDTO = this.helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
accrualBasedAccountingEnabled);
final AccountingProcessorForLoan accountingProcessorForLoan = this.accountingProcessorForLoanFactory
.determineProcessor(loanDTO);
@@ -209,13 +317,13 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
@Transactional
@Override
public void createJournalEntriesForSavings(Map<String, Object> accountingBridgeData) {
public void createJournalEntriesForSavings(final Map<String, Object> accountingBridgeData) {
final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled");
final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled");
if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) {
final SavingsDTO savingsDTO = helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
final SavingsDTO savingsDTO = this.helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
accrualBasedAccountingEnabled);
final AccountingProcessorForSavings accountingProcessorForSavings = this.accountingProcessorForSavingsFactory
.determineProcessor(savingsDTO);
@@ -247,21 +355,7 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
if ((credits == null || credits.length <= 0) || (debits == null || debits.length <= 0)) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.NO_DEBITS_OR_CREDITS, null, null, null); }
// sum of all debits must be = sum of all credits
BigDecimal creditsSum = BigDecimal.ZERO;
BigDecimal debitsSum = BigDecimal.ZERO;
for (final SingleDebitOrCreditEntryCommand creditEntryCommand : credits) {
if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); }
creditsSum = creditsSum.add(creditEntryCommand.getAmount());
}
for (final SingleDebitOrCreditEntryCommand debitEntryCommand : debits) {
if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null); }
debitsSum = debitsSum.add(debitEntryCommand.getAmount());
}
if (creditsSum.compareTo(debitsSum) != 0) { throw new JournalEntryInvalidException(
GL_JOURNAL_ENTRY_INVALID_REASON.DEBIT_CREDIT_SUM_MISMATCH, null, null, null); }
checkDebitAndCreditAmounts(credits, debits);
}
}
@@ -0,0 +1,20 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.producttoaccountmapping.data;
public class ChargeToGLAccountMapper {
@SuppressWarnings("unused")
private final long chargeId;
@SuppressWarnings("unused")
private final long incomeAccountId;
public ChargeToGLAccountMapper(long chargeId, long incomeAccountId) {
this.chargeId = chargeId;
this.incomeAccountId = incomeAccountId;
}
}
@@ -1,3 +1,8 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.producttoaccountmapping.data;
public class PaymentTypeToGLAccountMapper {
@@ -15,6 +15,7 @@ import javax.persistence.UniqueConstraint;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.infrastructure.codes.domain.CodeValue;
import org.mifosplatform.portfolio.charge.domain.Charge;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@@ -33,6 +34,10 @@ public class ProductToGLAccountMapping extends AbstractPersistable<Long> {
@JoinColumn(name = "payment_type", nullable = true)
private CodeValue paymentType;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "charge_id", nullable = true)
private Charge charge;
@Column(name = "product_type", nullable = false)
private int productType;
@@ -49,16 +54,27 @@ public class ProductToGLAccountMapping extends AbstractPersistable<Long> {
}
public ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType, final int financialAccountType) {
this(glAccount, productId, productType, financialAccountType, null);
this(glAccount, productId, productType, financialAccountType, null, null);
}
public ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType,
final int financialAccountType, final Charge charge) {
this(glAccount, productId, productType, financialAccountType, null, charge);
}
public ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType,
final int financialAccountType, final CodeValue paymentType) {
this(glAccount, productId, productType, financialAccountType, paymentType, null);
}
private ProductToGLAccountMapping(final GLAccount glAccount, final Long productId, final int productType,
final int financialAccountType, final CodeValue paymentType, final Charge charge) {
this.glAccount = glAccount;
this.productId = productId;
this.productType = productType;
this.financialAccountType = financialAccountType;
this.paymentType = paymentType;
this.charge = charge;
}
public GLAccount getGlAccount() {
@@ -101,4 +117,12 @@ public class ProductToGLAccountMapping extends AbstractPersistable<Long> {
this.paymentType = paymentType;
}
public Charge getCharge() {
return this.charge;
}
public void setCharge(Charge charge) {
this.charge = charge;
}
}
@@ -18,7 +18,10 @@ public interface ProductToGLAccountMappingRepository extends JpaRepository<Produ
ProductToGLAccountMapping findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(Long productId, int productType,
int financialAccountType, Long paymentType);
@Query("from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL")
ProductToGLAccountMapping findByProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(Long productId, int productType,
int financialAccountType, Long chargeId);
@Query("from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL")
ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType,
@Param("financialAccountType") int financialAccountType);
@@ -27,5 +30,15 @@ public interface ProductToGLAccountMappingRepository extends JpaRepository<Produ
List<ProductToGLAccountMapping> findAllPaymentTypeToFundSourceMappings(@Param("productId") Long productId,
@Param("productType") int productType);
/*** The financial Account Type for income from interest will always be 4 ***/
@Query("from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=4 and mapping.charge is not NULL")
List<ProductToGLAccountMapping> findAllFeeToIncomeAccountMappings(@Param("productId") Long productId,
@Param("productType") int productType);
/*** The financial Account Type for income from interest will always be 5 ***/
@Query("from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=5 and mapping.charge is not NULL")
List<ProductToGLAccountMapping> findAllPenaltyToIncomeAccountMappings(@Param("productId") Long productId,
@Param("productType") int productType);
List<ProductToGLAccountMapping> findByProductIdAndProductType(Long productId, int productType);
}
@@ -1,12 +1,17 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.producttoaccountmapping.service;
import java.util.HashMap;
import java.util.Map;
import org.mifosplatform.accounting.common.AccountingRuleType;
import org.mifosplatform.accounting.common.AccountingConstants.ACCRUAL_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR_LOAN;
import org.mifosplatform.accounting.common.AccountingConstants.LOAN_PRODUCT_ACCOUNTING_PARAMS;
import org.mifosplatform.accounting.common.AccountingRuleType;
import org.mifosplatform.accounting.glaccount.domain.GLAccountRepository;
import org.mifosplatform.accounting.glaccount.domain.GLAccountType;
import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType;
@@ -14,6 +19,7 @@ import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAc
import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -23,10 +29,10 @@ import com.google.gson.JsonElement;
public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappingHelper {
@Autowired
public LoanProductToGLAccountMappingHelper(GLAccountRepository glAccountRepository,
ProductToGLAccountMappingRepository glAccountMappingRepository, FromJsonHelper fromApiJsonHelper,
CodeValueRepositoryWrapper codeValueRepositoryWrapper) {
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper);
public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
final CodeValueRepositoryWrapper codeValueRepositoryWrapper, final ChargeRepositoryWrapper chargeRepositoryWrapper) {
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper, chargeRepositoryWrapper);
}
/*** Set of abstractions for saving Loan Products to GL Account Mappings ***/
@@ -68,15 +74,29 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
/*** Abstractions for payments channel related to loan products ***/
public void savePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId,
Map<String, Object> changes) {
final Map<String, Object> changes) {
savePaymentChannelToFundSourceMappings(command, element, productId, changes, PortfolioProductType.LOAN);
}
public void updatePaymentChannelToFundSourceMappings(final JsonCommand command, final JsonElement element, final Long productId,
Map<String, Object> changes) {
final Map<String, Object> changes) {
updatePaymentChannelToFundSourceMappings(command, element, productId, changes, PortfolioProductType.LOAN);
}
public void saveChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes) {
// save both fee and penalty charges
saveChargesToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, true);
saveChargesToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, false);
}
public void updateChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes) {
// update both fee and penalty charges
updateChargeToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, true);
updateChargeToIncomeAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, false);
}
public Map<String, Object> populateChangesForNewLoanProductToGLAccountMappingCreation(final JsonElement element,
final AccountingRuleType accountingRuleType) {
final Map<String, Object> changes = new HashMap<String, Object>();
@@ -1,3 +1,8 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.producttoaccountmapping.service;
import java.util.HashMap;
@@ -21,6 +26,8 @@ import org.mifosplatform.infrastructure.codes.domain.CodeValue;
import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.charge.domain.Charge;
import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.mifosplatform.portfolio.paymentdetail.PaymentDetailConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -36,15 +43,17 @@ public class ProductToGLAccountMappingHelper {
protected final ProductToGLAccountMappingRepository accountMappingRepository;
protected final FromJsonHelper fromApiJsonHelper;
private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
private final ChargeRepositoryWrapper chargeRepositoryWrapper;
@Autowired
public ProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
final CodeValueRepositoryWrapper codeValueRepositoryWrapper) {
final CodeValueRepositoryWrapper codeValueRepositoryWrapper, final ChargeRepositoryWrapper chargeRepositoryWrapper) {
this.accountRepository = glAccountRepository;
this.accountMappingRepository = glAccountMappingRepository;
this.fromApiJsonHelper = fromApiJsonHelper;
this.codeValueRepositoryWrapper = codeValueRepositoryWrapper;
this.chargeRepositoryWrapper = chargeRepositoryWrapper;
}
@@ -105,6 +114,128 @@ public class ProductToGLAccountMappingHelper {
}
}
/**
* Saves the Charge to Income account mappings for a particular
* product/product type (also populates the changes array if passed in)
*
* @param command
* @param element
* @param productId
* @param changes
*/
public void saveChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
Map<String, Object> changes, final PortfolioProductType portfolioProductType, final boolean isPenalty) {
String arrayName;
if (isPenalty) {
arrayName = LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTY_INCOME_ACCOUNT_MAPPING.getValue();
} else {
arrayName = LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue();
}
JsonArray chargeToIncomeAccountMappingArray = fromApiJsonHelper.extractJsonArrayNamed(arrayName, element);
if (chargeToIncomeAccountMappingArray != null) {
if (changes != null) {
changes.put(LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue(),
command.jsonFragment(LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue()));
}
for (int i = 0; i < chargeToIncomeAccountMappingArray.size(); i++) {
final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject();
final Long chargeId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.CHARGE_ID.getValue()).getAsLong();
final Long incomeAccountId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue()).getAsLong();
saveChargeToFundSourceMapping(productId, chargeId, incomeAccountId, portfolioProductType, isPenalty);
}
}
}
/**
* @param command
* @param element
* @param productId
* @param changes
*/
public void updateChargeToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
Map<String, Object> changes, final PortfolioProductType portfolioProductType, boolean isPenalty) {
// find all existing payment Channel to Fund source Mappings
List<ProductToGLAccountMapping> existingChargeToIncomeAccountMappings;
String arrayFragmentName;
if (isPenalty) {
existingChargeToIncomeAccountMappings = accountMappingRepository.findAllPenaltyToIncomeAccountMappings(productId,
portfolioProductType.getValue());
arrayFragmentName = LOAN_PRODUCT_ACCOUNTING_PARAMS.PENALTY_INCOME_ACCOUNT_MAPPING.getValue();
} else {
existingChargeToIncomeAccountMappings = accountMappingRepository.findAllFeeToIncomeAccountMappings(productId,
portfolioProductType.getValue());
arrayFragmentName = LOAN_PRODUCT_ACCOUNTING_PARAMS.FEE_INCOME_ACCOUNT_MAPPING.getValue();
}
JsonArray chargeToIncomeAccountMappingArray = fromApiJsonHelper.extractJsonArrayNamed(arrayFragmentName, element);
/**
* Variable stores a map representation of charges (key) and their
* associated income Id's (value) extracted from the passed in
* Jsoncommand
**/
Map<Long, Long> inputChargeToIncomeAccountMap = new HashMap<Long, Long>();
/***
* Variable stores all charges which have already been mapped to Income
* Accounts in the system
**/
Set<Long> existingCharges = new HashSet<Long>();
if (chargeToIncomeAccountMappingArray != null) {
if (changes != null) {
changes.put(arrayFragmentName, command.jsonFragment(arrayFragmentName));
}
for (int i = 0; i < chargeToIncomeAccountMappingArray.size(); i++) {
final JsonObject jsonObject = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject();
final Long chargeId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.CHARGE_ID.getValue()).getAsLong();
final Long incomeAccountId = jsonObject.get(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue()).getAsLong();
inputChargeToIncomeAccountMap.put(chargeId, incomeAccountId);
}
// If input map is empty, delete all existing mappings
if (inputChargeToIncomeAccountMap.size() == 0) {
accountMappingRepository.deleteInBatch(existingChargeToIncomeAccountMappings);
}/**
* Else, <br/>
* update existing mappings OR <br/>
* delete old mappings (which are already present, but not passed in
* as a part of Jsoncommand)<br/>
* Create new mappings for charges that are passed in as a part of
* the Jsoncommand but not already present
*
**/
else {
for (ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) {
Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId();
existingCharges.add(currentCharge);
// update existing mappings (if required)
if (inputChargeToIncomeAccountMap.containsKey(currentCharge)) {
Long newGLAccountId = inputChargeToIncomeAccountMap.get(currentCharge);
if (newGLAccountId != chargeToIncomeAccountMapping.getGlAccount().getId()) {
final GLAccount glAccount = getAccountByIdAndType(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue(),
GLAccountType.INCOME, newGLAccountId);
chargeToIncomeAccountMapping.setGlAccount(glAccount);
accountMappingRepository.save(chargeToIncomeAccountMapping);
}
}// deleted payment type
else {
accountMappingRepository.delete(chargeToIncomeAccountMapping);
}
}
// create new mappings
Set<Long> incomingCharges = inputChargeToIncomeAccountMap.keySet();
incomingCharges.removeAll(existingCharges);
// incomingPaymentTypes now only contains the newly added
// payment Type mappings
for (Long newCharge : incomingCharges) {
Long newGLAccountId = inputChargeToIncomeAccountMap.get(newCharge);
saveChargeToFundSourceMapping(productId, newCharge, newGLAccountId, portfolioProductType, isPenalty);
}
}
}
}
/**
* @param command
* @param element
@@ -199,6 +330,34 @@ public class ProductToGLAccountMappingHelper {
this.accountMappingRepository.save(accountMapping);
}
/**
* @param productId
* @param jsonObject
*/
private void saveChargeToFundSourceMapping(final Long productId, final Long chargeId, final Long incomeAccountId,
final PortfolioProductType portfolioProductType, final boolean isPenalty) {
Charge charge = chargeRepositoryWrapper.findOneWithNotFoundDetection(chargeId);
// TODO Vishwas: Need to validate if given charge is fee or Penalty
// based on input condition
final GLAccount glAccount = getAccountByIdAndType(LOAN_PRODUCT_ACCOUNTING_PARAMS.INCOME_ACCOUNT_ID.getValue(),
GLAccountType.INCOME, incomeAccountId);
/**
* Both CASH and Accrual placeholders have the same value for income
* from Interest and penalties
**/
CASH_ACCOUNTS_FOR_LOAN placeHolderAccountType;
if (isPenalty) {
placeHolderAccountType = CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_PENALTIES;
} else {
placeHolderAccountType = CASH_ACCOUNTS_FOR_LOAN.INCOME_FROM_FEES;
}
final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping(glAccount, productId,
portfolioProductType.getValue(), placeHolderAccountType.getValue(), charge);
this.accountMappingRepository.save(accountMapping);
}
/**
* Fetches account with a particular Id and throws and Exception it is not
* of the expected Account Category ('ASSET','liability' etc)
@@ -8,6 +8,7 @@ package org.mifosplatform.accounting.producttoaccountmapping.service;
import java.util.List;
import java.util.Map;
import org.mifosplatform.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
import org.mifosplatform.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
public interface ProductToGLAccountMappingReadPlatformService {
@@ -16,7 +17,12 @@ public interface ProductToGLAccountMappingReadPlatformService {
public List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForLoanProduct(final Long loanProductId);
public List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForLoanProduct(final Long loanProductId);
public List<ChargeToGLAccountMapper> fetchPenaltyToIncomeAccountMappingsForLoanProduct(final Long loanProductId);
public Map<String, Object> fetchAccountMappingDetailsForSavingsProduct(final Long savingsProductId, final Integer accountingType);
public List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForSavingsProduct(final Long savingsProductId);
}
@@ -18,8 +18,10 @@ import org.mifosplatform.accounting.common.AccountingConstants.CASH_ACCOUNTS_FOR
import org.mifosplatform.accounting.common.AccountingConstants.LOAN_PRODUCT_ACCOUNTING_PARAMS;
import org.mifosplatform.accounting.common.AccountingConstants.SAVINGS_PRODUCT_ACCOUNTING_PARAMS;
import org.mifosplatform.accounting.common.AccountingRuleType;
import org.mifosplatform.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
import org.mifosplatform.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
import org.mifosplatform.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -39,8 +41,11 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
private static final class ProductToGLAccountMappingMapper implements RowMapper<Map<String, Object>> {
public String schema() {
return " id as id, gl_account_id as glAccountId,product_id as productId,product_type as productType,financial_account_type as financialAccountType,"
+ " payment_type as paymentTypeId from acc_product_mapping " + " where product_type= ? ";
return " mapping.id as id, mapping.gl_account_id as glAccountId, mapping.product_id as productId, "
+ " mapping.product_type as productType,mapping.financial_account_type as financialAccountType, "
+ " mapping.payment_type as paymentTypeId, mapping.charge_id as chargeId, charge.is_penalty as penalty "
+ " from acc_product_mapping mapping left join m_charge charge on mapping.charge_id=charge.id "
+ " where mapping.product_type= ? ";
}
@Override
@@ -49,7 +54,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
final Long id = rs.getLong("id");
final Long glAccountId = rs.getLong("glAccountId");
final Long productId = rs.getLong("productId");
final Long paymentTypeId = rs.getLong("paymentTypeId");
final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentTypeId");
final Long chargeId = rs.getLong("chargeId");
final Integer productType = rs.getInt("productType");
final Integer financialAccountType = rs.getInt("financialAccountType");
@@ -60,6 +66,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
loanProductToGLAccountMap.put("productType", productType);
loanProductToGLAccountMap.put("financialAccountType", financialAccountType);
loanProductToGLAccountMap.put("paymentTypeId", paymentTypeId);
loanProductToGLAccountMap.put("chargeId", chargeId);
return loanProductToGLAccountMap;
}
@@ -71,7 +78,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
final Map<String, Object> accountMappingDetails = new LinkedHashMap<String, Object>(8);
final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper();
final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null ";
final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null";
final List<Map<String, Object>> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, new Object[] {
PortfolioProductType.LOAN.getValue(), loanProductId });
@@ -138,7 +145,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
final Map<String, Object> accountMappingDetails = new LinkedHashMap<String, Object>(8);
final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper();
final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null ";
final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null ";
final List<Map<String, Object>> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, new Object[] {
PortfolioProductType.SAVING.getValue(), savingsProductId });
@@ -168,14 +175,12 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
@Override
public List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForLoanProduct(Long loanProductId) {
List<PaymentTypeToGLAccountMapper> paymentTypeToGLAccountMappers = null;
return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.LOAN, loanProductId, paymentTypeToGLAccountMappers);
return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.LOAN, loanProductId);
}
@Override
public List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForSavingsProduct(Long savingsProductId) {
List<PaymentTypeToGLAccountMapper> paymentTypeToGLAccountMappers = null;
return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.SAVING, savingsProductId, paymentTypeToGLAccountMappers);
return fetchPaymentTypeToFundSourceMappings(PortfolioProductType.SAVING, savingsProductId);
}
/**
@@ -184,13 +189,14 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
* @return
*/
private List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappings(PortfolioProductType portfolioProductType,
Long loanProductId, List<PaymentTypeToGLAccountMapper> paymentTypeToGLAccountMappers) {
Long loanProductId) {
final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper();
final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is not null";
final List<Map<String, Object>> paymentTypeToFundSourceMappingsList = this.jdbcTemplate.query(sql, rm, new Object[] {
portfolioProductType.getValue(), loanProductId });
List<PaymentTypeToGLAccountMapper> paymentTypeToGLAccountMappers = null;
for (final Map<String, Object> productToGLAccountMap : paymentTypeToFundSourceMappingsList) {
if (paymentTypeToGLAccountMappers == null) {
paymentTypeToGLAccountMappers = new ArrayList<PaymentTypeToGLAccountMapper>();
@@ -203,4 +209,38 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
return paymentTypeToGLAccountMappers;
}
@Override
public List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForLoanProduct(Long loanProductId) {
return fetchChargeToIncomeAccountMappings(PortfolioProductType.LOAN, loanProductId, false);
}
@Override
public List<ChargeToGLAccountMapper> fetchPenaltyToIncomeAccountMappingsForLoanProduct(Long loanProductId) {
return fetchChargeToIncomeAccountMappings(PortfolioProductType.LOAN, loanProductId, true);
}
private List<ChargeToGLAccountMapper> fetchChargeToIncomeAccountMappings(PortfolioProductType portfolioProductType, Long loanProductId,
boolean penalty) {
final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper();
String sql = "select " + rm.schema() + " and product_id = ? and mapping.charge_id is not null and charge.is_penalty=";
if (penalty) {
sql = sql + " 1";
} else {
sql = sql + " 0";
}
final List<Map<String, Object>> chargeToFundSourceMappingsList = this.jdbcTemplate.query(sql, rm, new Object[] {
portfolioProductType.getValue(), loanProductId });
List<ChargeToGLAccountMapper> chargeToGLAccountMappers = null;
for (final Map<String, Object> chargeToIncomeAccountMap : chargeToFundSourceMappingsList) {
if (chargeToGLAccountMappers == null) {
chargeToGLAccountMappers = new ArrayList<ChargeToGLAccountMapper>();
}
final Long glAccountId = (Long) chargeToIncomeAccountMap.get("glAccountId");
final Long chargeId = (Long) chargeToIncomeAccountMap.get("chargeId");
ChargeToGLAccountMapper chargeToGLAccountMapper = new ChargeToGLAccountMapper(chargeId, glAccountId);
chargeToGLAccountMappers.add(chargeToGLAccountMapper);
}
return chargeToGLAccountMappers;
}
}
@@ -81,6 +81,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
// advanced accounting mappings
loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
break;
case ACCRUAL_BASED:
@@ -119,6 +120,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
// advanced accounting mappings
loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
break;
}
}
@@ -187,12 +189,12 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
createLoanProductToGLAccountMapping(loanProductId, command);
changes = loanProductToGLAccountMappingHelper.populateChangesForNewLoanProductToGLAccountMappingCreation(element,
accountingRuleType);
loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, changes);
}/*** else examine and update individual changes ***/
else {
loanProductToGLAccountMappingHelper.handleChangesToLoanProductToGLAccountMappings(loanProductId, changes, element,
accountingRuleType);
loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes);
loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
}
return changes;
}
@@ -218,7 +220,6 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
createSavingProductToGLAccountMapping(savingsProductId, command);
changes = savingsProductToGLAccountMappingHelper.populateChangesForNewSavingsProductToGLAccountMappingCreation(element,
accountingRuleType);
savingsProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, savingsProductId, changes);
}/*** else examine and update individual changes ***/
else {
savingsProductToGLAccountMappingHelper.handleChangesToSavingsProductToGLAccountMappings(savingsProductId, changes, element,
@@ -1,3 +1,8 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.producttoaccountmapping.service;
import java.util.HashMap;
@@ -13,6 +18,7 @@ import org.mifosplatform.accounting.producttoaccountmapping.domain.ProductToGLAc
import org.mifosplatform.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -24,8 +30,8 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
@Autowired
public SavingsProductToGLAccountMappingHelper(GLAccountRepository glAccountRepository,
ProductToGLAccountMappingRepository glAccountMappingRepository, FromJsonHelper fromApiJsonHelper,
CodeValueRepositoryWrapper codeValueRepositoryWrapper) {
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper);
CodeValueRepositoryWrapper codeValueRepositoryWrapper, ChargeRepositoryWrapper chargeRepositoryWrapper) {
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, codeValueRepositoryWrapper, chargeRepositoryWrapper);
}
/*** Set of abstractions for saving Saving Products to GL Account Mappings ***/
@@ -5,9 +5,13 @@
*/
package org.mifosplatform.accounting.rule.api;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.Consumes;
@@ -18,18 +22,22 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import org.mifosplatform.accounting.common.AccountingConstants;
import org.mifosplatform.accounting.glaccount.data.GLAccountData;
import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService;
import org.mifosplatform.accounting.rule.data.AccountingRuleData;
import org.mifosplatform.accounting.rule.data.AccountingTagRuleData;
import org.mifosplatform.accounting.rule.service.AccountingRuleReadPlatformService;
import org.mifosplatform.commands.domain.CommandWrapper;
import org.mifosplatform.commands.service.CommandWrapperBuilder;
import org.mifosplatform.commands.service.PortfolioCommandSourceWritePlatformService;
import org.mifosplatform.infrastructure.codes.data.CodeValueData;
import org.mifosplatform.infrastructure.codes.service.CodeValueReadPlatformService;
import org.mifosplatform.infrastructure.core.api.ApiParameterHelper;
import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
@@ -47,7 +55,9 @@ import org.springframework.stereotype.Component;
public class AccountingRuleApiResource {
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<String>(Arrays.asList("id", "officeId", "officeName",
"accountToDebitId", "accountToCreditId", "name", "description", "systemDefined"));
"accountToDebitId", "accountToCreditId", "name", "description", "systemDefined", "allowedCreditTagOptions",
"allowedDebitTagOptions", "debitTags", "creditTags", "creditAccounts", "debitAccounts", "allowMultipleCreditEntries",
"allowMultipleDebitEntries", "tag"));
private final String resourceNameForPermission = "ACCOUNTINGRULE";
@@ -58,6 +68,7 @@ public class AccountingRuleApiResource {
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final CodeValueReadPlatformService codeValueReadPlatformService;
@Autowired
public AccountingRuleApiResource(final PlatformSecurityContext context,
@@ -65,7 +76,7 @@ public class AccountingRuleApiResource {
final DefaultToApiJsonSerializer<AccountingRuleData> toApiJsonSerializer,
final ApiRequestParameterHelper apiRequestParameterHelper, final GLAccountReadPlatformService accountReadPlatformService,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
final OfficeReadPlatformService officeReadPlatformService) {
final OfficeReadPlatformService officeReadPlatformService, final CodeValueReadPlatformService codeValueReadPlatformService) {
this.context = context;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
@@ -73,30 +84,40 @@ public class AccountingRuleApiResource {
this.accountingRuleReadPlatformService = accountingRuleReadPlatformService;
this.officeReadPlatformService = officeReadPlatformService;
this.accountReadPlatformService = accountReadPlatformService;
this.codeValueReadPlatformService = codeValueReadPlatformService;
}
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveTemplate(@Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(resourceNameForPermission);
AccountingRuleData accountingRuleData = null;
accountingRuleData = handleTemplate(accountingRuleData);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS);
}
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveTemplate(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
AccountingRuleData accountingRuleData = null;
accountingRuleData = handleTemplate(accountingRuleData);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveAllAccountingRules(@Context final UriInfo uriInfo, @QueryParam("officeId") final Long officeId) {
public String retrieveAllAccountingRules(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
final List<AccountingRuleData> accountingRuleDatas = this.accountingRuleReadPlatformService.retrieveAllAccountingRules(officeId);
final Set<String> associationParameters = ApiParameterHelper.extractAssociationsForResponseIfProvided(uriInfo.getQueryParameters());
final Long officeId = this.context.authenticatedUser().getOffice().getId();
boolean isAssociationParametersExists = false;
if (!associationParameters.isEmpty()) {
if (associationParameters.contains("all")) {
isAssociationParametersExists = true; // If true, retrieve additional fields for journal entry form.
}
}
final List<AccountingRuleData> accountingRuleDatas = this.accountingRuleReadPlatformService.retrieveAllAccountingRules(officeId,
isAssociationParametersExists);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, accountingRuleDatas, RESPONSE_DATA_PARAMETERS);
@@ -109,12 +130,11 @@ public class AccountingRuleApiResource {
public String retreiveAccountingRule(@PathParam("accountingRuleId") final Long accountingRuleId, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermission);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
AccountingRuleData accountingRuleData = this.accountingRuleReadPlatformService.retrieveAccountingRuleById(accountingRuleId);
if (settings.isTemplate()) {
accountingRuleData = handleTemplate(accountingRuleData);
accountingRuleData = handleTemplate(accountingRuleData);
}
return this.apiJsonSerializerService.serialize(settings, accountingRuleData, RESPONSE_DATA_PARAMETERS);
@@ -157,13 +177,60 @@ public class AccountingRuleApiResource {
return this.apiJsonSerializerService.serialize(result);
}
private AccountingRuleData handleTemplate(AccountingRuleData accountingRuleData) {
List<GLAccountData> allowedAccounts = this.accountReadPlatformService.retrieveAllEnabledDetailGLAccounts();
List<OfficeData> allowedOffices = (List<OfficeData>) this.officeReadPlatformService.retrieveAllOfficesForDropdown();
if (accountingRuleData == null) {
return new AccountingRuleData(allowedAccounts, allowedOffices);
}
return new AccountingRuleData(accountingRuleData, allowedAccounts, allowedOffices);
}
private AccountingRuleData handleTemplate(AccountingRuleData accountingRuleData) {
final List<GLAccountData> allowedAccounts = this.accountReadPlatformService.retrieveAllEnabledDetailGLAccounts();
final List<OfficeData> allowedOffices = (List<OfficeData>) this.officeReadPlatformService.retrieveAllOfficesForDropdown();
final Collection<CodeValueData> allowedTagOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(AccountingConstants.ASSESTS_TAG_OPTION_CODE_NAME);
allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.LIABILITIES_TAG_OPTION_CODE_NAME));
allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EQUITY_TAG_OPTION_CODE_NAME));
allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.INCOME_TAG_OPTION_CODE_NAME));
allowedTagOptions.addAll(this.codeValueReadPlatformService.retrieveCodeValuesByCode(AccountingConstants.EXPENSES_TAG_OPTION_CODE_NAME));
if (accountingRuleData == null) {
final Collection<CodeValueData> allowedCreditTagOptions = allowedTagOptions;
final Collection<CodeValueData> allowedDebitTagOptions = allowedTagOptions;
accountingRuleData = new AccountingRuleData(allowedAccounts, allowedOffices, allowedCreditTagOptions, allowedDebitTagOptions);
} else {
final Collection<CodeValueData> allowedCreditTagOptions;
final Collection<CodeValueData> allowedDebitTagOptions;
if (accountingRuleData.getCreditTags() != null) {
allowedCreditTagOptions = retrieveSelectedTags(allowedTagOptions, accountingRuleData.getCreditTags());
} else {
allowedCreditTagOptions = allowedTagOptions;
}
if (accountingRuleData.getDebitTags() != null) {
allowedDebitTagOptions = retrieveSelectedTags(allowedTagOptions, accountingRuleData.getDebitTags());
} else {
allowedDebitTagOptions = allowedTagOptions;
}
accountingRuleData = new AccountingRuleData(accountingRuleData, allowedAccounts, allowedOffices, allowedCreditTagOptions,
allowedDebitTagOptions);
}
return accountingRuleData;
}
private Collection<CodeValueData> retrieveSelectedTags(final Collection<CodeValueData> allowedTagOptions,
final List<AccountingTagRuleData> existedTags) {
final Collection<CodeValueData> tempOptions = new ArrayList<CodeValueData>(allowedTagOptions);
final Map<Long, CodeValueData> selectedTags = new HashMap<Long, CodeValueData>();
for (final AccountingTagRuleData accountingTagRuleData : existedTags) {
for (final CodeValueData codeValueData : tempOptions) {
if (codeValueData.getId().equals(accountingTagRuleData.getTag().getId())) {
selectedTags.put(codeValueData.getId(), codeValueData);
}
}
}
tempOptions.removeAll(selectedTags.values());
return tempOptions;
}
}
@@ -13,7 +13,8 @@ import java.util.Set;
***/
public enum AccountingRuleJsonInputParams {
ID("id"), OFFICE_ID("officeId"), ACCOUNT_TO_DEBIT("debitAccountHead"), ACCOUNT_TO_CREDIT("creditAccountHead"), NAME("name"), DESCRIPTION(
"description"), SYSTEM_DEFINED("systemDefined");
"description"), SYSTEM_DEFINED("systemDefined"), DEBIT_ACCOUNT_TAGS("debitTags"), CREDIT_ACCOUNT_TAGS("creditTags"), ALLOW_MULTIPLE_CREDIT_ENTRIES(
"allowMultipleCreditEntries"), ALLOW_MULTIPLE_DEBIT_ENTRIES("allowMultipleDebitEntries");
private final String value;
@@ -1,79 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.rule.command;
import java.util.ArrayList;
import java.util.List;
import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
/**
* Immutable command for adding an accounting closure
*/
public class AccountingRuleCommand {
@SuppressWarnings("unused")
private final Long id;
private final Long officeId;
private final Long accountToDebitId;
private final Long accountToCreditId;
private final String name;
private final String description;
public AccountingRuleCommand(Long id, Long officeId, Long accountToDebitId, Long accountToCreditId, String name, String description) {
this.id = id;
this.officeId = officeId;
this.accountToDebitId = accountToDebitId;
this.accountToCreditId = accountToCreditId;
this.name = name;
this.description = description;
}
public void validateForCreate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule");
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(this.accountToDebitId)
.notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(this.accountToCreditId)
.notNull().integerGreaterThanZero();
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(this.officeId).ignoreIfNull()
.integerGreaterThanZero();
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(this.name).notBlank()
.notExceedingLengthOf(100);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(this.description).ignoreIfNull()
.notExceedingLengthOf(500);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
public void validateForUpdate() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule");
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(this.accountToDebitId)
.ignoreIfNull().notBlank().integerGreaterThanZero();
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(this.accountToCreditId)
.ignoreIfNull().notBlank().integerGreaterThanZero();
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(this.officeId).ignoreIfNull()
.integerGreaterThanZero();
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(this.name).ignoreIfNull().notBlank()
.notExceedingLengthOf(100);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(this.description).ignoreIfNull()
.notExceedingLengthOf(500);
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
@@ -6,9 +6,12 @@
package org.mifosplatform.accounting.rule.data;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.mifosplatform.accounting.glaccount.data.GLAccountData;
import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup;
import org.mifosplatform.infrastructure.codes.data.CodeValueData;
import org.mifosplatform.organisation.office.data.OfficeData;
/**
@@ -19,85 +22,139 @@ import org.mifosplatform.organisation.office.data.OfficeData;
*/
public class AccountingRuleData {
private final Long id;
private final Long officeId;
private final String officeName;
private final String name;
private final String description;
private final boolean systemDefined;
private final GLAccountData debitAccountHead;
private final GLAccountData creditAccountHead;
private final Long id;
private final Long officeId;
private final String officeName;
private final String name;
private final String description;
private final boolean systemDefined;
private final boolean allowMultipleDebitEntries;
private final boolean allowMultipleCreditEntries;
private final GLAccountData debitAccountHead;
private final GLAccountData creditAccountHead;
private final List<AccountingTagRuleData> creditTags;
private final List<AccountingTagRuleData> debitTags;
// template
@SuppressWarnings("unused")
private List<OfficeData> allowedOffices = new ArrayList<OfficeData>();
@SuppressWarnings("unused")
private List<GLAccountData> allowedAccounts = new ArrayList<GLAccountData>();
// template
private List<OfficeData> allowedOffices = new ArrayList<OfficeData>();
private List<GLAccountData> allowedAccounts = new ArrayList<GLAccountData>();
private final Collection<CodeValueData> allowedCreditTagOptions;
private final Collection<CodeValueData> allowedDebitTagOptions;
private final List<GLAccountDataForLookup> creditAccounts;
private final List<GLAccountDataForLookup> debitAccounts;
/*public AccountingRuleData(final Long id, final Long accountToDebitId, final Long accountToCreditId) {
this(id, null, null, accountToDebitId, accountToCreditId, null, null, true, null, null, null, null, null, null);
}*/
public AccountingRuleData(final AccountingRuleData accountingRuleData, final List<GLAccountData> allowedAccounts,
final List<OfficeData> allowedOffices, final Collection<CodeValueData> allowedCreditTagOptions,
final Collection<CodeValueData> allowedDebitTagOptions) {
this.id = accountingRuleData.id;
this.officeId = accountingRuleData.officeId;
this.officeName = accountingRuleData.officeName;
this.name = accountingRuleData.name;
this.description = accountingRuleData.description;
this.systemDefined = accountingRuleData.systemDefined;
this.allowMultipleDebitEntries = accountingRuleData.allowMultipleDebitEntries;
this.allowMultipleCreditEntries = accountingRuleData.allowMultipleCreditEntries;
this.allowedOffices = allowedOffices;
this.allowedAccounts = allowedAccounts;
this.debitAccountHead = accountingRuleData.debitAccountHead;
this.creditAccountHead = accountingRuleData.creditAccountHead;
this.allowedCreditTagOptions = allowedCreditTagOptions;
this.allowedDebitTagOptions = allowedDebitTagOptions;
this.creditTags = accountingRuleData.creditTags;
this.debitTags = accountingRuleData.debitTags;
this.creditAccounts = accountingRuleData.creditAccounts;
this.debitAccounts = accountingRuleData.debitAccounts;
}
/*public AccountingRuleData(final Long id, final Long officeId, final String officeName, final Long accountToDebitId,
final Long accountToCreditId, final String name, final String description, final boolean systemDefined,
final String debitAccountName, final String creditAccountName, final String debitAccountGLCode,
final String creditAccountGLCode) {
this(id, officeId, officeName, accountToDebitId, accountToCreditId, name, description, systemDefined, debitAccountName,
creditAccountName, debitAccountGLCode, creditAccountGLCode, null, null);
}*/
public AccountingRuleData(final List<GLAccountData> allowedAccounts, final List<OfficeData> allowedOffices,
final Collection<CodeValueData> allowedCreditTagOptions, final Collection<CodeValueData> allowedDebitTagOptions) {
this.id = null;
this.officeId = null;
this.officeName = null;
this.name = null;
this.description = null;
this.systemDefined = false;
this.allowMultipleDebitEntries = false;
this.allowMultipleCreditEntries = false;
this.allowedOffices = allowedOffices;
this.allowedAccounts = allowedAccounts;
this.debitAccountHead = null;
this.creditAccountHead = null;
this.allowedCreditTagOptions = allowedCreditTagOptions;
this.allowedDebitTagOptions = allowedDebitTagOptions;
this.creditTags = null;
this.debitTags = null;
this.creditAccounts = null;
this.debitAccounts = null;
}
public AccountingRuleData(final Long id, final Long officeId, final String officeName, final String name, final String description,
final boolean systemDefined, final List<OfficeData> allowedOffices, final List<GLAccountData> allowedAccounts) {
this.id = id;
this.officeId = officeId;
this.officeName = officeName;
this.name = name;
this.description = description;
this.systemDefined = systemDefined;
this.allowedOffices = allowedOffices;
this.allowedAccounts = allowedAccounts;
this.debitAccountHead = null;
this.creditAccountHead = null;
}
public AccountingRuleData(final Long id, final Long officeId, final String officeName, final String name, final String description,
final boolean systemDefined, final boolean allowMultipleDebitEntries, final boolean allowMultipleCreditEntries,
final GLAccountData debitAccountData, final GLAccountData creditAccountData) {
this.id = id;
this.officeId = officeId;
this.officeName = officeName;
this.name = name;
this.description = description;
this.systemDefined = systemDefined;
this.allowMultipleDebitEntries = allowMultipleDebitEntries;
this.allowMultipleCreditEntries = allowMultipleCreditEntries;
this.allowedOffices = null;
this.allowedAccounts = null;
this.debitAccountHead = debitAccountData;
this.creditAccountHead = creditAccountData;
this.allowedCreditTagOptions = null;
this.allowedDebitTagOptions = null;
this.creditTags = null;
this.debitTags = null;
this.creditAccounts = new ArrayList<GLAccountDataForLookup>();
this.debitAccounts = new ArrayList<GLAccountDataForLookup>();
}
public AccountingRuleData(final AccountingRuleData accountingRuleData,
final List<GLAccountData> allowedAccounts, final List<OfficeData> allowedOffices) {
this.id = accountingRuleData.id;
this.officeId = accountingRuleData.officeId;
this.officeName = accountingRuleData.officeName;
this.name = accountingRuleData.name;
this.description = accountingRuleData.description;
this.systemDefined = accountingRuleData.systemDefined;
this.allowedOffices = allowedOffices;
this.allowedAccounts = allowedAccounts;
this.debitAccountHead = accountingRuleData.debitAccountHead;
this.creditAccountHead = accountingRuleData.creditAccountHead;
}
public AccountingRuleData(final AccountingRuleData accountingRuleData, final List<AccountingTagRuleData> creditTags,
final List<GLAccountDataForLookup> creditAccounts, final List<AccountingTagRuleData> debitTags,
final List<GLAccountDataForLookup> debitAccounts) {
this.id = accountingRuleData.id;
this.officeId = accountingRuleData.officeId;
this.officeName = accountingRuleData.officeName;
this.name = accountingRuleData.name;
this.description = accountingRuleData.description;
this.systemDefined = accountingRuleData.systemDefined;
this.allowMultipleDebitEntries = accountingRuleData.allowMultipleDebitEntries;
this.allowMultipleCreditEntries = accountingRuleData.allowMultipleCreditEntries;
this.allowedOffices = accountingRuleData.allowedOffices;
this.allowedAccounts = accountingRuleData.allowedAccounts;
this.debitAccountHead = accountingRuleData.debitAccountHead;
this.creditAccountHead = accountingRuleData.creditAccountHead;
this.allowedCreditTagOptions = accountingRuleData.allowedCreditTagOptions;
this.allowedDebitTagOptions = accountingRuleData.allowedDebitTagOptions;
this.creditTags = creditTags;
this.creditAccounts = creditAccounts;
this.debitTags = debitTags;
this.debitAccounts = debitAccounts;
}
public AccountingRuleData(final List<GLAccountData> allowedAccounts, final List<OfficeData> allowedOffices) {
this.id = null;
this.officeId = null;
this.officeName = null;
this.name = null;
this.description = null;
this.systemDefined = false;
this.allowedOffices = allowedOffices;
this.allowedAccounts = allowedAccounts;
this.debitAccountHead = null;
this.creditAccountHead = null;
}
public GLAccountData getDebitAccountHead() {
return this.debitAccountHead;
}
public AccountingRuleData(Long id, Long officeId, String officeName, String name, String description,
boolean systemDefined, GLAccountData debitAccountData, GLAccountData creditAccountData) {
this.id = id;
this.officeId = officeId;
this.officeName = officeName;
this.name = name;
this.description = description;
this.systemDefined = systemDefined;
this.allowedOffices = null;
this.allowedAccounts = null;
this.debitAccountHead = debitAccountData;
this.creditAccountHead = creditAccountData;
}
public GLAccountData getCreditAccountHead() {
return this.creditAccountHead;
}
public List<AccountingTagRuleData> getCreditTags() {
return this.creditTags;
}
public List<AccountingTagRuleData> getDebitTags() {
return this.debitTags;
}
public List<GLAccountDataForLookup> getCreditAccounts() {
return this.creditAccounts;
}
public List<GLAccountDataForLookup> getDebitAccounts() {
return this.debitAccounts;
}
}
@@ -0,0 +1,29 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.rule.data;
import org.mifosplatform.infrastructure.codes.data.CodeValueData;
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
public class AccountingTagRuleData {
@SuppressWarnings("unused")
private final Long id;
private final CodeValueData tag;
@SuppressWarnings("unused")
private final EnumOptionData transactionType;
public AccountingTagRuleData(final Long id, final CodeValueData tag, final EnumOptionData transactionType) {
this.id = id;
this.tag = tag;
this.transactionType = transactionType;
}
public CodeValueData getTag() {
return tag;
}
}
@@ -5,18 +5,27 @@
*/
package org.mifosplatform.accounting.rule.domain;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.apache.commons.lang.StringUtils;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryType;
import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.organisation.office.domain.Office;
@@ -34,11 +43,11 @@ public class AccountingRule extends AbstractPersistable<Long> {
private Office office;
@ManyToOne
@JoinColumn(name = "debit_account_id", nullable = false)
@JoinColumn(name = "debit_account_id", nullable = true)
private GLAccount accountToDebit;
@ManyToOne
@JoinColumn(name = "credit_account_id", nullable = false)
@JoinColumn(name = "credit_account_id", nullable = true)
private GLAccount accountToCredit;
@Column(name = "description", nullable = true, length = 500)
@@ -47,10 +56,21 @@ public class AccountingRule extends AbstractPersistable<Long> {
@Column(name = "system_defined", nullable = false)
private Boolean systemDefined;
@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "accountingRule", orphanRemoval = true)
private final List<AccountingTagRule> accountingTagRules = new ArrayList<AccountingTagRule>();
@Column(name = "allow_multiple_credits", nullable = false)
private boolean allowMultipleCreditEntries;
@Column(name = "allow_multiple_debits", nullable = false)
private boolean allowMultipleDebitEntries;
protected AccountingRule() {}
private AccountingRule(Office office, GLAccount accountToDebit, GLAccount accountToCredit, String name, String description,
boolean systemDefined) {
private AccountingRule(final Office office, final GLAccount accountToDebit, final GLAccount accountToCredit, final String name,
final String description, final boolean systemDefined, final boolean allowMultipleCreditEntries,
final boolean allowMultipleDebitEntries) {
this.accountToDebit = accountToDebit;
this.accountToCredit = accountToCredit;
this.name = name;
@@ -60,24 +80,34 @@ public class AccountingRule extends AbstractPersistable<Long> {
this.description = this.description.trim();
}
this.systemDefined = systemDefined;
this.allowMultipleCreditEntries = allowMultipleCreditEntries;
this.allowMultipleDebitEntries = allowMultipleDebitEntries;
}
public static AccountingRule fromJson(final Office office, final GLAccount accountToDebit, final GLAccount accountToCredit,
JsonCommand command) {
final JsonCommand command, final boolean allowMultipleCreditEntries, final boolean allowMultipleDebitEntries) {
final String name = command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.NAME.getValue());
final String description = command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue());
final boolean systemDefined = false;
return new AccountingRule(office, accountToDebit, accountToCredit, name, description, systemDefined);
return new AccountingRule(office, accountToDebit, accountToCredit, name, description, systemDefined, allowMultipleCreditEntries,
allowMultipleDebitEntries);
}
public Map<String, Object> update(final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>(5);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.OFFICE_ID.getValue(), 0L);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), 0L);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(), 0L);
final Map<String, Object> actualChanges = new LinkedHashMap<String, Object>(10);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.OFFICE_ID.getValue(), this.office == null ? 0L
: this.office.getId());
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(),
this.accountToDebit == null ? 0L : this.accountToDebit.getId());
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(),
this.accountToCredit == null ? 0L : this.accountToCredit.getId());
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.NAME.getValue(), this.name);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.DESCRIPTION.getValue(), this.description);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.SYSTEM_DEFINED.getValue(), this.systemDefined);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(),
this.allowMultipleCreditEntries);
handlePropertyUpdate(command, actualChanges, AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(),
this.allowMultipleDebitEntries);
return actualChanges;
}
@@ -103,6 +133,14 @@ public class AccountingRule extends AbstractPersistable<Long> {
// now update actual property
if (paramName.equals(AccountingRuleJsonInputParams.SYSTEM_DEFINED.getValue())) {
this.systemDefined = newValue;
} else if (paramName.equals(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())) {
if (this.accountToCredit == null) {
this.allowMultipleCreditEntries = newValue;
}
} else if (paramName.equals(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())) {
if (this.accountToDebit == null) {
this.allowMultipleDebitEntries = newValue;
}
}
}
}
@@ -123,7 +161,7 @@ public class AccountingRule extends AbstractPersistable<Long> {
}
}
public void setOffice(Office office) {
public void setOffice(final Office office) {
this.office = office;
}
@@ -139,11 +177,11 @@ public class AccountingRule extends AbstractPersistable<Long> {
return this.accountToCredit;
}
public void setAccountToDebit(GLAccount accountToDebit) {
public void setAccountToDebit(final GLAccount accountToDebit) {
this.accountToDebit = accountToDebit;
}
public void setAccountToCredit(GLAccount accountToCredit) {
public void setAccountToCredit(final GLAccount accountToCredit) {
this.accountToCredit = accountToCredit;
}
@@ -151,4 +189,66 @@ public class AccountingRule extends AbstractPersistable<Long> {
return this.description;
}
public List<AccountingTagRule> getAccountingTagRules() {
return this.accountingTagRules;
}
public void updateAccountingRuleForTags(final List<AccountingTagRule> debitAccountingTagRules) {
for (final AccountingTagRule accountingTagRule : debitAccountingTagRules) {
accountingTagRule.updateAccountingTagRule(this);
this.accountingTagRules.add(accountingTagRule);
}
}
public void updateDebitAccount(final GLAccount accountToDebit) {
this.accountToDebit = accountToDebit;
this.allowMultipleDebitEntries = false;
}
public void updateCreditAccount(final GLAccount accountToCredit) {
this.accountToCredit = accountToCredit;
this.allowMultipleCreditEntries = false;
}
public void updateAllowMultipleCreditEntries(final boolean allowMultipleCreditEntries) {
this.allowMultipleCreditEntries = allowMultipleCreditEntries;
}
public void updateAllowMultipleDebitEntries(final boolean allowMultipleDebitEntries) {
this.allowMultipleDebitEntries = allowMultipleDebitEntries;
}
public void updateTags(final JournalEntryType type) {
final Set<AccountingTagRule> existedCreditTags = new HashSet<AccountingTagRule>();
final Set<AccountingTagRule> existedDebitTags = new HashSet<AccountingTagRule>();
for (final AccountingTagRule accountingTagRule : this.accountingTagRules) {
if (accountingTagRule.isCreditAccount()) {
existedCreditTags.add(accountingTagRule);
} else if (accountingTagRule.isDebitAccount()) {
existedDebitTags.add(accountingTagRule);
}
}
if (type.isCreditType()) {
this.accountingTagRules.retainAll(existedCreditTags);
} else if (type.isDebitType()) {
this.accountingTagRules.retainAll(existedDebitTags);
}
}
public Set<AccountingTagRule> getAccountingTagRulesByType(final JournalEntryType type) {
final Set<AccountingTagRule> existedTags = new HashSet<AccountingTagRule>();
for (final AccountingTagRule accountingTagRule : this.accountingTagRules) {
if (accountingTagRule.getAccountType().equals(type.getValue())) {
existedTags.add(accountingTagRule);
}
}
return existedTags;
}
public void removeOldTags(final List<AccountingTagRule> accountsToRemove) {
this.accountingTagRules.removeAll(accountsToRemove);
}
}
@@ -0,0 +1,68 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.accounting.rule.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryType;
import org.mifosplatform.infrastructure.codes.domain.CodeValue;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@Table(name = "acc_rule_tags", uniqueConstraints = { @UniqueConstraint(columnNames = { "acc_rule_id", "tag_id", "acc_type_enum" }, name = "UNIQUE_ACCOUNT_RULE_TAGS") })
public class AccountingTagRule extends AbstractPersistable<Long> {
@SuppressWarnings("unused")
@ManyToOne
@JoinColumn(name = "acc_rule_id", nullable = false)
private AccountingRule accountingRule;
@ManyToOne
@JoinColumn(name = "tag_id", nullable = false)
private CodeValue tagId;
@Column(name = "acc_type_enum", nullable = false)
private Integer accountType;
public static AccountingTagRule create(final CodeValue tagId, final Integer accountType) {
return new AccountingTagRule(tagId, accountType);
}
public AccountingTagRule(final CodeValue tagId, final Integer accountType) {
this.tagId = tagId;
this.accountType = accountType;
}
public void updateAccountingTagRule(final AccountingRule accountingRule) {
this.accountingRule = accountingRule;
}
public AccountingTagRule() {
// TODO Auto-generated constructor stub
}
public Integer getAccountType() {
return this.accountType;
}
public boolean isDebitAccount() {
return JournalEntryType.fromInt(this.accountType).isDebitType();
}
public boolean isCreditAccount() {
return JournalEntryType.fromInt(this.accountType).isCreditType();
}
public Long getTagId() {
return this.tagId.getId();
}
}
@@ -0,0 +1,12 @@
package org.mifosplatform.accounting.rule.exception;
import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class AccountingRuleDataException extends AbstractPlatformDomainRuleException {
public AccountingRuleDataException(final String debitOrCreditAccount, final String debitOrCreditTags) {
super("error.msg.accounting.rule." + debitOrCreditAccount + ".or." + debitOrCreditTags + ".required", debitOrCreditAccount + " or "
+ debitOrCreditTags + " required", debitOrCreditAccount, debitOrCreditTags);
}
}
@@ -16,5 +16,9 @@ public class AccountingRuleDuplicateException extends AbstractPlatformDomainRule
public AccountingRuleDuplicateException(String name) {
super("error.msg.accounting.rule.duplicate", "An accounting rule with the name " + name + " already exists" + name);
}
public AccountingRuleDuplicateException() {
super("error.msg.accounting.rule.tag.duplicate", "The accounting rule already have the tags which you defined");
}
}
@@ -6,56 +6,206 @@
package org.mifosplatform.accounting.rule.serialization;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams;
import org.mifosplatform.accounting.rule.command.AccountingRuleCommand;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.serialization.AbstractFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.portfolio.loanaccount.guarantor.command.GuarantorCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
/**
* Implementation of {@link FromApiJsonDeserializer} for
* {@link GuarantorCommand}'s.
*/
@Component
public final class AccountingRuleCommandFromApiJsonDeserializer extends AbstractFromApiJsonDeserializer<AccountingRuleCommand> {
public class AccountingRuleCommandFromApiJsonDeserializer {
/**
* The parameters supported for this command.
*/
private final Set<String> supportedParameters = AccountingRuleJsonInputParams.getAllValues();
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public AccountingRuleCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonfromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonfromApiJsonHelper;
public AccountingRuleCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
@Override
public AccountingRuleCommand commandFromApiJson(final String json) {
public void validateForCreate(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
final Set<String> supportedParameters = AccountingRuleJsonInputParams.getAllValues();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParameters);
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Long id = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ID.getValue(), element);
final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element);
final Long accountToDebitId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(accountToDebitId)
.ignoreIfNull().integerGreaterThanZero();
final Long accountToCreditId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(accountToCreditId)
.ignoreIfNull().integerGreaterThanZero();
final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(officeId).notNull()
.integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(name).notBlank().notExceedingLengthOf(100);
final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(description).ignoreIfNull()
.notExceedingLengthOf(500);
final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()).value(creditTags).ignoreIfNull()
.arrayNotEmpty();
validateCreditOrDebitTagArray(creditTags, baseDataValidator, AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue());
final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()).value(debitTags).ignoreIfNull()
.arrayNotEmpty();
validateCreditOrDebitTagArray(debitTags, baseDataValidator, AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue());
if (creditTags == null && accountToCreditId == null) {
final String creditTag = AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue();
final String creditAccount = AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue();
final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(creditAccount).append(".or.")
.append(creditTag).append(".required");
final StringBuilder defaultUserMessage = new StringBuilder("The parameter ").append(creditAccount).append(" or")
.append(creditTag).append(" required");
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultUserMessage.toString(),
creditAccount + "," + creditTag, new Object[] { creditAccount, creditTag });
dataValidationErrors.add(error);
}
if (debitTags == null && accountToDebitId == null) {
final String debitTag = AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue();
final String debitAccount = AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue();
final StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(debitAccount).append(".or.")
.append(debitTag).append(".required");
final StringBuilder defaultUserMessage = new StringBuilder("The parameter ").append(debitAccount).append(" or")
.append(debitTag).append(" required");
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultUserMessage.toString(),
debitAccount + "," + debitTag, new Object[] { debitAccount, debitTag });
dataValidationErrors.add(error);
}
final String allowMultipleCredits = this.fromApiJsonHelper.extractStringNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())
.value(allowMultipleCredits).ignoreIfNull().notBlank();
final Boolean allowMultipleCreditEntries = this.fromApiJsonHelper.extractBooleanNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())
.value(allowMultipleCreditEntries).ignoreIfNull();
final String allowMultipleDebits = this.fromApiJsonHelper.extractStringNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())
.value(allowMultipleDebits).ignoreIfNull().notBlank();
final Boolean allowMultipleDebitEntries = this.fromApiJsonHelper.extractBooleanNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())
.value(allowMultipleDebitEntries).ignoreIfNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
}
public void validateCreditOrDebitTagArray(final String[] creditOrDebitTagArray, final DataValidatorBuilder baseDataValidator,
final String parameter) {
if (creditOrDebitTagArray != null && !ObjectUtils.isEmpty(creditOrDebitTagArray)) {
for (final String tag : creditOrDebitTagArray) {
baseDataValidator.reset().parameter(parameter).value(tag).ignoreIfNull().notBlank().longGreaterThanZero();
}
}
}
public void validateForUpdate(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.supportedParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("AccountingRule");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Long accountToDebitId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue()).value(accountToDebitId)
.ignoreIfNull().notBlank().integerGreaterThanZero();
final Long accountToCreditId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue()).value(accountToCreditId)
.ignoreIfNull().notBlank().integerGreaterThanZero();
final Long officeId = this.fromApiJsonHelper.extractLongNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.OFFICE_ID.getValue()).value(officeId).ignoreIfNull()
.integerGreaterThanZero();
final String name = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.NAME.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.NAME.getValue()).value(name).ignoreIfNull().notBlank()
.notExceedingLengthOf(100);
final String description = this.fromApiJsonHelper.extractStringNamed(AccountingRuleJsonInputParams.DESCRIPTION.getValue(), element);
return new AccountingRuleCommand(id, officeId, accountToDebitId, accountToCreditId, name, description);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DESCRIPTION.getValue()).value(description).ignoreIfNull()
.notExceedingLengthOf(500);
final String[] creditTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()).value(creditTags).ignoreIfNull()
.arrayNotEmpty();
validateCreditOrDebitTagArray(creditTags, baseDataValidator, AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue());
final String[] debitTags = this.fromApiJsonHelper.extractArrayNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(),
element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()).value(debitTags).ignoreIfNull()
.arrayNotEmpty();
validateCreditOrDebitTagArray(debitTags, baseDataValidator, AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue());
final String allowMultipleCredits = this.fromApiJsonHelper.extractStringNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())
.value(allowMultipleCredits).ignoreIfNull().notBlank();
final Boolean allowMultipleCreditEntries = this.fromApiJsonHelper.extractBooleanNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())
.value(allowMultipleCreditEntries).ignoreIfNull();
final String allowMultipleDebits = this.fromApiJsonHelper.extractStringNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())
.value(allowMultipleDebits).ignoreIfNull().notBlank();
final Boolean allowMultipleDebitEntries = this.fromApiJsonHelper.extractBooleanNamed(
AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue(), element);
baseDataValidator.reset().parameter(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())
.value(allowMultipleDebitEntries).ignoreIfNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
}
}
@@ -11,8 +11,8 @@ import org.mifosplatform.accounting.rule.data.AccountingRuleData;
public interface AccountingRuleReadPlatformService {
List<AccountingRuleData> retrieveAllAccountingRules(Long OfficeId);
List<AccountingRuleData> retrieveAllAccountingRules(Long OfficeId, boolean isAssociationParametersExists);
AccountingRuleData retrieveAccountingRuleById(long accountingRuleId);
AccountingRuleData retrieveAccountingRuleById(Long accountingRuleId);
}
@@ -7,16 +7,28 @@ package org.mifosplatform.accounting.rule.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mifosplatform.accounting.common.AccountingEnumerations;
import org.mifosplatform.accounting.glaccount.data.GLAccountData;
import org.mifosplatform.accounting.glaccount.data.GLAccountDataForLookup;
import org.mifosplatform.accounting.glaccount.service.GLAccountReadPlatformService;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryType;
import org.mifosplatform.accounting.rule.data.AccountingRuleData;
import org.mifosplatform.accounting.rule.data.AccountingTagRuleData;
import org.mifosplatform.accounting.rule.exception.AccountingRuleNotFoundException;
import org.mifosplatform.infrastructure.codes.data.CodeValueData;
import org.mifosplatform.infrastructure.core.data.EnumOptionData;
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@@ -24,29 +36,37 @@ import org.springframework.stereotype.Service;
public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final GLAccountReadPlatformService glAccountReadPlatformService;
@Autowired
public AccountingRuleReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) {
public AccountingRuleReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource,
final GLAccountReadPlatformService glAccountReadPlatformService) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.glAccountReadPlatformService = glAccountReadPlatformService;
}
private static final class AccountingRuleMapper implements RowMapper<AccountingRuleData> {
private static final class AccountingRuleDataExtractor implements ResultSetExtractor<Map<Long, AccountingRuleData>> {
private final String schemaSql;
private final JdbcTemplate jdbcTemplate;
private final GLAccountReadPlatformService glAccountReadPlatformService;
private final boolean isAssociationParametersExists;
public AccountingRuleMapper() {
public AccountingRuleDataExtractor(final JdbcTemplate jdbcTemplate,
final GLAccountReadPlatformService glAccountReadPlatformService, final boolean isAssociationParametersExists) {
this.jdbcTemplate = jdbcTemplate;
this.glAccountReadPlatformService = glAccountReadPlatformService;
this.isAssociationParametersExists = isAssociationParametersExists;
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("distinct rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,")
.append(" rule.debit_account_id as debitAccountId,rule.credit_account_id as creditAccountId,")
.append(" rule.description as description, rule.system_defined as systemDefined, ")
.append("debitAccount.glName as debitAccountName, debitAccount.glCode as debitAccountGLCode,")
.append("creditAccount.glName as creditAccountName, creditAccount.glCode as creditAccountGLCode ")
.append("from (Select gl.id as id,gl.name as glName, gl.gl_code as glCode from acc_gl_account gl,acc_accounting_rule rule ")
.append("where gl.id=rule.debit_account_id ) as debitAccount,")
.append("(Select gl.id as id,gl.name as glName, gl.gl_code as glCode from acc_gl_account gl,acc_accounting_rule rule ")
.append("where gl.id=rule.credit_account_id ) as creditAccount,")
.append("acc_accounting_rule rule left join m_office office on rule.office_id=office.id ")
.append("where debitAccount.id=rule.debit_account_id and creditAccount.id = rule.credit_account_id");
sqlBuilder
.append(" rule.id as id,rule.name as name, rule.office_id as officeId,office.name as officeName,")
.append(" rule.description as description, rule.system_defined as systemDefined, rule.allow_multiple_debits as allowMultipleDebitEntries, rule.allow_multiple_credits as allowMultipleCreditEntries, ")
.append("debitAccount.id AS debitAccountId, debitAccount.name as debitAccountName, debitAccount.gl_code as debitAccountGLCode, ")
.append("creditAccount.id AS creditAccountId, creditAccount.name as creditAccountName, creditAccount.gl_code as creditAccountGLCode")
.append(" from m_office AS office, acc_accounting_rule AS rule ")
.append(" LEFT JOIN acc_gl_account AS creditAccount ON rule.credit_account_id = creditAccount.id ")
.append(" LEFT JOIN acc_gl_account AS debitAccount ON rule.debit_account_id = debitAccount.id ")
.append("WHERE office.id=rule.office_id ");
this.schemaSql = sqlBuilder.toString();
}
@@ -55,52 +75,112 @@ public class AccountingRuleReadPlatformServiceImpl implements AccountingRuleRead
}
@Override
public AccountingRuleData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
public Map<Long, AccountingRuleData> extractData(final ResultSet rs) throws SQLException, DataAccessException {
final Map<Long, AccountingRuleData> extractedData = new HashMap<Long, AccountingRuleData>();
final Long id = rs.getLong("id");
final Long officeId = JdbcSupport.getLong(rs, "officeId");
final String officeName = rs.getString("officeName");
final String name = rs.getString("name");
final String description = rs.getString("description");
final Long accountToDebitId = rs.getLong("debitAccountId");
final Long accountToCreditId = rs.getLong("creditAccountId");
final boolean systemDefined = rs.getBoolean("systemDefined");
final String debitAccountName = rs.getString("debitAccountName");
final String creditAccountName = rs.getString("creditAccountName");
final String debitAccountGLCode = rs.getString("debitAccountGLCode");
final String creditAccountGLCode = rs.getString("creditAccountGLCode");
final GLAccountData debitAccountData = new GLAccountData(accountToDebitId, debitAccountName, debitAccountGLCode);
final GLAccountData creditAccountData = new GLAccountData(accountToCreditId, creditAccountName, creditAccountGLCode);
while (rs.next()) {
final Long id = rs.getLong("id");
AccountingRuleData accountingRuleData = extractedData.get(id);
if (accountingRuleData == null) {
final Long officeId = JdbcSupport.getLong(rs, "officeId");
final String officeName = rs.getString("officeName");
final String name = rs.getString("name");
final String description = rs.getString("description");
final Long accountToDebitId = JdbcSupport.getLong(rs, "debitAccountId");
final Long accountToCreditId = JdbcSupport.getLong(rs, "creditAccountId");
final boolean systemDefined = rs.getBoolean("systemDefined");
final boolean allowMultipleDebitEntries = rs.getBoolean("allowMultipleDebitEntries");
final boolean allowMultipleCreditEntries = rs.getBoolean("allowMultipleCreditEntries");
final String debitAccountName = rs.getString("debitAccountName");
final String creditAccountName = rs.getString("creditAccountName");
final String debitAccountGLCode = rs.getString("debitAccountGLCode");
final String creditAccountGLCode = rs.getString("creditAccountGLCode");
return new AccountingRuleData(id, officeId, officeName, name, description, systemDefined, debitAccountData, creditAccountData);
final GLAccountData debitAccountData = accountToDebitId == null ? null : new GLAccountData(accountToDebitId,
debitAccountName, debitAccountGLCode);
final GLAccountData creditAccountData = accountToCreditId == null ? null : new GLAccountData(accountToCreditId,
creditAccountName, creditAccountGLCode);
accountingRuleData = new AccountingRuleData(id, officeId, officeName, name, description, systemDefined,
allowMultipleDebitEntries, allowMultipleCreditEntries, debitAccountData, creditAccountData);
}
if (accountingRuleData.getCreditAccountHead() == null) {
final List<AccountingTagRuleData> creditTags = !this.isAssociationParametersExists ? getCreditOrDebitTags(id,
JournalEntryType.CREDIT.getValue()) : null;
final List<GLAccountDataForLookup> creditAccounts = this.isAssociationParametersExists ? this.glAccountReadPlatformService
.retrieveAccountsByTagId(id, JournalEntryType.CREDIT.getValue()) : null;
accountingRuleData = new AccountingRuleData(accountingRuleData, creditTags, creditAccounts,
accountingRuleData.getDebitTags(), accountingRuleData.getDebitAccounts());
}
if (accountingRuleData.getDebitAccountHead() == null) {
final List<AccountingTagRuleData> debitTags = !this.isAssociationParametersExists ? getCreditOrDebitTags(id,
JournalEntryType.DEBIT.getValue()) : null;
final List<GLAccountDataForLookup> debitAccounts = this.isAssociationParametersExists ? this.glAccountReadPlatformService
.retrieveAccountsByTagId(id, JournalEntryType.DEBIT.getValue()) : null;
accountingRuleData = new AccountingRuleData(accountingRuleData, accountingRuleData.getCreditTags(),
accountingRuleData.getCreditAccounts(), debitTags, debitAccounts);
}
extractedData.put(id, accountingRuleData);
}
return extractedData;
}
private List<AccountingTagRuleData> getCreditOrDebitTags(final Long creditOrDebitAccount, final Integer transactionType) {
final AccountingTagRuleDataMapper mapper = new AccountingTagRuleDataMapper();
final String taggedAccountsSchema = "Select " + mapper.taggedAccountSchema() + " where rule.id = ? and tag.acc_type_enum=?";
return this.jdbcTemplate.query(taggedAccountsSchema, mapper, new Object[] { creditOrDebitAccount, transactionType });
}
}
@Override
public List<AccountingRuleData> retrieveAllAccountingRules(Long officeId) {
final AccountingRuleMapper rm = new AccountingRuleMapper();
Object[] arguments = new Object[]{};
String sql = "select " + rm.schema() +" and system_defined=0 ";
if(officeId != null) {
sql = sql +" and office.id = ?";
arguments = new Object[] { officeId };
public List<AccountingRuleData> retrieveAllAccountingRules(final Long officeId, final boolean isAssociationParametersExists) {
final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate,
this.glAccountReadPlatformService, isAssociationParametersExists);
Object[] arguments = new Object[] {};
String sql = "select " + resultSetExtractor.schema() + " and system_defined=0 ";
if (officeId != null) {
sql = sql + " and office.id = ?";
arguments = new Object[] { officeId };
}
sql = sql + " order by rule.id asc";
return this.jdbcTemplate.query(sql, rm, arguments);
sql = sql + " order by rule.id asc";
final Map<Long, AccountingRuleData> extractedData = this.jdbcTemplate.query(sql, resultSetExtractor, arguments);
return new ArrayList<AccountingRuleData>(extractedData.values());
}
@Override
public AccountingRuleData retrieveAccountingRuleById(long accountingRuleId) {
public AccountingRuleData retrieveAccountingRuleById(final Long accountingRuleId) {
try {
final AccountingRuleDataExtractor resultSetExtractor = new AccountingRuleDataExtractor(this.jdbcTemplate,
this.glAccountReadPlatformService, false);
final String sql = "select " + resultSetExtractor.schema() + " and rule.id = ?";
final AccountingRuleMapper rm = new AccountingRuleMapper();
final String sql = "select " + rm.schema() + " and rule.id = ?";
final AccountingRuleData accountingRuleData = this.jdbcTemplate.queryForObject(sql, rm, new Object[] { accountingRuleId });
final Map<Long, AccountingRuleData> extractedData = this.jdbcTemplate.query(sql, resultSetExtractor,
new Object[] { accountingRuleId });
final AccountingRuleData accountingRuleData = extractedData.get(accountingRuleId);
if (accountingRuleData == null) { throw new AccountingRuleNotFoundException(accountingRuleId); }
return accountingRuleData;
} catch (final EmptyResultDataAccessException e) {
throw new AccountingRuleNotFoundException(accountingRuleId);
}
}
private static final class AccountingTagRuleDataMapper implements RowMapper<AccountingTagRuleData> {
public String taggedAccountSchema() {
return " tag.id as id,tag.tag_id as tagId, tag.acc_type_enum as transactionType, cv.code_value as tagName from m_code_value cv join acc_rule_tags tag on tag.tag_id=cv.id "
+ "join acc_accounting_rule rule on tag.acc_rule_id=rule.id ";
}
@Override
public AccountingTagRuleData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final Long tagId = rs.getLong("tagId");
final Integer transactionType = JdbcSupport.getInteger(rs, "transactionType");
final String tagName = rs.getString("tagName");
final CodeValueData tag = CodeValueData.instance(tagId, tagName);
final EnumOptionData transactionTypeEnum = AccountingEnumerations.journalEntryType(transactionType);
return new AccountingTagRuleData(id, tag, transactionTypeEnum);
}
}
}
@@ -5,18 +5,30 @@
*/
package org.mifosplatform.accounting.rule.service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.accounting.closure.api.GLClosureJsonInputParams;
import org.mifosplatform.accounting.glaccount.domain.GLAccount;
import org.mifosplatform.accounting.glaccount.domain.GLAccountRepositoryWrapper;
import org.mifosplatform.accounting.journalentry.domain.JournalEntryType;
import org.mifosplatform.accounting.rule.api.AccountingRuleJsonInputParams;
import org.mifosplatform.accounting.rule.command.AccountingRuleCommand;
import org.mifosplatform.accounting.rule.domain.AccountingRule;
import org.mifosplatform.accounting.rule.domain.AccountingRuleRepository;
import org.mifosplatform.accounting.rule.domain.AccountingRuleRepositoryWrapper;
import org.mifosplatform.accounting.rule.domain.AccountingTagRule;
import org.mifosplatform.accounting.rule.exception.AccountingRuleDataException;
import org.mifosplatform.accounting.rule.exception.AccountingRuleDuplicateException;
import org.mifosplatform.accounting.rule.serialization.AccountingRuleCommandFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.codes.domain.CodeValue;
import org.mifosplatform.infrastructure.codes.domain.CodeValueRepository;
import org.mifosplatform.infrastructure.codes.exception.CodeValueNotFoundException;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
@@ -41,16 +53,19 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco
private final GLAccountRepositoryWrapper accountRepositoryWrapper;
private final OfficeRepository officeRepository;
private final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final CodeValueRepository codeValueRepository;
@Autowired
public AccountingRuleWritePlatformServiceJpaRepositoryImpl(final AccountingRuleRepositoryWrapper accountingRuleRepositoryWrapper,
final GLAccountRepositoryWrapper accountRepositoryWrapper, final OfficeRepository officeRepository,
final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer, final AccountingRuleRepository ruleRepository) {
final AccountingRuleRepository ruleRepository, final AccountingRuleCommandFromApiJsonDeserializer fromApiJsonDeserializer,
final CodeValueRepository codeValueRepository) {
this.accountRepositoryWrapper = accountRepositoryWrapper;
this.officeRepository = officeRepository;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.accountingRuleRepository = ruleRepository;
this.accountingRuleRepositoryWrapper = accountingRuleRepositoryWrapper;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.codeValueRepository = codeValueRepository;
}
/**
@@ -59,8 +74,9 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco
*/
private void handleAccountingRuleIntegrityIssues(final JsonCommand command, final DataIntegrityViolationException dve) {
final Throwable realCause = dve.getMostSpecificCause();
if (realCause.getMessage().contains("accounting_rule_name_unique")) { throw new AccountingRuleDuplicateException(
command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.NAME.getValue())); }
if (realCause.getMessage().contains("accounting_rule_name_unique")) {
throw new AccountingRuleDuplicateException(command.stringValueOfParameterNamed(AccountingRuleJsonInputParams.NAME.getValue()));
} else if (realCause.getMessage().contains("UNIQUE_ACCOUNT_RULE_TAGS")) { throw new AccountingRuleDuplicateException(); }
logger.error(dve.getMessage(), dve);
throw new PlatformDataIntegrityException("error.msg.accounting.rule.unknown.data.integrity.issue",
@@ -69,10 +85,10 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco
@Transactional
@Override
public CommandProcessingResult createAccountingRule(JsonCommand command) {
public CommandProcessingResult createAccountingRule(final JsonCommand command) {
try {
final AccountingRuleCommand accountingRuleCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
accountingRuleCommand.validateForCreate();
this.fromApiJsonDeserializer.validateForCreate(command.json());
// check office is valid
final Long officeId = command.longValueOfParameterNamed(GLClosureJsonInputParams.OFFICE_ID.getValue());
@@ -82,15 +98,8 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco
if (office == null) { throw new OfficeNotFoundException(officeId); }
}
// get the GL Accounts to Debit and Credit
final Long accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue());
final Long accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue());
final GLAccount accountToDebit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId);
final GLAccount accountToCredit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId);
final AccountingRule accountingRule = AccountingRule.fromJson(office, accountToDebit, accountToCredit, command);
final AccountingRule accountingRule = assembleAccountingRuleAndTags(office, command);
this.accountingRuleRepository.saveAndFlush(accountingRule);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(officeId)
.withEntityId(accountingRule.getId()).build();
} catch (final DataIntegrityViolationException dve) {
@@ -99,59 +108,229 @@ public class AccountingRuleWritePlatformServiceJpaRepositoryImpl implements Acco
}
}
@Transactional
@Override
public CommandProcessingResult updateAccountingRule(Long accountingRuleId, JsonCommand command) {
final AccountingRuleCommand accountingRuleCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
accountingRuleCommand.validateForUpdate();
private AccountingRule assembleAccountingRuleAndTags(final Office office, final JsonCommand command) {
// get the GL Accounts or tags to Debit and Credit
final String[] debitTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue());
final String[] creditTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue());
final Set<String> incomingDebitTags = debitTags == null ? new HashSet<String>(): new HashSet<String>(Arrays.asList(debitTags));
final Set<String> incomingCreditTags = creditTags == null ? new HashSet<String>() : new HashSet<String>(Arrays.asList(creditTags));
final Long accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue());
final Long accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue());
Long officeId = null;
if (command.parameterExists(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) {
officeId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue());
boolean allowMultipleCreditEntries = false;
boolean allowMultipleDebitEntries = false;
GLAccount debitAccount = null;
GLAccount creditAccount = null;
List<AccountingTagRule> accountingTagRules = new ArrayList<AccountingTagRule>();
if ((accountToDebitId != null && debitTags != null) || (accountToDebitId == null && debitTags == null)) {
throw new AccountingRuleDataException(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(),
AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue());
} else if (accountToDebitId != null) {
debitAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId);
} else if (debitTags != null) {
accountingTagRules = saveDebitOrCreditTags(incomingDebitTags, JournalEntryType.DEBIT, accountingTagRules);
allowMultipleDebitEntries = command
.booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue());
}
Long accountToDebitId = null;
if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) {
accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue());
if ((accountToCreditId != null && creditTags != null) || (accountToCreditId == null && creditTags == null)) {
throw new AccountingRuleDataException(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(),
AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue());
} else if (accountToCreditId != null) {
creditAccount = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId);
} else if (creditTags != null) {
accountingTagRules = saveDebitOrCreditTags(incomingCreditTags, JournalEntryType.CREDIT, accountingTagRules);
allowMultipleCreditEntries = command
.booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue());
}
Long accountToCreditId = null;
if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) {
accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue());
}
final AccountingRule accountingRule = AccountingRule.fromJson(office, debitAccount, creditAccount, command,
allowMultipleCreditEntries, allowMultipleDebitEntries);
accountingRule.updateAccountingRuleForTags(accountingTagRules);
final AccountingRule accountingRule = this.accountingRuleRepositoryWrapper.findOneWithNotFoundDetection(accountingRuleId);
final Map<String, Object> changesOnly = accountingRule.update(command);
if (accountToDebitId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) {
final GLAccount accountToDebit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId);
accountingRule.setAccountToDebit(accountToDebit);
}
if (accountToCreditId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) {
final GLAccount accountToCredit = accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId);
accountingRule.setAccountToCredit(accountToCredit);
}
if (officeId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) {
final Office userOffice = this.officeRepository.findOne(officeId);
if (userOffice == null) { throw new OfficeNotFoundException(officeId); }
accountingRule.setOffice(userOffice);
}
if (!changesOnly.isEmpty()) {
this.accountingRuleRepository.saveAndFlush(accountingRule);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(accountingRule.getId())
.with(changesOnly).build();
return accountingRule;
}
@Transactional
@Override
public CommandProcessingResult deleteAccountingRule(Long accountingRuleId) {
public CommandProcessingResult updateAccountingRule(final Long accountingRuleId, final JsonCommand command) {
try {
this.fromApiJsonDeserializer.validateForUpdate(command.json());
Long officeId = null;
if (command.parameterExists(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) {
officeId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.OFFICE_ID.getValue());
}
Long accountToDebitId = null;
if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) {
accountToDebitId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue());
}
Long accountToCreditId = null;
if (command.parameterExists(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) {
accountToCreditId = command.longValueOfParameterNamed(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue());
}
String[] debitTags = null;
if (command.parameterExists(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue())) {
debitTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue());
}
String[] creditTags = null;
if (command.parameterExists(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue())) {
creditTags = command.arrayValueOfParameterNamed(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue());
}
if (accountToDebitId != null && debitTags != null) { throw new AccountingRuleDataException(
AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue(), AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue()); }
if (accountToCreditId != null && creditTags != null) { throw new AccountingRuleDataException(
AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue(),
AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue()); }
boolean allowMultipleCreditEntries = false;
if (command.parameterExists(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue())) {
allowMultipleCreditEntries = command
.booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_CREDIT_ENTRIES.getValue());
}
boolean allowMultipleDebitEntries = false;
if (command.parameterExists(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue())) {
allowMultipleDebitEntries = command
.booleanPrimitiveValueOfParameterNamed(AccountingRuleJsonInputParams.ALLOW_MULTIPLE_DEBIT_ENTRIES.getValue());
}
final AccountingRule accountingRule = this.accountingRuleRepositoryWrapper.findOneWithNotFoundDetection(accountingRuleId);
final Map<String, Object> changesOnly = accountingRule.update(command);
if (accountToDebitId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_DEBIT.getValue())) {
final GLAccount accountToDebit = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToDebitId);
accountingRule.updateDebitAccount(accountToDebit);
accountingRule.updateTags(JournalEntryType.CREDIT);
}
if (accountToCreditId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.ACCOUNT_TO_CREDIT.getValue())) {
final GLAccount accountToCredit = this.accountRepositoryWrapper.findOneWithNotFoundDetection(accountToCreditId);
accountingRule.updateCreditAccount(accountToCredit);
accountingRule.updateTags(JournalEntryType.DEBIT);
}
if (creditTags != null && creditTags.length > 0
&& command.parameterExists(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue())) {
final Set<String> creditTagsToAdd = determineCreditTagToAddAndRemoveOldTags(creditTags, JournalEntryType.CREDIT,
accountingRule);
if (!creditTagsToAdd.isEmpty()) {
List<AccountingTagRule> accountingTagRules = new ArrayList<AccountingTagRule>();
accountingTagRules = saveDebitOrCreditTags(creditTagsToAdd, JournalEntryType.CREDIT, accountingTagRules);
accountingRule.updateAccountingRuleForTags(accountingTagRules);
accountingRule.updateCreditAccount(null);
if (allowMultipleCreditEntries) {
accountingRule.updateAllowMultipleCreditEntries(allowMultipleCreditEntries);
}
changesOnly.put(AccountingRuleJsonInputParams.CREDIT_ACCOUNT_TAGS.getValue(), creditTagsToAdd);
}
}
if (debitTags != null && debitTags.length > 0
&& command.parameterExists(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue())) {
final Set<String> debitTagsToAdd = determineCreditTagToAddAndRemoveOldTags(debitTags, JournalEntryType.DEBIT,
accountingRule);
if (!debitTagsToAdd.isEmpty()) {
List<AccountingTagRule> accountingTagRules = new ArrayList<AccountingTagRule>();
accountingTagRules = saveDebitOrCreditTags(debitTagsToAdd, JournalEntryType.DEBIT, accountingTagRules);
accountingRule.updateAccountingRuleForTags(accountingTagRules);
accountingRule.updateDebitAccount(null);
if (allowMultipleDebitEntries) {
accountingRule.updateAllowMultipleDebitEntries(allowMultipleDebitEntries);
}
changesOnly.put(AccountingRuleJsonInputParams.DEBIT_ACCOUNT_TAGS.getValue(), debitTagsToAdd);
}
}
if (officeId != null && changesOnly.containsKey(AccountingRuleJsonInputParams.OFFICE_ID.getValue())) {
final Office userOffice = this.officeRepository.findOne(officeId);
if (userOffice == null) { throw new OfficeNotFoundException(officeId); }
accountingRule.setOffice(userOffice);
}
if (!changesOnly.isEmpty()) {
this.accountingRuleRepository.saveAndFlush(accountingRule);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(accountingRule.getId())
.with(changesOnly).build();
} catch (final DataIntegrityViolationException dve) {
handleAccountingRuleIntegrityIssues(command, dve);
return CommandProcessingResult.empty();
}
}
private Set<String> determineCreditTagToAddAndRemoveOldTags(final String[] creditOrDebitTags, final JournalEntryType type,
final AccountingRule accountingRule) {
final Set<String> incomingTags = new HashSet<String>(Arrays.asList(creditOrDebitTags));
final Set<AccountingTagRule> existingTags = accountingRule.getAccountingTagRulesByType(type);
final Set<String> existingTagIds = retrieveExistingTagIds(existingTags);
final Set<String> tagsToAdd = new HashSet<String>();
final Set<String> tagsToRemove = existingTagIds;
final Map<Long, AccountingTagRule> accountsToRemove = new HashMap<Long, AccountingTagRule>();
for (final String tagId : incomingTags) {
if (existingTagIds.contains(tagId)) {
tagsToRemove.remove(tagId);
} else {
tagsToAdd.add(tagId);
}
}
if (!tagsToRemove.isEmpty()) {
for (final String tagId : tagsToRemove) {
for (final AccountingTagRule accountingTagRule : existingTags) {
if (tagId.equals(accountingTagRule.getTagId().toString())) {
accountsToRemove.put(accountingTagRule.getId(), accountingTagRule);
}
}
}
accountingRule.removeOldTags(new ArrayList<AccountingTagRule>(accountsToRemove.values()));
}
return tagsToAdd;
}
private Set<String> retrieveExistingTagIds(final Set<AccountingTagRule> existingCreditTags) {
final Set<String> existingCreditTagIds = new HashSet<String>();
for (final AccountingTagRule accountingTagRule : existingCreditTags) {
existingCreditTagIds.add(accountingTagRule.getTagId().toString());
}
return existingCreditTagIds;
}
@Transactional
@Override
public CommandProcessingResult deleteAccountingRule(final Long accountingRuleId) {
final AccountingRule accountingRule = this.accountingRuleRepositoryWrapper.findOneWithNotFoundDetection(accountingRuleId);
this.accountingRuleRepository.delete(accountingRule);
return new CommandProcessingResultBuilder().withEntityId(accountingRule.getId()).build();
}
private List<AccountingTagRule> saveDebitOrCreditTags(final Set<String> creditOrDebitTagArray, final JournalEntryType transactionType,
final List<AccountingTagRule> accountingTagRules) {
for (final String creditOrDebitTag : creditOrDebitTagArray) {
if (creditOrDebitTag != null && StringUtils.isNotBlank(creditOrDebitTag)) {
final Long creditOrDebitTagIdLongValue = Long.valueOf(creditOrDebitTag);
final CodeValue creditOrDebitAccount = this.codeValueRepository.findOne(creditOrDebitTagIdLongValue);
if (creditOrDebitAccount == null) { throw new CodeValueNotFoundException(creditOrDebitTagIdLongValue); }
final AccountingTagRule accountingTagRule = AccountingTagRule.create(creditOrDebitAccount, transactionType.getValue());
accountingTagRules.add(accountingTagRule);
}
}
return accountingTagRules;
}
}
@@ -108,6 +108,21 @@ public class CommandWrapper {
return this.actionName.equalsIgnoreCase("CREATE");
}
public boolean isCreateDatatable() {
return (this.actionName.equalsIgnoreCase("CREATE") && this.href.startsWith("/datatables/")
&& this.entityId == null);
}
public boolean isDeleteDatatable() {
return (this.actionName.equalsIgnoreCase("DELETE") && this.href.startsWith("/datatables/")
&& this.entityId == null);
}
public boolean isUpdateDatatable() {
return (this.actionName.equalsIgnoreCase("UPDATE") && this.href.startsWith("/datatables/")
&& this.entityId == null);
}
public String getTaskPermissionName() {
return this.taskPermissionName;
}
@@ -356,6 +356,33 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createDBDatatable(final String json) {
this.actionName = "CREATE";
this.entityName = "DATATABLE";
this.entityId = null;
this.href = "/datatables/";
this.json = json;
return this;
}
public CommandWrapperBuilder updateDBDatatable(final String datatable, final String json) {
this.actionName = "UPDATE";
this.entityName = "DATATABLE";
this.entityId = null;
this.href = "/datatables/" + datatable;
this.json = json;
return this;
}
public CommandWrapperBuilder deleteDBDatatable(final String datatable, final String json) {
this.actionName = "DELETE";
this.entityName = "DATATABLE";
this.entityId = null;
this.href = "/datatables/" + datatable;
this.json = json;
return this;
}
public CommandWrapperBuilder createDatatable(final String datatable, final Long apptableId, final Long datatableId) {
this.actionName = "CREATE";
commonDatatableSettings(datatable, apptableId, datatableId);
@@ -63,7 +63,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
final JsonCommand command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(),
wrapper.getEntityId(), wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(),
wrapper.getSavingsId(), wrapper.getCodeId(), wrapper.getSupportedEntityType(), wrapper.getSupportedEntityId(),
wrapper.getTransactionId());
wrapper.getTransactionId(), wrapper.getHref());
result = this.processAndLogCommandService.processAndLogCommand(wrapper, command, isApprovedByChecker);
} catch (RollbackTransactionAsCommandIsNotApprovedByCheckerException e) {
@@ -86,7 +86,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.json());
final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.json(), parsedCommand,
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(),
commandSourceInput.subresourceId());
commandSourceInput.subresourceId(), commandSourceInput.getResourceGetUrl());
final boolean makerCheckerApproval = true;
return this.processAndLogCommandService.processAndLogCommand(wrapper, command, makerCheckerApproval);
@@ -78,7 +78,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
commandSourceResult.updateJsonTo(changesOnlyJson);
}
if (!result.hasChanges() && wrapper.isUpdateOperation()) {
if (!result.hasChanges() && wrapper.isUpdateOperation() && !wrapper.isUpdateDatatable()) {
commandSourceResult.updateJsonTo(null);
}
@@ -108,7 +108,13 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
if (wrapper.isConfigurationResource()) {
handler = applicationContext.getBean("updateGlobalConfigurationCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDatatableResource()) {
if (wrapper.isCreate()) {
if (wrapper.isCreateDatatable()) {
handler = applicationContext.getBean("createDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDeleteDatatable()) {
handler = applicationContext.getBean("deleteDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdateDatatable()) {
handler = applicationContext.getBean("updateDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isCreate()) {
handler = applicationContext.getBean("createDatatableEntryCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdateMultiple()) {
handler = applicationContext.getBean("updateOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class);
@@ -10,7 +10,6 @@ package org.mifosplatform.infrastructure.codes.data;
*/
public class CodeValueData {
@SuppressWarnings("unused")
private final Long id;
@SuppressWarnings("unused")
@@ -32,4 +31,8 @@ public class CodeValueData {
this.name = name;
this.position = position;
}
public Long getId() {
return id;
}
}
@@ -0,0 +1,27 @@
package org.mifosplatform.infrastructure.configuration.data;
public class S3CredentialsData {
private final String bucketName;
private final String accessKey;
private final String secretKey;
public S3CredentialsData(String bucketName, String accessKey, String secretKey) {
this.bucketName = bucketName;
this.accessKey = accessKey;
this.secretKey = secretKey;
}
public String getBucketName() {
return this.bucketName;
}
public String getAccessKey() {
return this.accessKey;
}
public String getSecretKey() {
return this.secretKey;
}
}
@@ -8,4 +8,5 @@ package org.mifosplatform.infrastructure.configuration.domain;
public interface ConfigurationDomainService {
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
boolean isAmazonS3Enabled();
}
@@ -39,4 +39,9 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
return thisTask.hasMakerCheckerEnabled() && property.isEnabled();
}
@Override
public boolean isAmazonS3Enabled(){
return this.globalConfigurationRepository.findOneByName("amazon-S3").isEnabled();
}
}
@@ -0,0 +1,37 @@
package org.mifosplatform.infrastructure.configuration.domain;
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@Table(name = "c_external_service", uniqueConstraints = { @UniqueConstraint(columnNames = ("name")) })
public class ExternalService extends AbstractPersistable<Long> {
@SuppressWarnings("unused")
@Column(name = "name", nullable = false)
private String name;
@Column(name = "value", nullable = false)
private String value;
public ExternalService(final String name, final String value) {
this.name = name;
this.value = value;
}
protected ExternalService() {}
public String getValue() {
return this.value;
}
}
@@ -0,0 +1,9 @@
package org.mifosplatform.infrastructure.configuration.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ExternalServiceRepository extends JpaRepository<ExternalService, Long>, JpaSpecificationExecutor<ExternalService> {
ExternalService findOneByName(String name);
}
@@ -0,0 +1,9 @@
package org.mifosplatform.infrastructure.configuration.service;
public class ExternalServicesConstants {
public static final String S3_BUCKET_NAME = "s3_bucket_name";
public static final String S3_ACCESS_KEY = "s3_access_key";
public static final String S3_SECRET_KEY = "s3_secret_key";
}
@@ -0,0 +1,9 @@
package org.mifosplatform.infrastructure.configuration.service;
import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData;
public interface ExternalServicesReadPlatformService {
S3CredentialsData getS3Credentials();
}
@@ -0,0 +1,51 @@
package org.mifosplatform.infrastructure.configuration.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData;
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Service;
@Service
public class ExternalServicesReadPlatformServiceImpl implements ExternalServicesReadPlatformService {
private final JdbcTemplate jdbcTemplate;
@Autowired
public ExternalServicesReadPlatformServiceImpl(final TenantAwareRoutingDataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
private static final class S3CredentialsDataExtractor implements ResultSetExtractor<S3CredentialsData> {
@Override
public S3CredentialsData extractData(ResultSet rs) throws SQLException, DataAccessException {
String accessKey = null;
String bucketName = null;
String secretKey = null;
while (rs.next()) {
if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_ACCESS_KEY)) {
accessKey = rs.getString("value");
} else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_BUCKET_NAME)) {
bucketName = rs.getString("value");
} else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.S3_SECRET_KEY)) {
secretKey = rs.getString("value");
}
}
return new S3CredentialsData(bucketName, accessKey, secretKey);
}
}
@Override
public S3CredentialsData getS3Credentials() {
final ResultSetExtractor<S3CredentialsData> resultSetExtractor = new S3CredentialsDataExtractor();
final String sql = "SELECT es.name, es.value FROM c_external_service es where es.name in('s3_bucket_name','s3_access_key','s3_secret_key')";
final S3CredentialsData s3CredentialsData = this.jdbcTemplate.query(sql, resultSetExtractor, new Object[] {});
return s3CredentialsData;
}
}
@@ -1,15 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.core.api;
public class ApiConstants {
// TODO:Vishwas Need to move these settings to the Database
public static final Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5;
// TODO:Vishwas Need to move these settings to the Database
public static final Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1;
}
@@ -45,24 +45,27 @@ public final class JsonCommand {
private final String supportedEntityType;
private final Long supportedEntityId;
private final String transactionId;
private final String url;
public static JsonCommand from(final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper,
final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId,
final Long loanId, final Long savingsId, final Long codeId, final String supportedEntityType, final Long supportedEntityId,
final String transactionId) {
final String transactionId, final String url) {
return new JsonCommand(null, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId,
clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId);
clientId, loanId, savingsId, codeId, supportedEntityType, supportedEntityId, transactionId, url);
}
public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId) {
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final String url) {
return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, null, null,
null, null, null, null, null, null);
null, null, null, null, null, null, url);
}
public JsonCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final Long codeId,
final String supportedEntityType, final Long supportedEntityId, final String transactionId) {
final String supportedEntityType, final Long supportedEntityId, final String transactionId,
final String url) {
this.commandId = commandId;
this.jsonCommand = jsonCommand;
this.parsedCommand = parsedCommand;
@@ -78,6 +81,7 @@ public final class JsonCommand {
this.supportedEntityType = supportedEntityType;
this.supportedEntityId = supportedEntityId;
this.transactionId = transactionId;
this.url = url;
}
public String json() {
@@ -144,6 +148,10 @@ public final class JsonCommand {
public String getTransactionId() {
return this.transactionId;
}
public String getUrl() {
return this.url;
}
private boolean differenceExists(final LocalDate baseValue, final LocalDate workingCopyValue) {
boolean differenceExists = false;
@@ -16,6 +16,8 @@ import net.fortuna.ical4j.model.property.RRule;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.ObjectUtils;
import com.google.gson.JsonArray;
public class DataValidatorBuilder {
private final List<ApiParameterError> dataValidationErrors;
@@ -206,12 +208,35 @@ public class DataValidatorBuilder {
if (value != null) {
List<Object> valuesList = Arrays.asList(values);
String valuesListStr = StringUtils.join(valuesList, ", ");
if (!valuesList.contains(this.value)) {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
.append(".is.not.one.of.expected.enumerations");
StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must be one of [ ")
.append(values).append(" ] ").append(".");
.append(valuesListStr).append(" ] ").append(".");
ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
defaultEnglishMessage.toString(), parameter, this.value, values);
dataValidationErrors.add(error);
}
}
return this;
}
public DataValidatorBuilder isNotOneOfTheseValues(final Object... values) {
if (value == null && ignoreNullValue) { return this; }
if (value != null) {
List<Object> valuesList = Arrays.asList(values);
String valuesListStr = StringUtils.join(valuesList, ", ");
if (valuesList.contains(this.value)) {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
.append(".is.one.of.unwanted.enumerations");
StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(" must not be any of [ ")
.append(valuesListStr).append(" ] ").append(".");
ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
defaultEnglishMessage.toString(), parameter, this.value, values);
@@ -309,6 +334,22 @@ public class DataValidatorBuilder {
}
return this;
}
public DataValidatorBuilder jsonArrayNotEmpty() {
if (value == null && ignoreNullValue) { return this; }
JsonArray array = (JsonArray)value;
if (value != null && !array.iterator().hasNext()) {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
.append(".cannot.be.empty");
StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter).append(
" cannot be empty. You must select at least one.");
ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(),
parameter);
dataValidationErrors.add(error);
}
return this;
}
public void expectedArrayButIsNot() {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
@@ -385,7 +426,7 @@ public class DataValidatorBuilder {
}
public DataValidatorBuilder cantBeBlankWhenParameterProvidedIs(final String parameterName, final Object parameterValue) {
if (StringUtils.isNotBlank(value.toString())) { return this; }
if (value != null && StringUtils.isNotBlank(value.toString())) { return this; }
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
.append(".must.be.provided.when.").append(parameterName).append(".is.").append(parameterValue);
@@ -533,4 +574,23 @@ public class DataValidatorBuilder {
}
return this;
}
public DataValidatorBuilder matchesRegularExpression(final String expression) {
if (value == null && ignoreNullValue) { return this; }
if (value != null && !value.toString().matches(expression)) {
StringBuilder validationErrorCode = new StringBuilder("validation.msg.").append(resource).append(".").append(parameter)
.append(".does.not.match.regexp");
StringBuilder defaultEnglishMessage = new StringBuilder("The parameter ").append(parameter)
.append(" must match the provided regular expression [ ")
.append(expression).append(" ] ").append(".");
ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(),
defaultEnglishMessage.toString(), parameter, this.value, expression);
dataValidationErrors.add(error);
}
return this;
}
}
@@ -13,12 +13,13 @@ import org.springframework.security.authentication.dao.SaltSource;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.stereotype.Service;
@SuppressWarnings("deprecation")
@Service(value = "applicationPasswordEncoder")
@Scope("singleton")
public class DefaultPlatformPasswordEncoder implements PlatformPasswordEncoder {
private final PasswordEncoder passwordEncoder;
private final SaltSource saltSource;
private final SaltSource saltSource;
@Autowired
public DefaultPlatformPasswordEncoder(final PasswordEncoder passwordEncoder, final SaltSource saltSource) {
@@ -47,6 +47,19 @@ public class JdbcSupport {
return (Integer) JdbcUtils.getResultSetValue(rs, rs.findColumn(columnName), Integer.class);
}
public static Integer getIntegerDefaultToNullIfZero(final ResultSet rs, final String columnName) throws SQLException {
Integer value = (Integer) JdbcUtils.getResultSetValue(rs, rs.findColumn(columnName), Integer.class);
return defaultToNullIfZero(value);
}
private static Integer defaultToNullIfZero(final Integer value) {
Integer result = value;
if (result != null && Integer.valueOf(0).equals(value)) {
result = null;
}
return result;
}
public static BigDecimal getBigDecimalDefaultToZeroIfNull(final ResultSet rs, final String columnName) throws SQLException {
final BigDecimal value = rs.getBigDecimal(columnName);
return defaultToZeroIfNull(value);
@@ -3,7 +3,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.savings.domain;
package org.mifosplatform.infrastructure.core.domain;
import org.joda.time.Days;
import org.joda.time.LocalDate;
@@ -0,0 +1,239 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.core.serialization;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder;
import org.mifosplatform.infrastructure.core.exception.InvalidJsonException;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
@Component
public class DatatableCommandFromApiJsonDeserializer {
private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$";
private final static String DATATABLE_COLUMN_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,}[a-zA-Z0-9]$";
/**
* The parameters supported for this command.
*/
private final Set<String> supportedParametersForCreate = new HashSet<String>(Arrays.asList(
"datatableName", "apptableName", "multiRow", "columns"));
private final Set<String> supportedParametersForCreateColumns = new HashSet<String>(Arrays.asList(
"name", "type", "length", "mandatory", "code"));
private final Set<String> supportedParametersForUpdate = new HashSet<String>(Arrays.asList(
"apptableName", "changeColumns", "addColumns", "dropColumns"));
private final Set<String> supportedParametersForAddColumns = new HashSet<String>(Arrays.asList(
"name", "type", "length", "mandatory", "after", "code"));
private final Set<String> supportedParametersForChangeColumns = new HashSet<String>(Arrays.asList(
"name", "newName", "mandatory", "after", "code", "newCode"));
private final Set<String> supportedParametersForDropColumns = new HashSet<String>(Arrays.asList(
"name"));
private final Object[] supportedColumnTypes = { "String", "Number", "Decimal", "Date", "Text", "Dropdown" };
private final Object[] supportedApptableNames = { "m_loan", "m_savings_account", "m_client", "m_group", "m_office" };
private final FromJsonHelper fromApiJsonHelper;
@Autowired
public DatatableCommandFromApiJsonDeserializer(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
private void validateType(final DataValidatorBuilder baseDataValidator, final JsonElement column) {
final String type = fromApiJsonHelper.extractStringNamed("type", column);
baseDataValidator.reset().parameter("type").value(type).notBlank()
.isOneOfTheseValues(supportedColumnTypes);
final Integer length = fromApiJsonHelper.extractIntegerSansLocaleNamed("length", column);
if (type != null && type.equals("String")) {
if (length != null) {
baseDataValidator.reset().parameter("length").value(length).notBlank()
.zeroOrPositiveAmount();
} else {
baseDataValidator.reset().parameter("length").value(length)
.cantBeBlankWhenParameterProvidedIs("type", type);
}
} else {
baseDataValidator.reset().parameter("length").value(length)
.mustBeBlankWhenParameterProvidedIs("type", type);
}
final String code = fromApiJsonHelper.extractStringNamed("code", column);
if (type != null && type.equals("Dropdown")) {
baseDataValidator.reset().parameter("code").value(code).notBlank()
.matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
} else {
baseDataValidator.reset().parameter("code").value(code).mustBeBlankWhenParameterProvided("type", type);
}
}
public void validateForCreate(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForCreate);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable");
final JsonElement element = fromApiJsonHelper.parse(json);
final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element);
baseDataValidator.reset().parameter("apptableName").value(apptableName).notBlank().notExceedingLengthOf(50)
.isOneOfTheseValues(supportedApptableNames);
final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : "";
final String datatableName = fromApiJsonHelper.extractStringNamed("datatableName", element);
baseDataValidator.reset().parameter("datatableName").value(datatableName).notBlank().notExceedingLengthOf(50)
.matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final Boolean multiRow = fromApiJsonHelper.extractBooleanNamed("multiRow", element);
baseDataValidator.reset().parameter("multiRow").value(multiRow).ignoreIfNull().notBlank()
.isOneOfTheseValues(true, false);
final JsonArray columns = fromApiJsonHelper.extractJsonArrayNamed("columns", element);
baseDataValidator.reset().parameter("columns").value(columns).notNull().jsonArrayNotEmpty();
if (columns != null) {
for (JsonElement column : columns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForCreateColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notBlank()
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String type = fromApiJsonHelper.extractStringNamed("type", column);
baseDataValidator.reset().parameter("type").value(type).notBlank().isOneOfTheseValues(supportedColumnTypes);
validateType(baseDataValidator, column);
final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column);
baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank()
.isOneOfTheseValues(true, false);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateForUpdate(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
// Because all parameters are optional, a check to see if at least one parameter
// has been specified is necessary in order to avoid JSON requests with no parameters
if (!json.matches("(?s)\\A\\{.*?(\\\".*?\\\"\\s*?:\\s*?)+.*?\\}\\z")) {
throw new PlatformDataIntegrityException("error.msg.invalid.request.body.no.parameters",
"Provided JSON request body does not have any parameters.");
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParametersForUpdate);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("datatable");
final JsonElement element = fromApiJsonHelper.parse(json);
final String apptableName = fromApiJsonHelper.extractStringNamed("apptableName", element);
baseDataValidator.reset().parameter("apptableName").value(apptableName).ignoreIfNull().notBlank()
.isOneOfTheseValues(supportedApptableNames);
final String fkColumnName = (apptableName != null) ? apptableName.substring(2) + "_id" : "";
final JsonArray changeColumns = fromApiJsonHelper.extractJsonArrayNamed("changeColumns", element);
baseDataValidator.reset().parameter("changeColumns").value(changeColumns).ignoreIfNull().jsonArrayNotEmpty();
if (changeColumns != null) {
for (JsonElement column : changeColumns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForChangeColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notBlank()
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String newName = fromApiJsonHelper.extractStringNamed("newName", column);
baseDataValidator.reset().parameter("newName").value(newName).ignoreIfNull().notBlank()
.notExceedingLengthOf(50).isNotOneOfTheseValues("id", fkColumnName)
.matchesRegularExpression(DATATABLE_NAME_REGEX_PATTERN);
final String code = fromApiJsonHelper.extractStringNamed("code", column);
baseDataValidator.reset().parameter("code").value(code).ignoreIfNull().notBlank()
.notExceedingLengthOf(100).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
final String newCode = fromApiJsonHelper.extractStringNamed("newCode", column);
baseDataValidator.reset().parameter("newCode").value(newCode).ignoreIfNull().notBlank()
.notExceedingLengthOf(100).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
if (StringUtils.isBlank(code) && StringUtils.isNotBlank(newCode)) {
baseDataValidator.reset().parameter("code").value(code)
.cantBeBlankWhenParameterProvidedIs("newCode", newCode);
}
final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column);
baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank()
.isOneOfTheseValues(true, false);
final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column);
baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank()
.isOneOfTheseValues(true, false);
}
}
final JsonArray addColumns = fromApiJsonHelper.extractJsonArrayNamed("addColumns", element);
baseDataValidator.reset().parameter("addColumns").value(addColumns).ignoreIfNull().jsonArrayNotEmpty();
if (addColumns != null) {
for (JsonElement column : addColumns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForAddColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notBlank()
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
validateType(baseDataValidator, column);
final Boolean mandatory = fromApiJsonHelper.extractBooleanNamed("mandatory", column);
baseDataValidator.reset().parameter("mandatory").value(mandatory).ignoreIfNull().notBlank()
.isOneOfTheseValues(true, false);
final Boolean after = fromApiJsonHelper.extractBooleanNamed("after", column);
baseDataValidator.reset().parameter("after").value(after).ignoreIfNull().notBlank()
.isOneOfTheseValues(true, false);
}
}
final JsonArray dropColumns = fromApiJsonHelper.extractJsonArrayNamed("dropColumns", element);
baseDataValidator.reset().parameter("dropColumns").value(dropColumns).ignoreIfNull().jsonArrayNotEmpty();
if (dropColumns != null) {
for (JsonElement column : dropColumns) {
fromApiJsonHelper.checkForUnsupportedParameters(column.getAsJsonObject(), supportedParametersForDropColumns);
final String name = fromApiJsonHelper.extractStringNamed("name", column);
baseDataValidator.reset().parameter("name").value(name).notBlank()
.isNotOneOfTheseValues("id", fkColumnName).matchesRegularExpression(DATATABLE_COLUMN_NAME_REGEX_PATTERN);
}
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Validation errors exist.", dataValidationErrors); }
}
}
@@ -7,11 +7,13 @@ package org.mifosplatform.infrastructure.core.serialization;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
@@ -78,6 +80,21 @@ public class FromJsonHelper {
if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); }
}
public void checkForUnsupportedParameters(final JsonObject object, final Set<String> supportedParams) {
if (object == null) { throw new InvalidParameterException(); }
Set<Entry<String, JsonElement>> entries = object.entrySet();
List<String> unsupportedParameterList = new ArrayList<String>();
for (Entry<String, JsonElement> providedParameter : entries) {
if (!supportedParams.contains(providedParameter.getKey())) {
unsupportedParameterList.add(providedParameter.getKey());
}
}
if (!unsupportedParameterList.isEmpty()) { throw new UnsupportedParameterException(unsupportedParameterList); }
}
public JsonElement parse(final String json) {
return parser.parse(json);
@@ -70,6 +70,46 @@ public class DatatablesApiResource {
return this.toApiJsonSerializer.serializePretty(prettyPrint, result);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String createDatatable(final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.createDBDatatable(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@PUT
@Path("{datatableName}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String updateDatatable(@PathParam("datatableName") final String datatableName,
final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.updateDBDatatable(datatableName, apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@DELETE
@Path("{datatableName}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String deleteDatatable(@PathParam("datatableName") final String datatableName,
final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.deleteDBDatatable(datatableName, apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("register/{datatable}/{apptable}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -95,6 +135,18 @@ public class DatatablesApiResource {
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("{datatable}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String getDatatable(@PathParam("datatable") final String datatable, @Context final UriInfo uriInfo) {
final DatatableData result = this.readWriteNonCoreDataService.retrieveDatatable(datatable);
final boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serializePretty(prettyPrint, result);
}
@GET
@Path("{datatable}/{apptableId}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -5,6 +5,8 @@
*/
package org.mifosplatform.infrastructure.dataqueries.data;
import java.util.List;
/**
* Immutable data object representing datatable data.
*/
@@ -14,13 +16,18 @@ public class DatatableData {
private final String applicationTableName;
@SuppressWarnings("unused")
private final String registeredTableName;
public static DatatableData create(final String applicationTableName, final String registeredTableName) {
return new DatatableData(applicationTableName, registeredTableName);
@SuppressWarnings("unused")
private final List<ResultsetColumnHeaderData> columnHeaderData;
public static DatatableData create(final String applicationTableName, final String registeredTableName,
final List<ResultsetColumnHeaderData> columnHeaderData) {
return new DatatableData(applicationTableName, registeredTableName, columnHeaderData);
}
private DatatableData(final String applicationTableName, final String registeredTableName) {
private DatatableData(final String applicationTableName, final String registeredTableName,
final List<ResultsetColumnHeaderData> columnHeaderData) {
this.applicationTableName = applicationTableName;
this.registeredTableName = registeredTableName;
this.columnHeaderData = columnHeaderData;
}
}
@@ -17,7 +17,6 @@ public final class ResultsetColumnHeaderData {
private final String columnName;
private final String columnType;
@SuppressWarnings("unused")
private final Long columnLength;
private final String columnDisplayType;
private final boolean isColumnNullable;
@@ -161,6 +160,10 @@ public final class ResultsetColumnHeaderData {
public String getColumnType() {
return this.columnType;
}
public Long getColumnLength() {
return this.columnLength;
}
public String getColumnDisplayType() {
return this.columnDisplayType;
@@ -0,0 +1,32 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.dataqueries.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CreateDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public CreateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createDatatable(command);
}
}
@@ -0,0 +1,40 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.dataqueries.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeleteDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public DeleteDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final String datatableName = command.getUrl().replaceAll("/datatables/", "");
this.writePlatformService.deleteDatatable(datatableName);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withResourceIdAsString(datatableName)
.build();
}
}
@@ -0,0 +1,40 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.dataqueries.handler;
import org.mifosplatform.commands.handler.NewCommandSourceHandler;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UpdateDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public UpdateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final String datatableName = command.getUrl().replaceAll("/datatables/", "");
this.writePlatformService.updateDatatable(datatableName, command);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withResourceIdAsString(datatableName)
.build();
}
}
@@ -29,7 +29,6 @@ import javax.ws.rs.core.StreamingOutput;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.infrastructure.core.service.FileUtils;
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
import org.mifosplatform.infrastructure.dataqueries.data.GenericResultsetData;
import org.mifosplatform.infrastructure.dataqueries.data.ReportData;
@@ -38,6 +37,7 @@ import org.mifosplatform.infrastructure.dataqueries.data.ReportParameterJoinData
import org.mifosplatform.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
import org.mifosplatform.infrastructure.dataqueries.data.ResultsetRowData;
import org.mifosplatform.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.mifosplatform.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.useradministration.domain.AppUser;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
@@ -243,7 +243,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
if (noPentaho) { throw new PlatformDataIntegrityException("error.msg.no.pentaho", "Pentaho is not enabled",
"Pentaho is not enabled"); }
final String reportPath = FileUtils.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt";
final String reportPath = FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + "pentahoReports" + File.separator + reportName + ".prpt";
logger.info("Report path: " + reportPath);
// load report definition
@@ -357,7 +357,7 @@ public class ReadReportingServiceImpl implements ReadReportingService {
@Override
public String retrieveReportPDF(final String reportName, final String type, final Map<String, String> queryParams) {
String fileLocation = FileUtils.MIFOSX_BASE_DIR + File.separator + "";
String fileLocation = FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator + "";
if (!new File(fileLocation).isDirectory()) {
new File(fileLocation).mkdirs();
}
@@ -16,7 +16,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
public interface ReadWriteNonCoreDataService {
List<DatatableData> retrieveDatatableNames(String appTable);
DatatableData retrieveDatatable(String datatable);
@PreAuthorize(value = "hasAnyRole('ALL_FUNCTIONS', 'REGISTER_DATATABLE')")
void registerDatatable(String datatable, String appTable);
@@ -25,6 +27,12 @@ public interface ReadWriteNonCoreDataService {
GenericResultsetData retrieveDataTableGenericResultSet(String datatable, Long appTableId, String order, Long id);
CommandProcessingResult createDatatable(JsonCommand command);
void updateDatatable(String datatableName, JsonCommand command);
void deleteDatatable(String datatableName);
CommandProcessingResult createNewDatatableEntry(String datatable, Long appTableId, JsonCommand command);
CommandProcessingResult updateDatatableEntryOneToOne(String datatable, Long appTableId, JsonCommand command);
@@ -18,6 +18,7 @@ import javax.sql.DataSource;
import org.apache.commons.lang.StringUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.exception.SQLGrammarException;
import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
@@ -25,6 +26,7 @@ import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.core.serialization.FromJsonHelper;
import org.mifosplatform.infrastructure.core.serialization.JsonParserHelper;
import org.mifosplatform.infrastructure.core.service.TenantAwareRoutingDataSource;
@@ -47,12 +49,21 @@ import org.springframework.jdbc.support.rowset.SqlRowSetMetaData;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
@Service
public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataService {
private final static String DATATABLE_NAME_REGEX_PATTERN = "^[a-zA-Z][a-zA-Z0-9\\-_\\s]{0,48}[a-zA-Z0-9]$";
private final static Logger logger = LoggerFactory.getLogger(ReadWriteNonCoreDataServiceImpl.class);
private final static HashMap<String, String> apiTypeToMySQL = new HashMap<String, String>() {{
put("String", "VARCHAR"); put("Number", "INT"); put("Decimal", "DECIMAL"); put("Date", "DATE");
put("Text", "TEXT"); put ("Dropdown", "INT");
}};
private final JdbcTemplate jdbcTemplate;
private final DataSource dataSource;
@@ -60,16 +71,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
private final FromJsonHelper fromJsonHelper;
private final JsonParserHelper helper;
private final GenericDataService genericDataService;
private final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer;
@Autowired
public ReadWriteNonCoreDataServiceImpl(final TenantAwareRoutingDataSource dataSource, final PlatformSecurityContext context,
final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService) {
final FromJsonHelper fromJsonHelper, final GenericDataService genericDataService,
final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer) {
this.dataSource = dataSource;
this.jdbcTemplate = new JdbcTemplate(this.dataSource);
this.context = context;
this.fromJsonHelper = fromJsonHelper;
this.helper = new JsonParserHelper();
this.genericDataService = genericDataService;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
}
@Override
@@ -96,13 +110,41 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
while (rs.next()) {
final String appTableName = rs.getString("application_table_name");
final String registeredDatatableName = rs.getString("registered_table_name");
final List<ResultsetColumnHeaderData> columnHeaderData = this.genericDataService
.fillResultsetColumnHeaders(registeredDatatableName);
datatables.add(DatatableData.create(appTableName, registeredDatatableName));
datatables.add(DatatableData.create(appTableName, registeredDatatableName, columnHeaderData));
}
return datatables;
}
@Override
public DatatableData retrieveDatatable(final String datatable) {
// PERMITTED datatables
final String sql = "select application_table_name, registered_table_name" + " from x_registered_table " + " where exists"
+ " (select 'f'" + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id"
+ " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id"
+ " where ur.appuser_id = " + context.authenticatedUser().getId() + " and registered_table_name='" + datatable + "'"
+ " and (p.code in ('ALL_FUNCTIONS', 'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) "
+ " order by application_table_name, registered_table_name";
final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql);
DatatableData datatableData = null;
while (rs.next()) {
final String appTableName = rs.getString("application_table_name");
final String registeredDatatableName = rs.getString("registered_table_name");
final List<ResultsetColumnHeaderData> columnHeaderData = this.genericDataService
.fillResultsetColumnHeaders(registeredDatatableName);
datatableData = DatatableData.create(appTableName, registeredDatatableName, columnHeaderData);
}
return datatableData;
}
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
logger.error(dve.getMessage(), dve);
}
@@ -115,7 +157,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
final String registerDatatableSql = "insert into x_registered_table (registered_table_name, application_table_name) values ('"
+ dataTableName + "', '" + applicationTableName + "')";
final String createPermission = "'CREATE_" + dataTableName + "'";
final String createPermissionChecker = "'CREATE_" + dataTableName + "_CHECKER'";
final String readPermission = "'READ_" + dataTableName + "'";
@@ -160,15 +202,14 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
+ ", 'DELETE', '"
+ dataTableName + "', false)";
try {
String[] sqlArray = {registerDatatableSql, permissionsSql};
String[] sqlArray = { registerDatatableSql, permissionsSql };
this.jdbcTemplate.batchUpdate(sqlArray);
} catch (DataIntegrityViolationException dve) {
Throwable realCause = dve.getMostSpecificCause();
//even if duplicate is only due to permission duplicate, okay to show duplicate datatable error msg
// even if duplicate is only due to permission duplicate, okay to
// show duplicate datatable error msg
if (realCause.getMessage().contains("Duplicate entry")) { throw new PlatformDataIntegrityException(
"error.msg.datatable.registered", "Datatable `" + dataTableName
+ "` is already registered against an application table.", "dataTableName", dataTableName); }
@@ -191,9 +232,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
final String deletePermissionsSql = "delete from m_permission where code in " + permissionList;
final String deleteRegisteredDatatableSql = "delete from x_registered_table where registered_table_name = '" + datatable + "'";
final String deleteRegisteredDatatableSql = "delete from x_registered_table where registered_table_name = '" + datatable + "'";
String[] sqlArray = {deleteRolePermissionsSql, deletePermissionsSql, deleteRegisteredDatatableSql};
String[] sqlArray = { deleteRolePermissionsSql, deletePermissionsSql, deleteRegisteredDatatableSql };
this.jdbcTemplate.batchUpdate(sqlArray);
}
@@ -213,9 +254,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
final String sql = getAddSql(columnHeaders, dataTableName, getFKField(appTable), appTableId, dataParams);
this.jdbcTemplate.update(sql);
return commandProcessingResult; //
} catch (ConstraintViolationException dve) {
// NOTE: jdbctemplate throws a
// org.hibernate.exception.ConstraintViolationException even though
@@ -240,11 +281,356 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
}
}
private void validateDatatableName(final String name) {
if (name == null || name.isEmpty()) {
throw new PlatformDataIntegrityException("error.msg.datatables.datatable.null.name",
"Data table name must not be blank.");
} else if (!name.matches(DATATABLE_NAME_REGEX_PATTERN)) {
throw new PlatformDataIntegrityException("error.msg.datatables.datatable.invalid.name.regex",
"Invalid data table name.", name);
}
}
private String datatableColumnNameToCodeValueName(final String columnName, final String code) {
return (code + "_cd_" + columnName);
}
private void parseDatatableColumnObjectForCreate(final JsonObject column, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
String type = (column.has("type")) ? column.get("type").getAsString() : null;
Integer length = (column.has("length")) ? column.get("length").getAsInt() : null;
Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false;
String code = (column.has("code")) ? column.get("code").getAsString() : null;
if (StringUtils.isNotBlank(code)) {
name = datatableColumnNameToCodeValueName(name, code);
}
String mysqlType = apiTypeToMySQL.get(type);
sqlBuilder = sqlBuilder.append("`" + name + "` " + mysqlType);
if (type != null)
{
if (type.equals("String")) {
sqlBuilder = sqlBuilder.append("(" + length + ")");
} else if (type.equals("Decimal")) {
sqlBuilder = sqlBuilder.append("(19,6)");
} else if (type.equals("Dropdown")) {
sqlBuilder = sqlBuilder.append("(11)");
}
}
if (mandatory != null) {
if (mandatory) {
sqlBuilder = sqlBuilder.append(" NOT NULL");
} else {
sqlBuilder = sqlBuilder.append(" DEFAULT NULL");
}
}
sqlBuilder = sqlBuilder.append(", ");
}
@Transactional
@Override
public CommandProcessingResult createDatatable(final JsonCommand command) {
String datatableName = null;
try {
context.authenticatedUser();
this.fromApiJsonDeserializer.validateForCreate(command.json());
JsonElement element = this.fromJsonHelper.parse(command.json());
JsonArray columns = this.fromJsonHelper.extractJsonArrayNamed("columns", element);
datatableName = this.fromJsonHelper.extractStringNamed("datatableName", element);
String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element);
Boolean multiRow = this.fromJsonHelper.extractBooleanNamed("multiRow", element);
if (multiRow == null) {
multiRow = false;
}
validateDatatableName(datatableName);
validateAppTable(apptableName);
String fkColumnName = apptableName.substring(2) + "_id";
String fkName = datatableName.toLowerCase().replaceAll("\\s", "_");
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder = sqlBuilder.append("CREATE TABLE `" + datatableName + "` (");
if (multiRow) {
sqlBuilder = sqlBuilder
.append("`id` BIGINT(20) NOT NULL AUTO_INCREMENT, ")
.append("`" + fkColumnName + "` BIGINT(20) NOT NULL, ");
} else {
sqlBuilder = sqlBuilder
.append("`" + fkColumnName + "` BIGINT(20) NOT NULL, ");
}
for (JsonElement column : columns) {
parseDatatableColumnObjectForCreate(column.getAsJsonObject(), sqlBuilder);
}
// Remove trailing comma and space
sqlBuilder = sqlBuilder.delete(sqlBuilder.length() - 2, sqlBuilder.length());
if (multiRow) {
sqlBuilder = sqlBuilder
.append(", PRIMARY KEY (`id`)")
.append(", KEY `fk_" + apptableName + "` (`" + fkColumnName + "`)")
.append(", CONSTRAINT `fk_" + fkName + "` ")
.append("FOREIGN KEY (`" + fkColumnName + "`) ")
.append("REFERENCES `" + apptableName + "` (`id`)");
} else {
sqlBuilder = sqlBuilder
.append(", PRIMARY KEY (`" + fkColumnName + "`)")
.append(", CONSTRAINT `fk_" + fkName + "` ")
.append("FOREIGN KEY (`" + fkColumnName + "`) ")
.append("REFERENCES `" + apptableName + "` (`id`)");
}
sqlBuilder = sqlBuilder.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8;");
this.jdbcTemplate.execute(sqlBuilder.toString());
registerDatatable(datatableName, apptableName);
} catch (SQLGrammarException e) {
Throwable realCause = e.getCause();
if (realCause.getMessage().contains("Table") && realCause.getMessage().contains("already exists")) {
throw new PlatformDataIntegrityException(
"error.msg.datatable.already.exists", realCause.getMessage());
}
}
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withResourceIdAsString(datatableName)
.build();
}
private void parseDatatableColumnForUpdate(final JsonObject column,
final Map<String, ResultsetColumnHeaderData> mapColumnNameDefinition, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
String newName = (column.has("newName")) ? column.get("newName").getAsString() : name;
Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false;
String after = (column.has("after")) ? column.get("after").getAsString() : null;
String code = (column.has("code")) ? column.get("code").getAsString() : null;
String newCode = (column.has("newCode")) ? column.get("newCode").getAsString() : null;
if (StringUtils.isNotBlank(code)) {
name = datatableColumnNameToCodeValueName(name, code);
if (StringUtils.isNotBlank(newCode)) {
newName = datatableColumnNameToCodeValueName(newName, newCode);
} else {
newName = datatableColumnNameToCodeValueName(newName, code);
}
}
if (!mapColumnNameDefinition.containsKey(name)) {
throw new PlatformDataIntegrityException("error.msg.datatable.column.missing.update.parse",
"Column " + name + " does not exist.", name);
}
String type = mapColumnNameDefinition.get(name).getColumnType();
Long length = mapColumnNameDefinition.get(name).getColumnLength();
sqlBuilder = sqlBuilder.append(", CHANGE `" + name + "` `" + newName + "` " + type);
if (length != null && length > 0) {
if (type.equals("DECIMAL")) {
sqlBuilder.append("(19,6)");
} else if (type.equals("VARCHAR")) {
sqlBuilder.append("(" + length + ")");
}
}
if (mandatory != null) {
if (mandatory) {
sqlBuilder = sqlBuilder.append(" NOT NULL");
} else {
sqlBuilder = sqlBuilder.append(" DEFAULT NULL");
}
}
if (after != null) {
sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`");
}
}
private void parseDatatableColumnForAdd(final JsonObject column, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
String type = (column.has("type")) ? column.get("type").getAsString() : null;
Integer length = (column.has("length")) ? column.get("length").getAsInt() : null;
Boolean mandatory = (column.has("mandatory")) ? column.get("mandatory").getAsBoolean() : false;
String after = (column.has("after")) ? column.get("after").getAsString() : null;
String code = (column.has("code")) ? column.get("code").getAsString() : null;
if (StringUtils.isNotBlank(code)) {
name = datatableColumnNameToCodeValueName(name, code);
}
String mysqlType = apiTypeToMySQL.get(type);
sqlBuilder = sqlBuilder.append(", ADD `" + name + "` " + mysqlType);
if (type != null) {
if (type.equals("String") && length != null) {
sqlBuilder = sqlBuilder.append("(" + length + ")");
} else if (type.equals("Decimal")) {
sqlBuilder = sqlBuilder.append("(19,6)");
} else if (type.equals("Dropdown")) {
sqlBuilder = sqlBuilder.append("(11)");
}
}
if (mandatory != null) {
if (mandatory) {
sqlBuilder = sqlBuilder.append(" NOT NULL");
} else {
sqlBuilder = sqlBuilder.append(" DEFAULT NULL");
}
}
if (after != null) {
sqlBuilder = sqlBuilder.append(" AFTER `" + after + "`");
}
}
private void parseDatatableColumnForDrop(final JsonObject column, StringBuilder sqlBuilder) {
String name = (column.has("name")) ? column.get("name").getAsString() : null;
sqlBuilder = sqlBuilder.append(", DROP COLUMN `" + name + "`");
}
@Transactional
@Override
public void updateDatatable(final String datatableName, final JsonCommand command) {
try {
context.authenticatedUser();
this.fromApiJsonDeserializer.validateForUpdate(command.json());
JsonElement element = this.fromJsonHelper.parse(command.json());
JsonArray changeColumns = this.fromJsonHelper.extractJsonArrayNamed("changeColumns", element);
JsonArray addColumns = this.fromJsonHelper.extractJsonArrayNamed("addColumns", element);
JsonArray dropColumns = this.fromJsonHelper.extractJsonArrayNamed("dropColumns", element);
String apptableName = this.fromJsonHelper.extractStringNamed("apptableName", element);
validateDatatableName(datatableName);
List<ResultsetColumnHeaderData> columnHeaderData = this.genericDataService.fillResultsetColumnHeaders(datatableName);
Map<String, ResultsetColumnHeaderData> mapColumnNameDefinition = new HashMap<String, ResultsetColumnHeaderData>();
for (ResultsetColumnHeaderData columnHeader : columnHeaderData) {
mapColumnNameDefinition.put(columnHeader.getColumnName(), columnHeader);
}
if (!StringUtils.isBlank(apptableName)) {
validateAppTable(apptableName);
String oldApptableName = this.queryForApplicationTableName(datatableName);
if (!StringUtils.equals(oldApptableName, apptableName)) {
deregisterDatatable(datatableName);
registerDatatable(datatableName, apptableName);
}
}
if (changeColumns == null && addColumns == null && dropColumns == null) {
return;
}
if (dropColumns != null) {
StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`");
for (JsonElement column : dropColumns) {
parseDatatableColumnForDrop(column.getAsJsonObject(), sqlBuilder);
}
// Remove the first comma, right after ALTER TABLE `datatable`
int indexOfFirstComma = sqlBuilder.indexOf(",");
if (indexOfFirstComma != -1) {
sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma);
}
this.jdbcTemplate.execute(sqlBuilder.toString());
}
if (addColumns != null) {
StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`");
for (JsonElement column : addColumns) {
parseDatatableColumnForAdd(column.getAsJsonObject(), sqlBuilder);
}
// Remove the first comma, right after ALTER TABLE `datatable`
int indexOfFirstComma = sqlBuilder.indexOf(",");
if (indexOfFirstComma != -1) {
sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma);
}
this.jdbcTemplate.execute(sqlBuilder.toString());
}
if (changeColumns != null) {
StringBuilder sqlBuilder = new StringBuilder("ALTER TABLE `" + datatableName + "`");
for (JsonElement column : changeColumns) {
parseDatatableColumnForUpdate(column.getAsJsonObject(), mapColumnNameDefinition, sqlBuilder);
}
// Remove the first comma, right after ALTER TABLE `datatable`
int indexOfFirstComma = sqlBuilder.indexOf(",");
if (indexOfFirstComma != -1) {
sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma);
}
this.jdbcTemplate.execute(sqlBuilder.toString());
}
} catch (SQLGrammarException e) {
Throwable realCause = e.getCause();
if (realCause.getMessage().toLowerCase().contains("unknown column")) {
throw new PlatformDataIntegrityException(
"error.msg.datatable.column.missing.update", realCause.getMessage());
} else if (realCause.getMessage().toLowerCase().contains("can't drop")) {
throw new PlatformDataIntegrityException(
"error.msg.datatable.column.missing.drop", realCause.getMessage());
} else if (realCause.getMessage().toLowerCase().contains("duplicate column")) {
throw new PlatformDataIntegrityException(
"error.msg.datatable.duplicate.column", realCause.getMessage());
}
}
}
@Transactional
@Override
public void deleteDatatable(final String datatableName) {
try {
context.authenticatedUser();
validateDatatableName(datatableName);
deregisterDatatable(datatableName);
String sql = "DROP TABLE `" + datatableName + "`";
this.jdbcTemplate.execute(sql);
} catch (SQLGrammarException e) {
Throwable realCause = e.getCause();
if (realCause.getMessage().contains("Unknown table")) {
throw new PlatformDataIntegrityException(
"error.msg.datatable.missing", realCause.getMessage());
}
}
}
@Transactional
@Override
public CommandProcessingResult updateDatatableEntryOneToOne(final String dataTableName, final Long appTableId, final JsonCommand command) {
return updateDatatableEntry(dataTableName, appTableId, null, command);
return updateDatatableEntry(dataTableName, appTableId, null, command);
}
@Transactional
@@ -252,34 +638,37 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
public CommandProcessingResult updateDatatableEntryOneToMany(final String dataTableName, final Long appTableId, final Long datatableId,
final JsonCommand command) {
return updateDatatableEntry(dataTableName, appTableId, datatableId, command);
return updateDatatableEntry(dataTableName, appTableId, datatableId, command);
}
private CommandProcessingResult updateDatatableEntry(final String dataTableName, final Long appTableId, final Long datatableId,
final JsonCommand command) {
final String appTable = queryForApplicationTableName(dataTableName);
CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId);
final GenericResultsetData grs = retrieveDataTableGenericResultSetForUpdate(appTable, dataTableName, appTableId, datatableId);
if (grs.hasNoEntries()) { throw new DatatableNotFoundException(dataTableName, appTableId); }
if (grs.hasMoreThanOneEntry()) { throw new PlatformDataIntegrityException("error.msg.attempting.multiple.update",
"Application table: " + dataTableName + " Foreign key id: " + appTableId); }
final Type typeOfMap = new TypeToken<Map<String, String>>() {}.getType();
Map<String, String> dataParams = this.fromJsonHelper.extractDataMap(typeOfMap, command.json());
String pkName = "id"; //1:M datatable
if (datatableId == null) { pkName = getFKField(appTable);} //1:1 datatable
String pkName = "id"; // 1:M datatable
if (datatableId == null) {
pkName = getFKField(appTable);
} // 1:1 datatable
final Map<String, Object> changes = getAffectedAndChangedColumns(grs, dataParams, pkName);
if (!changes.isEmpty()) {
Long pkValue = appTableId;
if (datatableId != null) { pkValue = datatableId;}
Long pkValue = appTableId;
if (datatableId != null) {
pkValue = datatableId;
}
final String sql = getUpdateSql(dataTableName, pkName, pkValue, changes);
logger.info("Update sql: " + sql);
if (StringUtils.isNotBlank(sql)) {
@@ -287,20 +676,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
changes.put("locale", dataParams.get("locale"));
changes.put("dateFormat", "yyyy-MM-dd");
} else {
logger.info("No Changes");
}
logger.info("No Changes");
}
}
return new CommandProcessingResultBuilder() //
.withOfficeId(commandProcessingResult.getOfficeId()) //
.withGroupId(commandProcessingResult.getGroupId()) //
.withClientId(commandProcessingResult.getClientId()) //
.withSavingsId(commandProcessingResult.getSavingsId()) //
.withLoanId(commandProcessingResult.getLoanId()) //
.with(changes) //
.build();
.withOfficeId(commandProcessingResult.getOfficeId()) //
.withGroupId(commandProcessingResult.getGroupId()) //
.withClientId(commandProcessingResult.getClientId()) //
.withSavingsId(commandProcessingResult.getSavingsId()) //
.withLoanId(commandProcessingResult.getLoanId()) //
.with(changes) //
.build();
}
@Transactional
@Override
@@ -308,13 +696,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
final String appTable = queryForApplicationTableName(dataTableName);
CommandProcessingResult commandProcessingResult = checkMainResourceExistsWithinScope(appTable, appTableId);
final String deleteOneToOneEntrySql = getDeleteEntriesSql(dataTableName, getFKField(appTable), appTableId);
int rowsDeleted = this.jdbcTemplate.update(deleteOneToOneEntrySql);
if (rowsDeleted < 1) { throw new DatatableNotFoundException(dataTableName, appTableId); }
return commandProcessingResult;
}
@@ -358,10 +745,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
return new GenericResultsetData(columnHeaders, result);
}
private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final String appTable, String dataTableName, final Long appTableId,
final Long id) {
private GenericResultsetData retrieveDataTableGenericResultSetForUpdate(final String appTable, final String dataTableName,
final Long appTableId, final Long id) {
final List<ResultsetColumnHeaderData> columnHeaders = this.genericDataService.fillResultsetColumnHeaders(dataTableName);
@@ -383,9 +769,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
private CommandProcessingResult checkMainResourceExistsWithinScope(final String appTable, final Long appTableId) {
final String sql = dataScopedSQL(appTable, appTableId);
logger.info("data scoped sql: " + sql);
logger.info("data scoped sql: " + sql);
final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql);
if (!rs.next()) { throw new DatatableNotFoundException(appTable, appTableId); }
Long officeId = getLongSqlRowSet(rs, "officeId");
@@ -393,90 +779,86 @@ logger.info("data scoped sql: " + sql);
Long clientId = getLongSqlRowSet(rs, "clientId");
Long savingsId = getLongSqlRowSet(rs, "savingsId");
Long LoanId = getLongSqlRowSet(rs, "loanId");
if (rs.next()) { throw new DatatableSystemErrorException("System Error: More than one row returned from data scoping query"); }
return new CommandProcessingResultBuilder() //
.withOfficeId(officeId) //
.withGroupId(groupId) //
.withClientId(clientId) //
.withSavingsId(savingsId) //
.withLoanId(LoanId) //
.build();
.withOfficeId(officeId) //
.withGroupId(groupId) //
.withClientId(clientId) //
.withSavingsId(savingsId) //
.withLoanId(LoanId) //
.build();
}
private Long getLongSqlRowSet(SqlRowSet rs, String column) {
private Long getLongSqlRowSet(final SqlRowSet rs, final String column) {
Long val = rs.getLong(column);
if (val == 0) val = null;
return val;
return val;
}
private String dataScopedSQL(final String appTable, final Long appTableId) {
/*
* unfortunately have to, one way or another, be able to restrict data
* to the users office hierarchy. Here, a few key tables are done.
* But if additional fields are needed on other tables the
* same pattern applies
* to the users office hierarchy. Here, a few key tables are done. But
* if additional fields are needed on other tables the same pattern
* applies
*/
AppUser currentUser = context.authenticatedUser();
String scopedSQL = null;
/*
* m_loan and m_savings_account are connected to an m_office thru either an m_client or an m_group
* If both it means it relates to an m_client that is in a group (still an m_client account)
* m_loan and m_savings_account are connected to an m_office thru either
* an m_client or an m_group If both it means it relates to an m_client
* that is in a group (still an m_client account)
*/
if (appTable.equalsIgnoreCase("m_loan")) {
scopedSQL = "select distinctrow x.* from (" +
" (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " +
" join m_client c on c.id = l.client_id " +
" join m_office o on o.id = c.office_id and o.hierarchy like '" +
currentUser.getOffice().getHierarchy() + "%'" +
" where l.id = " + appTableId + ")" +
" union all " +
" (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l " +
" join m_group g on g.id = l.group_id " +
" join m_office o on o.id = g.office_id and o.hierarchy like '" +
currentUser.getOffice().getHierarchy() + "%'" +
" where l.id = " + appTableId + ")" +
" ) x";
scopedSQL = "select distinctrow x.* from ("
+ " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l "
+ " join m_client c on c.id = l.client_id "
+ " join m_office o on o.id = c.office_id and o.hierarchy like '"
+ currentUser.getOffice().getHierarchy()
+ "%'"
+ " where l.id = "
+ appTableId
+ ")"
+ " union all "
+ " (select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId from m_loan l "
+ " join m_group g on g.id = l.group_id " + " join m_office o on o.id = g.office_id and o.hierarchy like '"
+ currentUser.getOffice().getHierarchy() + "%'" + " where l.id = " + appTableId + ")" + " ) x";
}
if (appTable.equalsIgnoreCase("m_savings_account")) {
scopedSQL = "select distinctrow x.* from (" +
" (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " +
" join m_client c on c.id = s.client_id " +
" join m_office o on o.id = c.office_id and o.hierarchy like '" +
currentUser.getOffice().getHierarchy() + "%'" +
" where s.id = " + appTableId+ ")" +
" union all " +
" (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s " +
" join m_group g on g.id = s.group_id " +
" join m_office o on o.id = g.office_id and o.hierarchy like '" +
currentUser.getOffice().getHierarchy() + "%'" +
" where s.id = " + appTableId+ ")" +
" ) x";
scopedSQL = "select distinctrow x.* from ("
+ " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s "
+ " join m_client c on c.id = s.client_id "
+ " join m_office o on o.id = c.office_id and o.hierarchy like '"
+ currentUser.getOffice().getHierarchy()
+ "%'"
+ " where s.id = "
+ appTableId
+ ")"
+ " union all "
+ " (select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId from m_savings_account s "
+ " join m_group g on g.id = s.group_id " + " join m_office o on o.id = g.office_id and o.hierarchy like '"
+ currentUser.getOffice().getHierarchy() + "%'" + " where s.id = " + appTableId + ")" + " ) x";
}
if (appTable.equalsIgnoreCase("m_client")) {
scopedSQL = "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId from m_client c " +
" join m_office o on o.id = c.office_id and o.hierarchy like '" +
currentUser.getOffice().getHierarchy() + "%'" +
" where c.id = " + appTableId;
scopedSQL = "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId from m_client c "
+ " join m_office o on o.id = c.office_id and o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'"
+ " where c.id = " + appTableId;
}
if (appTable.equalsIgnoreCase("m_group")) {
scopedSQL = "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId from m_group g "
+ " join m_office o on o.id = g.office_id and o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'"
+ " where g.id = " + appTableId;
}
if (appTable.equalsIgnoreCase("m_group")) {
scopedSQL = "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId from m_group g " +
" join m_office o on o.id = g.office_id and o.hierarchy like '" +
currentUser.getOffice().getHierarchy() + "%'" +
" where g.id = " + appTableId;
}
if (appTable.equalsIgnoreCase("m_office")) {
scopedSQL = "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId from m_office o " +
" where o.hierarchy like '" +
currentUser.getOffice().getHierarchy() + "%'" +
" and o.id = " + appTableId;
}
scopedSQL = "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId from m_office o "
+ " where o.hierarchy like '" + currentUser.getOffice().getHierarchy() + "%'" + " and o.id = " + appTableId;
}
if (scopedSQL == null) { throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria",
"Application Table: " + appTable + " not catered for in data Scoping"); }
if (scopedSQL == null) { throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria", "Application Table: "
+ appTable + " not catered for in data Scoping"); }
return scopedSQL;
@@ -621,7 +1003,7 @@ logger.info("data scoped sql: " + sql);
affectedAndChangedColumns.put(key, columnValue);
}
}
return affectedAndChangedColumns;
}
@@ -635,9 +1017,7 @@ logger.info("data scoped sql: " + sql);
if (key.equals(grs.getColumnHeaders().get(i).getColumnName())) {
columnValue = columnValues.get(i);
if (notTheSame(columnValue, keyValue, colType)) {
return true;
}
if (notTheSame(columnValue, keyValue, colType)) { return true; }
return false;
}
}
@@ -5,36 +5,16 @@
*/
package org.mifosplatform.infrastructure.documentmanagement.api;
import java.io.File;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriInfo;
import org.mifosplatform.infrastructure.core.api.ApiConstants;
import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataParam;
import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer;
import org.mifosplatform.infrastructure.core.service.FileUtils;
import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand;
import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData;
import org.mifosplatform.infrastructure.documentmanagement.data.FileData;
import org.mifosplatform.infrastructure.documentmanagement.service.DocumentReadPlatformService;
import org.mifosplatform.infrastructure.documentmanagement.service.DocumentWritePlatformService;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
@@ -42,9 +22,17 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataParam;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Path("{entityType}/{entityId}/documents")
@Component
@@ -95,8 +83,6 @@ public class DocumentManagementApiResource {
@FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart,
@FormDataParam("name") final String name, @FormDataParam("description") final String description) {
FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB);
/**
* TODO: also need to have a backup and stop reading from stream after
* max size is reached to protect against malicious clients
@@ -124,8 +110,6 @@ public class DocumentManagementApiResource {
@FormDataParam("file") final FormDataBodyPart bodyPart, @FormDataParam("name") final String name,
@FormDataParam("description") final String description) {
FileUtils.validateFileSizeWithinPermissibleRange(fileSize, name, ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB);
final Set<String> modifiedParams = new HashSet<String>();
modifiedParams.add("name");
modifiedParams.add("description");
@@ -166,9 +150,6 @@ public class DocumentManagementApiResource {
final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
// we do not want to send document location as a part of the response
documentData.setLocation(null);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, documentData, this.RESPONSE_DATA_PARAMETERS);
}
@@ -182,11 +163,10 @@ public class DocumentManagementApiResource {
this.context.authenticatedUser().validateHasReadPermission(this.SystemEntityType);
final DocumentData documentData = this.documentReadPlatformService.retrieveDocument(entityType, entityId, documentId);
final File file = new File(documentData.fileLocation());
final ResponseBuilder response = Response.ok(file);
response.header("Content-Disposition", "attachment; filename=\"" + documentData.fileName() + "\"");
response.header("Content-Type", documentData.contentType());
final FileData fileData = this.documentReadPlatformService.retrieveFileData(entityType, entityId, documentId);
final ResponseBuilder response = Response.ok(fileData.file());
response.header("Content-Disposition", "attachment; filename=\"" + fileData.name() + "\"");
response.header("Content-Type", fileData.contentType());
return response.build();
}
@@ -3,9 +3,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.client.api;
package org.mifosplatform.infrastructure.documentmanagement.api;
import java.io.File;
import java.io.InputStream;
import javax.ws.rs.Consumes;
@@ -22,19 +21,16 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import org.apache.commons.lang.StringUtils;
import org.mifosplatform.infrastructure.core.api.ApiConstants;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage;
import org.mifosplatform.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.mifosplatform.infrastructure.core.service.FileUtils;
import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_DATA_URI_SUFFIX;
import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_FILE_EXTENSION;
import org.mifosplatform.infrastructure.core.service.FileUtils.IMAGE_MIME_TYPE;
import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils;
import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils.IMAGE_FILE_EXTENSION;
import org.mifosplatform.infrastructure.documentmanagement.data.ImageData;
import org.mifosplatform.infrastructure.documentmanagement.service.ImageReadPlatformService;
import org.mifosplatform.infrastructure.documentmanagement.service.ImageWritePlatformService;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.portfolio.client.data.ClientData;
import org.mifosplatform.portfolio.client.exception.ImageNotFoundException;
import org.mifosplatform.portfolio.client.service.ClientReadPlatformService;
import org.mifosplatform.portfolio.client.service.ClientWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -44,22 +40,26 @@ import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataParam;
@Path("/clients/{clientId}/images")
/**
* TODO Vishwas need to change the url to entity/entityId/images to make the
* image management API's generic for other entities like staff etc
**/
@Path("{clients}/{clientId}/images")
@Component
@Scope("singleton")
public class ClientImagesApiResource {
public class ImagesApiResource {
private final PlatformSecurityContext context;
private final ClientReadPlatformService clientReadPlatformService;
private final ClientWritePlatformService clientWritePlatformService;
private final ImageReadPlatformService imageReadPlatformService;
private final ImageWritePlatformService imageWritePlatformService;
private final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer;
@Autowired
public ClientImagesApiResource(final PlatformSecurityContext context, final ClientReadPlatformService readPlatformService,
final ClientWritePlatformService clientWritePlatformService, final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer) {
public ImagesApiResource(final PlatformSecurityContext context, final ImageReadPlatformService readPlatformService,
final ImageWritePlatformService imageWritePlatformService, final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer) {
this.context = context;
this.clientReadPlatformService = readPlatformService;
this.clientWritePlatformService = clientWritePlatformService;
this.imageReadPlatformService = readPlatformService;
this.imageWritePlatformService = imageWritePlatformService;
this.toApiJsonSerializer = toApiJsonSerializer;
}
@@ -76,12 +76,11 @@ public class ClientImagesApiResource {
// TODO: vishwas might need more advances validation (like reading magic
// number) for handling malicious clients
// and clients not setting mime type
FileUtils.validateClientImageNotEmpty(fileDetails.getFileName());
FileUtils.validateImageMimeType(bodyPart.getMediaType().toString());
FileUtils.validateFileSizeWithinPermissibleRange(fileSize, fileDetails.getFileName(), ApiConstants.MAX_FILE_UPLOAD_SIZE_IN_MB);
ContentRepositoryUtils.validateClientImageNotEmpty(fileDetails.getFileName());
ContentRepositoryUtils.validateImageMimeType(bodyPart.getMediaType().toString());
final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(),
inputStream);
final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateClientImage(clientId, fileDetails.getFileName(),
inputStream, fileSize);
return this.toApiJsonSerializer.serialize(result);
}
@@ -94,9 +93,9 @@ public class ClientImagesApiResource {
@Produces({ MediaType.APPLICATION_JSON })
public String addNewClientImage(@PathParam("clientId") final Long clientId, final String jsonRequestBody) {
final Base64EncodedImage base64EncodedImage = FileUtils.extractImageFromDataURL(jsonRequestBody);
final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody);
final CommandProcessingResult result = this.clientWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage);
final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateClientImage(clientId, base64EncodedImage);
return this.toApiJsonSerializer.serialize(result);
}
@@ -111,20 +110,17 @@ public class ClientImagesApiResource {
context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE");
final ClientData clientData = this.clientReadPlatformService.retrieveOne(clientId);
if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); }
final ImageData imageData = this.imageReadPlatformService.retrieveClientImage(clientId);
// TODO: Need a better way of determining image type
String imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.JPEG.getValue();
if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.GIF.getValue())) {
imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.GIF.getValue();
} else if (StringUtils.endsWith(clientData.imageKey(), IMAGE_FILE_EXTENSION.PNG.getValue())) {
imageDataURISuffix = IMAGE_DATA_URI_SUFFIX.PNG.getValue();
String imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.JPEG.getValue();
if (StringUtils.endsWith(imageData.location(), ContentRepositoryUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) {
imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.GIF.getValue();
} else if (StringUtils.endsWith(imageData.location(), ContentRepositoryUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) {
imageDataURISuffix = ContentRepositoryUtils.IMAGE_DATA_URI_SUFFIX.PNG.getValue();
}
String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeFromFile(clientData.imageKey());
String clientImageAsBase64Text = imageDataURISuffix + Base64.encodeBytes(imageData.getContent());
return clientImageAsBase64Text;
}
@@ -134,26 +130,15 @@ public class ClientImagesApiResource {
public Response downloadClientImage(@PathParam("clientId") final Long clientId) {
context.authenticatedUser().validateHasReadPermission("CLIENTIMAGE");
final ClientData clientData = this.clientReadPlatformService.retrieveOne(clientId);
final ImageData imageData = this.imageReadPlatformService.retrieveClientImage(clientId);
if (clientData.imageKeyDoesNotExist()) { throw new ImageNotFoundException("clients", clientId); }
File image = new File(clientData.imageKey());
String imageName = image.getName();
ResponseBuilder response = Response.ok(image);
response.header("Content-Disposition", "attachment; filename=\"" + imageName + "\"");
ResponseBuilder response = Response.ok(imageData.getContent());
response.header("Content-Disposition", "attachment; filename=\"" + imageData.getEntityDisplayName() + IMAGE_FILE_EXTENSION.JPEG
+ "\"");
// TODO: Need a better way of determining image type
// determine image type
String contentType = IMAGE_MIME_TYPE.JPEG.getValue();
if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.GIF.getValue())) {
contentType = IMAGE_MIME_TYPE.GIF.getValue();
} else if (StringUtils.endsWith(imageName, IMAGE_FILE_EXTENSION.PNG.getValue())) {
contentType = IMAGE_MIME_TYPE.PNG.getValue();
}
response.header("Content-Type", contentType);
response.header("Content-Type", imageData.contentType());
return response.build();
}
@@ -187,7 +172,7 @@ public class ClientImagesApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String deleteClientImage(@PathParam("clientId") final Long clientId) {
this.clientWritePlatformService.deleteClientImage(clientId);
this.imageWritePlatformService.deleteClientImage(clientId);
return this.toApiJsonSerializer.serialize(new CommandProcessingResult(clientId));
}
@@ -22,6 +22,7 @@ public class DocumentCommand {
private Long size;
private String type;
private String location;
private Integer storageType;
private final Set<String> modifiedParameters;
@@ -95,6 +96,14 @@ public class DocumentCommand {
this.location = location;
}
public Integer getStorageType() {
return this.storageType;
}
public void setStorageType(Integer storageType) {
this.storageType = storageType;
}
public boolean isNameChanged() {
return this.modifiedParameters.contains("name");
}
@@ -0,0 +1,38 @@
package org.mifosplatform.infrastructure.documentmanagement.contentrepository;
import java.io.InputStream;
import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage;
import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand;
import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData;
import org.mifosplatform.infrastructure.documentmanagement.data.FileData;
import org.mifosplatform.infrastructure.documentmanagement.data.ImageData;
import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType;
public interface ContentRepository {
public StorageType type = null;
// TODO:Vishwas Need to move these settings to the Database
public static final Integer MAX_FILE_UPLOAD_SIZE_IN_MB = 5;
// TODO:Vishwas Need to move these settings to the Database
public static final Integer MAX_IMAGE_UPLOAD_SIZE_IN_MB = 1;
public abstract String saveFile(InputStream uploadedInputStream, DocumentCommand documentCommand);
public abstract void deleteFile(String fileName, String documentPath);
public abstract FileData fetchFile(DocumentData documentData);
public abstract String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize);
public abstract String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName);
public abstract void deleteImage(final Long resourceId, final String location);
public abstract ImageData fetchImage(ImageData imageData);
public abstract StorageType getStorageType();
}
@@ -0,0 +1,41 @@
package org.mifosplatform.infrastructure.documentmanagement.contentrepository;
import org.mifosplatform.infrastructure.configuration.data.S3CredentialsData;
import org.mifosplatform.infrastructure.configuration.domain.ConfigurationDomainService;
import org.mifosplatform.infrastructure.configuration.service.ExternalServicesReadPlatformService;
import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class ContentRepositoryFactory {
private ApplicationContext applicationContext;
private ExternalServicesReadPlatformService externalServicesReadPlatformService;
@Autowired
public ContentRepositoryFactory(final ApplicationContext applicationContext,
final ExternalServicesReadPlatformService externalServicesReadPlatformService) {
this.applicationContext = applicationContext;
this.externalServicesReadPlatformService = externalServicesReadPlatformService;
}
public ContentRepository getRepository() {
ConfigurationDomainService configurationDomainServiceJpa = applicationContext.getBean("configurationDomainServiceJpa",
ConfigurationDomainService.class);
if (configurationDomainServiceJpa.isAmazonS3Enabled()) { return createS3DocumentStore(); }
return new FileSystemContentRepository();
}
public ContentRepository getRepository(StorageType documentStoreType) {
if (documentStoreType == StorageType.FILE_SYSTEM) { return new FileSystemContentRepository(); }
return createS3DocumentStore();
}
private ContentRepository createS3DocumentStore() {
S3CredentialsData s3CredentialsData = externalServicesReadPlatformService.getS3Credentials();
return new S3ContentRepository(s3CredentialsData.getBucketName(), s3CredentialsData.getSecretKey(),
s3CredentialsData.getAccessKey());
}
}
@@ -1,15 +1,5 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.core.service;
package org.mifosplatform.infrastructure.documentmanagement.contentrepository;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@@ -20,17 +10,11 @@ import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage;
import org.mifosplatform.infrastructure.core.exception.ImageDataURLNotValidException;
import org.mifosplatform.infrastructure.core.exception.ImageUploadException;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentManagementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException;
import com.lowagie.text.pdf.codec.Base64;
public class ContentRepositoryUtils {
public class FileUtils {
private final static Logger logger = LoggerFactory.getLogger(FileUtils.class);
public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx";
public static Random random = new Random();
public static enum IMAGE_MIME_TYPE {
GIF("image/gif"), JPEG("image/jpeg"), PNG("image/png");
@@ -44,7 +28,7 @@ public class FileUtils {
public String getValue() {
return this.value;
}
};
}
public static enum IMAGE_FILE_EXTENSION {
GIF(".gif"), JPEG(".jpeg"), JPG(".jpg"), PNG(".png");
@@ -58,7 +42,7 @@ public class FileUtils {
public String getValue() {
return this.value;
}
};
}
public static enum IMAGE_DATA_URI_SUFFIX {
GIF("data:" + IMAGE_MIME_TYPE.GIF.getValue() + ";base64,"), JPEG("data:" + IMAGE_MIME_TYPE.JPEG.getValue() + ";base64,"), PNG(
@@ -73,107 +57,6 @@ public class FileUtils {
public String getValue() {
return this.value;
}
};
public static Random random = new Random();
/**
* Generate a random String
*
* @return
*/
public static String generateRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyz123456789";
int length = generateRandomNumber();
char[] text = new char[length];
for (int i = 0; i < length; i++) {
text[i] = characters.charAt(random.nextInt(characters.length()));
}
return new String(text);
}
/**
* Generate a random number between 5 to 16
*
* @return
*/
public static int generateRandomNumber() {
Random randomGenerator = new Random();
return randomGenerator.nextInt(11) + 5;
}
/**
* Generate the directory path for storing the new document
*
* @param entityType
* @param entityId
* @return
*/
public static String generateFileParentDirectory(String entityType, Long entityId) {
return FileUtils.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim()
+ File.separator + "documents" + File.separator + entityType + File.separator + entityId + File.separator
+ FileUtils.generateRandomString();
}
/**
* Generate directory path for storing new Image
*/
public static String generateClientImageParentDirectory(final Long resourceId) {
return FileUtils.MIFOSX_BASE_DIR + File.separator + ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim()
+ File.separator + "images" + File.separator + "clients" + File.separator + resourceId;
}
/**
* @param uploadedInputStream
* @param uploadedFileLocation
* @return
* @throws IOException
*/
public static String saveToFileSystem(InputStream uploadedInputStream, String uploadedFileLocation, String fileName) throws IOException {
String fileLocation = uploadedFileLocation + File.separator + fileName;
OutputStream out = new FileOutputStream(new File(fileLocation));
int read = 0;
byte[] bytes = new byte[1024];
while ((read = uploadedInputStream.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
out.flush();
out.close();
return fileLocation;
}
/**
* @param base64EncodedImage
* @param uploadedFileLocation
* @param fileName
* @return
* @throws IOException
*/
public static String saveToFileSystem(Base64EncodedImage base64EncodedImage, String uploadedFileLocation, String fileName)
throws IOException {
String fileLocation = uploadedFileLocation + File.separator + fileName + base64EncodedImage.getFileExtension();
OutputStream out = new FileOutputStream(new File(fileLocation));
byte[] imgBytes = Base64.decode(base64EncodedImage.getBase64EncodedString());
out.write(imgBytes);
out.flush();
out.close();
return fileLocation;
}
/**
* @param fileSize
* @param name
*/
public static void validateFileSizeWithinPermissibleRange(Long fileSize, String name, int maxFileSize) {
/**
* Using Content-Length gives me size of the entire request, which is
* good enough for now for a fast fail as the length of the rest of the
* content i.e name and description while compared to the uploaded file
* size is negligible
**/
if (fileSize != null && ((fileSize / (1024 * 1024)) > maxFileSize)) { throw new DocumentManagementException(name, fileSize,
maxFileSize); }
}
/**
@@ -210,6 +93,17 @@ public class FileUtils {
return new Base64EncodedImage(base64EncodedString, fileExtension);
}
public static void validateFileSizeWithinPermissibleRange(Long fileSize, String name) {
/**
* Using Content-Length gives me size of the entire request, which is
* good enough for now for a fast fail as the length of the rest of the
* content i.e name and description while compared to the uploaded file
* size is negligible
**/
if (fileSize != null && ((fileSize / (1024 * 1024)) > ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB)) { throw new ContentManagementException(
name, fileSize, ContentRepository.MAX_FILE_UPLOAD_SIZE_IN_MB); }
}
public static void validateClientImageNotEmpty(String imageFileName) {
List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
if (imageFileName == null) {
@@ -223,12 +117,28 @@ public class FileUtils {
}
}
public static void deleteClientImage(final Long resourceId, final String location) {
File fileToBeDeleted = new File(location);
boolean fileDeleted = fileToBeDeleted.delete();
if (!fileDeleted) {
// no need to throw an Error, simply log a warning
logger.warn("Unable to delete image associated with clients with Id " + resourceId);
/**
* Generate a random String
*
* @return
*/
public static String generateRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyz123456789";
int length = generateRandomNumber();
char[] text = new char[length];
for (int i = 0; i < length; i++) {
text[i] = characters.charAt(random.nextInt(characters.length()));
}
return new String(text);
}
/**
* Generate a random number between 5 to 16
*
* @return
*/
public static int generateRandomNumber() {
Random randomGenerator = new Random();
return randomGenerator.nextInt(11) + 5;
}
}
@@ -0,0 +1,164 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.documentmanagement.contentrepository;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand;
import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData;
import org.mifosplatform.infrastructure.documentmanagement.data.FileData;
import org.mifosplatform.infrastructure.documentmanagement.data.ImageData;
import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType;
import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.lowagie.text.pdf.codec.Base64;
public class FileSystemContentRepository implements ContentRepository {
private final static Logger logger = LoggerFactory.getLogger(FileSystemContentRepository.class);
public static final String MIFOSX_BASE_DIR = System.getProperty("user.home") + File.separator + ".mifosx";
@Override
public String saveFile(InputStream uploadedInputStream, DocumentCommand documentCommand) {
String fileName = documentCommand.getFileName();
String uploadDocumentLocation = generateFileParentDirectory(documentCommand.getParentEntityType(),
documentCommand.getParentEntityId());
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName);
makeDirectories(uploadDocumentLocation);
String fileLocation = uploadDocumentLocation + File.separator + fileName;
writeFileToFileSystem(fileName, uploadedInputStream, fileLocation);
return fileLocation;
}
@Override
public String saveImage(InputStream uploadedInputStream, Long resourceId, String imageName, Long fileSize) {
String uploadImageLocation = generateClientImageParentDirectory(resourceId);
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName);
makeDirectories(uploadImageLocation);
String fileLocation = uploadImageLocation + File.separator + imageName;
writeFileToFileSystem(imageName, uploadedInputStream, fileLocation);
return fileLocation;
}
@Override
public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) {
String uploadImageLocation = generateClientImageParentDirectory(resourceId);
makeDirectories(uploadImageLocation);
String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension();
try {
OutputStream out = new FileOutputStream(new File(fileLocation));
byte[] imgBytes = Base64.decode(base64EncodedImage.getBase64EncodedString());
out.write(imgBytes);
out.flush();
out.close();
} catch (IOException ioe) {
throw new ContentManagementException(imageName, ioe.getMessage());
}
return fileLocation;
}
@Override
public void deleteImage(final Long resourceId, final String location) {
final boolean fileDeleted = deleteFile(location);
if (!fileDeleted) {
// no need to throw an Error, simply log a warning
logger.warn("Unable to delete image associated with clients with Id " + resourceId);
}
}
@Override
public void deleteFile(String fileName, String documentPath) {
final boolean fileDeleted = deleteFile(documentPath);
if (!fileDeleted) { throw new ContentManagementException(fileName, null); }
}
private boolean deleteFile(String documentPath) {
final File fileToBeDeleted = new File(documentPath);
return fileToBeDeleted.delete();
}
@Override
public StorageType getStorageType() {
return StorageType.FILE_SYSTEM;
}
@Override
public FileData fetchFile(DocumentData documentData) {
File file = new File(documentData.fileLocation());
return new FileData(file, documentData.fileName(), documentData.contentType());
}
@Override
public ImageData fetchImage(ImageData imageData) {
File file = new File(imageData.location());
imageData.updateContent(file);
return imageData;
}
/**
* Generate the directory path for storing the new document
*
* @param entityType
* @param entityId
* @return
*/
private String generateFileParentDirectory(String entityType, Long entityId) {
return FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator
+ ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "documents" + File.separator
+ entityType + File.separator + entityId + File.separator + ContentRepositoryUtils.generateRandomString();
}
/**
* Generate directory path for storing new Image
*/
private String generateClientImageParentDirectory(final Long resourceId) {
return FileSystemContentRepository.MIFOSX_BASE_DIR + File.separator
+ ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim() + File.separator + "images" + File.separator
+ "clients" + File.separator + resourceId;
}
/**
* Recursively create the directory if it does not exist *
*/
private void makeDirectories(String uploadDocumentLocation) {
if (!new File(uploadDocumentLocation).isDirectory()) {
new File(uploadDocumentLocation).mkdirs();
}
}
private void writeFileToFileSystem(String fileName, InputStream uploadedInputStream, String fileLocation) {
try {
OutputStream out = new FileOutputStream(new File(fileLocation));
int read = 0;
byte[] bytes = new byte[1024];
while ((read = uploadedInputStream.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
out.flush();
out.close();
} catch (IOException ioException) {
throw new ContentManagementException(fileName, ioException.getMessage());
}
}
}
@@ -0,0 +1,157 @@
package org.mifosplatform.infrastructure.documentmanagement.contentrepository;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import org.mifosplatform.infrastructure.core.domain.Base64EncodedImage;
import org.mifosplatform.infrastructure.documentmanagement.command.DocumentCommand;
import org.mifosplatform.infrastructure.documentmanagement.data.DocumentData;
import org.mifosplatform.infrastructure.documentmanagement.data.FileData;
import org.mifosplatform.infrastructure.documentmanagement.data.ImageData;
import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType;
import org.mifosplatform.infrastructure.documentmanagement.exception.ContentManagementException;
import org.mifosplatform.infrastructure.documentmanagement.exception.DocumentNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.lowagie.text.pdf.codec.Base64;
public class S3ContentRepository implements ContentRepository {
private final static Logger logger = LoggerFactory.getLogger(S3ContentRepository.class);
private final String s3BucketName;
private AmazonS3 s3Client;
public S3ContentRepository(String bucketName, String secretKey, String accessKey) {
this.s3BucketName = bucketName;
this.s3Client = new AmazonS3Client(new BasicAWSCredentials(accessKey, secretKey));
}
@Override
public String saveFile(InputStream toUpload, DocumentCommand documentCommand) {
String fileName = documentCommand.getFileName();
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(documentCommand.getSize(), fileName);
String uploadDocFolder = generateFileParentDirectory(documentCommand.getParentEntityType(), documentCommand.getParentEntityId());
String uploadDocFullPath = uploadDocFolder + File.separator + fileName;
uploadDocument(fileName, toUpload, uploadDocFullPath);
return uploadDocFullPath;
}
@Override
public void deleteFile(String documentName, String documentPath) {
try {
deleteObjectFromS3(documentPath);
} catch (AmazonClientException ace) {
throw new ContentManagementException(documentName, ace.getMessage());
}
}
@Override
public String saveImage(InputStream toUploadInputStream, Long resourceId, String imageName, Long fileSize) {
ContentRepositoryUtils.validateFileSizeWithinPermissibleRange(fileSize, imageName);
String uploadImageLocation = generateClientImageParentDirectory(resourceId);
String fileLocation = uploadImageLocation + File.separator + imageName;
uploadDocument(imageName, toUploadInputStream, fileLocation);
return fileLocation;
}
@Override
public String saveImage(Base64EncodedImage base64EncodedImage, Long resourceId, String imageName) {
String uploadImageLocation = generateClientImageParentDirectory(resourceId);
String fileLocation = uploadImageLocation + File.separator + imageName + base64EncodedImage.getFileExtension();
InputStream toUploadInputStream = new ByteArrayInputStream(Base64.decode(base64EncodedImage.getBase64EncodedString()));
uploadDocument(imageName, toUploadInputStream, fileLocation);
return fileLocation;
}
@Override
public void deleteImage(Long resourceId, String location) {
try {
deleteObjectFromS3(location);
} catch (AmazonServiceException ase) {
deleteObjectAmazonServiceExceptionMessage(ase);
logger.warn("Unable to delete image associated with clients with Id " + resourceId);
} catch (AmazonClientException ace) {
deleteObjectAmazonClientExceptionMessage(ace);
logger.warn("Unable to delete image associated with clients with Id " + resourceId);
}
}
@Override
public StorageType getStorageType() {
return StorageType.S3;
}
@Override
public FileData fetchFile(DocumentData documentData) throws DocumentNotFoundException {
FileData fileData = null;
String fileName = documentData.fileName();
try {
logger.info("Downloading an object");
S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, documentData.fileLocation()));
fileData = new FileData(s3object.getObjectContent(), fileName, documentData.contentType());
} catch (AmazonClientException ace) {
logger.error(ace.getMessage());
throw new DocumentNotFoundException(documentData.getParentEntityType(), documentData.getParentEntityId(), documentData.getId());
}
return fileData;
}
@Override
public ImageData fetchImage(ImageData imageData) {
S3Object s3object = s3Client.getObject(new GetObjectRequest(s3BucketName, imageData.location()));
imageData.updateContent(s3object.getObjectContent());
return imageData;
}
private void deleteObjectAmazonClientExceptionMessage(AmazonClientException ace) {
String message = "Caught an AmazonClientException." + "Error Message: " + ace.getMessage();
logger.error(message);
}
private void deleteObjectAmazonServiceExceptionMessage(AmazonServiceException ase) {
String message = "Caught an AmazonServiceException." + "Error Message: " + ase.getMessage() + "HTTP Status Code: "
+ ase.getStatusCode() + "AWS Error Code: " + ase.getErrorCode() + "Error Type: " + ase.getErrorType()
+ "Request ID: " + ase.getRequestId();
logger.error(message);
}
private String generateFileParentDirectory(String entityType, Long entityId) {
return "documents" + File.separator + entityType + File.separator + entityId + File.separator
+ ContentRepositoryUtils.generateRandomString();
}
private String generateClientImageParentDirectory(Long resourceId) {
return "images" + File.separator + "clients" + File.separator + resourceId;
}
private void deleteObjectFromS3(String location) {
s3Client.deleteObject(new DeleteObjectRequest(s3BucketName, location));
}
private void uploadDocument(String filename, InputStream inputStream, String s3UploadLocation) throws ContentManagementException {
try {
logger.info("Uploading a new object to S3 from a file to " + s3UploadLocation);
s3Client.putObject(new PutObjectRequest(this.s3BucketName, s3UploadLocation, inputStream, new ObjectMetadata()));
} catch (AmazonClientException ace) {
String message = ace.getMessage();
throw new ContentManagementException(filename, message);
}
}
}
@@ -5,16 +5,15 @@
*/
package org.mifosplatform.infrastructure.documentmanagement.data;
import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType;
/**
* Immutable data object represent document being managed on platform.
*/
public class DocumentData {
@SuppressWarnings("unused")
private final Long id;
@SuppressWarnings("unused")
private final String parentEntityType;
@SuppressWarnings("unused")
private final Long parentEntityId;
@SuppressWarnings("unused")
private final String name;
@@ -24,10 +23,11 @@ public class DocumentData {
private final String type;
@SuppressWarnings("unused")
private final String description;
private String location;
private final String location;
private final Integer storageType;
public DocumentData(final Long id, final String parentEntityType, final Long parentEntityId, final String name, final String fileName,
final Long size, final String type, final String description, final String location) {
final Long size, final String type, final String description, final String location, Integer storageType) {
this.id = id;
this.parentEntityType = parentEntityType;
this.parentEntityId = parentEntityId;
@@ -37,6 +37,7 @@ public class DocumentData {
this.type = type;
this.description = description;
this.location = location;
this.storageType = storageType;
}
public String contentType() {
@@ -51,8 +52,20 @@ public class DocumentData {
return this.location;
}
public void setLocation(final String location) {
this.location = location;
public StorageType storageType() {
return StorageType.fromInt(storageType);
}
public String getParentEntityType() {
return this.parentEntityType;
}
public Long getParentEntityId() {
return this.parentEntityId;
}
public Long getId() {
return this.id;
}
}
@@ -0,0 +1,51 @@
package org.mifosplatform.infrastructure.documentmanagement.data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
public class FileData {
private static final Logger logger = LoggerFactory.getLogger(FileData.class);
private final File file;
private final String fileName;
private final String contentType;
private final InputStream inputStream;
public FileData(File file, String fileName, String contentType) {
this.file = file;
this.fileName = fileName;
this.contentType = contentType;
this.inputStream = null;
}
public FileData(InputStream inputStream, String fileName, String contentType) {
this.file = null;
this.inputStream = inputStream;
this.fileName = fileName;
this.contentType = contentType;
}
public String contentType() {
return contentType;
}
public String name() {
return fileName;
}
public InputStream file() {
try {
if (inputStream == null) { return new FileInputStream(file); }
return inputStream;
} catch (FileNotFoundException e) {
logger.error(e.toString());
return null;
}
}
}
@@ -0,0 +1,90 @@
package org.mifosplatform.infrastructure.documentmanagement.data;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.util.IOUtils;
import org.mifosplatform.infrastructure.documentmanagement.contentrepository.ContentRepositoryUtils;
import org.mifosplatform.infrastructure.documentmanagement.domain.StorageType;
public class ImageData {
@SuppressWarnings("unused")
private final Long imageId;
private final String location;
private final Integer storageType;
private final String entityDisplayName;
private File file;
private String contentType;
private InputStream inputStream;
public ImageData(Long imageId, String location, Integer storageType, String entityDisplayName) {
this.imageId = imageId;
this.location = location;
this.storageType = storageType;
this.entityDisplayName = entityDisplayName;
}
public byte[] getContent() {
// TODO Vishwas Fix error handling
try {
if (inputStream == null) {
FileInputStream fileInputStream = new FileInputStream(file);
return IOUtils.toByteArray(fileInputStream);
}
return IOUtils.toByteArray(inputStream);
} catch (IOException e) {
return null;
}
}
private String setImageContentType() {
String contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.JPEG.getValue();
if (file != null) {
String fileName = file.getName();
if (StringUtils.endsWith(fileName, ContentRepositoryUtils.IMAGE_FILE_EXTENSION.GIF.getValue())) {
contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.GIF.getValue();
} else if (StringUtils.endsWith(fileName, ContentRepositoryUtils.IMAGE_FILE_EXTENSION.PNG.getValue())) {
contentType = ContentRepositoryUtils.IMAGE_MIME_TYPE.PNG.getValue();
}
}
return contentType;
}
public String contentType() {
return this.contentType;
}
public StorageType storageType() {
return StorageType.fromInt(storageType);
}
public String name() {
return this.file.getName();
}
public String location() {
return this.location;
}
public void updateContent(InputStream objectContent) {
this.inputStream = objectContent;
}
public void updateContent(File file) {
this.file = file;
setImageContentType();
}
public String getEntityDisplayName() {
return this.entityDisplayName;
}
}
@@ -41,15 +41,18 @@ public class Document extends AbstractPersistable<Long> {
@Column(name = "location", length = 500)
private String location;
@Column(name = "storage_type_enum")
private Integer storageType;
public Document() {}
public static Document createNew(final String parentEntityType, final Long parentEntityId, final String name, final String fileName,
final Long size, final String type, final String description, final String location) {
return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location);
final Long size, final String type, final String description, final String location, final StorageType storageType) {
return new Document(parentEntityType, parentEntityId, name, fileName, size, type, description, location, storageType);
}
private Document(final String parentEntityType, final Long parentEntityId, final String name, final String fileName, final Long size,
final String type, final String description, final String location) {
final String type, final String description, final String location, final StorageType storageType) {
this.parentEntityType = StringUtils.defaultIfEmpty(parentEntityType, null);
this.parentEntityId = parentEntityId;
this.name = StringUtils.defaultIfEmpty(name, null);
@@ -58,6 +61,7 @@ public class Document extends AbstractPersistable<Long> {
this.type = StringUtils.defaultIfEmpty(type, null);
this.description = StringUtils.defaultIfEmpty(description, null);
this.location = StringUtils.defaultIfEmpty(location, null);
this.storageType = storageType.getValue();
}
public void update(final DocumentCommand command) {
@@ -79,7 +83,6 @@ public class Document extends AbstractPersistable<Long> {
if (command.isSizeChanged()) {
this.size = command.getSize();
}
}
public String getParentEntityType() {
@@ -146,4 +149,7 @@ public class Document extends AbstractPersistable<Long> {
this.location = location;
}
public StorageType storageType() {
return StorageType.fromInt(this.storageType);
}
}
@@ -0,0 +1,49 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.documentmanagement.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@Table(name = "m_image")
public final class Image extends AbstractPersistable<Long> {
@Column(name = "location", length = 500)
private String location;
@Column(name = "storage_type_enum")
private Integer storageType;
public Image(String location, StorageType storageType) {
this.location = location;
this.storageType = storageType.getValue();
}
protected Image() {
}
public String getLocation() {
return this.location;
}
public Integer getStorageType() {
return this.storageType;
}
public void setLocation(String location) {
this.location = location;
}
public void setStorageType(Integer storageType) {
this.storageType = storageType;
}
}
@@ -0,0 +1,12 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.infrastructure.documentmanagement.domain;
import org.mifosplatform.portfolio.client.domain.Client;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ImageRepository extends JpaRepository<Image, Long>, JpaSpecificationExecutor<Client> {}

Some files were not shown because too many files have changed in this diff Show More