NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Tickets for Laravel

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

TypeClassTriggerRecipients
createdTicketCreatedNotificationTicketCreatedThe requester — a receipt with the reference, even when they filed it themselves.
assignedTicketAssignedNotificationTicketAssignedWhoever the assignee names: Assignee::ticketNotifiables() (a team’s members).
repliedTicketRepliedNotificationa published public reply by an agent or the systemThe requester plus the watchers who are not agents for the ticket.
requester_repliedTicketRequesterRepliedNotificationa published public reply by the requester or a participantThe assignee’s notifiables plus the agents watching (the agent scope applies).
note_addedTicketNoteAddedNotificationTicketNoteAddedThe assignee’s notifiables plus the agents watching — agents only.
mentionedTicketMentionedNotificationTicketMentionedThe mentioned person, if the message’s own notification does not already reach them.
status_changedTicketStatusChangedNotificationTicketResolved, TicketClosed, TicketReopenedThe requester plus every watcher.
overdueTicketOverdueNotificationTicketOverdue (the sweep)The assignee’s notifiables — nobody for an unassigned ticket.
due_soonTicketDueSoonNotificationTicketDueSoon (the sweep)The assignee’s notifiables.
mergedTicketMergedNotificationTicketMergedThe 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:

TypeAdds
assignedassignee (display name)
replied, requester_replied, note_added, mentionedcomment (id), author (display name)
status_changedchange (resolved, closed, reopened), status (the state value)
overdue, due_soondue_at (ISO-8601)
mergedtarget (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):

TypeArguments
every typeticket
assignedassignee (the assigned model)
replied, requester_replied, note_added, mentionedcomment (the comments Comment)
status_changedchange (TicketStatusChangedNotification::RESOLVED / CLOSED / REOPENED)
overdue, due_soondueAt (CarbonImmutable)
mergedtarget (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:

MethodVisibility and use
type(): NotificationTypepublic, abstract — the type the class sends
via(object $notifiable): arraypublic — tickets.notifications.channels
shouldSend(object $notifiable, string $channel): boolpublic — the visibility re-check on delivery; call the parent
toMail(object $notifiable): MailMessagepublic — subject, greeting, lines(), the button from url()
toArray(object $notifiable): arraypublic — type, ticket, subject plus data()
internal(): boolpublic — internal content, agents only; true for note_added and mentioned
subject(): stringprotected, abstract — the mail subject ([TCK-42] …)
lines(object $notifiable): arrayprotected, abstract — the mail’s lines, user-written text already escaped
data(): arrayprotected — what the type adds to toArray()
linkedTicket(): Ticketprotected — the ticket the button opens (merged points at the target)
url(object $notifiable): ?stringprotected — a guest link for a TicketGuest, else tickets.notifications.url
guestLink(Ticket $ticket, TicketGuest $guest): ?stringprotected — a freshly issued guest link, or null when none can be issued
escapedSubject(): stringprotected — the ticket subject, escaped for a mail line
static line(string $key, array $replace = []): stringprotected — 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 crypto

By 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.