Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
RunWorkflowActionsJob
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 3
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handle
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
132
 resolveActor
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Automation\Infrastructure\Jobs;
6
7use App\Models\User;
8use Illuminate\Bus\Queueable;
9use Illuminate\Contracts\Queue\ShouldQueue;
10use Illuminate\Foundation\Bus\Dispatchable;
11use Illuminate\Queue\InteractsWithQueue;
12use Illuminate\Queue\SerializesModels;
13use ImovelZapAi\Automation\Application\Actions\DispatchWorkflowsForEventAction;
14use ImovelZapAi\Automation\Application\Services\ActionRunner;
15use ImovelZapAi\Automation\Domain\Enums\ExecutionStatus;
16use ImovelZapAi\Automation\Domain\Enums\LogLevel;
17use ImovelZapAi\Automation\Infrastructure\Persistence\Eloquent\AutomationExecutionModel;
18use ImovelZapAi\Automation\Infrastructure\Persistence\Eloquent\AutomationLogModel;
19use ImovelZapAi\Tenancy\Support\TenantContext;
20
21/**
22 * Executa ações de um workflow. O primeiro disparo é síncrono (dispatchSync)
23 * para não depender de worker; atrasos reais só ocorrem se honor_action_delays=true.
24 */
25final class RunWorkflowActionsJob implements ShouldQueue
26{
27    use Dispatchable;
28    use InteractsWithQueue;
29    use Queueable;
30    use SerializesModels;
31
32    public function __construct(
33        public readonly string $executionId,
34        public readonly int $actionIndex = 0,
35        public readonly bool $delayApplied = false,
36    ) {}
37
38    public function handle(ActionRunner $runner): void
39    {
40        $execution = AutomationExecutionModel::query()
41            ->with(['workflow.actions'])
42            ->find($this->executionId);
43
44        if ($execution === null || $execution->status === ExecutionStatus::Skipped) {
45            return;
46        }
47
48        TenantContext::set((string) $execution->tenant_id);
49
50        $actions = $execution->workflow?->actions?->sortBy('sort_order')->values() ?? collect();
51
52        if ($this->actionIndex >= $actions->count()) {
53            DispatchWorkflowsForEventAction::markExecutionFinished($execution);
54
55            return;
56        }
57
58        $action = $actions[$this->actionIndex];
59        $context = $execution->context ?? [];
60
61        if ($action->delay_seconds > 0 && ! $this->delayApplied) {
62            $honorDelay = (bool) config('automation.honor_action_delays', false)
63                && ! app()->environment('testing')
64                && config('queue.default') !== 'sync';
65
66            if ($honorDelay) {
67                self::dispatch($this->executionId, $this->actionIndex, true)
68                    ->delay(now()->addSeconds((int) $action->delay_seconds));
69
70                AutomationLogModel::query()->create([
71                    'tenant_id' => $execution->tenant_id,
72                    'execution_id' => $execution->id,
73                    'level' => LogLevel::Info,
74                    'message' => "Ação agendada para daqui a {$action->delay_seconds}s.",
75                    'payload' => ['action_index' => $this->actionIndex],
76                    'created_at' => now(),
77                ]);
78
79                return;
80            }
81
82            AutomationLogModel::query()->create([
83                'tenant_id' => $execution->tenant_id,
84                'execution_id' => $execution->id,
85                'level' => LogLevel::Info,
86                'message' => "Atraso de {$action->delay_seconds}s registrado (execução imediata sem worker de fila).",
87                'payload' => ['action_index' => $this->actionIndex, 'delay_seconds' => $action->delay_seconds],
88                'created_at' => now(),
89            ]);
90        }
91
92        $actor = $this->resolveActor($execution, $context);
93
94        try {
95            $runner->run($execution, $action, $context, $actor);
96        } catch (\Throwable $e) {
97            AutomationLogModel::query()->create([
98                'tenant_id' => $execution->tenant_id,
99                'execution_id' => $execution->id,
100                'level' => LogLevel::Error,
101                'message' => 'Falha na ação: '.$e->getMessage(),
102                'payload' => ['action_index' => $this->actionIndex],
103                'created_at' => now(),
104            ]);
105
106            DispatchWorkflowsForEventAction::markExecutionFinished($execution, true, $e->getMessage());
107
108            return;
109        }
110
111        $nextIndex = $this->actionIndex + 1;
112
113        if ($nextIndex < $actions->count()) {
114            self::dispatchSync($this->executionId, $nextIndex);
115
116            return;
117        }
118
119        DispatchWorkflowsForEventAction::markExecutionFinished($execution);
120    }
121
122    /** @param  array<string, mixed>  $context */
123    private function resolveActor(AutomationExecutionModel $execution, array $context): ?User
124    {
125        $userId = $context['actor_user_id'] ?? $context['created_by'] ?? $context['userId'] ?? null;
126
127        if ($userId !== null) {
128            $user = User::query()->find((int) $userId);
129            if ($user !== null) {
130                return $user;
131            }
132        }
133
134        return User::query()
135            ->where('tenant_id', $execution->tenant_id)
136            ->orderBy('id')
137            ->first();
138    }
139}