- Triggers (Admin > Wyzwalacze): event-driven rules that fire immediately on a ticket lifecycle event (created/updated/status/priority/assignee/team/ category changed, new reply), with AND-conditions and ordered actions (set status/priority/team/assignee, send e-mail). Ships its own dedicated, freely add/edit/delete-able e-mail templates, kept separate from the fixed system templates. - Ticket watching: operators can star/"Obserwuj" any ticket to follow it regardless of assignment/team. - Real-time notification bell (private per-user broadcast channel, 30s fallback poll) with an opt-in in-tab browser push notification. - Per-user notification preferences (/settings/notifications): scope (mine/unassigned/watched/all) and e-mail toggle per event category. - Admin > Integracje: new tab for LDAP/AD + BookStack config, split out of Konfiguracja. - Operator queue: Podkategoria/Zespół/Utworzono columns (off by default). - Obserwuj button moved next to the auto-refresh countdown; trigger condition builder shows subcategory/zgłaszający as name dropdowns instead of raw IDs; /settings/notifications got a back link, full-width push card, and a bordered table container; admin panel tab and operator queue view now persist across a plain page refresh. - Docs: README/ARCHITECTURE/wiki updated for all of the above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
3.9 KiB
PHP
96 lines
3.9 KiB
PHP
<?php
|
|
|
|
use App\Models\NotificationPreference;
|
|
use App\Models\NotificationSetting;
|
|
use App\Notifications\TicketNotification;
|
|
use App\Services\TicketService;
|
|
use Illuminate\Support\Facades\Notification;
|
|
|
|
test('an assignee with scope_mine enabled for escalations is not double-notified on top of the direct sla_breached send', function () {
|
|
Notification::fake();
|
|
$this->seed();
|
|
NotificationSetting::query()->where('trigger_key', 'sla_breached')->update(['enabled' => true]);
|
|
|
|
$ticket = makeTicket();
|
|
$assignee = operatorUser('assignee-sla@example.com');
|
|
$ticket->update(['assignee_id' => $assignee->id]);
|
|
|
|
app(TicketService::class)->notify($ticket->fresh(), 'sla_breached');
|
|
|
|
// NotificationPreference::DEFAULTS['escalation'] has scope_mine=true, so
|
|
// without the notify()->notifyStaffForCategory() dedup, the assignee
|
|
// would receive this twice: once as the fixed NotificationSetting
|
|
// recipient, once again from the scope_mine fan-out.
|
|
Notification::assertSentToTimes($assignee, TicketNotification::class, 1);
|
|
});
|
|
|
|
test('a staff member with the e-mail column off for an event still gets the bell but not a mail', function () {
|
|
Notification::fake();
|
|
$this->seed();
|
|
|
|
$operator = operatorUser('bell-only@example.com');
|
|
NotificationPreference::query()->create(array_merge(
|
|
['user_id' => $operator->id, 'event_category' => 'ticket_update'],
|
|
array_merge(NotificationPreference::DEFAULTS['ticket_update'], ['scope_all' => true, 'email' => false])
|
|
));
|
|
|
|
NotificationSetting::query()->where('trigger_key', 'priority_changed')->update(['enabled' => true]);
|
|
|
|
$ticket = makeTicket();
|
|
app(TicketService::class)->setPriority($ticket, 'high');
|
|
|
|
Notification::assertSentTo($operator, TicketNotification::class, function ($notification, $channels) {
|
|
return $channels === ['database'];
|
|
});
|
|
});
|
|
|
|
test('a staff member with the e-mail column on for an event gets both the bell and a mail', function () {
|
|
Notification::fake();
|
|
$this->seed();
|
|
|
|
$operator = operatorUser('bell-and-mail@example.com');
|
|
NotificationPreference::query()->create(array_merge(
|
|
['user_id' => $operator->id, 'event_category' => 'ticket_update'],
|
|
array_merge(NotificationPreference::DEFAULTS['ticket_update'], ['scope_all' => true, 'email' => true])
|
|
));
|
|
NotificationSetting::query()->where('trigger_key', 'priority_changed')->update(['enabled' => true]);
|
|
|
|
$ticket = makeTicket();
|
|
app(TicketService::class)->setPriority($ticket, 'high');
|
|
|
|
Notification::assertSentTo($operator, TicketNotification::class, function ($notification, $channels) {
|
|
return $channels === ['mail', 'database'];
|
|
});
|
|
});
|
|
|
|
test('the operator performing the action is never notified about their own change', function () {
|
|
Notification::fake();
|
|
$this->seed();
|
|
NotificationSetting::query()->where('trigger_key', 'priority_changed')->update(['enabled' => true]);
|
|
|
|
$actor = operatorUser('actor@example.com');
|
|
$this->actingAs($actor);
|
|
|
|
$ticket = makeTicket();
|
|
app(TicketService::class)->setPriority($ticket, 'high');
|
|
|
|
Notification::assertNotSentTo($actor, TicketNotification::class);
|
|
});
|
|
|
|
test('disabling a trigger instance-wide silences the staff fan-out too, regardless of any individual preference', function () {
|
|
Notification::fake();
|
|
$this->seed();
|
|
|
|
$operator = operatorUser('kill-switch@example.com');
|
|
NotificationPreference::query()->create(array_merge(
|
|
['user_id' => $operator->id, 'event_category' => 'ticket_update'],
|
|
array_merge(NotificationPreference::DEFAULTS['ticket_update'], ['scope_all' => true, 'email' => true])
|
|
));
|
|
NotificationSetting::query()->where('trigger_key', 'priority_changed')->update(['enabled' => false]);
|
|
|
|
$ticket = makeTicket();
|
|
app(TicketService::class)->setPriority($ticket, 'high');
|
|
|
|
Notification::assertNotSentTo($operator, TicketNotification::class);
|
|
});
|