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 */ 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], ]); } }