Dispatch post-trigger automations asynchronously via job

This commit is contained in:
Paulo Castellano 2026-06-10 17:46:46 -03:00
parent 6d3a375add
commit ab7c7c8bdf
4 changed files with 91 additions and 51 deletions

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Jobs\Automation;
use App\Actions\Automation\Trigger\DispatchPostTriggerAutomations;
use App\Enums\Automation\Trigger\Type as TriggerType;
use App\Models\Post;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DispatchPostTriggerAutomationsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 1;
public function __construct(
public Post $post,
public TriggerType $triggerType,
) {
$this->onQueue('automations');
}
public function handle(DispatchPostTriggerAutomations $dispatch): void
{
$dispatch($this->post, $this->triggerType);
}
}

View file

@ -4,31 +4,29 @@
namespace App\Observers;
use App\Actions\Automation\Trigger\DispatchPostTriggerAutomations;
use App\Enums\Automation\Trigger\Type as TriggerType;
use App\Enums\Post\Status as PostStatus;
use App\Jobs\Automation\DispatchPostTriggerAutomationsJob;
use App\Models\Post;
class PostObserver
{
public function __construct(private DispatchPostTriggerAutomations $dispatch) {}
public function saved(Post $post): void
{
if (! $post->wasChanged('status')) {
return;
}
$status = $post->status;
if ($status === PostStatus::Published) {
($this->dispatch)($post, TriggerType::PostPublished);
$triggerType = match ($post->status) {
PostStatus::Published => TriggerType::PostPublished,
PostStatus::Scheduled => TriggerType::PostScheduled,
default => null,
};
if ($triggerType === null) {
return;
}
if ($status === PostStatus::Scheduled) {
($this->dispatch)($post, TriggerType::PostScheduled);
}
DispatchPostTriggerAutomationsJob::dispatch($post, $triggerType)->afterCommit();
}
}

View file

@ -5,7 +5,7 @@ import { computed, ref } from 'vue';
import { Avatar } from '@/components/ui/avatar';
import { getMediaValidationWarning } from '@/composables/useMedia';
import { getPlatformLogo } from '@/composables/usePlatformLogo';
import { ContentType } from '@/enums/content-type';
import { ContentType } from '@/types/content-type';
import type { MediaItem } from '@/types/media';
interface SocialAccount {

View file

@ -2,7 +2,10 @@
declare(strict_types=1);
use App\Actions\Automation\Trigger\DispatchPostTriggerAutomations;
use App\Enums\Automation\Trigger\Type as TriggerType;
use App\Enums\Post\Status as PostStatus;
use App\Jobs\Automation\DispatchPostTriggerAutomationsJob;
use App\Jobs\Automation\ProcessAutomationNode;
use App\Models\Automation;
use App\Models\AutomationRun;
@ -12,7 +15,42 @@
beforeEach(fn () => Bus::fake());
it('dispatches a run when a post becomes published in the workspace', function () {
it('dispatches the trigger job when a post becomes published', function () {
$post = Post::factory()->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Published]);
Bus::assertDispatched(
DispatchPostTriggerAutomationsJob::class,
fn (DispatchPostTriggerAutomationsJob $job) => $job->post->is($post)
&& $job->triggerType === TriggerType::PostPublished,
);
});
it('dispatches the trigger job when a post becomes scheduled', function () {
$post = Post::factory()->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Scheduled]);
Bus::assertDispatched(
DispatchPostTriggerAutomationsJob::class,
fn (DispatchPostTriggerAutomationsJob $job) => $job->triggerType === TriggerType::PostScheduled,
);
});
it('does not dispatch for status changes other than published or scheduled', function () {
$post = Post::factory()->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Publishing]);
Bus::assertNotDispatched(DispatchPostTriggerAutomationsJob::class);
});
it('does not dispatch when the status did not change', function () {
$post = Post::factory()->create(['status' => PostStatus::Draft]);
$post->update(['content' => 'edited body']);
Bus::assertNotDispatched(DispatchPostTriggerAutomationsJob::class);
});
it('creates a run and advances to the next node for a matching active automation', function () {
$workspace = Workspace::factory()->create();
$automation = Automation::factory()->active()->for($workspace)->create([
'nodes' => [
@ -22,8 +60,9 @@
'connections' => [['id' => 'e1', 'source' => 'trigger_1', 'target' => 'end_1']],
]);
$post = Post::factory()->for($workspace)->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Published]);
$post = Post::factory()->for($workspace)->create(['status' => PostStatus::Published]);
app(DispatchPostTriggerAutomations::class)($post, TriggerType::PostPublished);
$runs = AutomationRun::where('automation_id', $automation->id)->get();
expect($runs)->toHaveCount(1);
@ -33,23 +72,7 @@
Bus::assertDispatched(ProcessAutomationNode::class);
});
it('dispatches when a post becomes scheduled', function () {
$workspace = Workspace::factory()->create();
$automation = Automation::factory()->active()->for($workspace)->create([
'nodes' => [
['id' => 'trigger_1', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], 'data' => ['trigger_type' => 'post_scheduled']],
['id' => 'end_1', 'type' => 'end', 'position' => ['x' => 200, 'y' => 0], 'data' => []],
],
'connections' => [['id' => 'e1', 'source' => 'trigger_1', 'target' => 'end_1']],
]);
$post = Post::factory()->for($workspace)->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Scheduled]);
expect(AutomationRun::where('automation_id', $automation->id)->count())->toBe(1);
});
it('does not dispatch for automations in a different workspace', function () {
it('does not create a run for automations in a different workspace', function () {
$workspaceA = Workspace::factory()->create();
$workspaceB = Workspace::factory()->create();
@ -61,28 +84,13 @@
'connections' => [['id' => 'e1', 'source' => 'trigger_1', 'target' => 'end_1']],
]);
$post = Post::factory()->for($workspaceB)->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Published]);
$post = Post::factory()->for($workspaceB)->create(['status' => PostStatus::Published]);
app(DispatchPostTriggerAutomations::class)($post, TriggerType::PostPublished);
expect(AutomationRun::where('automation_id', $automation->id)->count())->toBe(0);
});
it('does not dispatch when status change is not to Published or Scheduled', function () {
$workspace = Workspace::factory()->create();
Automation::factory()->active()->for($workspace)->create([
'nodes' => [
['id' => 'trigger_1', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], 'data' => ['trigger_type' => 'post_published']],
['id' => 'end_1', 'type' => 'end', 'position' => ['x' => 200, 'y' => 0], 'data' => []],
],
'connections' => [['id' => 'e1', 'source' => 'trigger_1', 'target' => 'end_1']],
]);
$post = Post::factory()->for($workspace)->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Publishing]);
expect(AutomationRun::count())->toBe(0);
});
it('skips paused automations', function () {
$workspace = Workspace::factory()->create();
Automation::factory()->paused()->for($workspace)->create([
@ -93,8 +101,9 @@
'connections' => [['id' => 'e1', 'source' => 'trigger_1', 'target' => 'end_1']],
]);
$post = Post::factory()->for($workspace)->create(['status' => PostStatus::Draft]);
$post->update(['status' => PostStatus::Published]);
$post = Post::factory()->for($workspace)->create(['status' => PostStatus::Published]);
app(DispatchPostTriggerAutomations::class)($post, TriggerType::PostPublished);
expect(AutomationRun::count())->toBe(0);
});