Ticketing joins the notification platform (closes #8)
No ticketing.* category existed, so the shared Notification Log offered a filter for
every other app and nothing for this one. Verified after this change: 2 categories
register, 39 total.
THIS WAS NOT A MAIL SWEEP. Every other app's integration redirected an existing Mail::
call; here there was nothing to redirect. TicketMessageController::notifySubmitter()
was an empty stub:
// Placeholder for email notification
// In production: Mail::to($submitterEmail)->send(new TicketReplyMail(…));
So nobody has ever been told their ticket was answered. The reporter read the absence
of a Notifications directory as "not wired up", and it was more literal than that.
TWO CATEGORIES, matching the two moments somebody is waiting on another person:
ticketing.replied → the submitter, when an agent replies. Internal notes are
excluded at the call site, which already distinguishes them — the entire point of
an internal note is that the submitter does not see it.
ticketing.assigned → the agent a ticket is handed to. The previous assignee is
captured BEFORE the write so a real hand-off is distinguishable from a save that
merely touched other fields, and assigning to yourself sends nothing.
Neither is mandatory: a reply notice is a convenience, not a security or financial
record, so it is opt-out in a person's preferences.
Bridge shape copied from Dashboard\Tools\Support\ToolsNotifier: one class owning the
platform contact, available() false when the platform is absent or dark, and a queue
failure logged rather than thrown — sending a reply must succeed even when the email
cannot be queued. DELIBERATELY NOT a manifest dependency, for the documented reason:
requires.apps is a HARD requirement and Notifications is still beta and ships dark.
There is no direct-mail fallback, unlike the swept apps, because there was never any
direct mail to fall back to. With Notifications dark, ticketing behaves exactly as it
does today: silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDA3fQbsa3gaHCFoV1Q8Gg
This commit is contained in:
132
src/Support/TicketingNotifier.php
Normal file
132
src/Support/TicketingNotifier.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Dashboard\Ticketing\Support;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* The ONE place Ticketing reaches into the Notifications platform app (bridge
|
||||
* shape: Dashboard\Tools\Support\ToolsNotifier).
|
||||
*
|
||||
* Issue #8: ticketing had NO notification integration at all — no `ticketing.*`
|
||||
* category existed, so the shared Notification Log listed a category for every
|
||||
* other app and nothing for this one. Note this was not a mail sweep like the
|
||||
* other apps: there was no existing Mail:: call to redirect.
|
||||
* TicketMessageController::notifySubmitter() was an empty stub with the comment
|
||||
* "In production: Mail::to($submitterEmail)->send(new TicketReplyMail(…))", so
|
||||
* nobody has ever been told their ticket was answered.
|
||||
*
|
||||
* TWO CATEGORIES, matching the two moments a person is waiting on somebody else:
|
||||
*
|
||||
* - ticketing.replied → the submitter, when an agent answers. Internal notes are
|
||||
* excluded by the caller: the whole point of an internal note is that the
|
||||
* submitter does not see it.
|
||||
* - ticketing.assigned → the agent a ticket has just been handed to. Skipped when
|
||||
* somebody assigns a ticket to themselves, which needs no telling.
|
||||
*
|
||||
* Neither is mandatory. A reply notice is a convenience, not a security or
|
||||
* financial record, so a person may turn it off in their notification preferences.
|
||||
*
|
||||
* SHIP-DARK SAFETY. available() is false when the platform is absent, and every
|
||||
* caller treats a false return as "nothing was sent" rather than an error — there is
|
||||
* no direct-mail fallback to run, because there was never any direct mail here.
|
||||
* DELIBERATELY NOT a manifest dependency: requires.apps is a HARD requirement (a
|
||||
* merely-disabled dep fails dashboard:doctor, which gates update.sh and promote.sh),
|
||||
* and Notifications is a beta app that ships dark. Add it once Notifications is
|
||||
* stability: stable.
|
||||
*/
|
||||
final class TicketingNotifier
|
||||
{
|
||||
public const REPLIED = 'ticketing.replied';
|
||||
|
||||
public const ASSIGNED = 'ticketing.assigned';
|
||||
|
||||
/** @var array<string, array{0:string,1:string}> key => [label, description] */
|
||||
public const CATEGORIES = [
|
||||
self::REPLIED => [
|
||||
'Your help desk ticket was answered',
|
||||
'Sent to the person who raised a ticket when an agent replies. Internal notes never trigger this.',
|
||||
],
|
||||
self::ASSIGNED => [
|
||||
'A help desk ticket was assigned to you',
|
||||
'Sent to an agent when a ticket is assigned to them by somebody else.',
|
||||
],
|
||||
];
|
||||
|
||||
/** Test hook — behave as though Notifications isn't installed. */
|
||||
private static bool $pretendMissing = false;
|
||||
|
||||
public static function available(): bool
|
||||
{
|
||||
return ! self::$pretendMissing
|
||||
&& class_exists(\Central\Notifications\Support\Notify::class)
|
||||
&& class_exists(\Central\Notifications\Support\Message::class);
|
||||
}
|
||||
|
||||
public static function pretendPlatformMissing(bool $missing = true): void
|
||||
{
|
||||
self::$pretendMissing = $missing;
|
||||
}
|
||||
|
||||
/** Declare the categories. Called from TicketingServiceProvider::boot(). */
|
||||
public static function registerCategories(): void
|
||||
{
|
||||
if (! class_exists(\Central\Notifications\Support\NotificationRegistry::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
foreach (self::CATEGORIES as $key => [$label, $description]) {
|
||||
\Central\Notifications\Support\NotificationRegistry::register(
|
||||
$key, $label, $description, ['mail'], mandatory: false,
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Ticketing: could not register notification categories: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue one email through the platform.
|
||||
*
|
||||
* @param string $to recipient address, verbatim
|
||||
* @return array{handled:bool,status:?string,error:?string,id:?int}
|
||||
*/
|
||||
public static function deliver(
|
||||
string $category,
|
||||
string $to,
|
||||
string $subject,
|
||||
string $bodyHtml,
|
||||
?Model $context = null,
|
||||
): array {
|
||||
$unhandled = ['handled' => false, 'status' => null, 'error' => null, 'id' => null];
|
||||
|
||||
if (! self::available() || trim($to) === '') {
|
||||
return $unhandled;
|
||||
}
|
||||
|
||||
try {
|
||||
$row = \Central\Notifications\Support\Notify::send(new \Central\Notifications\Support\Message(
|
||||
category: $category,
|
||||
to: $to,
|
||||
subject: $subject,
|
||||
body: $bodyHtml,
|
||||
context: $context,
|
||||
));
|
||||
} catch (\Throwable $e) {
|
||||
// Never let a notification failure break the action that caused it:
|
||||
// sending a reply must succeed even if the email cannot be queued.
|
||||
Log::warning("Ticketing: notification '{$category}' could not be queued: ".$e->getMessage());
|
||||
|
||||
return $unhandled;
|
||||
}
|
||||
|
||||
return [
|
||||
'handled' => true,
|
||||
'status' => $row->status,
|
||||
'error' => $row->status === 'failed' ? ($row->error ?: 'The notification could not be queued.') : null,
|
||||
'id' => $row->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user