From ace48d57c2087a12c845a90ace7083f277a55298 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 12 Mar 2026 12:24:20 +0000 Subject: [PATCH] feat(actions): add ScheduledAction model and migration Co-Authored-By: Claude Opus 4.6 --- ..._000002_create_scheduled_actions_table.php | 31 ++++++ src/Core/Actions/ScheduledAction.php | 100 ++++++++++++++++++ tests/Feature/ScheduledActionModelTest.php | 91 ++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 database/migrations/2024_01_01_000002_create_scheduled_actions_table.php create mode 100644 src/Core/Actions/ScheduledAction.php create mode 100644 tests/Feature/ScheduledActionModelTest.php diff --git a/database/migrations/2024_01_01_000002_create_scheduled_actions_table.php b/database/migrations/2024_01_01_000002_create_scheduled_actions_table.php new file mode 100644 index 0000000..777e47f --- /dev/null +++ b/database/migrations/2024_01_01_000002_create_scheduled_actions_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('action_class')->unique(); + $table->string('frequency', 100); + $table->string('timezone', 50)->nullable(); + $table->boolean('without_overlapping')->default(true); + $table->boolean('run_in_background')->default(true); + $table->boolean('is_enabled')->default(true); + $table->timestamp('last_run_at')->nullable(); + $table->timestamp('next_run_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('scheduled_actions'); + } +}; diff --git a/src/Core/Actions/ScheduledAction.php b/src/Core/Actions/ScheduledAction.php new file mode 100644 index 0000000..a32d1be --- /dev/null +++ b/src/Core/Actions/ScheduledAction.php @@ -0,0 +1,100 @@ + 'boolean', + 'run_in_background' => 'boolean', + 'is_enabled' => 'boolean', + 'last_run_at' => 'datetime', + 'next_run_at' => 'datetime', + ]; + } + + /** + * Scope to only enabled actions. + */ + public function scopeEnabled(Builder $query): Builder + { + return $query->where('is_enabled', true); + } + + /** + * Parse the frequency string and return the method name. + * + * 'dailyAt:09:00' → 'dailyAt' + * 'everyMinute' → 'everyMinute' + */ + public function frequencyMethod(): string + { + return explode(':', $this->frequency, 2)[0]; + } + + /** + * Parse the frequency string and return the arguments. + * + * 'dailyAt:09:00' → ['09:00'] + * 'weeklyOn:1,09:00' → ['1', '09:00'] + * 'everyMinute' → [] + */ + public function frequencyArgs(): array + { + $parts = explode(':', $this->frequency, 2); + + if (! isset($parts[1])) { + return []; + } + + return explode(',', $parts[1]); + } + + /** + * Record that this action has just run. + */ + public function markRun(): void + { + $this->update(['last_run_at' => now()]); + } +} diff --git a/tests/Feature/ScheduledActionModelTest.php b/tests/Feature/ScheduledActionModelTest.php new file mode 100644 index 0000000..f7225c2 --- /dev/null +++ b/tests/Feature/ScheduledActionModelTest.php @@ -0,0 +1,91 @@ +loadMigrationsFrom(__DIR__.'/../../database/migrations'); + } + + public function test_model_can_be_created(): void + { + $action = ScheduledAction::create([ + 'action_class' => 'App\\Actions\\TestAction', + 'frequency' => 'dailyAt:09:00', + 'timezone' => 'Europe/London', + 'without_overlapping' => true, + 'run_in_background' => true, + 'is_enabled' => true, + ]); + + $this->assertDatabaseHas('scheduled_actions', [ + 'action_class' => 'App\\Actions\\TestAction', + 'frequency' => 'dailyAt:09:00', + ]); + } + + public function test_enabled_scope(): void + { + ScheduledAction::create([ + 'action_class' => 'App\\Actions\\Enabled', + 'frequency' => 'hourly', + 'is_enabled' => true, + ]); + ScheduledAction::create([ + 'action_class' => 'App\\Actions\\Disabled', + 'frequency' => 'hourly', + 'is_enabled' => false, + ]); + + $enabled = ScheduledAction::enabled()->get(); + $this->assertCount(1, $enabled); + $this->assertSame('App\\Actions\\Enabled', $enabled->first()->action_class); + } + + public function test_frequency_method_parses_simple_frequency(): void + { + $action = new ScheduledAction(['frequency' => 'everyMinute']); + $this->assertSame('everyMinute', $action->frequencyMethod()); + $this->assertSame([], $action->frequencyArgs()); + } + + public function test_frequency_method_parses_frequency_with_args(): void + { + $action = new ScheduledAction(['frequency' => 'dailyAt:09:00']); + $this->assertSame('dailyAt', $action->frequencyMethod()); + $this->assertSame(['09:00'], $action->frequencyArgs()); + } + + public function test_frequency_method_parses_multiple_args(): void + { + $action = new ScheduledAction(['frequency' => 'weeklyOn:1,09:00']); + $this->assertSame('weeklyOn', $action->frequencyMethod()); + $this->assertSame(['1', '09:00'], $action->frequencyArgs()); + } + + public function test_mark_run_updates_last_run_at(): void + { + $action = ScheduledAction::create([ + 'action_class' => 'App\\Actions\\Runnable', + 'frequency' => 'hourly', + 'is_enabled' => true, + ]); + + $this->assertNull($action->last_run_at); + + $action->markRun(); + $action->refresh(); + + $this->assertNotNull($action->last_run_at); + } +}