A system note is a ticket message source
Merge and Split both 500ed. The audit note each writes uses `source = 'system'`
and the column is `enum('web','email')`, so the insert threw inside the
transaction and nothing committed — which is why the operator saw no state change
at all rather than a half-done merge.
Widened to an enum rather than a varchar, and the reasoning is the same test that
made `tickets.status` a varchar in `2026_04_17_000002`: statuses became
operator-definable rows, so their domain is open. `source` is the opposite — all
three values are set by code, an operator can never add a fourth, and the enum is
what keeps a typo'd source loud instead of a silently unfilterable row.
`TicketMessage::system()` is now the only place a system note is written. Three
scattered literals is how this reopens.
A SECOND 500 ON THE SAME ACTION, unreported and one longer title away. `split()`
appended ' (split)' to a title the product validates at max:255, so splitting a
full-length title produced 263 characters and the identical hard failure. Bounded
at the write rather than widened, and the distinction is written down: widen when
the data is right and the column is wrong; bound when the column expresses a real
product rule. 255 is the product's own limit, so a 263-character title would be
un-editable forever — the edit form would bounce it every time.
And `split()` now honours its own comment. Line 682 has always said "exclude
system notes" while the code copied them; it never mattered because no system row
could exist. The moment the enum accepts them, splitting a merged ticket would
carry "Merged from ticket QAX-0005." onto a ticket that was never merged.
This commit is contained in:
@@ -5,9 +5,20 @@ namespace Dashboard\Ticketing\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TicketMessage extends Model
|
||||
{
|
||||
/**
|
||||
* Every value `ticket_messages.source` accepts, written down once.
|
||||
*
|
||||
* The column is an enum and this is the same list, so the two can be compared
|
||||
* rather than remembered. #415 was three `'source' => 'system'` literals against
|
||||
* a column that only knew 'web' and 'email' — the value existed in the code for
|
||||
* as long as merge and split have, and nowhere in the schema.
|
||||
*/
|
||||
public const SOURCES = ['web', 'email', 'system'];
|
||||
|
||||
protected $fillable = [
|
||||
'ticket_id', 'user_id', 'author_email', 'body',
|
||||
'is_internal', 'source', 'email_message_id',
|
||||
@@ -17,6 +28,33 @@ class TicketMessage extends Model
|
||||
'is_internal' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* THE ONLY WAY A SYSTEM NOTE IS WRITTEN.
|
||||
*
|
||||
* Merge writes one and split writes two, and before this they were three
|
||||
* separate literals in one controller — which is how all three carried a
|
||||
* `source` the column rejected, and how a fourth would have joined them.
|
||||
*
|
||||
* A system note is always internal: it is the audit trail of an agent action,
|
||||
* addressed to whoever works the ticket next, and never to the submitter.
|
||||
*/
|
||||
public static function system(Ticket|int $ticket, string $body): self
|
||||
{
|
||||
return static::create([
|
||||
'ticket_id' => $ticket instanceof Ticket ? $ticket->id : $ticket,
|
||||
'user_id' => Auth::id(),
|
||||
'body' => $body,
|
||||
'is_internal' => true,
|
||||
'source' => 'system',
|
||||
]);
|
||||
}
|
||||
|
||||
/** A note this class wrote, rather than a person or a mailbox. */
|
||||
public function isSystem(): bool
|
||||
{
|
||||
return $this->source === 'system';
|
||||
}
|
||||
|
||||
public function ticket(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ticket::class, 'ticket_id');
|
||||
|
||||
Reference in New Issue
Block a user