CRM/backend/app/Services/SlaMonitorService.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],
]);
}
}