From 08f02100fe41cc7c5d14bc8240ea7bddae962e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 12 May 2026 14:43:45 +0300 Subject: [PATCH] fix(projects-bulk): treat empty scope.filter as valid scope Replace !empty() check with has()+is_array() so scope:{filter:{}} is accepted as "all projects" rather than rejected as missing selection. Expand scope.filter to IDs in the controller (500-row limit guard) so the service receives a typed array[]; add Pest coverage for this case. Update phpstan baseline count for new actingAs() call. Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/Api/ProjectController.php | 32 ++++++++++++++++--- .../Requests/BulkProjectActionRequest.php | 2 +- app/phpstan-baseline.neon | 2 +- .../Feature/Api/ProjectBulkActionsTest.php | 12 +++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/app/app/Http/Controllers/Api/ProjectController.php b/app/app/Http/Controllers/Api/ProjectController.php index 8df72c51..b11c15ce 100644 --- a/app/app/Http/Controllers/Api/ProjectController.php +++ b/app/app/Http/Controllers/Api/ProjectController.php @@ -142,11 +142,33 @@ class ProjectController extends Controller /** POST /api/projects/bulk — batch pause/resume/archive */ public function bulk(BulkProjectActionRequest $request): JsonResponse { - $updated = $this->projects->bulkAction( - $request->user()->tenant_id, - $request->validated('action'), - $request->validated('ids'), - ); + $tenantId = $request->user()->tenant_id; + $action = $request->validated('action'); + + // Resolve target IDs: explicit list takes priority; otherwise expand from scope.filter. + if ($request->has('scope.filter')) { + $filter = $request->validated('scope.filter') ?? []; + $query = Project::where('tenant_id', $tenantId); + if (! empty($filter['signal_type'])) { + $query->where('signal_type', $filter['signal_type']); + } + if (! empty($filter['status'])) { + match ($filter['status']) { + 'active' => $query->whereNull('archived_at')->where('is_active', true), + 'paused' => $query->whereNull('archived_at')->where('is_active', false), + 'archived' => $query->whereNotNull('archived_at'), + default => null, + }; + } + if (! empty($filter['search'])) { + $query->where('name', 'ilike', '%'.$filter['search'].'%'); + } + $ids = $query->limit(500)->pluck('id')->all(); + } else { + $ids = $request->validated('ids') ?? []; + } + + $updated = $this->projects->bulkAction($tenantId, $action, $ids); return response()->json(['updated' => $updated]); } diff --git a/app/app/Http/Requests/BulkProjectActionRequest.php b/app/app/Http/Requests/BulkProjectActionRequest.php index 6d9f1f82..dcdcf3e7 100644 --- a/app/app/Http/Requests/BulkProjectActionRequest.php +++ b/app/app/Http/Requests/BulkProjectActionRequest.php @@ -50,7 +50,7 @@ class BulkProjectActionRequest extends FormRequest { $validator->after(function ($v) { $hasIds = ! empty($this->input('ids')); - $hasScope = ! empty($this->input('scope.filter')); + $hasScope = $this->has('scope.filter') && is_array($this->input('scope.filter')); if (! $hasIds && ! $hasScope) { $v->errors()->add('ids', 'Either ids or scope.filter is required.'); } diff --git a/app/phpstan-baseline.neon b/app/phpstan-baseline.neon index e4630dff..75589277 100644 --- a/app/phpstan-baseline.neon +++ b/app/phpstan-baseline.neon @@ -1257,5 +1257,5 @@ parameters: - message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#' identifier: method.notFound - count: 4 + count: 5 path: tests/Feature/Api/ProjectBulkActionsTest.php diff --git a/app/tests/Feature/Api/ProjectBulkActionsTest.php b/app/tests/Feature/Api/ProjectBulkActionsTest.php index 7164ad6f..6b35df9e 100644 --- a/app/tests/Feature/Api/ProjectBulkActionsTest.php +++ b/app/tests/Feature/Api/ProjectBulkActionsTest.php @@ -56,3 +56,15 @@ it('rejects empty ids without scope', function () { ]) ->assertStatus(422); }); + +it('accepts empty scope.filter as valid scope (all projects)', function () { + $tenant = Tenant::factory()->create(); + $user = User::factory()->for($tenant)->create(); + + $this->actingAs($user) + ->postJson('/api/projects/bulk', [ + 'action' => 'pause', + 'scope' => ['filter' => []], + ]) + ->assertOk(); +});