Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
EvaluateAnalyticsAlertsAction
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 4
110
0.00% covered (danger)
0.00%
0 / 1
 execute
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 evaluateSignaturePending
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
20
 evaluateOpportunityInactive
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
20
 alertExists
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Analytics\Application\Actions;
6
7use Illuminate\Support\Facades\Event;
8use ImovelZapAi\Analytics\Domain\Enums\AlertSeverity;
9use ImovelZapAi\Analytics\Domain\Enums\AlertStatus;
10use ImovelZapAi\Analytics\Domain\Enums\AnalyticsEventKey;
11use ImovelZapAi\Analytics\Domain\Events\AnalyticsAlertTriggered;
12use ImovelZapAi\Analytics\Infrastructure\Persistence\Eloquent\AnalyticsAlertModel;
13use ImovelZapAi\Analytics\Infrastructure\Persistence\Eloquent\AnalyticsFactModel;
14
15final class EvaluateAnalyticsAlertsAction
16{
17    public function execute(string $tenantId): int
18    {
19        $created = 0;
20        $created += $this->evaluateSignaturePending($tenantId);
21        $created += $this->evaluateOpportunityInactive($tenantId);
22
23        return $created;
24    }
25
26    private function evaluateSignaturePending(string $tenantId): int
27    {
28        $thresholdDays = (int) config('analytics.signature_pending_days', 3);
29        $cutoff = now()->subDays($thresholdDays);
30        $created = 0;
31
32        $requested = AnalyticsFactModel::query()
33            ->where('tenant_id', $tenantId)
34            ->where('event_key', AnalyticsEventKey::SignatureRequested->value)
35            ->where('occurred_at', '<=', $cutoff)
36            ->where('entity_type', 'signature_request')
37            ->get();
38
39        foreach ($requested as $fact) {
40            $completed = AnalyticsFactModel::query()
41                ->where('tenant_id', $tenantId)
42                ->where('event_key', AnalyticsEventKey::SignatureCompleted->value)
43                ->where('entity_type', 'signature_request')
44                ->where('entity_id', $fact->entity_id)
45                ->exists();
46
47            if ($completed) {
48                continue;
49            }
50
51            if ($this->alertExists($tenantId, 'signature.pending', 'signature_request', (string) $fact->entity_id)) {
52                continue;
53            }
54
55            $alert = AnalyticsAlertModel::query()->create([
56                'tenant_id' => $tenantId,
57                'alert_key' => 'signature.pending',
58                'title' => 'Assinatura pendente',
59                'body' => 'Assinatura solicitada há mais de '.$thresholdDays.' dias sem conclusão.',
60                'severity' => AlertSeverity::Warning->value,
61                'status' => AlertStatus::Open->value,
62                'entity_type' => 'signature_request',
63                'entity_id' => $fact->entity_id,
64                'meta' => ['requested_at' => $fact->occurred_at?->toIso8601String()],
65                'triggered_at' => now(),
66            ]);
67
68            Event::dispatch(AnalyticsAlertTriggered::fromModel($alert));
69            $created++;
70        }
71
72        return $created;
73    }
74
75    private function evaluateOpportunityInactive(string $tenantId): int
76    {
77        $thresholdHours = (int) config('analytics.opportunity_inactive_hours', 24);
78        $cutoff = now()->subHours($thresholdHours);
79        $created = 0;
80
81        $deals = AnalyticsFactModel::query()
82            ->where('tenant_id', $tenantId)
83            ->where('event_key', AnalyticsEventKey::DealCreated->value)
84            ->where('occurred_at', '<=', $cutoff)
85            ->where('entity_type', 'deal')
86            ->get();
87
88        foreach ($deals as $fact) {
89            $hasActivity = AnalyticsFactModel::query()
90                ->where('tenant_id', $tenantId)
91                ->where('entity_type', 'deal')
92                ->where('entity_id', $fact->entity_id)
93                ->where('occurred_at', '>', $fact->occurred_at)
94                ->exists();
95
96            if ($hasActivity) {
97                continue;
98            }
99
100            if ($this->alertExists($tenantId, 'opportunity.inactive', 'deal', (string) $fact->entity_id)) {
101                continue;
102            }
103
104            $alert = AnalyticsAlertModel::query()->create([
105                'tenant_id' => $tenantId,
106                'alert_key' => 'opportunity.inactive',
107                'title' => 'Oportunidade sem atividade',
108                'body' => 'Oportunidade sem movimentação há mais de '.$thresholdHours.' horas.',
109                'severity' => AlertSeverity::Info->value,
110                'status' => AlertStatus::Open->value,
111                'entity_type' => 'deal',
112                'entity_id' => $fact->entity_id,
113                'meta' => ['created_at' => $fact->occurred_at?->toIso8601String()],
114                'triggered_at' => now(),
115            ]);
116
117            Event::dispatch(AnalyticsAlertTriggered::fromModel($alert));
118            $created++;
119        }
120
121        return $created;
122    }
123
124    private function alertExists(string $tenantId, string $alertKey, string $entityType, string $entityId): bool
125    {
126        return AnalyticsAlertModel::query()
127            ->where('tenant_id', $tenantId)
128            ->where('alert_key', $alertKey)
129            ->where('entity_type', $entityType)
130            ->where('entity_id', $entityId)
131            ->whereIn('status', [AlertStatus::Open->value, AlertStatus::Acked->value])
132            ->exists();
133    }
134}