175 lines
7.3 KiB
PHP
175 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Services;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Illuminate\Support\Str;
|
|
use Modules\Helpdesk\Models\Ticket;
|
|
use Modules\Helpdesk\Models\WorkflowLog;
|
|
use Modules\Helpdesk\Models\WorkflowTemplate;
|
|
use Throwable;
|
|
|
|
final class WorkflowNotificationService
|
|
{
|
|
/**
|
|
* Send all active workflow messages for the supplied ticket event.
|
|
*/
|
|
public function dispatch(Ticket $ticket, string $event, ?User $actor = null): void
|
|
{
|
|
$this->hydrateTicketUsers($ticket);
|
|
$ticket->loadMissing(['status', 'priority', 'category', 'project', 'team', 'product']);
|
|
$actor?->loadMissing('company');
|
|
|
|
WorkflowTemplate::query()
|
|
->where('event', $event)
|
|
->where('is_active', true)
|
|
->get()
|
|
->each(function (WorkflowTemplate $template) use ($ticket, $event, $actor): void {
|
|
$recipient = $this->recipient($template->to_role, $ticket, $actor);
|
|
|
|
if ($recipient === null || blank($recipient->email ?? null)) {
|
|
$this->log($template, $ticket, $event, null, 'skipped', 'No recipient email was available.');
|
|
|
|
return;
|
|
}
|
|
|
|
$trackingToken = Str::random(48);
|
|
$subject = $this->renderText($template->subject, $ticket, $actor);
|
|
$html = $this->wrapHtml($this->renderText($template->html_body, $ticket, $actor), $ticket, $trackingToken);
|
|
$text = $this->renderText($template->text_body ?: strip_tags($template->html_body), $ticket, $actor);
|
|
|
|
try {
|
|
Mail::html($html, function ($message) use ($recipient, $template, $subject): void {
|
|
$message->to($recipient->email, $recipient->name ?? null)->subject($subject);
|
|
|
|
if (filled($template->cc)) {
|
|
$message->cc(array_map('trim', explode(',', (string) $template->cc)));
|
|
}
|
|
|
|
if (filled($template->bcc)) {
|
|
$message->bcc(array_map('trim', explode(',', (string) $template->bcc)));
|
|
}
|
|
});
|
|
|
|
$this->log($template, $ticket, $event, (string) $recipient->email, 'sent', null, $subject, $trackingToken);
|
|
} catch (Throwable $exception) {
|
|
Log::warning('Helpdesk workflow email failed.', ['event' => $event, 'ticket_id' => $ticket->id, 'error' => $exception->getMessage()]);
|
|
$this->log($template, $ticket, $event, (string) $recipient->email, 'failed', $exception->getMessage(), $subject);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load requester/agent with one query if the caller has not already attached them.
|
|
*/
|
|
private function hydrateTicketUsers(Ticket $ticket): void
|
|
{
|
|
if ($ticket->relationLoaded('owner') && $ticket->relationLoaded('agent')) {
|
|
return;
|
|
}
|
|
|
|
$userIds = collect([$ticket->user_id, $ticket->agent_id])
|
|
->filter()
|
|
->map(static fn (mixed $id): int => (int) $id)
|
|
->unique()
|
|
->values();
|
|
|
|
if ($userIds->isEmpty()) {
|
|
$ticket->setRelation('user', null);
|
|
$ticket->setRelation('owner', null);
|
|
$ticket->setRelation('agent', null);
|
|
|
|
return;
|
|
}
|
|
|
|
$users = User::query()
|
|
->with('company')
|
|
->whereIn('id', $userIds)
|
|
->get()
|
|
->keyBy('id');
|
|
|
|
$owner = $users->get((int) $ticket->user_id);
|
|
$agent = $ticket->agent_id === null ? null : $users->get((int) $ticket->agent_id);
|
|
|
|
$ticket->setRelation('user', $owner);
|
|
$ticket->setRelation('owner', $owner);
|
|
$ticket->setRelation('agent', $agent);
|
|
}
|
|
|
|
private function recipient(string $role, Ticket $ticket, ?User $actor): ?User
|
|
{
|
|
return match ($role) {
|
|
'agent' => $ticket->agent,
|
|
'actor' => $actor,
|
|
default => $ticket->owner,
|
|
};
|
|
}
|
|
|
|
private function renderText(string $template, Ticket $ticket, ?User $actor): string
|
|
{
|
|
$tokens = [
|
|
'{{ ticket.reference }}' => $ticket->reference,
|
|
'{{ ticket.subject }}' => $ticket->subject,
|
|
'{{ ticket.status }}' => (string) ($ticket->status?->name ?? ''),
|
|
'{{ ticket.priority }}' => (string) ($ticket->priority?->name ?? ''),
|
|
'{{ ticket.category }}' => (string) ($ticket->category?->name ?? ''),
|
|
'{{ ticket.project }}' => (string) ($ticket->project?->title ?? ''),
|
|
'{{ ticket.team }}' => (string) ($ticket->team?->name ?? ''),
|
|
'{{ requester.name }}' => (string) ($ticket->owner?->name ?? ''),
|
|
'{{ agent.name }}' => (string) ($ticket->agent?->name ?? ''),
|
|
'{{ actor.name }}' => (string) ($actor?->name ?? ''),
|
|
'{{ ticket.url }}' => route('helpdesk.tickets.show', ['ticket' => $ticket->getKey()]),
|
|
];
|
|
|
|
return strtr($template, $tokens);
|
|
}
|
|
|
|
private function wrapHtml(string $body, Ticket $ticket, ?string $trackingToken = null): string
|
|
{
|
|
return '<div style="font-family:Inter,Arial,sans-serif;background:#f6f8fb;padding:24px">'
|
|
. '<div style="max-width:720px;margin:0 auto;background:#ffffff;border:1px solid #d8dee8;border-radius:18px;overflow:hidden">'
|
|
. '<div style="background:#0f172a;color:#ffffff;padding:24px 28px">'
|
|
. '<div style="font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:#bfdbfe">Helpdesk notification</div>'
|
|
. '<h1 style="margin:8px 0 0;font-size:22px">' . e($ticket->reference) . '</h1>'
|
|
. '<p style="margin:8px 0 0;color:#dbeafe">' . e($ticket->subject) . '</p>'
|
|
. '</div><div style="padding:28px;color:#1f2937;line-height:1.65">'
|
|
. $body
|
|
. '<p style="margin-top:28px"><a href="' . e(route('helpdesk.tickets.show', ['ticket' => $ticket->getKey()])) . '" style="display:inline-block;background:#2563eb;color:#ffffff;text-decoration:none;padding:11px 16px;border-radius:10px">Open ticket</a></p>'
|
|
. $this->trackingPixel($trackingToken)
|
|
. '</div></div></div>';
|
|
}
|
|
|
|
private function trackingPixel(?string $trackingToken): string
|
|
{
|
|
if ($trackingToken === null) {
|
|
return '';
|
|
}
|
|
|
|
$url = Route::has('helpdesk.workflow.open')
|
|
? route('helpdesk.workflow.open', ['token' => $trackingToken])
|
|
: URL::to('/' . trim((string) config('helpdesk.route_prefix', 'helpdesk'), '/') . '/workflow/open/' . $trackingToken);
|
|
|
|
return '<img src="' . e($url) . '" width="1" height="1" alt="" style="display:none" />';
|
|
}
|
|
|
|
private function log(WorkflowTemplate $template, Ticket $ticket, string $event, ?string $recipient, string $status, ?string $error = null, ?string $subject = null, ?string $trackingToken = null): void
|
|
{
|
|
WorkflowLog::query()->create([
|
|
'template_id' => $template->id,
|
|
'ticket_id' => $ticket->id,
|
|
'event' => $event,
|
|
'recipient' => $recipient,
|
|
'subject' => $subject,
|
|
'status' => $status,
|
|
'tracking_token' => $trackingToken,
|
|
'error' => $error,
|
|
]);
|
|
}
|
|
}
|