MIFOSX-871 Advanced search query with search operators
This commit is contained in:
@@ -1406,6 +1406,14 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="alt">
|
||||||
|
<td><a href="#advance_search">AdHoc Search</a></td>
|
||||||
|
<td>search/advance</td>
|
||||||
|
<td><a href="#advance_search_resource">Advance Search</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -20994,6 +21002,211 @@ GET https://DomainName/api/v1/search?query=000000111
|
|||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a id="advance_search" name="advance_search" class="old-syle-anchor"> </a>
|
||||||
|
<div class="method-section">
|
||||||
|
<div class="method-description">
|
||||||
|
<h3>Advance Search</h3>
|
||||||
|
<p>AdHoc Query search supported parameters and field descriptions</p>
|
||||||
|
<table class=matrixHeading>
|
||||||
|
<tr class="matrixHeadingBG">
|
||||||
|
<td><div class="mifosXHeading2">Field Descriptions</div></td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>entities</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Entities to be included for search.(presently support added only for loans)<br></td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>loanStatus</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Loan status values to be included for search.<br>
|
||||||
|
Supported loan status:["all","active","overpaid","closed","writeoff"]
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>loanProducts</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Array of loanproduct ids.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>offices</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Array of office ids.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>loanDateOption</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Date condition name for search query.<br>
|
||||||
|
Supported values "approvalDate" or "createdDate" or "disbursalDate".
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>loanFromDate</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Start date for 'loanDateOption' in 'yyyy-MM-dd' format. </td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>loanToDate</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>End date for 'loanDateOption' in 'yyyy-MM-dd' format. </td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>includeOutStandingAmountPercentage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>It is a Boolean value, allows to search percentage of loan outstanding amount with in a given range</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>outStandingAmountPercentageCondition</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Condition type for outstanding amount percentage.
|
||||||
|
Supported values: between or <= or >= or < or > or = <br>
|
||||||
|
When 'outStandingAmountPercentageCondition' is <b>between</b> required fields <b>minOutStandingAmountPercentage</b> and <b>maxOutStandingAmountPercentage</b>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>minOutStandingAmountPercentage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Minimum percentage of outstanding amount. <br> Mandatory when 'outStandingAmountPercentageCondition' type is 'between' </td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>maxOutStandingAmountPercentage </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Maximum percentage of outstanding amount. <br> Mandatory when 'outStandingAmountPercentageCondition' type is 'between' </td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>outStandingAmountPercentage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Percentage of outstanding amount for search. <br> Mandatory when 'outStandingAmountPercentageCondition' type is otherthan 'between' </td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>includeOutstandingAmount</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Boolean value, allows user to search loan outstanding amount with in a given range</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>outstandingAmountCondition</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Condition type to search outstanding amount.
|
||||||
|
Supported values: between or <= or >= or < or > or = <br>
|
||||||
|
When 'outstandingAmountCondition' is <b>between</b> required fields <b>minOutStandingAmount</b> and <b>maxOutStandingAmount</b>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>minOutStandingAmount</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Minimum value of outstanding amount for outstandingAmountCondition type between. <br> Mandatory when 'outstandingAmountCondition' type is 'between' </td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>maxOutStandingAmount</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Maximum value of outstanding amount for outstandingAmountCondition type between. <br> Mandatory when 'outstandingAmountCondition' type is 'between' </td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>outStandingAmount</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class=fielddesc>Default value of outstanding amount for search. <br> Mandatory when 'outstandingAmountCondition' type is otherthan 'between' </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a id="advance_search_resource" name="advance_search_resource" class="old-syle-anchor"> </a>
|
||||||
|
<div class="method-section">
|
||||||
|
<div class="method-description">
|
||||||
|
<h4>AdHoc Query Search</h4>
|
||||||
|
<p>
|
||||||
|
AdHocQuery search has more search options, it is a <b>POST</b> request, it uses request body to send search parameters<br>
|
||||||
|
</p>
|
||||||
|
<table class=matrixHeading>
|
||||||
|
<tr class="matrixHeadingBG">
|
||||||
|
<td><div class="mifosXHeading2">Mandatory Fields</div></td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>entities</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
<table class=matrixHeading>
|
||||||
|
<tr class="matrixHeadingBG">
|
||||||
|
<td><div class="mifosXHeading2">Optional Fields</div></td>
|
||||||
|
</tr>
|
||||||
|
<tr class=alt>
|
||||||
|
<td>
|
||||||
|
loanStatus, loanProducts, offices, loanDateOption, loanFromDate, loanToDate, <br>includeOutStandingAmountPercentage, outStandingAmountPercentageCondition, <br>
|
||||||
|
minOutStandingAmountPercentage and maxOutStandingAmountPercentage OR outStandingAmountPercentage, <br>includeOutstandingAmount, outstandingAmountCondition, <br>
|
||||||
|
minOutstandingAmount and maxOutstandingAmount OR outstandingAmount</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="method-example">
|
||||||
|
<code class="method-declaration">
|
||||||
|
POST https://DomainName/api/v1/search/advance
|
||||||
|
</code>
|
||||||
|
<code class="method-request">
|
||||||
|
POST search/advance
|
||||||
|
Content-Type: application/json Request Body:
|
||||||
|
{
|
||||||
|
"locale": "en",
|
||||||
|
"dateFormat": "yyyy-MM-dd",
|
||||||
|
"entities": ["loans"],
|
||||||
|
"loanStatus": ["all","active","overpaid"],
|
||||||
|
"loanProducts": ["65","97","73","82"],
|
||||||
|
"offices": ["1","10","100"],
|
||||||
|
"loanDateOption": "approvalDate",
|
||||||
|
"loanFromDate": "2013-01-01",
|
||||||
|
"loanToDate": "2014-01-27",
|
||||||
|
"includeOutStandingAmountPercentage": true,
|
||||||
|
"outStandingAmountPercentageCondition": "<=",
|
||||||
|
"outStandingAmountPercentage": "80",
|
||||||
|
"includeOutstandingAmount": true,
|
||||||
|
"outstandingAmountCondition": "between",
|
||||||
|
"minOutstandingAmount": "100",
|
||||||
|
"maxOutstandingAmount": "10000"
|
||||||
|
}
|
||||||
|
</code>
|
||||||
|
<code class="method-response">
|
||||||
|
[{
|
||||||
|
"officeName": "HFC",
|
||||||
|
"loanProductName": "01 BC3M",
|
||||||
|
"count": 86,
|
||||||
|
"loanOutStanding": 5692.41,
|
||||||
|
"percentage": 76.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"officeName": "keithoffice",
|
||||||
|
"loanProductName": "IGL",
|
||||||
|
"count": 3,
|
||||||
|
"loanOutStanding": 4168.23,
|
||||||
|
"percentage": 56.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"officeName": "Rio",
|
||||||
|
"loanProductName": "IGL",
|
||||||
|
"count": 1,
|
||||||
|
"loanOutStanding": 1681.12,
|
||||||
|
"percentage": 55.56
|
||||||
|
}]
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- ends Search API docs-->
|
<!-- ends Search API docs-->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@ public class LoanProductData {
|
|||||||
* Used when returning lookup information about loan product for dropdowns.
|
* Used when returning lookup information about loan product for dropdowns.
|
||||||
*/
|
*/
|
||||||
public static LoanProductData lookup(final Long id, final String name) {
|
public static LoanProductData lookup(final Long id, final String name) {
|
||||||
final String shortName = null;
|
final String shortName = null;
|
||||||
final String description = null;
|
final String description = null;
|
||||||
final CurrencyData currency = null;
|
final CurrencyData currency = null;
|
||||||
final BigDecimal principal = null;
|
final BigDecimal principal = null;
|
||||||
|
|||||||
+30
@@ -100,4 +100,34 @@ public class SearchConstants {
|
|||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static enum SEARCH_LOAN_DATE {
|
||||||
|
APPROVAL_DATE("approvalDate"), CREATED_DATE("createdDate"), DISBURSAL_DATE("disbursalDate");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
private SEARCH_LOAN_DATE(final String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Set<String> values = new HashSet<String>();
|
||||||
|
static {
|
||||||
|
for (final SEARCH_LOAN_DATE param : SEARCH_LOAN_DATE.values()) {
|
||||||
|
values.add(param.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> getAllValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name().toString().replaceAll("_", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-2
@@ -10,6 +10,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
@@ -21,6 +22,9 @@ import org.mifosplatform.infrastructure.core.api.ApiRequestParameterHelper;
|
|||||||
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
import org.mifosplatform.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer;
|
import org.mifosplatform.infrastructure.core.serialization.ToApiJsonSerializer;
|
||||||
import org.mifosplatform.portfolio.search.SearchConstants.SEARCH_RESPONSE_PARAMETERS;
|
import org.mifosplatform.portfolio.search.SearchConstants.SEARCH_RESPONSE_PARAMETERS;
|
||||||
|
import org.mifosplatform.portfolio.search.data.AdHocQueryDataValidator;
|
||||||
|
import org.mifosplatform.portfolio.search.data.AdHocQuerySearchConditions;
|
||||||
|
import org.mifosplatform.portfolio.search.data.AdHocSearchQueryData;
|
||||||
import org.mifosplatform.portfolio.search.data.SearchConditions;
|
import org.mifosplatform.portfolio.search.data.SearchConditions;
|
||||||
import org.mifosplatform.portfolio.search.data.SearchData;
|
import org.mifosplatform.portfolio.search.data.SearchData;
|
||||||
import org.mifosplatform.portfolio.search.service.SearchReadPlatformService;
|
import org.mifosplatform.portfolio.search.service.SearchReadPlatformService;
|
||||||
@@ -36,18 +40,33 @@ public class SearchApiResource {
|
|||||||
private final Set<String> searchResponseParameters = SEARCH_RESPONSE_PARAMETERS.getAllValues();
|
private final Set<String> searchResponseParameters = SEARCH_RESPONSE_PARAMETERS.getAllValues();
|
||||||
|
|
||||||
private final SearchReadPlatformService searchReadPlatformService;
|
private final SearchReadPlatformService searchReadPlatformService;
|
||||||
private final ToApiJsonSerializer<SearchData> toApiJsonSerializer;
|
private final ToApiJsonSerializer<Object> toApiJsonSerializer;
|
||||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||||
|
private final AdHocQueryDataValidator fromApiJsonDeserializer;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public SearchApiResource(final SearchReadPlatformService searchReadPlatformService,
|
public SearchApiResource(final SearchReadPlatformService searchReadPlatformService,
|
||||||
final ToApiJsonSerializer<SearchData> toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper) {
|
final ToApiJsonSerializer<Object> toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper,
|
||||||
|
final AdHocQueryDataValidator fromApiJsonDeserializer) {
|
||||||
|
|
||||||
this.searchReadPlatformService = searchReadPlatformService;
|
this.searchReadPlatformService = searchReadPlatformService;
|
||||||
this.toApiJsonSerializer = toApiJsonSerializer;
|
this.toApiJsonSerializer = toApiJsonSerializer;
|
||||||
this.apiRequestParameterHelper = apiRequestParameterHelper;
|
this.apiRequestParameterHelper = apiRequestParameterHelper;
|
||||||
|
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/template")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public String retrieveAdHocSearchQueryTemplate(@Context final UriInfo uriInfo) {
|
||||||
|
|
||||||
|
final AdHocSearchQueryData templateData = this.searchReadPlatformService.retrieveAdHocQueryTemplate();
|
||||||
|
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.toApiJsonSerializer.serialize(settings, templateData);
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@@ -62,4 +81,18 @@ public class SearchApiResource {
|
|||||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
return this.toApiJsonSerializer.serialize(settings, searchResults, this.searchResponseParameters);
|
return this.toApiJsonSerializer.serialize(settings, searchResults, this.searchResponseParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/advance")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
public String advancedSearch(@Context final UriInfo uriInfo,final String json) {
|
||||||
|
|
||||||
|
final AdHocQuerySearchConditions searchConditions = this.fromApiJsonDeserializer.retrieveSearchConditions(json);
|
||||||
|
|
||||||
|
final Collection<AdHocSearchQueryData> searchResults = this.searchReadPlatformService.retrieveAdHocQueryMatchingData(searchConditions);
|
||||||
|
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.toApiJsonSerializer.serialize(settings, searchResults);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
package org.mifosplatform.portfolio.search.data;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.joda.time.LocalDate;
|
||||||
|
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.serialization.FromJsonHelper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class AdHocQueryDataValidator {
|
||||||
|
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public AdHocQueryDataValidator(final FromJsonHelper fromApiJsonHelper) {
|
||||||
|
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateAdHocQueryParameters(final String json) {
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
|
||||||
|
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
||||||
|
AdHocQuerySearchConstants.AD_HOC_SEARCH_QUERY_REQUEST_DATA_PARAMETERS);
|
||||||
|
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
|
||||||
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
|
||||||
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||||
|
.resource(AdHocQuerySearchConstants.AD_HOC_SEARCH_QUERY_RESOURCE_NAME);
|
||||||
|
|
||||||
|
final String[] entities = this.fromApiJsonHelper.extractArrayNamed(AdHocQuerySearchConstants.entitiesParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.entitiesParamName).value(entities).arrayNotEmpty();
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanStatusParamName, element)) {
|
||||||
|
final String[] loanStatus = this.fromApiJsonHelper.extractArrayNamed(AdHocQuerySearchConstants.loanStatusParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.loanStatusParamName).value(loanStatus).arrayNotEmpty();
|
||||||
|
if (loanStatus != null && loanStatus.length > 0) {
|
||||||
|
for (String status : loanStatus) {
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.loanStatusParamName).value(status)
|
||||||
|
.isOneOfTheseValues(AdHocQuerySearchConstants.loanStatusOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanProductsParamName, element)) {
|
||||||
|
final String[] loanProducts = this.fromApiJsonHelper
|
||||||
|
.extractArrayNamed(AdHocQuerySearchConstants.loanProductsParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.loanProductsParamName).value(loanProducts).arrayNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.officesParamName, element)) {
|
||||||
|
final String[] offices = this.fromApiJsonHelper.extractArrayNamed(AdHocQuerySearchConstants.officesParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.officesParamName).value(offices).arrayNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanDateOptionParamName, element)) {
|
||||||
|
final String loanDateOption = this.fromApiJsonHelper.extractStringNamed(AdHocQuerySearchConstants.loanDateOptionParamName,
|
||||||
|
element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.loanDateOptionParamName).value(loanDateOption)
|
||||||
|
.isOneOfTheseValues(AdHocQuerySearchConstants.loanDateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanFromDateParamName, element)) {
|
||||||
|
final LocalDate loanFromDate = this.fromApiJsonHelper.extractLocalDateNamed(AdHocQuerySearchConstants.loanFromDateParamName,
|
||||||
|
element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.loanFromDateParamName).value(loanFromDate).notNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanToDateParamName, element)) {
|
||||||
|
final LocalDate loanToDate = this.fromApiJsonHelper.extractLocalDateNamed(AdHocQuerySearchConstants.loanToDateParamName,
|
||||||
|
element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.loanToDateParamName).value(loanToDate).notNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.includeOutStandingAmountPercentageParamName, element)) {
|
||||||
|
final boolean includeOutStandingAmountPercentage = this.fromApiJsonHelper.extractBooleanNamed(
|
||||||
|
AdHocQuerySearchConstants.includeOutStandingAmountPercentageParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.includeOutStandingAmountPercentageParamName)
|
||||||
|
.value(includeOutStandingAmountPercentage).notNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element)) {
|
||||||
|
final String outStandingAmountPercentageCondition = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName)
|
||||||
|
.value(outStandingAmountPercentageCondition)
|
||||||
|
.isNotOneOfTheseValues(AdHocQuerySearchConstants.AD_HOC_SEARCH_QUERY_CONDITIONS);
|
||||||
|
if (outStandingAmountPercentageCondition.equals("between")) {
|
||||||
|
final BigDecimal minOutStandingAmountPercentage = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.minOutStandingAmountPercentageParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.minOutStandingAmountPercentageParamName)
|
||||||
|
.value(minOutStandingAmountPercentage).notNull().notLessThanMin(BigDecimal.ZERO);
|
||||||
|
final BigDecimal maxOutStandingAmountPercentage = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.maxOutStandingAmountPercentageParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.maxOutStandingAmountPercentageParamName)
|
||||||
|
.value(maxOutStandingAmountPercentage).notNull().notLessThanMin(BigDecimal.ZERO);
|
||||||
|
baseDataValidator.reset().comapareMinAndMaxOfTwoBigDecmimalNos(minOutStandingAmountPercentage,
|
||||||
|
maxOutStandingAmountPercentage);
|
||||||
|
} else {
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outStandingAmountPercentageParamName, element)) {
|
||||||
|
final BigDecimal outStandingAmountPercentage = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.outStandingAmountPercentageParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.outStandingAmountPercentageParamName)
|
||||||
|
.value(outStandingAmountPercentage).notNull().notLessThanMin(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.includeOutstandingAmountParamName, element)) {
|
||||||
|
final Boolean includeOutstandingAmountParamName = this.fromApiJsonHelper.extractBooleanNamed(
|
||||||
|
AdHocQuerySearchConstants.includeOutstandingAmountParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.includeOutstandingAmountParamName)
|
||||||
|
.value(includeOutstandingAmountParamName).notNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outstandingAmountConditionParamName, element)) {
|
||||||
|
final String outstandingAmountCondition = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
AdHocQuerySearchConstants.outstandingAmountConditionParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.outstandingAmountConditionParamName)
|
||||||
|
.value(outstandingAmountCondition).isNotOneOfTheseValues(AdHocQuerySearchConstants.AD_HOC_SEARCH_QUERY_CONDITIONS);
|
||||||
|
if (outstandingAmountCondition.equals("between")) {
|
||||||
|
final BigDecimal minOutstandingAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.minOutstandingAmountParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.minOutstandingAmountParamName).value(minOutstandingAmount)
|
||||||
|
.notNull().notLessThanMin(BigDecimal.ZERO);
|
||||||
|
final BigDecimal maxOutstandingAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.maxOutstandingAmountParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.maxOutstandingAmountParamName).value(maxOutstandingAmount)
|
||||||
|
.notNull().notLessThanMin(BigDecimal.ZERO);
|
||||||
|
baseDataValidator.reset().comapareMinAndMaxOfTwoBigDecmimalNos(minOutstandingAmount, maxOutstandingAmount);
|
||||||
|
} else {
|
||||||
|
final BigDecimal outstandingAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.outstandingAmountParamName, element);
|
||||||
|
baseDataValidator.reset().parameter(AdHocQuerySearchConstants.outstandingAmountParamName).value(outstandingAmount)
|
||||||
|
.notNull().notLessThanMin(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||||
|
if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdHocQuerySearchConditions retrieveSearchConditions(String json) {
|
||||||
|
|
||||||
|
validateAdHocQueryParameters(json);
|
||||||
|
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
|
||||||
|
List<String> loanStatus = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanStatusParamName, element)) {
|
||||||
|
loanStatus = Arrays.asList(this.fromApiJsonHelper.extractArrayNamed(AdHocQuerySearchConstants.loanStatusParamName, element));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> loanProducts = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanProductsParamName, element)) {
|
||||||
|
loanProducts = extractLongValuesList(Arrays.asList(this.fromApiJsonHelper.extractArrayNamed(
|
||||||
|
AdHocQuerySearchConstants.loanProductsParamName, element)));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> offices = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.officesParamName, element)) {
|
||||||
|
offices = extractLongValuesList(Arrays.asList(this.fromApiJsonHelper.extractArrayNamed(
|
||||||
|
AdHocQuerySearchConstants.officesParamName, element)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String loanDateOption = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanDateOptionParamName, element)) {
|
||||||
|
loanDateOption = this.fromApiJsonHelper.extractStringNamed(AdHocQuerySearchConstants.loanDateOptionParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate loanFromDate = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanFromDateParamName, element)) {
|
||||||
|
loanFromDate = this.fromApiJsonHelper.extractLocalDateNamed(AdHocQuerySearchConstants.loanFromDateParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate loanToDate = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanToDateParamName, element)) {
|
||||||
|
loanToDate = this.fromApiJsonHelper.extractLocalDateNamed(AdHocQuerySearchConstants.loanToDateParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean includeOutStandingAmountPercentage = false;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.includeOutStandingAmountPercentageParamName, element)) {
|
||||||
|
includeOutStandingAmountPercentage = this.fromApiJsonHelper.extractBooleanNamed(
|
||||||
|
AdHocQuerySearchConstants.includeOutStandingAmountPercentageParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
String outStandingAmountPercentageCondition = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element)) {
|
||||||
|
outStandingAmountPercentageCondition = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal minOutStandingAmountPercentage = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.minOutStandingAmountPercentageParamName, element)) {
|
||||||
|
minOutStandingAmountPercentage = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.minOutStandingAmountPercentageParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal maxOutStandingAmountPercentage = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.maxOutStandingAmountPercentageParamName, element)) {
|
||||||
|
maxOutStandingAmountPercentage = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.maxOutStandingAmountPercentageParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal outStandingAmountPercentage = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outStandingAmountPercentageParamName, element)) {
|
||||||
|
outStandingAmountPercentage = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.outStandingAmountPercentageParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean includeOutstandingAmountParamName = false;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.includeOutstandingAmountParamName, element)) {
|
||||||
|
includeOutstandingAmountParamName = this.fromApiJsonHelper.extractBooleanNamed(
|
||||||
|
AdHocQuerySearchConstants.includeOutstandingAmountParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
String outstandingAmountCondition = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outstandingAmountConditionParamName, element)) {
|
||||||
|
outstandingAmountCondition = this.fromApiJsonHelper.extractStringNamed(
|
||||||
|
AdHocQuerySearchConstants.outstandingAmountConditionParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal minOutstandingAmount = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.minOutstandingAmountParamName, element)) {
|
||||||
|
minOutstandingAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.minOutstandingAmountParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal maxOutstandingAmount = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.maxOutstandingAmountParamName, element)) {
|
||||||
|
maxOutstandingAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.maxOutstandingAmountParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal outstandingAmount = null;
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outstandingAmountParamName, element)) {
|
||||||
|
outstandingAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
|
||||||
|
AdHocQuerySearchConstants.outstandingAmountParamName, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdHocQuerySearchConditions.instance(loanStatus, loanProducts, offices, loanDateOption, loanFromDate, loanToDate,
|
||||||
|
includeOutStandingAmountPercentage, outStandingAmountPercentageCondition, minOutStandingAmountPercentage,
|
||||||
|
maxOutStandingAmountPercentage, outStandingAmountPercentage, includeOutstandingAmountParamName, outstandingAmountCondition,
|
||||||
|
minOutstandingAmount, maxOutstandingAmount, outstandingAmount);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> extractLongValuesList(List<String> listTobeConverted) {
|
||||||
|
List<Long> tempList = new ArrayList<Long>();
|
||||||
|
for (String temp : listTobeConverted) {
|
||||||
|
tempList.add(Long.valueOf(temp));
|
||||||
|
}
|
||||||
|
return tempList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
package org.mifosplatform.portfolio.search.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.joda.time.LocalDate;
|
||||||
|
import org.mifosplatform.portfolio.loanaccount.domain.LoanStatus;
|
||||||
|
|
||||||
|
public class AdHocQuerySearchConditions {
|
||||||
|
|
||||||
|
private final List<String> loanStatus;
|
||||||
|
private final List<Long> loanProducts;
|
||||||
|
private final List<Long> offices;
|
||||||
|
private final String loanDateOption;
|
||||||
|
private final LocalDate loanFromDate;
|
||||||
|
private final LocalDate loanToDate;
|
||||||
|
private final Boolean includeOutStandingAmountPercentage;
|
||||||
|
private final String outStandingAmountPercentageCondition;
|
||||||
|
private final BigDecimal minOutStandingAmountPercentage;
|
||||||
|
private final BigDecimal maxOutStandingAmountPercentage;
|
||||||
|
private final BigDecimal outStandingAmountPercentage;
|
||||||
|
private final Boolean includeOutstandingAmount;
|
||||||
|
private final String outstandingAmountCondition;
|
||||||
|
private final BigDecimal minOutstandingAmount;
|
||||||
|
private final BigDecimal maxOutstandingAmount;
|
||||||
|
private final BigDecimal outstandingAmount;
|
||||||
|
|
||||||
|
public static AdHocQuerySearchConditions instance(final List<String> loanStatus,
|
||||||
|
final List<Long> loanProducts, final List<Long> offices, final String loanDateOption, final LocalDate loanFromDate,
|
||||||
|
final LocalDate loanToDate, final Boolean includeOutStandingAmountPercentage,
|
||||||
|
final String outStandingAmountPercentageCondition, final BigDecimal minOutStandingAmountPercentage,
|
||||||
|
final BigDecimal maxOutStandingAmountPercentage, final BigDecimal outStandingAmountPercentage,
|
||||||
|
final Boolean includeOutstandingAmountParamName, final String outstandingAmountCondition,
|
||||||
|
final BigDecimal minOutstandingAmount, final BigDecimal maxOutstandingAmount, final BigDecimal outstandingAmount) {
|
||||||
|
|
||||||
|
return new AdHocQuerySearchConditions(loanStatus, loanProducts, offices, loanDateOption, loanFromDate, loanToDate,
|
||||||
|
includeOutStandingAmountPercentage, outStandingAmountPercentageCondition, minOutStandingAmountPercentage,
|
||||||
|
maxOutStandingAmountPercentage, outStandingAmountPercentage, includeOutstandingAmountParamName, outstandingAmountCondition,
|
||||||
|
minOutstandingAmount, maxOutstandingAmount, outstandingAmount);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdHocQuerySearchConditions(final List<String> loanStatus, final List<Long> loanProducts,
|
||||||
|
final List<Long> offices, final String loanDateOption, final LocalDate loanFromDate, final LocalDate loanToDate,
|
||||||
|
final Boolean includeOutStandingAmountPercentage, final String outStandingAmountPercentageCondition,
|
||||||
|
final BigDecimal minOutStandingAmountPercentage, final BigDecimal maxOutStandingAmountPercentage,
|
||||||
|
final BigDecimal outStandingAmountPercentage, final Boolean includeOutstandingAmount, final String outstandingAmountCondition,
|
||||||
|
final BigDecimal minOutstandingAmount, final BigDecimal maxOutstandingAmount, final BigDecimal outstandingAmount) {
|
||||||
|
|
||||||
|
this.loanStatus = loanStatus;
|
||||||
|
this.loanProducts = loanProducts;
|
||||||
|
this.offices = offices;
|
||||||
|
this.loanDateOption = loanDateOption;
|
||||||
|
this.loanFromDate = loanFromDate;
|
||||||
|
this.loanToDate = loanToDate;
|
||||||
|
this.includeOutStandingAmountPercentage = includeOutStandingAmountPercentage;
|
||||||
|
this.outStandingAmountPercentageCondition = outStandingAmountPercentageCondition;
|
||||||
|
this.minOutStandingAmountPercentage = minOutStandingAmountPercentage;
|
||||||
|
this.maxOutStandingAmountPercentage = maxOutStandingAmountPercentage;
|
||||||
|
this.outStandingAmountPercentage = outStandingAmountPercentage;
|
||||||
|
this.includeOutstandingAmount = includeOutstandingAmount;
|
||||||
|
this.outstandingAmountCondition = outstandingAmountCondition;
|
||||||
|
this.minOutstandingAmount = minOutstandingAmount;
|
||||||
|
this.maxOutstandingAmount = maxOutstandingAmount;
|
||||||
|
this.outstandingAmount = outstandingAmount;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getLoanStatus() {
|
||||||
|
return getStatusVluesFromStatusCodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getStatusVluesFromStatusCodes() {
|
||||||
|
List<String> loanStatusValues = new ArrayList<String>();
|
||||||
|
if (this.loanStatus != null) {
|
||||||
|
for (String statusCode : this.loanStatus) {
|
||||||
|
|
||||||
|
if (statusCode.equalsIgnoreCase("active")) {
|
||||||
|
loanStatusValues.add(LoanStatus.ACTIVE.getValue().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode.equalsIgnoreCase("overpaid")) {
|
||||||
|
loanStatusValues.add(LoanStatus.OVERPAID.getValue().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode.equalsIgnoreCase("closed")) {
|
||||||
|
loanStatusValues.add(LoanStatus.CLOSED_OBLIGATIONS_MET.getValue().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode.equalsIgnoreCase("writeoff")) {
|
||||||
|
loanStatusValues.add(LoanStatus.CLOSED_WRITTEN_OFF.getValue().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode.equalsIgnoreCase("arrears")) {
|
||||||
|
//TODO - No status code is there for loans which are in active bad standing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode.equalsIgnoreCase("all")) {
|
||||||
|
loanStatusValues.add("all");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loanStatusValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getLoanProducts() {
|
||||||
|
return this.loanProducts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Long> getOffices() {
|
||||||
|
return this.offices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoanDateOption() {
|
||||||
|
return this.loanDateOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getLoanFromDate() {
|
||||||
|
return this.loanFromDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getLoanToDate() {
|
||||||
|
return this.loanToDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIncludeOutStandingAmountPercentage() {
|
||||||
|
return this.includeOutStandingAmountPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOutStandingAmountPercentageCondition() {
|
||||||
|
return this.outStandingAmountPercentageCondition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMinOutStandingAmountPercentage() {
|
||||||
|
return this.minOutStandingAmountPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMaxOutStandingAmountPercentage() {
|
||||||
|
return this.maxOutStandingAmountPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getOutStandingAmountPercentage() {
|
||||||
|
return this.outStandingAmountPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getIncludeOutstandingAmount() {
|
||||||
|
return this.includeOutstandingAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOutstandingAmountCondition() {
|
||||||
|
return this.outstandingAmountCondition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMinOutstandingAmount() {
|
||||||
|
return this.minOutstandingAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMaxOutstandingAmount() {
|
||||||
|
return this.maxOutstandingAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getOutstandingAmount() {
|
||||||
|
return this.outstandingAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package org.mifosplatform.portfolio.search.data;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public interface AdHocQuerySearchConstants {
|
||||||
|
|
||||||
|
public static final String AD_HOC_SEARCH_QUERY_RESOURCE_NAME = "adHocQuery";
|
||||||
|
|
||||||
|
public static final String localeParamName = "locale";
|
||||||
|
public static final String dateFormatParamName = "dateFormat";
|
||||||
|
public static final String entitiesParamName = "entities";
|
||||||
|
public static final String loanStatusParamName = "loanStatus";
|
||||||
|
public static final String loanProductsParamName = "loanProducts";
|
||||||
|
public static final String officesParamName = "offices";
|
||||||
|
public static final String loanDateOptionParamName = "loanDateOption";
|
||||||
|
public static final String loanFromDateParamName = "loanFromDate";
|
||||||
|
public static final String loanToDateParamName = "loanToDate";
|
||||||
|
public static final String includeOutStandingAmountPercentageParamName = "includeOutStandingAmountPercentage";
|
||||||
|
public static final String outStandingAmountPercentageConditionParamName = "outStandingAmountPercentageCondition";
|
||||||
|
public static final String minOutStandingAmountPercentageParamName = "minOutStandingAmountPercentage";
|
||||||
|
public static final String maxOutStandingAmountPercentageParamName = "maxOutStandingAmountPercentage";
|
||||||
|
public static final String outStandingAmountPercentageParamName = "outStandingAmountPercentage";
|
||||||
|
public static final String includeOutstandingAmountParamName = "includeOutstandingAmount";
|
||||||
|
public static final String outstandingAmountConditionParamName = "outstandingAmountCondition";
|
||||||
|
public static final String minOutstandingAmountParamName = "minOutstandingAmount";
|
||||||
|
public static final String maxOutstandingAmountParamName = "maxOutstandingAmount";
|
||||||
|
public static final String outstandingAmountParamName = "outstandingAmount";
|
||||||
|
|
||||||
|
public static final Set<String> AD_HOC_SEARCH_QUERY_REQUEST_DATA_PARAMETERS = new HashSet<String>(Arrays.asList(loanStatusParamName,
|
||||||
|
loanProductsParamName, officesParamName, loanDateOptionParamName, loanFromDateParamName, loanToDateParamName,
|
||||||
|
includeOutStandingAmountPercentageParamName, outStandingAmountPercentageConditionParamName,
|
||||||
|
minOutStandingAmountPercentageParamName, maxOutStandingAmountPercentageParamName, outStandingAmountPercentageParamName,
|
||||||
|
includeOutstandingAmountParamName, outstandingAmountConditionParamName, minOutstandingAmountParamName,
|
||||||
|
maxOutstandingAmountParamName, outstandingAmountParamName, localeParamName, dateFormatParamName));
|
||||||
|
|
||||||
|
public static final Set<String> AD_HOC_SEARCH_QUERY_CONDITIONS = new HashSet<String>(
|
||||||
|
Arrays.asList("between", "<=", ">=", "<", ">", "="));
|
||||||
|
|
||||||
|
public static final Object[] loanDateOptions = {"approvalDate", "createdDate", "disbursalDate"};
|
||||||
|
public static final Object[] entityTypeOptions = {"clients", "groups", "loans", "clientIdentifiers"};
|
||||||
|
public static final Object[] loanStatusOptions = {"all", "active", "overpaid", "arrears", "closed", "writeoff"};
|
||||||
|
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package org.mifosplatform.portfolio.search.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.mifosplatform.organisation.office.data.OfficeData;
|
||||||
|
import org.mifosplatform.portfolio.loanproduct.data.LoanProductData;
|
||||||
|
|
||||||
|
public class AdHocSearchQueryData {
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private final String officeName;
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private final String loanProductName;
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private final Integer count;
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private final BigDecimal loanOutStanding;
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private final Double percentage;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private final Collection<LoanProductData> loanProducts;
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private final Collection<OfficeData> offices;
|
||||||
|
|
||||||
|
public static AdHocSearchQueryData template(final Collection<LoanProductData> loanProducts, final Collection<OfficeData> offices) {
|
||||||
|
final String officeName = null;
|
||||||
|
final String loanProductName = null;
|
||||||
|
final Integer count = null;
|
||||||
|
final BigDecimal loanOutStanding = null;
|
||||||
|
final Double percentage = null;
|
||||||
|
return new AdHocSearchQueryData(officeName, loanProductName, count, loanOutStanding, percentage, loanProducts, offices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AdHocSearchQueryData matchedResult(final String officeName, final String loanProductName, final Integer count,
|
||||||
|
final BigDecimal loanOutStanding, final Double percentage) {
|
||||||
|
|
||||||
|
final Collection<LoanProductData> loanProducts = null;
|
||||||
|
final Collection<OfficeData> offices = null;
|
||||||
|
return new AdHocSearchQueryData(officeName, loanProductName, count, loanOutStanding, percentage, loanProducts, offices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdHocSearchQueryData(final String officeName, final String loanProductName, final Integer count,
|
||||||
|
final BigDecimal loanOutStanding, final Double percentage, final Collection<LoanProductData> loanProducts,
|
||||||
|
final Collection<OfficeData> offices) {
|
||||||
|
|
||||||
|
this.officeName = officeName;
|
||||||
|
this.loanProductName = loanProductName;
|
||||||
|
this.count = count;
|
||||||
|
this.loanOutStanding = loanOutStanding;
|
||||||
|
this.percentage = percentage;
|
||||||
|
|
||||||
|
this.loanProducts = loanProducts;
|
||||||
|
this.offices = offices;
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -7,10 +7,16 @@ package org.mifosplatform.portfolio.search.service;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.mifosplatform.portfolio.search.data.AdHocQuerySearchConditions;
|
||||||
|
import org.mifosplatform.portfolio.search.data.AdHocSearchQueryData;
|
||||||
import org.mifosplatform.portfolio.search.data.SearchConditions;
|
import org.mifosplatform.portfolio.search.data.SearchConditions;
|
||||||
import org.mifosplatform.portfolio.search.data.SearchData;
|
import org.mifosplatform.portfolio.search.data.SearchData;
|
||||||
|
|
||||||
public interface SearchReadPlatformService {
|
public interface SearchReadPlatformService {
|
||||||
|
|
||||||
Collection<SearchData> retriveMatchingData(SearchConditions searchConditions);
|
Collection<SearchData> retriveMatchingData(SearchConditions searchConditions);
|
||||||
|
|
||||||
|
AdHocSearchQueryData retrieveAdHocQueryTemplate();
|
||||||
|
|
||||||
|
Collection<AdHocSearchQueryData> retrieveAdHocQueryMatchingData(AdHocQuerySearchConditions searchConditions);
|
||||||
}
|
}
|
||||||
|
|||||||
+157
-1
@@ -5,13 +5,23 @@
|
|||||||
*/
|
*/
|
||||||
package org.mifosplatform.portfolio.search.service;
|
package org.mifosplatform.portfolio.search.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
|
import org.mifosplatform.infrastructure.core.domain.JdbcSupport;
|
||||||
import org.mifosplatform.infrastructure.core.service.RoutingDataSource;
|
import org.mifosplatform.infrastructure.core.service.RoutingDataSource;
|
||||||
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
|
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.mifosplatform.organisation.office.data.OfficeData;
|
||||||
|
import org.mifosplatform.organisation.office.service.OfficeReadPlatformService;
|
||||||
|
import org.mifosplatform.portfolio.loanproduct.data.LoanProductData;
|
||||||
|
import org.mifosplatform.portfolio.loanproduct.service.LoanProductReadPlatformService;
|
||||||
|
import org.mifosplatform.portfolio.search.SearchConstants;
|
||||||
|
import org.mifosplatform.portfolio.search.data.AdHocQuerySearchConditions;
|
||||||
|
import org.mifosplatform.portfolio.search.data.AdHocSearchQueryData;
|
||||||
import org.mifosplatform.portfolio.search.data.SearchConditions;
|
import org.mifosplatform.portfolio.search.data.SearchConditions;
|
||||||
import org.mifosplatform.portfolio.search.data.SearchData;
|
import org.mifosplatform.portfolio.search.data.SearchData;
|
||||||
import org.mifosplatform.useradministration.domain.AppUser;
|
import org.mifosplatform.useradministration.domain.AppUser;
|
||||||
@@ -26,11 +36,16 @@ public class SearchReadPlatformServiceImpl implements SearchReadPlatformService
|
|||||||
|
|
||||||
private final NamedParameterJdbcTemplate namedParameterjdbcTemplate;
|
private final NamedParameterJdbcTemplate namedParameterjdbcTemplate;
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
|
private final LoanProductReadPlatformService loanProductReadPlatformService;
|
||||||
|
private final OfficeReadPlatformService officeReadPlatformService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public SearchReadPlatformServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource) {
|
public SearchReadPlatformServiceImpl(final PlatformSecurityContext context, final RoutingDataSource dataSource,
|
||||||
|
final LoanProductReadPlatformService loanProductReadPlatformService, final OfficeReadPlatformService officeReadPlatformService ) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.namedParameterjdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
|
this.namedParameterjdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
|
||||||
|
this.loanProductReadPlatformService = loanProductReadPlatformService;
|
||||||
|
this.officeReadPlatformService = officeReadPlatformService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -145,4 +160,145 @@ public class SearchReadPlatformServiceImpl implements SearchReadPlatformService
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdHocSearchQueryData retrieveAdHocQueryTemplate() {
|
||||||
|
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final Collection<LoanProductData> loanProducts = this.loanProductReadPlatformService.retrieveAllLoanProductsForLookup();
|
||||||
|
final Collection<OfficeData> offices = this.officeReadPlatformService.retrieveAllOfficesForDropdown();
|
||||||
|
|
||||||
|
return AdHocSearchQueryData.template(loanProducts, offices);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<AdHocSearchQueryData> retrieveAdHocQueryMatchingData(final AdHocQuerySearchConditions searchConditions) {
|
||||||
|
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
|
final AdHocQuerySearchMapper rm = new AdHocQuerySearchMapper();
|
||||||
|
final MapSqlParameterSource params = new MapSqlParameterSource();
|
||||||
|
|
||||||
|
return this.namedParameterjdbcTemplate.query(rm.schema(searchConditions, params), params, rm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AdHocQuerySearchMapper implements RowMapper<AdHocSearchQueryData> {
|
||||||
|
|
||||||
|
private boolean isWhereClauseAdded = false;
|
||||||
|
|
||||||
|
//TODO- build the query dynamically based on selected entity types, for now adding query for only loan entity.
|
||||||
|
public String schema(final AdHocQuerySearchConditions searchConditions, final MapSqlParameterSource params) {
|
||||||
|
final StringBuffer sql = new StringBuffer();
|
||||||
|
sql.append("Select a.name as officeName, a.Product as productName, a.cnt as 'count', a.outstandingAmt as outstanding, a.percentOut as percentOut ")
|
||||||
|
.append("from (select mo.name, mp.name Product, sum(ifnull(ml.total_expected_repayment_derived,0.0)) TotalAmt, count(*) cnt, ")
|
||||||
|
.append("sum(ifnull(ml.total_outstanding_derived,0.0)) outstandingAmt, ")
|
||||||
|
.append("(sum(ifnull(ml.total_outstanding_derived,0.0)) * 100 / sum(ifnull(ml.total_expected_repayment_derived,0.0))) percentOut ")
|
||||||
|
.append("from m_loan ml inner join m_product_loan mp on mp.id=ml.product_id ")
|
||||||
|
.append("inner join m_client mc on mc.id=ml.client_id ")
|
||||||
|
.append("inner join m_office mo on mo.id=mc.office_id ");
|
||||||
|
|
||||||
|
if (searchConditions.getLoanStatus() != null && searchConditions.getLoanStatus().size() > 0) {
|
||||||
|
//If user requests for all statuses no need to add loanStatus filter
|
||||||
|
if (!searchConditions.getLoanStatus().contains("all")) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
params.addValue("loanStatus", searchConditions.getLoanStatus());
|
||||||
|
sql.append(" ml.loan_status_id in (:loanStatus) ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchConditions.getLoanProducts() != null && searchConditions.getLoanProducts().size() > 0 ) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
params.addValue("loanProducts", searchConditions.getLoanProducts());
|
||||||
|
sql.append(" mp.id in (:loanProducts) ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchConditions.getOffices() != null && searchConditions.getOffices().size() > 0) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
params.addValue("offices", searchConditions.getOffices());
|
||||||
|
sql.append(" mo.id in (:offices) ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(searchConditions.getLoanDateOption())) {
|
||||||
|
if (searchConditions.getLoanDateOption().equals(SearchConstants.SEARCH_LOAN_DATE.APPROVAL_DATE.getValue())) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
params.addValue("loanFromDate", searchConditions.getLoanFromDate().toDate());
|
||||||
|
params.addValue("loanToDate", searchConditions.getLoanToDate().toDate());
|
||||||
|
sql.append(" ( ml.approvedon_date between :loanFromDate and :loanToDate ) ");
|
||||||
|
} else if (searchConditions.getLoanDateOption().equals(SearchConstants.SEARCH_LOAN_DATE.CREATED_DATE.getValue())) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
params.addValue("loanFromDate", searchConditions.getLoanFromDate().toDate());
|
||||||
|
params.addValue("loanToDate", searchConditions.getLoanToDate().toDate());
|
||||||
|
sql.append(" ( ml.submittedon_date between :loanFromDate and :loanToDate ) ");
|
||||||
|
} else if (searchConditions.getLoanDateOption().equals(SearchConstants.SEARCH_LOAN_DATE.DISBURSAL_DATE.getValue())) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
params.addValue("loanFromDate", searchConditions.getLoanFromDate().toDate());
|
||||||
|
params.addValue("loanToDate", searchConditions.getLoanToDate().toDate());
|
||||||
|
sql.append(" ( ml.disbursedon_date between :loanFromDate and :loanToDate ) ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sql.append(" group by mo.id) a ");
|
||||||
|
|
||||||
|
//update isWhereClauseAdded to false to add filters for derived table
|
||||||
|
isWhereClauseAdded = false;
|
||||||
|
|
||||||
|
if (searchConditions.getIncludeOutStandingAmountPercentage()) {
|
||||||
|
if (searchConditions.getOutStandingAmountPercentageCondition().equals("between")) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
// params.addValue("outStandingAmountPercentageCondition", searchConditions.getOutStandingAmountPercentageCondition());
|
||||||
|
params.addValue("minOutStandingAmountPercentage", searchConditions.getMinOutStandingAmountPercentage());
|
||||||
|
params.addValue("maxOutStandingAmountPercentage", searchConditions.getMaxOutStandingAmountPercentage());
|
||||||
|
sql.append(" ( a.percentOut between :minOutStandingAmountPercentage and :maxOutStandingAmountPercentage ) ");
|
||||||
|
} else {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
// params.addValue("outStandingAmountPercentageCondition", searchConditions.getOutStandingAmountPercentageCondition());
|
||||||
|
params.addValue("outStandingAmountPercentage", searchConditions.getOutStandingAmountPercentage());
|
||||||
|
sql.append(" a.percentOut ")
|
||||||
|
.append(searchConditions.getOutStandingAmountPercentageCondition())
|
||||||
|
.append(" :outStandingAmountPercentage ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchConditions.getIncludeOutstandingAmount()) {
|
||||||
|
if (searchConditions.getOutstandingAmountCondition().equals("between")) {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
// params.addValue("outstandingAmountCondition", searchConditions.getOutstandingAmountCondition());
|
||||||
|
params.addValue("minOutstandingAmount", searchConditions.getMinOutstandingAmount());
|
||||||
|
params.addValue("maxOutstandingAmount", searchConditions.getMaxOutstandingAmount());
|
||||||
|
sql.append(" ( a.outstandingAmt between :minOutstandingAmount and :maxOutstandingAmount ) ");
|
||||||
|
} else {
|
||||||
|
checkAndUpdateWhereClause(sql);
|
||||||
|
// params.addValue("outstandingAmountCondition", searchConditions.getOutstandingAmountCondition());
|
||||||
|
params.addValue("outstandingAmount", searchConditions.getOutstandingAmount());
|
||||||
|
sql.append(" a.outstandingAmt ")
|
||||||
|
.append(searchConditions.getOutstandingAmountCondition())
|
||||||
|
.append(" :outstandingAmount ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAndUpdateWhereClause(final StringBuffer sql){
|
||||||
|
if (isWhereClauseAdded) {
|
||||||
|
sql.append(" and ");
|
||||||
|
} else {
|
||||||
|
sql.append(" where ");
|
||||||
|
isWhereClauseAdded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdHocSearchQueryData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
|
||||||
|
|
||||||
|
final String officeName = rs.getString("officeName");
|
||||||
|
final String loanProductName = rs.getString("productName");
|
||||||
|
final Integer count = JdbcSupport.getInteger(rs, "count");
|
||||||
|
final BigDecimal loanOutStanding = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "outstanding").setScale(2, RoundingMode.HALF_UP);
|
||||||
|
final Double percentage = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "percentOut").setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||||
|
return AdHocSearchQueryData.matchedResult(officeName, loanProductName, count, loanOutStanding, percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user