From 69aeac3756d69d45a25244cbc80fa95a28a132e6 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, 26 May 2026 11:27:53 +0300 Subject: [PATCH] feat(project-pause): set/clear paused_at on toggle and bulk pause-resume --- .../Controllers/Api/ProjectController.php | 9 +++- app/app/Services/Project/ProjectService.php | 8 ++- .../Project/PausedAtWriteSideTest.php | 54 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 app/tests/Unit/Services/Project/PausedAtWriteSideTest.php diff --git a/app/app/Http/Controllers/Api/ProjectController.php b/app/app/Http/Controllers/Api/ProjectController.php index ffc61aa3..d6eeb52c 100644 --- a/app/app/Http/Controllers/Api/ProjectController.php +++ b/app/app/Http/Controllers/Api/ProjectController.php @@ -164,7 +164,14 @@ class ProjectController extends Controller { $request->validate(['is_active' => ['required', 'boolean']]); $project = Project::where('tenant_id', $request->user()->tenant_id)->findOrFail($id); - $project->update(['is_active' => $request->boolean('is_active')]); + + // Spec: docs/superpowers/plans/2026-05-26-supplier-snapshot-guard.md (Task 11). + // paused_at — anchor для SupplierSnapshotGuard grace-расчёта. + $newActive = $request->boolean('is_active'); + $project->update([ + 'is_active' => $newActive, + 'paused_at' => $newActive ? null : now(), + ]); // #10: pause/resume must reach the supplier. The job's group recompute pushes // status=paused when no active project of the group remains (resume → active). diff --git a/app/app/Services/Project/ProjectService.php b/app/app/Services/Project/ProjectService.php index 7a6e8749..f06ba237 100644 --- a/app/app/Services/Project/ProjectService.php +++ b/app/app/Services/Project/ProjectService.php @@ -276,7 +276,13 @@ class ProjectService private function bulkPauseResume($query, bool $isActive): array { $ids = (clone $query)->pluck('id')->all(); - $updated = $query->update(['is_active' => $isActive]); + // Spec: docs/superpowers/plans/2026-05-26-supplier-snapshot-guard.md (Task 11). + // paused_at — anchor для SupplierSnapshotGuard grace-расчёта. Mass-update НЕ + // триггерит model events, поэтому пишем явно в одном UPDATE. + $updated = $query->update([ + 'is_active' => $isActive, + 'paused_at' => $isActive ? null : DB::raw('NOW()'), + ]); foreach ($ids as $id) { SyncSupplierProjectJob::dispatch((int) $id); } diff --git a/app/tests/Unit/Services/Project/PausedAtWriteSideTest.php b/app/tests/Unit/Services/Project/PausedAtWriteSideTest.php new file mode 100644 index 00000000..c1034603 --- /dev/null +++ b/app/tests/Unit/Services/Project/PausedAtWriteSideTest.php @@ -0,0 +1,54 @@ +methodBody(ProjectService::class, 'bulkPauseResume'); + + $this->assertStringContainsString('paused_at', $body, + 'bulkPauseResume должен явно обновлять paused_at вместе с is_active'); + $this->assertStringContainsString('is_active', $body); + } + + public function test_project_controller_toggle_active_writes_paused_at(): void + { + $body = $this->methodBody(ProjectController::class, 'toggleActive'); + + $this->assertStringContainsString('paused_at', $body, + 'toggleActive должен явно обновлять paused_at вместе с is_active'); + $this->assertStringContainsString('is_active', $body); + } + + private function methodBody(string $class, string $method): string + { + $rm = new ReflectionMethod($class, $method); + $lines = file($rm->getFileName()); + $body = array_slice($lines, $rm->getStartLine() - 1, $rm->getEndLine() - $rm->getStartLine() + 1); + + return implode('', $body); + } +}