diff --git a/ProcessMaker/Filters/BaseFilter.php b/ProcessMaker/Filters/BaseFilter.php new file mode 100644 index 0000000000..966ba9b76b --- /dev/null +++ b/ProcessMaker/Filters/BaseFilter.php @@ -0,0 +1,315 @@ +', + '<', + '>=', + '<=', + 'between', + 'in', + 'contains', + 'regex', + 'starts_with', + ]; + + public function __construct($definition) + { + $this->subjectType = $definition['subject']['type']; + $this->subjectValue = Arr::get($definition, 'subject.value'); + $this->operator = $definition['operator']; + $this->value = $definition['value']; + $this->or = Arr::get($definition, 'or', []); + + $this->detectRawValue(); + } + + public static function filter(Builder $query, string|array $filterDefinitions): void + { + if (is_string($filterDefinitions)) { + $filterDefinitions = json_decode($filterDefinitions, true); + } + + if (!$filterDefinitions) { + return; + } + + $query->where(function ($query) use ($filterDefinitions) { + foreach ($filterDefinitions as $filter) { + (new static($filter))->addToQuery($query); + } + }); + } + + public function addToQuery(Builder $query): void + { + if (!empty($this->or)) { + $query->where(fn ($query) => $this->apply($query)); + } else { + $this->apply($query); + } + } + + private function apply($query): void + { + if ($valueAliasMethod = $this->valueAliasMethod()) { + $this->valueAliasAdapter($valueAliasMethod, $query); + } elseif ($this->subjectType === self::TYPE_PROCESS) { + $this->filterByProcessId($query); + } elseif ($this->subjectType === self::TYPE_RELATIONSHIP) { + $this->filterByRelationship($query); + } elseif ($this->isJsonData() && $query->getModel() instanceof ProcessRequestToken) { + $this->filterByRequestData($query); + } else { + $this->applyQueryBuilderMethod($query); + } + + if (!empty($this->or)) { + $query->orWhere(function ($orQuery) { + foreach ($this->or as $or) { + (new static($or))->addToQuery($orQuery); + } + }); + } + } + + private function applyQueryBuilderMethod($query) + { + $method = $this->method(); + + if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { + $query->$method( + $this->subject(), + $this->value(), + ); + } elseif ($this->isJsonData()) { + $this->manuallyAddJsonWhere($query); + } else { + $query->$method( + $this->subject(), + $this->operator(), + $this->value(), + ); + } + } + + /** + * We must do this manually because Laravel bindings cast + * floats/doubles to strings and that wont work to compare + * json values + * + * @param [type] $query + * @return void + */ + private function manuallyAddJsonWhere($query): void + { + $parts = explode('.', $this->subjectValue); + + array_shift($parts); + + $selector = implode('"."', $parts); + $operator = $this->operator(); + $value = $this->value(); + + if (!is_numeric($value)) { + $value = DB::connection()->getPdo()->quote($value); + } + + if ($operator === 'like') { + // For JSON data is required to do a CAST in order to make insensitive the comparison + $query->whereRaw( + "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" + ); + } else { + $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); + } + } + + private function operator() + { + if (!in_array($this->operator, $this->operatorWhitelist)) { + abort(422, "Invalid operator: {$this->operator}"); + } + + if ($this->operator === 'contains' || $this->operator === 'starts_with') { + return 'like'; + } + + if ($this->operator === 'regex') { + $this->operator = 'REGEXP'; + } + + return $this->operator; + } + + private function method() + { + switch($this->operator) { + case 'in': + $method = 'whereIn'; + if ($this->isJsonData()) { + $method = 'whereJsonContains'; + } + break; + case 'between': + $method = 'whereBetween'; + break; + default: + $method = 'where'; + } + + return $method; + } + + private function isJsonData() + { + return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); + } + + private function subject() + { + if ($this->isJsonData()) { + return str_replace('.', '->', $this->subjectValue); + } + + if ($this->subjectType === self::TYPE_PARTICIPANTS) { + return 'user_id'; + } + + if ($this->subjectType === self::TYPE_PROCESS) { + return 'process_id'; + } + + if ($this->subjectType === self::TYPE_RELATIONSHIP) { + return $this->relationshipSubjectTypeParts()[1]; + } + + return $this->subjectValue; + } + + private function relationshipSubjectTypeParts(): array + { + return explode('.', $this->subjectValue); + } + + public function value() + { + if ($this->operator === 'contains') { + return '%' . $this->value . '%'; + } + + if ($this->operator === 'starts_with') { + return $this->value . '%'; + } + + if ($this->filteringWithRawValue()) { + return $this->getRawValue(); + } + + return $this->value; + } + + abstract protected function valueAliasMethod(); + + private function valueAliasAdapter(string $method, Builder $query): void + { + $operator = $this->operator(); + + if ($operator === 'in') { + $operator = '='; + } + + $values = (array) $this->value(); + $expression = (object) ['operator' => $operator]; + $model = $query->getModel(); + + if ($method === 'valueAliasParticipant') { + $values = $this->convertUserIdsToUsernames($values); + } + + foreach ($values as $i => $value) { + if ($i === 0) { + $query->where($model->$method($value, $expression)); + } else { + $query->orWhere($model->$method($value, $expression)); + } + } + } + + private function convertUserIdsToUsernames($values) + { + return array_map(function ($value) { + $username = User::find($value)?->username; + + return isset($username) ? $username : $value; + }, $values); + } + + private function filterByProcessId(Builder $query): void + { + if ($query->getModel() instanceof ProcessRequestToken) { + $query->whereIn('process_request_id', function ($query) { + $query->select('id') + ->from('process_requests') + ->whereIn('process_id', (array) $this->value()); + }); + } else { + $this->applyQueryBuilderMethod($query); + } + } + + private function filterByRelationship(Builder $query): void + { + $relationshipName = $this->relationshipSubjectTypeParts()[0]; + $query->whereHas($relationshipName, function ($rel) { + $this->applyQueryBuilderMethod($rel); + }); + } + + private function filterByRequestData(Builder $query): void + { + $query->whereHas('processRequest', function ($rel) { + $this->applyQueryBuilderMethod($rel); + }); + } +} diff --git a/ProcessMaker/Filters/CasesFilter.php b/ProcessMaker/Filters/CasesFilter.php new file mode 100644 index 0000000000..dbab3e3fc4 --- /dev/null +++ b/ProcessMaker/Filters/CasesFilter.php @@ -0,0 +1,15 @@ +subjectType === self::TYPE_STATUS) { + return 'valueAliasStatus'; + } + + return null; + } +} diff --git a/ProcessMaker/Filters/Filter.php b/ProcessMaker/Filters/Filter.php index 528995037b..843a468549 100644 --- a/ProcessMaker/Filters/Filter.php +++ b/ProcessMaker/Filters/Filter.php @@ -2,260 +2,23 @@ namespace ProcessMaker\Filters; -use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Arr; -use ProcessMaker\Models\ProcessRequestToken; -use ProcessMaker\Models\User; -use ProcessMaker\Traits\InteractsWithRawFilter; -class Filter +class Filter extends BaseFilter { - use InteractsWithRawFilter; - public const TYPE_PARTICIPANTS = 'Participants'; - public const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; - - public const TYPE_ASSIGNEES_FULLNAME = 'AssigneesFullName'; - - public const TYPE_STATUS = 'Status'; - - public const TYPE_ALTERNATIVE = 'Alternative'; - - public const TYPE_FIELD = 'Field'; - public const TYPE_PROCESS = 'Process'; public const TYPE_RELATIONSHIP = 'Relationship'; - public string|null $subjectValue; - - public string $subjectType; - - public string $operator; - - public $value; - - public array $or; - - public array $operatorWhitelist = [ - '=', - '!=', - '>', - '<', - '>=', - '<=', - 'between', - 'in', - 'contains', - 'regex', - 'starts_with', - ]; - - public function __construct($definition) - { - $this->subjectType = $definition['subject']['type']; - $this->subjectValue = Arr::get($definition, 'subject.value'); - $this->operator = $definition['operator']; - $this->value = $definition['value']; - $this->or = Arr::get($definition, 'or', []); - - $this->detectRawValue(); - } - - public static function filter(Builder $query, string|array $filterDefinitions): void - { - if (is_string($filterDefinitions)) { - $filterDefinitions = json_decode($filterDefinitions, true); - } - - if (!$filterDefinitions) { - return; - } - - $query->where(function ($query) use ($filterDefinitions) { - foreach ($filterDefinitions as $filter) { - (new self($filter))->addToQuery($query); - } - }); - } - - public function addToQuery(Builder $query): void - { - if (!empty($this->or)) { - $query->where(fn ($query) => $this->apply($query)); - } else { - $this->apply($query); - } - } - - private function apply($query): void - { - if ($valueAliasMethod = $this->valueAliasMethod()) { - $this->valueAliasAdapter($valueAliasMethod, $query); - } elseif ($this->subjectType === self::TYPE_PROCESS) { - $this->filterByProcessId($query); - } elseif ($this->subjectType === self::TYPE_RELATIONSHIP) { - $this->filterByRelationship($query); - } elseif ($this->isJsonData() && $query->getModel() instanceof ProcessRequestToken) { - $this->filterByRequestData($query); - } else { - $this->applyQueryBuilderMethod($query); - } - - if (!empty($this->or)) { - $query->orWhere(function ($orQuery) { - foreach ($this->or as $or) { - (new self($or))->addToQuery($orQuery); - } - }); - } - } - - private function applyQueryBuilderMethod($query) - { - $method = $this->method(); - - if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { - $query->$method( - $this->subject(), - $this->value(), - ); - } elseif ($this->isJsonData()) { - $this->manuallyAddJsonWhere($query); - } else { - $query->$method( - $this->subject(), - $this->operator(), - $this->value(), - ); - } - } - - /** - * We must do this manually because Laravel bindings cast - * floats/doubles to strings and that wont work to compare - * json values - * - * @param [type] $query - * @return void - */ - private function manuallyAddJsonWhere($query): void - { - $parts = explode('.', $this->subjectValue); - - array_shift($parts); - - $selector = implode('"."', $parts); - $operator = $this->operator(); - $value = $this->value(); - - if (!is_numeric($value)) { - $value = DB::connection()->getPdo()->quote($value); - } - - if ($operator === 'like') { - // For JSON data is required to do a CAST in order to make insensitive the comparison - $query->whereRaw( - "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" - ); - } else { - $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); - } - } - - private function operator() - { - if (!in_array($this->operator, $this->operatorWhitelist)) { - abort(422, "Invalid operator: {$this->operator}"); - } - - if ($this->operator === 'contains' || $this->operator === 'starts_with') { - return 'like'; - } - - if ($this->operator === 'regex') { - $this->operator = 'REGEXP'; - } - - return $this->operator; - } - - private function method() - { - switch($this->operator) { - case 'in': - $method = 'whereIn'; - if ($this->isJsonData()) { - $method = 'whereJsonContains'; - } - break; - case 'between': - $method = 'whereBetween'; - break; - default: - $method = 'where'; - } - - return $method; - } - - private function isJsonData() - { - return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); - } - - private function subject() - { - if ($this->isJsonData()) { - return str_replace('.', '->', $this->subjectValue); - } - - if ($this->subjectType === self::TYPE_PARTICIPANTS) { - return 'user_id'; - } - - if ($this->subjectType === self::TYPE_PROCESS) { - return 'process_id'; - } - - if ($this->subjectType === self::TYPE_RELATIONSHIP) { - return $this->relationshipSubjectTypeParts()[1]; - } - - return $this->subjectValue; - } - - private function relationshipSubjectTypeParts(): array - { - return explode('.', $this->subjectValue); - } - - public function value() - { - if ($this->operator === 'contains') { - return '%' . $this->value . '%'; - } - - if ($this->operator === 'starts_with') { - return $this->value . '%'; - } - - if ($this->filteringWithRawValue()) { - return $this->getRawValue(); - } - - return $this->value; - } - /** * Forward Status and Participant subjects to PMQL methods on the models. * * For now, we only need Participants and Status because Request and Requester * are columns on the tables (process_request_id and user_id). */ - private function valueAliasMethod() + protected function valueAliasMethod() { $method = null; @@ -279,66 +42,4 @@ private function valueAliasMethod() return $method; } - - private function valueAliasAdapter(string $method, Builder $query): void - { - $operator = $this->operator(); - - if ($operator === 'in') { - $operator = '='; - } - - $values = (array) $this->value(); - $expression = (object) ['operator' => $operator]; - $model = $query->getModel(); - - if ($method === 'valueAliasParticipant') { - $values = $this->convertUserIdsToUsernames($values); - } - - foreach ($values as $i => $value) { - if ($i === 0) { - $query->where($model->$method($value, $expression)); - } else { - $query->orWhere($model->$method($value, $expression)); - } - } - } - - private function convertUserIdsToUsernames($values) - { - return array_map(function ($value) { - $username = User::find($value)?->username; - - return isset($username) ? $username : $value; - }, $values); - } - - private function filterByProcessId(Builder $query): void - { - if ($query->getModel() instanceof ProcessRequestToken) { - $query->whereIn('process_request_id', function ($query) { - $query->select('id') - ->from('process_requests') - ->whereIn('process_id', (array) $this->value()); - }); - } else { - $this->applyQueryBuilderMethod($query); - } - } - - private function filterByRelationship(Builder $query): void - { - $relationshipName = $this->relationshipSubjectTypeParts()[0]; - $query->whereHas($relationshipName, function ($rel) { - $this->applyQueryBuilderMethod($rel); - }); - } - - private function filterByRequestData(Builder $query): void - { - $query->whereHas('processRequest', function ($rel) { - $this->applyQueryBuilderMethod($rel); - }); - } } diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php index 6e9b2d75b7..15ed67b60f 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php @@ -98,7 +98,7 @@ public function index(Request $laravel_request, ProcessRequest $request) FilesAccessed::dispatch($filter, $request); } - if ($id && $media instanceof \ProcessMaker\Models\Media) { + if ($id && $media instanceof Media) { // We retrieved a single item by ID, so no need to filter. // Just return a collection with one item. $media = [$media]; @@ -293,7 +293,7 @@ private function saveUploadedFile(UploadedFile $file, ProcessRequest $processReq $errors = []; $this->validateFile($file, $errors); if (count($errors) > 0) { - return abort(response($errors , 422)); + return abort(response($errors, 422)); } $parentId = $processRequest->parent_request_id; @@ -421,7 +421,7 @@ public function destroy(Request $laravel_request, ProcessRequest $request, $file private function validateFile(UploadedFile $file, &$errors) { if (strtolower($file->getClientOriginalExtension() === 'pdf')) { - $this->validatePDFFile($file, $errors); + $this->validatePDFFile($file, $errors); } return $errors; diff --git a/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php index 75f0c449eb..e0a2de0c82 100644 --- a/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php +++ b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php @@ -19,7 +19,8 @@ class CaseController extends Controller const DEFAULT_PAGE_SIZE = 15; - public function __construct(private Request $request, CaseApiRepository $caseRepository) { + public function __construct(private Request $request, CaseApiRepository $caseRepository) + { $this->caseRepository = $caseRepository; } @@ -40,9 +41,10 @@ public function __construct(private Request $request, CaseApiRepository $caseRep * * @return array */ - public function getAllCases(CaseListRequest $request): JSonResponse + public function getAllCases(CaseListRequest $request): JsonResponse { $query = $this->caseRepository->getAllCases($request); + return $this->paginateResponse($query); } @@ -60,9 +62,10 @@ public function getAllCases(CaseListRequest $request): JSonResponse * * @return array */ - public function getInProgress(CaseListRequest $request): JSonResponse + public function getInProgress(CaseListRequest $request): JsonResponse { $query = $this->caseRepository->getInProgressCases($request); + return $this->paginateResponse($query); } @@ -80,9 +83,10 @@ public function getInProgress(CaseListRequest $request): JSonResponse * * @return array */ - public function getCompleted(CaseListRequest $request): JSonResponse + public function getCompleted(CaseListRequest $request): JsonResponse { $query = $this->caseRepository->getCompletedCases($request); + return $this->paginateResponse($query); } diff --git a/ProcessMaker/Http/Requests/CaseListRequest.php b/ProcessMaker/Http/Requests/CaseListRequest.php index c55e944406..51708ecf9c 100644 --- a/ProcessMaker/Http/Requests/CaseListRequest.php +++ b/ProcessMaker/Http/Requests/CaseListRequest.php @@ -26,7 +26,7 @@ public function rules(): array 'userId' => 'sometimes|integer', 'status' => 'sometimes|in:IN_PROGRESS,COMPLETED', 'sortBy' => ['sometimes', 'string', new SortBy], - 'filterBy' => 'sometimes|array', + 'filterBy' => 'sometimes|json', 'search' => 'sometimes|string', 'pageSize' => 'sometimes|integer|min:1', 'page' => 'sometimes|integer|min:1', diff --git a/ProcessMaker/Models/CaseParticipated.php b/ProcessMaker/Models/CaseParticipated.php index 29ce39e7b0..206c4de742 100644 --- a/ProcessMaker/Models/CaseParticipated.php +++ b/ProcessMaker/Models/CaseParticipated.php @@ -7,10 +7,12 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use ProcessMaker\Models\ProcessMakerModel; +use ProcessMaker\Traits\HandlesValueAliasStatus; class CaseParticipated extends ProcessMakerModel { use HasFactory; + use HandlesValueAliasStatus; protected $table = 'cases_participated'; diff --git a/ProcessMaker/Models/CaseStarted.php b/ProcessMaker/Models/CaseStarted.php index 3840551816..a1eb1ff185 100644 --- a/ProcessMaker/Models/CaseStarted.php +++ b/ProcessMaker/Models/CaseStarted.php @@ -3,14 +3,16 @@ namespace ProcessMaker\Models; use Database\Factories\CaseStartedFactory; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Casts\AsCollection; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use ProcessMaker\Models\ProcessMakerModel; +use ProcessMaker\Traits\HandlesValueAliasStatus; class CaseStarted extends ProcessMakerModel { use HasFactory; + use HandlesValueAliasStatus; protected $table = 'cases_started'; diff --git a/ProcessMaker/Repositories/CaseApiRepository.php b/ProcessMaker/Repositories/CaseApiRepository.php index 08010b6ecf..216a04a6eb 100644 --- a/ProcessMaker/Repositories/CaseApiRepository.php +++ b/ProcessMaker/Repositories/CaseApiRepository.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use ProcessMaker\Contracts\CaseApiRepositoryInterface; use ProcessMaker\Exception\CaseValidationException; +use ProcessMaker\Filters\CasesFilter; use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; @@ -73,6 +74,7 @@ public function getAllCases(Request $request): Builder { $query = CaseStarted::select($this->defaultFields); $this->applyFilters($request, $query); + return $query; } @@ -88,6 +90,7 @@ public function getInProgressCases(Request $request): Builder $query = CaseParticipated::select($this->defaultFields) ->where('case_status', 'IN_PROGRESS'); $this->applyFilters($request, $query); + return $query; } @@ -103,6 +106,7 @@ public function getCompletedCases(Request $request): Builder $query = CaseParticipated::select($this->defaultFields) ->where('case_status', 'COMPLETED'); $this->applyFilters($request, $query); + return $query; } @@ -165,22 +169,31 @@ public function search(Request $request, Builder $query): void */ public function filterBy(Request $request, Builder $query): void { - if ($request->filled('filterBy')) { - $filterByValue = $request->get('filterBy'); + // Check if filterBy exists in the request + if (!$request->has('filterBy')) { + return; + } - foreach ($filterByValue as $key => $value) { - if (!in_array($key, $this->filterableFields)) { - throw new CaseValidationException("Filter by field $key is not allowed."); - } + // Get the filter input and apply the filter only if it's not empty + $filters = $request->input('filterBy', ''); + if (empty($filters)) { + return; + } - if (in_array($key, $this->dateFields)) { - $query->whereDate($key, $value); - continue; - } + // Apply the filters to the query + $this->executeFilters($query, $filters); + } - $query->where($key, $value); - } - } + /** + * Execute advanced filters to the query. + * + * @param Builder $query + * @param array|string $filters + * @return void + */ + protected function executeFilters(Builder $query, $filters): void + { + CasesFilter::filter($query, $filters); } /** diff --git a/ProcessMaker/Repositories/CaseRepository.php b/ProcessMaker/Repositories/CaseRepository.php index f1b72d3b8d..46e9b64fd5 100644 --- a/ProcessMaker/Repositories/CaseRepository.php +++ b/ProcessMaker/Repositories/CaseRepository.php @@ -17,6 +17,7 @@ class CaseRepository implements CaseRepositoryInterface * @var CaseParticipatedRepository */ protected CaseParticipatedRepository $caseParticipatedRepository; + /** * This property is used to store an instance of `CaseStarted` * when a case started is updated. @@ -28,6 +29,7 @@ public function __construct() { $this->caseParticipatedRepository = new CaseParticipatedRepository(); } + /** * Store a new case started. * @@ -38,6 +40,7 @@ public function create(ExecutionInstanceInterface $instance): void { if (is_null($instance->case_number)) { Log::error('case number is required, method=create, instance=' . $instance->getKey()); + return; } @@ -79,6 +82,7 @@ public function update(ExecutionInstanceInterface $instance, TokenInterface $tok { if (!$this->checkIfCaseStartedExist($instance->case_number)) { Log::error('case started not found, method=update, instance=' . $instance->getKey()); + return; } diff --git a/ProcessMaker/Repositories/CaseSyncRepository.php b/ProcessMaker/Repositories/CaseSyncRepository.php index 64ac329e20..a994c577dd 100644 --- a/ProcessMaker/Repositories/CaseSyncRepository.php +++ b/ProcessMaker/Repositories/CaseSyncRepository.php @@ -138,8 +138,7 @@ private static function processTokens($instance, &$cpData, &$csRequestTokens, &$ */ private static function processChildRequests( $instance, &$cpData, &$csProcesses, &$csRequests, &$participants, &$csRequestTokens, &$csTasks - ) - { + ) { foreach ($instance->childRequests as $subProcess) { $cpData['processes'] = CaseUtils::storeProcesses($subProcess, collect()); $cpData['requests'] = CaseUtils::storeRequests($subProcess, collect()); diff --git a/ProcessMaker/Traits/HandlesValueAliasStatus.php b/ProcessMaker/Traits/HandlesValueAliasStatus.php new file mode 100644 index 0000000000..7607ec506f --- /dev/null +++ b/ProcessMaker/Traits/HandlesValueAliasStatus.php @@ -0,0 +1,25 @@ + 'IN_PROGRESS', + 'completed' => 'COMPLETED', + 'error' => 'ERROR', + 'canceled' => 'CANCELED', + ]; + + $value = mb_strtolower($value); + + return function ($query) use ($value, $statusMap, $expression) { + if (array_key_exists($value, $statusMap)) { + $value = $statusMap[$value]; + } + $query->where('case_status', $expression->operator, $value); + }; + } +} diff --git a/routes/v1_1/api.php b/routes/v1_1/api.php index b147e8b5a7..9d22f77098 100644 --- a/routes/v1_1/api.php +++ b/routes/v1_1/api.php @@ -17,7 +17,7 @@ // Route to show a task Route::get('/{task}', [TaskController::class, 'show']) ->name('show') - ->middleware(['bindings','can:view,task']); + ->middleware(['bindings', 'can:view,task']); // Route to show the screen of a task Route::get('/{taskId}/screen', [TaskController::class, 'showScreen']) diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php index 397532133e..a512bdffdc 100644 --- a/tests/Feature/Api/V1_1/CaseControllerTest.php +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -158,6 +158,7 @@ public function test_get_all_cases_sort_by_case_number(): void { $userA = $this->createUser('user_a'); $cases = $this->createCasesStartedForUser($userA->id, 10); + $casesSorted = $cases->sortBy('case_number'); $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'case_number:asc'])); @@ -201,46 +202,208 @@ public function test_get_all_cases_sort_by_invalid_field(): void $response->assertJsonFragment(['message' => "Sort by field $invalidField is not allowed."]); } - public function test_get_all_cases_filter_by(): void + public function test_filter_by_case_number(): void { $userA = $this->createUser('user_a'); - $casesA = $this->createCasesStartedForUser($userA->id, 5); $caseNumber = 123456; - $casesB = $this->createCasesStartedForUser($userA->id, 1, ['case_number' => $caseNumber, 'case_status' => 'IN_PROGRESS']); - - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); - $response->assertStatus(200); - $response->assertJsonCount(6, 'data'); + $this->createCasesStartedForUser($userA->id, 5); + $this->createCasesStartedForUser($userA->id, 1, ['case_number' => $caseNumber]); - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => ['case_number' => $caseNumber]])); + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'case_number'], + 'operator' => '=', + 'value' => $caseNumber, + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $response->assertJsonCount(1, 'data'); $response->assertJsonFragment(['case_number' => $caseNumber]); + } - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => ['case_status' => 'IN_PROGRESS']])); + public function test_filter_by_case_status(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); - $total = $casesA->where('case_status', 'IN_PROGRESS')->count() + $casesB->where('case_status', 'IN_PROGRESS')->count(); + + $total = $casesA->where('case_status', 'IN_PROGRESS')->count() + + $casesB->where('case_status', 'IN_PROGRESS')->count(); $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonMissing(['case_status' => 'COMPLETED']); } + public function test_filter_by_user_and_case_status(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS')->count(); + $response->assertJsonCount($total, 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonFragment(['user_id' => $userA->id]); + } + + public function test_filter_by_user_case_status_and_created_at(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'created_at'], + 'operator' => '>', + 'value' => '2023-02-10', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count(); + $response->assertJsonCount($total, 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonFragment(['user_id' => $userA->id]); + } + + public function test_filter_by_user_case_status_created_at_and_completed_at(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); + + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'created_at'], + 'operator' => '>', + 'value' => '2023-02-10', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'completed_at'], + 'operator' => '>', + 'value' => '2023-04-01', + ], + ]), + ]; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count(); + + $response->assertJsonCount($total, 'data'); + $json = $response->json(); + $metaTotal = $json['meta']['total']; + $this->assertEquals($total, $metaTotal, 'The total count of cases does not match the expected value. ' . json_encode($json)); + } + public function test_get_all_cases_filter_by_invalid_field(): void { $invalidField = 'invalid_field'; - - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => ''])); + $filterBy = ['filterBy' => '[invalid_json']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(422); - $response->assertJsonPath('message', 'The Filter by field must be an array.'); - + $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.'); $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => [$invalidField => 'value']])); $response->assertStatus(422); - $response->assertJsonPath('message', "Filter by field $invalidField is not allowed."); + $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.'); } public function test_get_my_cases_counters_ok(): void { - /** + /** * Creating missing permissions, probably this part should be removed when * the permissions were added in another ticket */ @@ -267,7 +430,7 @@ public function test_get_my_cases_counters_ok(): void $casesD = $this->createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); $casesE = $this->createCasesStartedForUser($userB->id, 5, ['case_status' => 'COMPLETED']); - $casesF = $this->createCasesStartedForUser($userB->id, 5, ['case_status' => 'IN_PROGRESS']); + $casesF = $this->createCasesStartedForUser($userB->id, 5, ['case_status' => 'IN_PROGRESS']); $casesG = $this->createCasesParticipatedForUser($userB->id, 5, ['case_status' => 'COMPLETED']); $casesH = $this->createCasesParticipatedForUser($userB->id, 5, ['case_status' => 'IN_PROGRESS']); diff --git a/tests/Feature/Cases/CaseStartedSubProcessTest.php b/tests/Feature/Cases/CaseStartedSubProcessTest.php index 619c0e77f2..6ad81e35b1 100644 --- a/tests/Feature/Cases/CaseStartedSubProcessTest.php +++ b/tests/Feature/Cases/CaseStartedSubProcessTest.php @@ -3,13 +3,13 @@ namespace Tests\Feature\Cases; use Illuminate\Foundation\Testing\DatabaseTransactions; -use Tests\TestCase; use Mockery; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; use ProcessMaker\Models\User; use ProcessMaker\Repositories\CaseRepository; +use Tests\TestCase; class CaseStartedSubProcessTest extends TestCase { diff --git a/tests/Feature/Cases/CaseStartedTest.php b/tests/Feature/Cases/CaseStartedTest.php index d63cea61ee..3de58c265c 100644 --- a/tests/Feature/Cases/CaseStartedTest.php +++ b/tests/Feature/Cases/CaseStartedTest.php @@ -3,13 +3,13 @@ namespace Tests\Feature\Cases; use Illuminate\Foundation\Testing\DatabaseTransactions; -use Tests\TestCase; use Mockery; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; use ProcessMaker\Models\User; use ProcessMaker\Repositories\CaseRepository; +use Tests\TestCase; class CaseStartedTest extends TestCase { @@ -125,7 +125,6 @@ public function test_update_case_started_request_tokens() 'process_id' => $process->id, ]); - $repo = new CaseRepository(); $repo->create($instance); @@ -164,7 +163,6 @@ public function test_update_case_started_tasks() 'process_id' => $process->id, ]); - $repo = new CaseRepository(); $repo->create($instance); @@ -225,7 +223,6 @@ public function test_update_case_started_script_tasks() 'process_id' => $process->id, ]); - $repo = new CaseRepository(); $repo->create($instance); @@ -285,7 +282,6 @@ public function test_update_case_started_participants() 'process_id' => $process->id, ]); - $repo = new CaseRepository(); $repo->create($instance); @@ -343,7 +339,6 @@ public function test_update_case_started_status() 'process_id' => $process->id, ]); - $repo = new CaseRepository(); $repo->create($instance);