From d1313a26495b999872b23b81fb670b46151751f9 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 12 Sep 2024 13:51:37 -0400 Subject: [PATCH 1/6] feat: add cases_participated migration --- ...172734_create_cases_participated_table.php | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 database/migrations/2024_09_12_172734_create_cases_participated_table.php diff --git a/database/migrations/2024_09_12_172734_create_cases_participated_table.php b/database/migrations/2024_09_12_172734_create_cases_participated_table.php new file mode 100644 index 0000000000..0e28554f63 --- /dev/null +++ b/database/migrations/2024_09_12_172734_create_cases_participated_table.php @@ -0,0 +1,48 @@ +unsignedInteger('user_id'); + $table->unsignedInteger('case_number'); + $table->string('case_title', 255); + $table->text('case_title_formatted'); + $table->string('case_status', 20); + $table->json('processes'); + $table->json('requests'); + $table->json('request_tokens'); + $table->json('tasks'); + $table->json('participants'); + $table->timestamp('initiated_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + $table->text('keywords'); + + $table->primary(['user_id', 'case_number']); + $table->foreign('user_id')->references('id')->on('users'); + + $table->index(['user_id', 'case_status', 'created_at']); + $table->index(['user_id', 'case_status', 'completed_at']); + + $table->fullText('case_title'); + $table->fullText('keywords'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cases_participated'); + } +}; From 9d58af062932ce1761ec6ccf4e5ed5bcef1f4c4f Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Thu, 12 Sep 2024 14:27:09 -0400 Subject: [PATCH 2/6] feat: add cases_participated model, factory --- ProcessMaker/Models/CaseParticipated.php | 55 +++++++++++++ .../factories/CaseParticipatedFactory.php | 77 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 ProcessMaker/Models/CaseParticipated.php create mode 100644 database/factories/CaseParticipatedFactory.php diff --git a/ProcessMaker/Models/CaseParticipated.php b/ProcessMaker/Models/CaseParticipated.php new file mode 100644 index 0000000000..60b076d795 --- /dev/null +++ b/ProcessMaker/Models/CaseParticipated.php @@ -0,0 +1,55 @@ + AsArrayObject::class, + 'requests' => AsArrayObject::class, + 'request_tokens' => AsArrayObject::class, + 'tasks' => AsArrayObject::class, + 'participants' => AsArrayObject::class, + 'initiated_at' => 'datetime', + 'completed_at' => 'datetime', + ]; + + protected static function newFactory(): Factory + { + return CaseParticipatedFactory::new(); + } + + /** + * Get the user that owns the case. + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/database/factories/CaseParticipatedFactory.php b/database/factories/CaseParticipatedFactory.php new file mode 100644 index 0000000000..99157967f3 --- /dev/null +++ b/database/factories/CaseParticipatedFactory.php @@ -0,0 +1,77 @@ + + */ +class CaseParticipatedFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'user_id' => fake()->randomElement([1, 3]), + 'case_number' => fake()->unique()->randomNumber(), + 'case_title' => fake()->words(3, true),, + 'case_title_formatted' => fake()->words(3, true),, + 'case_status' => fake()->randomElement(['IN_PROGRESS', 'COMPLETED']), + 'processes' => array_map(function() { + return [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(2, true), + ]; + }, range(1, 3)), + 'requests' => [ + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(2, true), + 'parent_request' => fake()->randomNumber(), + ], + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->words(3, true), + 'parent_request' => fake()->randomNumber(), + ], + ], + 'request_tokens' => fake()->randomElement([fake()->randomNumber(), fake()->randomNumber(), fake()->randomNumber()]), + 'tasks' => [ + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(4, true), + ], + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(3, true), + ], + [ + 'id' => fake()->numerify('node_####'), + 'name' => fake()->words(2, true), + ], + ], + 'participants' => [ + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->name(), + ], + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->name(), + ], + [ + 'id' => fake()->randomNumber(), + 'name' => fake()->name(), + ], + ], + 'initiated_at' => fake()->dateTime(), + 'completed_at' => fake()->dateTime(), + 'keywords' => fake(), + ]; + } +} From 1b66edfd7582d4652f092d5e3f514fb2f820e499 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Fri, 13 Sep 2024 10:10:27 -0400 Subject: [PATCH 3/6] feat: add get_in_progress, get_completed endpoints --- .../Controllers/Api/V1_1/CaseController.php | 71 +++++++++++++++++++ .../factories/CaseParticipatedFactory.php | 9 ++- routes/v1_1/api.php | 8 +++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php index 301457724e..a6333d048d 100644 --- a/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php +++ b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php @@ -3,10 +3,12 @@ namespace ProcessMaker\Http\Controllers\Api\V1_1; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Http\Requests\GetAllCasesRequest; use ProcessMaker\Http\Resources\V1_1\CaseResource; +use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; class CaseController extends Controller @@ -60,8 +62,12 @@ class CaseController extends Controller 'updated_at', ]; + const DEFAULT_PAGE_SIZE = 15; + const DEFAULT_SORT_DIRECTION = 'asc'; + public function __construct(private Request $request) {} + /** * Get a list of all started cases. * @@ -98,6 +104,52 @@ public function getAllCases(GetAllCasesRequest $request): array ]; } + public function getInProgress(GetAllCasesRequest $request): array + { + // CaseParticipated::factory()->count(1000)->create(); + + $pageSize = $request->get('pageSize', 15); + + $query = CaseParticipated::select($this->defaultFields) + ->where('case_status', 'IN_PROGRESS'); + + $this->filters($request, $query); + + $pagination = CaseResource::collection($query->paginate($pageSize)); + + return [ + 'data' => $pagination->items(), + 'meta' => [ + 'total' => $pagination->total(), + 'perPage' => $pagination->perPage(), + 'currentPage' => $pagination->currentPage(), + 'lastPage' => $pagination->lastPage(), + ], + ]; + } + + public function getCompleted(GetAllCasesRequest $request): array + { + $pageSize = $request->get('pageSize', 15); + + $query = CaseParticipated::select($this->defaultFields) + ->where('case_status', 'COMPLETED'); + + $this->filters($request, $query); + + $pagination = CaseResource::collection($query->paginate($pageSize)); + + return [ + 'data' => $pagination->items(), + 'meta' => [ + 'total' => $pagination->total(), + 'perPage' => $pagination->perPage(), + 'currentPage' => $pagination->currentPage(), + 'lastPage' => $pagination->lastPage(), + ], + ]; + } + /** * Apply filters to the query. * @@ -201,4 +253,23 @@ private function search(Request $request, Builder $query): void }); } } + + /** + * Handle pagination and return JSON response. + */ + private function paginateResponse($query): JsonResponse + { + $pageSize = $this->request->get('pageSize', self::DEFAULT_PAGE_SIZE); + $pagination = CaseResource::collection($query->paginate($pageSize)); + + return response()->json([ + 'data' => $pagination->items(), + 'meta' => [ + 'total' => $pagination->total(), + 'perPage' => $pagination->perPage(), + 'currentPage' => $pagination->currentPage(), + 'lastPage' => $pagination->lastPage(), + ], + ]); + } } diff --git a/database/factories/CaseParticipatedFactory.php b/database/factories/CaseParticipatedFactory.php index 99157967f3..c10c8b27e8 100644 --- a/database/factories/CaseParticipatedFactory.php +++ b/database/factories/CaseParticipatedFactory.php @@ -3,12 +3,15 @@ namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; +use ProcessMaker\Models\CaseParticipated; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\ProcessMaker\Models\CaseParticipated> */ class CaseParticipatedFactory extends Factory { + protected $model = CaseParticipated::class; + /** * Define the model's default state. * @@ -19,8 +22,8 @@ public function definition(): array return [ 'user_id' => fake()->randomElement([1, 3]), 'case_number' => fake()->unique()->randomNumber(), - 'case_title' => fake()->words(3, true),, - 'case_title_formatted' => fake()->words(3, true),, + 'case_title' => fake()->words(3, true), + 'case_title_formatted' => fake()->words(3, true), 'case_status' => fake()->randomElement(['IN_PROGRESS', 'COMPLETED']), 'processes' => array_map(function() { return [ @@ -71,7 +74,7 @@ public function definition(): array ], 'initiated_at' => fake()->dateTime(), 'completed_at' => fake()->dateTime(), - 'keywords' => fake(), + 'keywords' => '', ]; } } diff --git a/routes/v1_1/api.php b/routes/v1_1/api.php index f15b2d8f77..99623048d8 100644 --- a/routes/v1_1/api.php +++ b/routes/v1_1/api.php @@ -33,5 +33,13 @@ // Route to list all cases Route::get('get_all_cases', [CaseController::class, 'getAllCases']) ->name('cases.all_cases'); + + // Route to list all in-progress cases + Route::get('get_in_progress', [CaseController::class, 'getInProgress']) + ->name('cases.in_progress'); + + // Route to list all completed cases + Route::get('get_completed', [CaseController::class, 'getCompleted']) + ->name('cases.completed'); }); }); From c2ee0112b5baec228f1fbfafd80ba8e0998a0203 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Fri, 13 Sep 2024 15:07:05 -0400 Subject: [PATCH 4/6] feat: add case repository --- .../Contracts/CaseRepositoryInterface.php | 62 +++++ .../Controllers/Api/V1_1/CaseController.php | 245 +++--------------- ...llCasesRequest.php => CaseListRequest.php} | 2 +- ProcessMaker/Repositories/CaseRepository.php | 207 +++++++++++++++ 4 files changed, 309 insertions(+), 207 deletions(-) create mode 100644 ProcessMaker/Contracts/CaseRepositoryInterface.php rename ProcessMaker/Http/Requests/{GetAllCasesRequest.php => CaseListRequest.php} (95%) create mode 100644 ProcessMaker/Repositories/CaseRepository.php diff --git a/ProcessMaker/Contracts/CaseRepositoryInterface.php b/ProcessMaker/Contracts/CaseRepositoryInterface.php new file mode 100644 index 0000000000..77ada71955 --- /dev/null +++ b/ProcessMaker/Contracts/CaseRepositoryInterface.php @@ -0,0 +1,62 @@ +caseRepository = $caseRepository; + } + /* The comment block you provided is a PHPDoc block. It is used to document the purpose and usage of a method in PHP + code. In this specific block: */ /** * Get a list of all started cases. * @@ -83,181 +37,60 @@ public function __construct(private Request $request) {} * * @return array */ - public function getAllCases(GetAllCasesRequest $request): array - { - $pageSize = $request->get('pageSize', 15); - - $query = CaseStarted::select($this->defaultFields); - - $this->filters($request, $query); - - $pagination = CaseResource::collection($query->paginate($pageSize)); - - return [ - 'data' => $pagination->items(), - 'meta' => [ - 'total' => $pagination->total(), - 'perPage' => $pagination->perPage(), - 'currentPage' => $pagination->currentPage(), - 'lastPage' => $pagination->lastPage(), - ], - ]; - } - - public function getInProgress(GetAllCasesRequest $request): array - { - // CaseParticipated::factory()->count(1000)->create(); - - $pageSize = $request->get('pageSize', 15); - - $query = CaseParticipated::select($this->defaultFields) - ->where('case_status', 'IN_PROGRESS'); - - $this->filters($request, $query); - - $pagination = CaseResource::collection($query->paginate($pageSize)); - - return [ - 'data' => $pagination->items(), - 'meta' => [ - 'total' => $pagination->total(), - 'perPage' => $pagination->perPage(), - 'currentPage' => $pagination->currentPage(), - 'lastPage' => $pagination->lastPage(), - ], - ]; - } - - public function getCompleted(GetAllCasesRequest $request): array + public function getAllCases(CaseListRequest $request): JSonResponse { - $pageSize = $request->get('pageSize', 15); - - $query = CaseParticipated::select($this->defaultFields) - ->where('case_status', 'COMPLETED'); - - $this->filters($request, $query); - - $pagination = CaseResource::collection($query->paginate($pageSize)); - - return [ - 'data' => $pagination->items(), - 'meta' => [ - 'total' => $pagination->total(), - 'perPage' => $pagination->perPage(), - 'currentPage' => $pagination->currentPage(), - 'lastPage' => $pagination->lastPage(), - ], - ]; + $query = $this->caseRepository->getAllCases($request); + return $this->paginateResponse($query); } /** - * Apply filters to the query. + * Get a list of all started cases. * * @param Request $request - * @param Builder $query - * - * @return void - */ - private function filters(Request $request, Builder $query): void - { - if ($request->has('userId')) { - $query->where('user_id', $request->get('userId')); - } - - if ($request->has('status')) { - $query->where('case_status', $request->get('status')); - } - - $this->search($request, $query); - $this->filterBy($request, $query); - $this->sortBy($request, $query); - } - - /** - * Sort the query. * - * @param Request $request: Query parameter format: sortBy=field:asc,field2:desc,... - * @param Builder $query + * @queryParam userId int Filter by user ID. + * @queryParam sortBy string Sort by field:asc,field2:desc,... + * @queryParam filterBy array Filter by field=value&field2=value2&... + * @queryParam search string Search by case number or case title. + * @queryParam pageSize int Number of items per page. + * @queryParam page int Page number. * - * @return void + * @return array */ - private function sortBy(Request $request, Builder $query): void + public function getInProgress(CaseListRequest $request): JSonResponse { - $sort = explode(',', $request->get('sortBy')); - - foreach ($sort as $value) { - if (!preg_match('/^[a-zA-Z_]+:(asc|desc)$/', $value)) { - continue; - } - - $sort = explode(':', $value); - $field = $sort[0]; - $order = $sort[1] ?? self::DEFAULT_SORT_DIRECTION; - - if (in_array($field, $this->sortableFields)) { - $query->orderBy($field, $order); - } - } + $query = $this->caseRepository->getInProgressCases($request); + return $this->paginateResponse($query); } /** - * Filter the query. + * Get a list of all started cases. * - * @param Request $request: Query parameter format: filterBy[field]=value&filterBy[field2]=value2&... - * @param Builder $query - * @param array $dateFields List of date fields in current model + * @param Request $request * - * @return void - */ - private function filterBy(Request $request, Builder $query): void - { - if ($request->has('filterBy')) { - $filterByValue = $request->get('filterBy'); - - foreach ($filterByValue as $key => $value) { - if (!in_array($key, $this->filterableFields)) { - continue; - } - - if (in_array($key, $this->dateFields)) { - $query->whereDate($key, $value); - continue; - } - - $query->where($key, $value); - } - } - } - - /** - * Search by case number or case title. - - * @param Request $request: Query parameter format: search=keyword - * @param Builder $query + * @queryParam userId int Filter by user ID. + * @queryParam sortBy string Sort by field:asc,field2:desc,... + * @queryParam filterBy array Filter by field=value&field2=value2&... + * @queryParam search string Search by case number or case title. + * @queryParam pageSize int Number of items per page. + * @queryParam page int Page number. * - * @return void + * @return array */ - private function search(Request $request, Builder $query): void + public function getCompleted(CaseListRequest $request): JSonResponse { - if ($request->has('search')) { - $search = $request->get('search'); - - $query->where(function ($q) use ($search) { - foreach ($this->searchableFields as $field) { - if ($field === 'case_number') { - $q->orWhere($field, $search); - } else { - $q->orWhereFullText($field, $search . '*', ['mode' => 'boolean']); - } - } - }); - } + $query = $this->caseRepository->getCompletedCases($request); + return $this->paginateResponse($query); } /** * Handle pagination and return JSON response. + * + * @param Builder $query + * + * @return JsonResponse */ - private function paginateResponse($query): JsonResponse + private function paginateResponse(Builder $query): JsonResponse { $pageSize = $this->request->get('pageSize', self::DEFAULT_PAGE_SIZE); $pagination = CaseResource::collection($query->paginate($pageSize)); diff --git a/ProcessMaker/Http/Requests/GetAllCasesRequest.php b/ProcessMaker/Http/Requests/CaseListRequest.php similarity index 95% rename from ProcessMaker/Http/Requests/GetAllCasesRequest.php rename to ProcessMaker/Http/Requests/CaseListRequest.php index 500cb15bf7..c55e944406 100644 --- a/ProcessMaker/Http/Requests/GetAllCasesRequest.php +++ b/ProcessMaker/Http/Requests/CaseListRequest.php @@ -5,7 +5,7 @@ use Illuminate\Foundation\Http\FormRequest; use ProcessMaker\Rules\SortBy; -class GetAllCasesRequest extends FormRequest +class CaseListRequest extends FormRequest { /** * Determine if the user is authorized to make this request. diff --git a/ProcessMaker/Repositories/CaseRepository.php b/ProcessMaker/Repositories/CaseRepository.php new file mode 100644 index 0000000000..532e5bff0b --- /dev/null +++ b/ProcessMaker/Repositories/CaseRepository.php @@ -0,0 +1,207 @@ +defaultFields); + $this->applyFilters($request, $query); + return $query; + } + + /** + * Get all cases in progress + * + * @param Request $request + * + * @return Builder + */ + public function getInProgressCases(Request $request): Builder + { + $query = CaseParticipated::select($this->defaultFields) + ->where('case_status', 'IN_PROGRESS'); + $this->applyFilters($request, $query); + return $query; + } + + /** + * Get all completed cases + * + * @param Request $request + * + * @return Builder + */ + public function getCompletedCases(Request $request): Builder + { + $query = CaseParticipated::select($this->defaultFields) + ->where('case_status', 'COMPLETED'); + $this->applyFilters($request, $query); + return $query; + } + + /** + * Apply filters to the query. + * + * @param Request $request + * @param Builder $query + * + * @return void + */ + protected function applyFilters(Request $request, Builder $query): void + { + if ($request->has('userId')) { + $query->where('user_id', $request->get('userId')); + } + + if ($request->has('status')) { + $query->where('case_status', $request->get('status')); + } + + $this->search($request, $query); + $this->filterBy($request, $query); + $this->sortBy($request, $query); + } + + /** + * Search by case number or case title. + + * @param Request $request: Query parameter format: search=keyword + * @param Builder $query + * + * @return void + */ + public function search(Request $request, Builder $query): void + { + if ($request->has('search')) { + $search = $request->get('search'); + + $query->where(function ($q) use ($search) { + foreach ($this->searchableFields as $field) { + if ($field === 'case_number') { + $q->where($field, $search); + } else { + $q->orWhereFullText($field, $search . '*', ['mode' => 'boolean']); + } + } + }); + } + } + + /** + * Filter the query. + * + * @param Request $request: Query parameter format: filterBy[field]=value&filterBy[field2]=value2&... + * @param Builder $query + * @param array $dateFields List of date fields in current model + * + * @return void + */ + public function filterBy(Request $request, Builder $query): void + { + if ($request->has('filterBy')) { + $filterByValue = $request->get('filterBy'); + + foreach ($filterByValue as $key => $value) { + if (!in_array($key, $this->filterableFields)) { + continue; + } + + if (in_array($key, $this->dateFields)) { + $query->whereDate($key, $value); + continue; + } + + $query->where($key, $value); + } + } + } + + /** + * Sort the query. + * + * @param Request $request: Query parameter format: sortBy=field:asc,field2:desc,... + * @param Builder $query + * + * @return void + */ + public function sortBy(Request $request, Builder $query): void + { + $sort = explode(',', $request->get('sortBy')); + + foreach ($sort as $value) { + $sort = explode(':', $value); + $field = $sort[0]; + $order = $sort[1] ?? self::DEFAULT_SORT_DIRECTION; + + if (in_array($field, $this->sortableFields)) { + $query->orderBy($field, $order); + } + } + } +} From 5e23427b579b88aefdbea2f76243b807063217c1 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Fri, 13 Sep 2024 17:25:48 -0400 Subject: [PATCH 5/6] feat: add validation exception --- .../Exception/CaseValidationException.php | 20 +++++++++++++++ ProcessMaker/Repositories/CaseRepository.php | 25 +++++++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 ProcessMaker/Exception/CaseValidationException.php diff --git a/ProcessMaker/Exception/CaseValidationException.php b/ProcessMaker/Exception/CaseValidationException.php new file mode 100644 index 0000000000..dc070dbf51 --- /dev/null +++ b/ProcessMaker/Exception/CaseValidationException.php @@ -0,0 +1,20 @@ +json([ + 'message' => $this->getMessage(), + ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY); + } +} diff --git a/ProcessMaker/Repositories/CaseRepository.php b/ProcessMaker/Repositories/CaseRepository.php index 532e5bff0b..89520985dc 100644 --- a/ProcessMaker/Repositories/CaseRepository.php +++ b/ProcessMaker/Repositories/CaseRepository.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use ProcessMaker\Contracts\CaseRepositoryInterface; +use ProcessMaker\Exception\CaseValidationException; use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; @@ -143,10 +144,10 @@ public function search(Request $request, Builder $query): void $query->where(function ($q) use ($search) { foreach ($this->searchableFields as $field) { - if ($field === 'case_number') { - $q->where($field, $search); - } else { + if ($field === 'case_title') { $q->orWhereFullText($field, $search . '*', ['mode' => 'boolean']); + } else { + $q->where($field, $search); } } }); @@ -169,7 +170,7 @@ public function filterBy(Request $request, Builder $query): void foreach ($filterByValue as $key => $value) { if (!in_array($key, $this->filterableFields)) { - continue; + throw new CaseValidationException("Filter by field $key is not allowed."); } if (in_array($key, $this->dateFields)) { @@ -192,14 +193,18 @@ public function filterBy(Request $request, Builder $query): void */ public function sortBy(Request $request, Builder $query): void { - $sort = explode(',', $request->get('sortBy')); + if ($request->has('sortBy')) { + $sort = explode(',', $request->get('sortBy')); + + foreach ($sort as $value) { + $sort = explode(':', $value); + $field = $sort[0]; + $order = $sort[1] ?? self::DEFAULT_SORT_DIRECTION; - foreach ($sort as $value) { - $sort = explode(':', $value); - $field = $sort[0]; - $order = $sort[1] ?? self::DEFAULT_SORT_DIRECTION; + if (!in_array($field, $this->sortableFields)) { + throw new CaseValidationException("Sort by field $field is not allowed."); + } - if (in_array($field, $this->sortableFields)) { $query->orderBy($field, $order); } } From 0936002b4646e8fd8d5e96578f9a27d73760f415 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Mon, 16 Sep 2024 16:18:20 -0400 Subject: [PATCH 6/6] tests: case controller ep tests --- ...ace.php => CaseApiRepositoryInterface.php} | 2 +- .../Controllers/Api/V1_1/CaseController.php | 4 +- ...seRepository.php => CaseApiRepository.php} | 4 +- routes/v1_1/api.php | 6 +- tests/Feature/Api/V1_1/CaseControllerTest.php | 237 ++++++++++++++++++ 5 files changed, 245 insertions(+), 8 deletions(-) rename ProcessMaker/Contracts/{CaseRepositoryInterface.php => CaseApiRepositoryInterface.php} (97%) rename ProcessMaker/Repositories/{CaseRepository.php => CaseApiRepository.php} (97%) create mode 100644 tests/Feature/Api/V1_1/CaseControllerTest.php diff --git a/ProcessMaker/Contracts/CaseRepositoryInterface.php b/ProcessMaker/Contracts/CaseApiRepositoryInterface.php similarity index 97% rename from ProcessMaker/Contracts/CaseRepositoryInterface.php rename to ProcessMaker/Contracts/CaseApiRepositoryInterface.php index 77ada71955..b71ffdd0d2 100644 --- a/ProcessMaker/Contracts/CaseRepositoryInterface.php +++ b/ProcessMaker/Contracts/CaseApiRepositoryInterface.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; -interface CaseRepositoryInterface +interface CaseApiRepositoryInterface { /** * Get all cases diff --git a/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php index e606de9ad2..ffa3056358 100644 --- a/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php +++ b/ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php @@ -8,7 +8,7 @@ use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Http\Requests\CaseListRequest; use ProcessMaker\Http\Resources\V1_1\CaseResource; -use ProcessMaker\Repositories\CaseRepository; +use ProcessMaker\Repositories\CaseApiRepository; class CaseController extends Controller { @@ -16,7 +16,7 @@ class CaseController extends Controller const DEFAULT_PAGE_SIZE = 15; - public function __construct(private Request $request, CaseRepository $caseRepository) { + public function __construct(private Request $request, CaseApiRepository $caseRepository) { $this->caseRepository = $caseRepository; } diff --git a/ProcessMaker/Repositories/CaseRepository.php b/ProcessMaker/Repositories/CaseApiRepository.php similarity index 97% rename from ProcessMaker/Repositories/CaseRepository.php rename to ProcessMaker/Repositories/CaseApiRepository.php index 89520985dc..ccc6b14b92 100644 --- a/ProcessMaker/Repositories/CaseRepository.php +++ b/ProcessMaker/Repositories/CaseApiRepository.php @@ -4,12 +4,12 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; -use ProcessMaker\Contracts\CaseRepositoryInterface; +use ProcessMaker\Contracts\CaseApiRepositoryInterface; use ProcessMaker\Exception\CaseValidationException; use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; -class CaseRepository implements CaseRepositoryInterface +class CaseApiRepository implements CaseApiRepositoryInterface { /** * Default fields used in the query select statement. diff --git a/routes/v1_1/api.php b/routes/v1_1/api.php index 99623048d8..1c722ad5cb 100644 --- a/routes/v1_1/api.php +++ b/routes/v1_1/api.php @@ -32,14 +32,14 @@ Route::name('cases.')->prefix('cases')->group(function () { // Route to list all cases Route::get('get_all_cases', [CaseController::class, 'getAllCases']) - ->name('cases.all_cases'); + ->name('all_cases'); // Route to list all in-progress cases Route::get('get_in_progress', [CaseController::class, 'getInProgress']) - ->name('cases.in_progress'); + ->name('in_progress'); // Route to list all completed cases Route::get('get_completed', [CaseController::class, 'getCompleted']) - ->name('cases.completed'); + ->name('completed'); }); }); diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php new file mode 100644 index 0000000000..f002e69643 --- /dev/null +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -0,0 +1,237 @@ +create([ + 'username' => $username, + 'password' => Hash::make($password), + 'status' => $status, + ]); + } + + private function createCasesStartedForUser(int $userId, int $count = 1, $data = []) + { + return CaseStarted::factory()->count($count)->create(array_merge(['user_id' => $userId], $data)); + } + + private function createCasesParticipatedForUser(int $userId, int $count = 1, $data = []) + { + return CaseParticipated::factory()->count($count)->create(array_merge(['user_id' => $userId], $data)); + } + + public function test_get_all_cases(): void + { + $userA = $this->createUser('user_a'); + $cases = $this->createCasesStartedForUser($userA->id, 10); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + } + + public function test_get_in_progress(): void + { + $userA = $this->createUser('user_a'); + $cases = $this->createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress')); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonMissing(['case_status' => 'COMPLETED']); + } + + public function test_get_completed(): void + { + $userA = $this->createUser('user_a'); + $cases = $this->createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'COMPLETED']); + + $response = $this->apiCall('GET', route('api.1.1.cases.completed')); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonFragment(['case_status' => 'COMPLETED']); + $response->assertJsonMissing(['case_status' => 'IN_PROGRESS']); + } + + public function test_get_all_cases_by_users(): void + { + $userA = $this->createUser('user_a'); + $userB = $this->createUser('user_b'); + + $casesA = $this->createCasesStartedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + $casesB = $this->createCasesStartedForUser($userB->id, 6, ['case_status' => 'COMPLETED']); + $casesC = $this->createCasesStartedForUser($userA->id, 4, ['case_status' => 'IN_PROGRESS']); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + + $total = $casesA->count() + $casesB->count() + $casesC->count(); + $response->assertStatus(200); + $response->assertJsonCount($total, 'data'); + + $totalUserA = $casesA->count() + $casesC->count(); + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['userId' => $userA->id])); + $response->assertStatus(200); + $response->assertJsonCount($totalUserA, 'data'); + $response->assertJsonMissing(['user_id' => $userB->id]); + + $totalUserB = $casesB->count(); + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['userId' => $userB->id])); + $response->assertStatus(200); + $response->assertJsonCount($totalUserB, 'data'); + $response->assertJsonMissing(['user_id' => $userA->id]); + } + + public function test_get_all_cases_by_status(): void + { + $userA = $this->createUser('user_a'); + $userB = $this->createUser('user_b'); + + $casesA = $this->createCasesStartedForUser($userA->id, 5, ['case_status' => 'COMPLETED']); + $casesB = $this->createCasesStartedForUser($userB->id, 6, ['case_status' => 'IN_PROGRESS']); + $casesC = $this->createCasesStartedForUser($userA->id, 4, ['case_status' => 'COMPLETED']); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['status' => 'IN_PROGRESS'])); + $response->assertStatus(200); + $response->assertJsonCount($casesB->count(), 'data'); + + $totalCompleted = $casesA->count() + $casesC->count(); + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['status' => 'COMPLETED'])); + $response->assertStatus(200); + $response->assertJsonCount($totalCompleted, 'data'); + } + + public function test_get_in_progress_by_user(): void + { + $userA = $this->createUser('user_a'); + $userB = $this->createUser('user_b'); + $userC = $this->createUser('user_c'); + $casesA = $this->createCasesParticipatedForUser($userA->id, 5, ['case_status' => 'IN_PROGRESS']); + $casesB = $this->createCasesParticipatedForUser($userB->id, 6, ['case_status' => 'IN_PROGRESS']); + $casesC = $this->createCasesParticipatedForUser($userC->id, 4, ['case_status' => 'IN_PROGRESS']); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['userId' => $userA->id])); + $response->assertStatus(200); + $response->assertJsonCount($casesA->count(), 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['userId' => $userB->id])); + $response->assertStatus(200); + $response->assertJsonCount($casesB->count(), 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.in_progress', ['userId' => $userC->id])); + $response->assertStatus(200); + $response->assertJsonCount($casesC->count(), 'data'); + } + + public function test_search_all_cases_by_case_number(): void + { + $userA = $this->createUser('user_a'); + $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $this->createCasesStartedForUser($userA->id, 1, ['case_number' => $caseNumber]); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); + $response->assertStatus(200); + $response->assertJsonCount(6, 'data'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['search' => $caseNumber])); + $response->assertStatus(200); + $response->assertJsonCount(1, 'data'); + } + + 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'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.case_number', $casesSorted->first()->case_number); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'case_number:desc'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.case_number', $casesSorted->last()->case_number); + } + + public function test_get_all_cases_sort_by_completed_at(): void + { + $userA = $this->createUser('user_a'); + $cases = $this->createCasesStartedForUser($userA->id, 10); + $casesSorted = $cases->sortBy('completed_at'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'completed_at:asc'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.completed_at', $casesSorted->first()->completed_at->format('Y-m-d\TH:i:s.u\Z')); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'completed_at:desc'])); + $response->assertStatus(200); + $response->assertJsonCount($cases->count(), 'data'); + $response->assertJsonPath('data.0.completed_at', $casesSorted->last()->completed_at->format('Y-m-d\TH:i:s.u\Z')); + } + + public function test_get_all_cases_sort_by_invalid_field(): void + { + $invalidField = 'invalid_field'; + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => $invalidField])); + $response->assertStatus(422); + $response->assertJsonPath('message', 'The sortBy must be a comma-separated list of field:asc|desc.'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => "$invalidField:asc"])); + $response->assertStatus(422); + $response->assertJsonFragment(['message' => "Sort by field $invalidField is not allowed."]); + } + + public function test_get_all_cases_filter_by(): 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'); + + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => ['case_number' => $caseNumber]])); + $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']])); + $response->assertStatus(200); + $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_get_all_cases_filter_by_invalid_field(): void + { + $invalidField = 'invalid_field'; + + $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 = $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."); + } +}