Move automation reads and delete into actions

This commit is contained in:
Paulo Castellano 2026-06-10 18:10:13 -03:00
parent ab7c7c8bdf
commit af4d83190e
9 changed files with 230 additions and 53 deletions

View file

@ -11,12 +11,12 @@
class CreateAutomation
{
public function __invoke(Workspace $workspace, User $user, string $name): Automation
public function __invoke(Workspace $workspace, User $user, ?string $name = null): Automation
{
return Automation::create([
'workspace_id' => $workspace->id,
'user_id' => $user->id,
'name' => $name,
'name' => $name ?: __('automations.default_name'),
'status' => Status::Draft,
'nodes' => [],
'connections' => [],

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Actions\Automation\Automation;
use App\Models\Automation;
class DeleteAutomation
{
public function __invoke(Automation $automation): void
{
$automation->delete();
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Actions\Automation\Automation;
use App\Models\Automation;
use App\Models\AutomationRun;
use App\Models\AutomationTriggerItem;
use Illuminate\Database\Eloquent\Collection;
class GetAutomationDetails
{
/**
* @return array{
* runs: Collection<int, AutomationRun>,
* triggerItems: Collection<int, AutomationTriggerItem>,
* }
*/
public function __invoke(Automation $automation): array
{
return [
'runs' => $automation->runs()->excludingDryRuns()->latest()->take(50)->get(),
'triggerItems' => $automation->triggerItems()->with('run')->latest()->take(50)->get(),
];
}
}

View file

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Actions\Automation\Automation;
use App\Enums\SocialAccount\Platform;
use App\Models\Automation;
use App\Models\SocialAccount;
use App\Services\Social\PinterestPublisher;
use App\Services\Social\TikTokCreatorInfo;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
class GetAutomationEditorData
{
public function __construct(
private PinterestPublisher $pinterestPublisher,
private TikTokCreatorInfo $tikTokCreatorInfo,
) {}
/**
* @return array{
* socialAccounts: Collection<int, SocialAccount>,
* pinterestBoards: SupportCollection<string, array<int, mixed>>,
* tiktokCreatorInfos: SupportCollection<string, mixed>,
* }
*/
public function __invoke(Automation $automation): array
{
$socialAccounts = $automation->workspace->socialAccounts()->active()->get();
$pinterestBoards = $socialAccounts
->where('platform', Platform::Pinterest)
->mapWithKeys(fn ($account) => [
$account->id => rescue(
fn () => $this->pinterestPublisher->getBoards($account),
[],
report: false,
),
]);
$tiktokCreatorInfos = $socialAccounts
->where('platform', Platform::TikTok)
->mapWithKeys(fn ($account) => [
$account->id => rescue(
fn () => $this->tikTokCreatorInfo->fetch($account),
null,
report: false,
),
])
->filter();
return [
'socialAccounts' => $socialAccounts,
'pinterestBoards' => $pinterestBoards,
'tiktokCreatorInfos' => $tiktokCreatorInfos,
];
}
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Actions\Automation\Automation;
use App\Models\Automation;
use App\Models\Workspace;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class ListAutomations
{
public function __invoke(Workspace $workspace, ?int $perPage = null): LengthAwarePaginator
{
return Automation::query()
->where('workspace_id', $workspace->id)
->orderByDesc('created_at')
->paginate($perPage ?? (int) config('app.pagination.default'));
}
}

View file

@ -6,11 +6,14 @@
use App\Actions\Automation\Automation\ActivateAutomation;
use App\Actions\Automation\Automation\CreateAutomation;
use App\Actions\Automation\Automation\DeleteAutomation;
use App\Actions\Automation\Automation\GetAutomationDetails;
use App\Actions\Automation\Automation\GetAutomationEditorData;
use App\Actions\Automation\Automation\ListAutomations;
use App\Actions\Automation\Automation\PauseAutomation;
use App\Actions\Automation\Automation\UpdateAutomation;
use App\Actions\Automation\Run\RetryRunFromNode;
use App\Actions\Automation\Run\TestAutomation;
use App\Enums\SocialAccount\Platform;
use App\Http\Controllers\Controller;
use App\Http\Requests\App\Automations\ActivateAutomationRequest;
use App\Http\Requests\App\Automations\PauseAutomationRequest;
@ -26,8 +29,6 @@
use App\Http\Resources\AutomationTriggerItemResource;
use App\Models\Automation;
use App\Models\AutomationRun;
use App\Services\Social\PinterestPublisher;
use App\Services\Social\TikTokCreatorInfo;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
@ -35,13 +36,12 @@
class AutomationController extends Controller
{
public function index(): Response
public function index(ListAutomations $list): Response
{
$workspace = request()->user()->currentWorkspace;
$automations = Inertia::scroll(fn () => AutomationResource::collection(
Automation::query()
->where('workspace_id', request()->user()->current_workspace_id)
->orderByDesc('created_at')
->paginate(config('app.pagination.default'))
$list($workspace)
));
return Inertia::render('automations/Index', [
@ -51,52 +51,24 @@ public function index(): Response
public function store(StoreAutomationRequest $request, CreateAutomation $create): RedirectResponse
{
$name = $request->validated('name');
if (! $name || $name === 'automations.default_name') {
$name = __('automations.default_name');
}
$automation = $create(
$request->user()->currentWorkspace,
$request->user(),
$name,
);
return redirect()->route('app.automations.edit', $automation->id);
}
public function edit(Automation $automation): Response
public function edit(Automation $automation, GetAutomationEditorData $editorData): Response
{
$this->authorize('update', $automation);
$socialAccounts = $automation->workspace->socialAccounts()->active()->get();
['socialAccounts' => $socialAccounts, 'pinterestBoards' => $pinterestBoards, 'tiktokCreatorInfos' => $tiktokCreatorInfos] = $editorData($automation);
$platformConfigs = $socialAccounts->mapWithKeys(fn ($account) => [
$account->id => new PlatformConfigResource($account),
]);
$pinterestBoards = $socialAccounts
->where('platform', Platform::Pinterest)
->mapWithKeys(fn ($account) => [
$account->id => rescue(
fn () => app(PinterestPublisher::class)->getBoards($account),
[],
report: false,
),
]);
$tiktokCreatorInfos = $socialAccounts
->where('platform', Platform::TikTok)
->mapWithKeys(fn ($account) => [
$account->id => rescue(
fn () => app(TikTokCreatorInfo::class)->fetch($account),
null,
report: false,
),
])
->filter();
return Inertia::render('automations/Form', [
'automation' => AutomationResource::make($automation),
'socialAccounts' => SocialAccountResource::collection($socialAccounts),
@ -106,16 +78,16 @@ public function edit(Automation $automation): Response
]);
}
public function show(Automation $automation): Response
public function show(Automation $automation, GetAutomationDetails $details): Response
{
$this->authorize('view', $automation);
['runs' => $runs, 'triggerItems' => $triggerItems] = $details($automation);
return Inertia::render('automations/Show', [
'automation' => AutomationResource::make($automation),
'runs' => AutomationRunResource::collection($automation->runs()->excludingDryRuns()->latest()->take(50)->get()),
'triggerItems' => AutomationTriggerItemResource::collection(
$automation->triggerItems()->with('run')->latest()->take(50)->get()
),
'runs' => AutomationRunResource::collection($runs),
'triggerItems' => AutomationTriggerItemResource::collection($triggerItems),
]);
}
@ -128,10 +100,10 @@ public function update(UpdateAutomationRequest $request, Automation $automation,
return back();
}
public function destroy(Automation $automation): RedirectResponse
public function destroy(Automation $automation, DeleteAutomation $delete): RedirectResponse
{
$this->authorize('delete', $automation);
$automation->delete();
$delete($automation);
session()->flash('flash.banner', __('automations.flash.deleted'));
session()->flash('flash.bannerStyle', 'success');

View file

@ -18,8 +18,6 @@ public function authorize(): bool
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:120'],
];
return [];
}
}

View file

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
use App\Actions\Automation\Automation\DeleteAutomation;
use App\Actions\Automation\Automation\GetAutomationDetails;
use App\Actions\Automation\Automation\GetAutomationEditorData;
use App\Actions\Automation\Automation\ListAutomations;
use App\Enums\SocialAccount\Platform;
use App\Models\Automation;
use App\Models\AutomationRun;
use App\Models\AutomationTriggerItem;
use App\Models\SocialAccount;
use App\Models\Workspace;
use App\Services\Social\PinterestPublisher;
use App\Services\Social\TikTokCreatorInfo;
it('lists only the workspace automations, newest first', function () {
$workspace = Workspace::factory()->create();
$other = Workspace::factory()->create();
$older = Automation::factory()->for($workspace)->create(['created_at' => now()->subDay()]);
$newer = Automation::factory()->for($workspace)->create(['created_at' => now()]);
Automation::factory()->for($other)->create();
$result = app(ListAutomations::class)($workspace);
expect($result->total())->toBe(2);
expect($result->items()[0]->id)->toBe($newer->id);
expect($result->items()[1]->id)->toBe($older->id);
});
it('deletes the automation', function () {
$automation = Automation::factory()->create();
app(DeleteAutomation::class)($automation);
expect(Automation::find($automation->id))->toBeNull();
});
it('returns non-dry runs and trigger items, newest first', function () {
$automation = Automation::factory()->create();
$real = AutomationRun::factory()->for($automation)->create();
AutomationRun::factory()->for($automation)->create(['is_dry_run' => true]);
$item = AutomationTriggerItem::factory()->for($automation)->create();
$result = app(GetAutomationDetails::class)($automation);
expect($result['runs'])->toHaveCount(1);
expect($result['runs']->first()->id)->toBe($real->id);
expect($result['triggerItems'])->toHaveCount(1);
expect($result['triggerItems']->first()->id)->toBe($item->id);
});
it('returns only active social accounts for the automation workspace', function () {
$this->mock(PinterestPublisher::class);
$this->mock(TikTokCreatorInfo::class);
$workspace = Workspace::factory()->create();
$automation = Automation::factory()->for($workspace)->create();
$active = SocialAccount::factory()->for($workspace)->create(['platform' => 'instagram', 'is_active' => true]);
SocialAccount::factory()->for($workspace)->create(['platform' => 'instagram', 'is_active' => false]);
$result = app(GetAutomationEditorData::class)($automation);
expect($result['socialAccounts'])->toHaveCount(1);
expect($result['socialAccounts']->first()->id)->toBe($active->id);
expect($result['pinterestBoards'])->toBeEmpty();
expect($result['tiktokCreatorInfos'])->toBeEmpty();
});
it('maps pinterest boards for pinterest accounts', function () {
$this->mock(PinterestPublisher::class, fn ($mock) => $mock->shouldReceive('getBoards')->andReturn([['id' => 'b1']]));
$this->mock(TikTokCreatorInfo::class);
$workspace = Workspace::factory()->create();
$automation = Automation::factory()->for($workspace)->create();
$pinterest = SocialAccount::factory()->for($workspace)->create(['platform' => Platform::Pinterest->value, 'is_active' => true]);
$result = app(GetAutomationEditorData::class)($automation);
expect($result['pinterestBoards']->get($pinterest->id))->toBe([['id' => 'b1']]);
});

View file

@ -16,12 +16,14 @@
$this->user->refresh();
});
it('creates an automation via POST', function () {
it('creates an automation with the default name (web sends no name)', function () {
$response = $this->actingAs($this->user)
->post(route('app.automations.store'), ['name' => 'My RSS auto']);
->post(route('app.automations.store'));
$response->assertRedirect();
expect(Automation::where('workspace_id', $this->workspace->id)->count())->toBe(1);
$automation = Automation::where('workspace_id', $this->workspace->id)->sole();
expect($automation->name)->toBe(__('automations.default_name'));
});
it('updates nodes and connections via PUT', function () {