policyFor($ticket); if (! $policy) { return $ticket; } $createdAt = $ticket->created_at ?? now(); $ticket->forceFill([ 'sla_policy_id' => $policy->id, 'sla_first_response_due_at' => $createdAt->copy()->addMinutes($policy->first_response_minutes), 'sla_resolution_due_at' => $createdAt->copy()->addMinutes($policy->resolution_minutes), ])->save(); TicketSlaEvent::query()->create([ 'ticket_id' => $ticket->id, 'sla_policy_id' => $policy->id, 'event' => 'sla.policy_applied', 'due_at' => $ticket->sla_resolution_due_at, 'occurred_at' => now(), 'meta' => ['policy' => $policy->name], ]); return $ticket->fresh() ?? $ticket; } public function recordPublicResponse(Ticket $ticket, User $actor): void { if ($ticket->first_response_at === null && (int) $ticket->user_id !== (int) $actor->id) { $breached = $ticket->sla_first_response_due_at !== null && now()->greaterThan($ticket->sla_first_response_due_at); $ticket->forceFill([ 'first_response_at' => now(), 'sla_first_response_breached_at' => $breached ? now() : $ticket->sla_first_response_breached_at, ])->save(); TicketSlaEvent::query()->create([ 'ticket_id' => $ticket->id, 'sla_policy_id' => $ticket->sla_policy_id, 'event' => 'sla.first_response', 'due_at' => $ticket->sla_first_response_due_at, 'occurred_at' => now(), 'breached' => $breached, ]); } $ticket->increment('public_replies_count'); } public function recordClosure(Ticket $ticket): void { $breached = $ticket->sla_resolution_due_at !== null && now()->greaterThan($ticket->sla_resolution_due_at); $ticket->forceFill(['sla_resolution_breached_at' => $breached ? now() : $ticket->sla_resolution_breached_at])->save(); TicketSlaEvent::query()->create([ 'ticket_id' => $ticket->id, 'sla_policy_id' => $ticket->sla_policy_id, 'event' => 'sla.resolved', 'due_at' => $ticket->sla_resolution_due_at, 'occurred_at' => now(), 'breached' => $breached, ]); } public function refreshBreaches(Ticket $ticket): void { $updates = []; if ($ticket->first_response_at === null && $ticket->sla_first_response_due_at !== null && now()->greaterThan($ticket->sla_first_response_due_at)) { $updates['sla_first_response_breached_at'] = $ticket->sla_first_response_breached_at ?? now(); } if ($ticket->completed_at === null && $ticket->sla_resolution_due_at !== null && now()->greaterThan($ticket->sla_resolution_due_at)) { $updates['sla_resolution_breached_at'] = $ticket->sla_resolution_breached_at ?? now(); } if ($updates !== []) { $ticket->forceFill($updates)->save(); } } private function policyFor(Ticket $ticket): ?SlaPolicy { return SlaPolicy::query() ->where('is_active', true) ->where(static function ($query) use ($ticket): void { $query->whereNull('company_id')->orWhere('company_id', $ticket->company_id); }) ->orderByRaw('CASE WHEN priority_id = ? THEN 0 WHEN priority_id IS NULL THEN 1 ELSE 2 END', [$ticket->priority_id]) ->orderByRaw('CASE WHEN category_id = ? THEN 0 WHEN category_id IS NULL THEN 1 ELSE 2 END', [$ticket->category_id]) ->orderByDesc('is_default') ->first(); } }