Notifications
Ten notification types, on by default (tickets.notifications.enabled) and sent on tickets.notifications.channels (['mail']). Each is a Laravel notification that is ShouldQueue and afterCommit: nothing is sent for a change that rolled back, and the mail is built when the queue delivers it. Plain MailMessage (no views) plus a toArray() for the database channel; every string comes from tickets::notifications in English and Slovak.
Types and recipients
| Type | Class | Trigger | Recipients |
|---|---|---|---|
created | TicketCreatedNotification | TicketCreated | The requester — a receipt with the reference, even when they filed it themselves. |
assigned | TicketAssignedNotification | TicketAssigned | Whoever the assignee names: Assignee::ticketNotifiables() (a team’s members). |
replied | TicketRepliedNotification | a published public reply by an agent or the system | The requester plus the watchers who are not agents for the ticket. |
requester_replied | TicketRequesterRepliedNotification | a published public reply by the requester or a participant | The assignee’s notifiables plus the agents watching (the agent scope applies). |
note_added | TicketNoteAddedNotification | TicketNoteAdded | The assignee’s notifiables plus the agents watching — agents only. |
mentioned | TicketMentionedNotification | TicketMentioned | The mentioned person, if the message’s own notification does not already reach them. |
status_changed | TicketStatusChangedNotification | TicketResolved, TicketClosed, TicketReopened | The requester plus every watcher. |
overdue | TicketOverdueNotification | TicketOverdue (the sweep) | The assignee’s notifiables — nobody for an unassigned ticket. |
due_soon | TicketDueSoonNotification | TicketDueSoon (the sweep) | The assignee’s notifiables. |
merged | TicketMergedNotification | TicketMerged | The source’s requester. |
- A merge’s close sends merged, not status_changed. An automation transition carries no actor, so the requester whose reply reopened a ticket gets reopened too.
- A reply held by the host’s moderation, and an internal note’s content, never reach a non-agent.
- No notification goes out for GuestAccessRevoked, and none to agents on created — listen to TicketCreated for a team inbox.
Every recipient list is:
- Visibility-checked — no one gets what they cannot see. Internal content reaches only agents the agent scope lets work on the ticket; anything else only someone who may view the ticket.
- Re-checked on delivery: a watcher removed meanwhile, an agent the scope now excludes, a deleted ticket, a message moved off the ticket — nothing is sent. A due-date notification is dropped when the due date moved or the ticket was resolved.
- Deduplicated — one notification per person, whatever roles they hold.
- Without the one who acted, unless tickets.notifications.notify_actor (the created receipt always goes out); without guests, unless tickets.notifications.guests (on); notifiable only — a model without routeNotificationFor() is skipped.
The mail
The subject reads [TCK-42] New reply: Printer on floor 3 is jammed, followed by a greeting with the recipient’s display name, the type’s lines and — for messages — the message text. What people wrote is escaped and shown as text, never rendered as Markdown. A guest’s button is a freshly issued signed guest link (none when no link can be issued, or when closing revoked the ticket’s links); anyone else gets tickets.notifications.url with {reference} filled in — blank means no button, because the package is headless. Dates are formatted in the rendering locale and the app’s time zone.
Payloads
toArray() — the database channel — always carries type, ticket (the reference) and subject, plus:
| Type | Adds |
|---|---|
assigned | assignee (display name) |
replied, requester_replied, note_added, mentioned | comment (id), author (display name) |
status_changed | change (resolved, closed, reopened), status (the state value) |
overdue, due_soon | due_at (ISO-8601) |
merged | target (the target’s reference) |
Locale
Laravel’s locale rules apply: a notifiable implementing HasLocalePreference gets its preferredLocale(); otherwise the queue worker’s app locale. Publish tickets-translations to change a line or add a language.
Swapping a class
Extend the packaged class (they are not final) and name the subclass:
use Illuminate\Notifications\Messages\MailMessage;
use RoundlyConsulting\Tickets\Notifications\TicketRepliedNotification;
final class BrandedReplyNotification extends TicketRepliedNotification
{
public function toMail(object $notifiable): MailMessage
{
return parent::toMail($notifiable)->from('[email protected]', 'Example Help Desk');
}
}// config/tickets.php
'notifications' => ['types' => ['replied' => App\Notifications\BrandedReplyNotification::class, /* … */]],A replacement must extend the packaged class; anything else is a configuration error. The package builds it through the container with named constructor arguments — keep these parameter names in an overridden constructor (other parameters are resolved by the container):
| Type | Arguments |
|---|---|
| every type | ticket |
assigned | assignee (the assigned model) |
replied, requester_replied, note_added, mentioned | comment (the comments Comment) |
status_changed | change (TicketStatusChangedNotification::RESOLVED / CLOSED / REOPENED) |
overdue, due_soon | dueAt (CarbonImmutable) |
merged | target (the target Ticket) |
Class hooks
Every type extends the abstract Notifications\TicketNotification (ShouldQueue, Queueable, afterCommit(), $deleteWhenMissingModels = true, public $ticket). The four message types extend CommentNotification (public $comment, plus author() and excerpt()); the two due-date types extend DueNotification (public $dueAt, plus due()). Override shouldSend() only by calling parent::shouldSend() — it holds the visibility re-check:
| Method | Visibility and use |
|---|---|
type(): NotificationType | public, abstract — the type the class sends |
via(object $notifiable): array | public — tickets.notifications.channels |
shouldSend(object $notifiable, string $channel): bool | public — the visibility re-check on delivery; call the parent |
toMail(object $notifiable): MailMessage | public — subject, greeting, lines(), the button from url() |
toArray(object $notifiable): array | public — type, ticket, subject plus data() |
internal(): bool | public — internal content, agents only; true for note_added and mentioned |
subject(): string | protected, abstract — the mail subject ([TCK-42] …) |
lines(object $notifiable): array | protected, abstract — the mail’s lines, user-written text already escaped |
data(): array | protected — what the type adds to toArray() |
linkedTicket(): Ticket | protected — the ticket the button opens (merged points at the target) |
url(object $notifiable): ?string | protected — a guest link for a TicketGuest, else tickets.notifications.url |
guestLink(Ticket $ticket, TicketGuest $guest): ?string | protected — a freshly issued guest link, or null when none can be issued |
escapedSubject(): string | protected — the ticket subject, escaped for a mail line |
static line(string $key, array $replace = []): string | protected — a line of tickets::notifications |
Recipients from your own code
Notifications\RecipientResolver (resolve it from the container) answers who would be told: created($ticket), assigned($ticket, $assignee, $actor), replied($ticket, AuthorRole $role, $actor), noteAdded($ticket, $actor), mentioned($ticket, $comment, $mentioned), statusChanged($ticket, $actor), due($ticket) and merged($source, $actor) — each a visibility-checked, deduplicated list<Model> — and replyType(AuthorRole $role). Use it to send the same audience a Slack DM, a push message or a webhook of your own.
Other channels
Only mail and database are built in. For any other channel, add it to notifications.channels and swap each type for a subclass that implements that channel’s method (toSlack(), toVonage(), …). A TicketGuest routes a channel through the guest field whose route names it (email → mail, phone → vonage in the shipped field set).
Failures
A notification failure — a broken setting, an unreachable queue, a mail that throws on the sync queue — is reported through your exception handler, never thrown: the change it tells about has already committed. Notifications for a ticket deleted for good before the queue reaches them are dropped. The mentioned type needs comments.mention_resolver.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.