79 خطوط
3.7 KiB
PHP
79 خطوط
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Deal;
|
|
use App\Models\FollowUp;
|
|
use App\Models\Lead;
|
|
use App\Models\Notification;
|
|
use App\Models\NotificationPreference;
|
|
use App\Models\SlaBreach;
|
|
use App\Models\SlaRule;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
class SlaMonitorService
|
|
{
|
|
public function __construct(private AutomationDispatcher $automations) {}
|
|
|
|
public function run(): int
|
|
{
|
|
$created = 0;
|
|
foreach (SlaRule::where('is_active', true)->where('event', '!=', 'stale_deal')->get() as $rule) {
|
|
foreach ($this->candidates($rule) as [$subject, $baseAt, $assignedTo]) {
|
|
$dueAt = $baseAt->copy()->addMinutes($rule->breach_minutes);
|
|
$status = $dueAt->isPast() ? 'breached' : 'warning';
|
|
$eventKey = "sla:{$rule->id}:".strtolower(class_basename($subject)).":{$subject->getKey()}";
|
|
$breach = SlaBreach::firstOrCreate(['event_key' => $eventKey], [
|
|
'sla_rule_id' => $rule->id, 'breachable_type' => $subject::class, 'breachable_id' => $subject->getKey(),
|
|
'assigned_to' => $assignedTo, 'status' => $status, 'due_at' => $dueAt,
|
|
'warned_at' => now(), 'breached_at' => $status === 'breached' ? now() : null,
|
|
'details' => ['event' => $rule->event],
|
|
]);
|
|
if (! $breach->wasRecentlyCreated) {
|
|
continue;
|
|
}
|
|
$created++;
|
|
$this->notify($breach, $rule->name);
|
|
$this->automations->dispatch('sla_breached', $subject, "sla:{$breach->id}", ['sla_breach_id' => $breach->id, 'status' => $status, 'rule_id' => $rule->id]);
|
|
}
|
|
}
|
|
|
|
return $created;
|
|
}
|
|
|
|
/** @return iterable<array{Model, Carbon, int|null}> */
|
|
private function candidates(SlaRule $rule): iterable
|
|
{
|
|
if ($rule->event === 'first_contact') {
|
|
foreach (Lead::whereDoesntHave('calls')->where('created_at', '<=', now()->subMinutes($rule->warning_minutes))->limit(500)->get() as $lead) {
|
|
yield [$lead, $lead->created_at, $lead->assigned_to];
|
|
}
|
|
} elseif ($rule->event === 'follow_up') {
|
|
foreach (FollowUp::where('status', 'pending')->where('scheduled_at', '<=', now()->subMinutes($rule->warning_minutes))->limit(500)->get() as $followUp) {
|
|
yield [$followUp, $followUp->scheduled_at, $followUp->user_id];
|
|
}
|
|
} elseif ($rule->event === 'stale_deal') {
|
|
foreach (Deal::whereNotIn('status', ['won', 'lost'])->where(fn ($q) => $q->where('last_activity_at', '<=', now()->subMinutes($rule->warning_minutes))->orWhere(fn ($empty) => $empty->whereNull('last_activity_at')->where('created_at', '<=', now()->subMinutes($rule->warning_minutes))))->limit(500)->get() as $deal) {
|
|
yield [$deal, $deal->last_activity_at ?? $deal->created_at, $deal->owner_id];
|
|
}
|
|
}
|
|
}
|
|
|
|
private function notify(SlaBreach $breach, string $ruleName): void
|
|
{
|
|
if (! $breach->assigned_to) {
|
|
return;
|
|
}
|
|
$preference = NotificationPreference::where('user_id', $breach->assigned_to)->where('notification_type', 'sla')->first();
|
|
if ($preference && (! $preference->in_app_enabled || $preference->is_muted)) {
|
|
return;
|
|
}
|
|
Notification::firstOrCreate(['idempotency_key' => "sla-breach:{$breach->id}"], [
|
|
'user_id' => $breach->assigned_to, 'title' => $breach->status === 'breached' ? 'نقض SLA' : 'هشدار SLA',
|
|
'message' => "مهلت قانون «{$ruleName}» نیازمند اقدام است.", 'type' => 'sla',
|
|
'data' => ['url' => '/operations', 'sla_breach_id' => $breach->id],
|
|
]);
|
|
}
|
|
}
|