A ticket reference is never re-issued, and notifications name their recipient (#224, #228)

#224 — the next number came from the live rows, so deleting a ticket freed its
reference and two tickets could both be QAX-0004. An old email then points at a
different ticket than it did. The sequence is now held on the group and only ever
moves forward.

#228 — ticketing notifications carried no user, so the Notification Log showed
the message type where the recipient belongs and the per-person opt-out was
ignored: somebody who had opted out still received them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ec3REU4LQmhuchRRn6dZHk
This commit is contained in:
2026-08-27 19:37:06 -04:00
parent a4a365da3b
commit 0e48949114
5 changed files with 173 additions and 15 deletions

View File

@@ -2,6 +2,7 @@
namespace Dashboard\Ticketing\Support;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
@@ -92,19 +93,45 @@ final class TicketingNotifier
/**
* Queue one email through the platform.
*
* @param string $to recipient address, verbatim
* PASS THE PERSON, NOT THE ADDRESS, whenever we have one.
*
* This took a bare string, and `Notify::sendAll()` derives everything
* user-shaped from the recipient it is handed: `$user = $recipient instanceof
* User ? $recipient : null`. With a string there is no `$user`, so three
* things silently went missing at once, all from the same line:
*
* - `to_user_id` was null, so the send never appeared in that person's
* notification history;
* - `to_name` was null, so the shared Notification Log fell through to its
* "we don't know who this went to" rendering and printed the message TYPE
* where the recipient's name belongs — for people we plainly do know, since
* both our notifications go to a Central user by definition (a ticket's
* submitter, or the agent it was just assigned to);
* - the preference branch is guarded by `if ($user && ! $mandatory && …)`, so
* it was skipped entirely and **the opt-out did nothing**. Neither of our
* categories is mandatory, so My Notifications offered a switch that was
* never read.
*
* A bare string is still accepted, for a future send to an address with no
* account behind it. It is simply no longer what our own callers hand over.
*
* Note we no longer resolve the address by hand either: `Notify` resolves a
* User school → personal → login, which is the platform's answer to "where do
* we email this person" and is what every other app gets.
*
* @param User|string $to the recipient — a User wherever one is known
* @return array{handled:bool,status:?string,error:?string,id:?int}
*/
public static function deliver(
string $category,
string $to,
User|string $to,
string $subject,
string $bodyHtml,
?Model $context = null,
): array {
$unhandled = ['handled' => false, 'status' => null, 'error' => null, 'id' => null];
if (! self::available() || trim($to) === '') {
if (! self::available() || (is_string($to) && trim($to) === '')) {
return $unhandled;
}